一、基本使用方式说明

// server/server.go
package main

import (
    "net"
    "net/rpc"
)

type Args struct {
    A, B int
}

type Calculator int

func (t *Calculator) Add(args *Args, reply *int) error {
    *reply = args.A + args.B
    return nil
}

func (t *Calculator) Sub(args *Args, reply *int) error {
    *reply = args.A - args.B
    return nil
}

func main() {
    // 1. 创建 rpc 服务端
    rpcServer := rpc.NewServer()

    // 2. 注册服务
    // 待注册的服务方法必须是公开的,2 个参数都是与 client 约定好的固定类型,且为指针;
    // 第 1 个为 client 提交的参数,第 2 个是给 client 的返回值。
    _ = rpcServer.Register(new(Calculator))

    // 3. 开启监听指定的公开端口(比如此处的 8090)
    l, _ := net.Listen("tcp", ":8090")

    // 4. 循环往复同 client 建立 tcp 连接,并开启一个 goroutine 处理
    // 调用了 go server.ServeConn(conn)
    rpcServer.Accept(l)

    // 5. server.ServeConn(conn) 循环往复 接收请求、处理请求

    // 6. 处理单个请求时,必然是以 gob 压缩数据, gob_encode(header) + gob_encode(body)
    //  header 为固定的数据结构 rpc.Request{},内容含 ServiceMethod、Seq(会返回给 client,client 可能会并发请求,根据返回的 Seq 区分是哪个请求)
    //  1) gob 先解析出固定结构的 header,
    //  2) 根据 header 中的 ServiceMethod 找到注册的服务,
    //  3) 根据找到的服务确定同 client 约定好的该服务的 body 结构(client 提交的参数),
    //  4) gob 根据 body 结构解析出请求参数信息,
    //  5) ServiceMethod + 参数,处理任务,完成后返回,
    //  6) 返回信息同样是 gob_encode(header) + gob_encode(body),header(rpc.Response{})中含 Seq,body 为同客户端约定好的返回结构,body为约定好的reply结构体
}

// client/client.go
package main

import (
    "fmt"
    "net"
    "net/rpc"
    "sync"
)

type Args struct {
    A, B int
}

func main() {
    // 1. 建立 tcp 连接
    conn, _ := net.Dial("tcp", "127.0.0.1:8090")

    // 2. 根据 tcp 连接创建 client
    //    同时开启一个 goroutine 循环往复读取 server 返回的结果
    //    server 返回按照 header(rpc.Response{}: ServiceName+Seq) + body(具体服务约定好的返回结构)
    //    此步骤由于还没有发出请求,暂时不会读取到数据
    client := rpc.NewClient(conn)

    wg := &sync.WaitGroup{}
    wg.Add(2)

    // 3. client 可以并发发起请求
    //    但是由于使用了同一个 tcp 连接,为了不互相影响,是排队写入的
    //    通过加锁,写入一个完整的请求后[ header(rpc.Request{}: ServiceName+Seq) + body(具体的参数结构) ],再另外写入一个请求
    //    server 读取是按照约定,先读取 header,确定 service,再读取 body(具体的参数)
    go func() {
        args := &Args{100, 20}
        reply := new(int)
        // client.Call() 方法使用了 channel 进行阻塞,直到步骤 2 中的读取到 server 返回的数据
        _ = client.Call("Calculator.Add", args, reply)
        fmt.Printf("Calculator.Add: %d + %d = %d\n", args.A, args.B, *reply)
        wg.Done()
    }()

    go func() {
        args := &Args{100, 20}
        reply := new(int)
        _ = client.Call("Calculator.Sub", args, reply)
        fmt.Printf("Calculator.Sub: %d - %d = %d\n", args.A, args.B, *reply)
        wg.Done()
    }()

    wg.Wait()
}

$ cd path/server
$ go run ./server.go

$ cd path/client
$ go run ./client.go
Calculator.Sub: 100 - 20 = 80
Calculator.Add: 100 + 20 = 120

二、利用已有的 DefaultServer 及 “http 转 rpc”

net/rpc 包已有一个初始化好的 DefaultServer

且提供了有先通过 http 连接转 rpc 连接的方法。

// server/server.go
package main

import (
    "net/http"
    "net/rpc"
)

type Args struct {
    A, B int
}

type Calculator int

func (t *Calculator) Add(args *Args, reply *int) error {
    *reply = args.A + args.B
    return nil
}

func (t *Calculator) Sub(args *Args, reply *int) error {
    *reply = args.A - args.B
    return nil
}

func main() {
    // 1. 将 Calculator 服务注册至默认的 rpc 服务器 DefaultServer
    _ = rpc.Register(new(Calculator))
    // 2. DefaultServer 注册至默认的 http 服务器 DefaultServeMux
    //    其注册的 http 地址为 /_goRPC_
    //    当 http 服务器收到访问地址 /_goRPC_ 的 http 请求时,会启动一个 goroutine 调用 DefaultServer.ServeHTTP() 处理 http 请求
    //    DefaultServer.ServeHTTP() 同 client 进行完一轮 http 请求后,不会释放当前 tcp 连接,而是转为普通的 rpc 请求
    rpc.HandleHTTP()
    // 3. http 服务器开始监听 "端口 8090、地址 /_goRPC_" 的 http 请求,处理完 http 请求(相当于校验)后,转为 rpc 请求
    _ = http.ListenAndServe(":8090", nil)
}

// client/client.go
package main

import (
    "fmt"
    "net/rpc"
    "sync"
)

type Args struct {
    A, B int
}

func main() {
    // 1. 发送 http 请求至 "端口 8090、地址 /_goRPC_",
    //    等到 http 成功返回并校验成功,将其转为 rpc 请求,并创建 client 返回
    client, _ := rpc.DialHTTP("tcp", "127.0.0.1:8090")

    wg := &sync.WaitGroup{}
    wg.Add(2)
    go func() {
        args := &Args{100, 20}
        reply := new(int)
        // client.Call() 方法使用了 channel 进行阻塞,直到步骤 2 中的读取到 server 返回的数据
        _ = client.Call("Calculator.Add", args, reply)
        fmt.Printf("Calculator.Add: %d + %d = %d\n", args.A, args.B, *reply)
        wg.Done()
    }()

    go func() {
        args := &Args{100, 20}
        reply := new(int)
        _ = client.Call("Calculator.Sub", args, reply)
        fmt.Printf("Calculator.Sub: %d - %d = %d\n", args.A, args.B, *reply)
        wg.Done()
    }()

    wg.Wait()
}


$ cd path/server
$ go run ./server.go

$ cd path/client
$ go run ./client.go
Calculator.Sub: 100 - 20 = 80
Calculator.Add: 100 + 20 = 120

三、基于前述http转rpc,加入token权限校验

// server/server.go
package main

import (
    "io"
    "net/http"
    "net/rpc"
)

type Args struct {
    A, B int
}

type Calculator int

func (t *Calculator) Add(args *Args, reply *int) error {
    *reply = args.A + args.B
    return nil
}

func (t *Calculator) Sub(args *Args, reply *int) error {
    *reply = args.A - args.B
    return nil
}

func main() {
    addr := ":8090"
    requestURI := "/_custom_http_to_rpc"
    token := "bb"

    rpcServer := rpc.NewServer()
    _ = rpcServer.Register(new(Calculator))
    http.Handle(requestURI, http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
        head := request.Header
        if head.Get("token") != token {
            writer.WriteHeader(http.StatusForbidden)
            _, _ = io.WriteString(writer, "403 Forbidden\n")
            return
        }
        rpcServer.ServeHTTP(writer, request)
    }))
    _ = http.ListenAndServe(addr, nil)
}


// client/client.go
package main

import (
    "bufio"
    "errors"
    "fmt"
    "io"
    "net"
    "net/http"
    "net/rpc"
    "sync"
)

type Args struct {
    A, B int
}

func dialHTTPPath(network, address, path, token string) (*rpc.Client, error) {
    connected := "200 Connected to Go RPC"
    conn, err := net.Dial(network, address)
    if err != nil {
        return nil, err
    }
    _, _ = io.WriteString(conn, "CONNECT "+path+" HTTP/1.0\nToken: "+token+"\n\n")

    // Require successful HTTP response
    // before switching to RPC protocol.
    resp, err := http.ReadResponse(bufio.NewReader(conn), &http.Request{Method: "CONNECT"})
    if err == nil && resp.Status == connected {
        return rpc.NewClient(conn), nil
    }
    if err == nil {
        err = errors.New("unexpected HTTP response: " + resp.Status)
    }
    _ = conn.Close()
    return nil, &net.OpError{
        Op:   "dial-http",
        Net:  network + " " + address,
        Addr: nil,
        Err:  err,
    }
}

func main() {
    addr := "127.0.0.1:8090"
    requestURI := "/_custom_http_to_rpc"
    token := "bb"

    client, err := dialHTTPPath("tcp", addr, requestURI, token)
    if err != nil {
        fmt.Println("创建客户端失败", err)
        return
    }
    wg := &sync.WaitGroup{}
    wg.Add(2)
    go func() {
        args := &Args{100, 20}
        reply := new(int)
        _ = client.Call("Calculator.Add", args, reply)
        fmt.Printf("Calculator.Add: %d + %d = %d\n", args.A, args.B, *reply)
        wg.Done()
    }()

    go func() {
        args := &Args{100, 20}
        reply := new(int)
        _ = client.Call("Calculator.Sub", args, reply)
        fmt.Printf("Calculator.Sub: %d - %d = %d\n", args.A, args.B, *reply)
        wg.Done()
    }()

    wg.Wait()
}

$ cd path/server
$ go run ./server.go

$ cd path/client
$ go run ./client.go
Calculator.Sub: 100 - 20 = 80
Calculator.Add: 100 + 20 = 120

夜游神
637 声望581 粉丝

哈哈。