什么是RPC
远程调用
net/rpc
net/rpcencoding/gob
package main import ( "fmt" "log" "net" "net/rpc" ) type Listener int type Reply struct { Data string } func (l *Listener) GetLine(line []byte, reply *Reply) error { rv := string(line) fmt.Printf("Receive: %v\n", rv) *reply = Reply{rv} return nil } func main() { addy, err := net.ResolveTCPAddr("tcp", "0.0.0.0:12345") if err != nil { log.Fatal(err) } inbound, err := net.ListenTCP("tcp", addy) if err != nil { log.Fatal(err) } listener := new(Listener) rpc.Register(listener) rpc.Accept(inbound) }
在这里例子中给Listener添加了GetLine方法,这个方法的返回值必须是error类型,第一个参数是客户端传来的内容,第二个参数是响应的内容:需要是一个指针,所以定义了一个叫做Reply的结构体,只有一个Data成员用于存储响应内容。
net.ResolveTCPAddrnet.ListenTCPrpc.Register
package main import ( "bufio" "log" "net/rpc" "os" ) type Reply struct { Data string } func main() { client, err := rpc.Dial("tcp", "localhost:12345") if err != nil { log.Fatal(err) } in := bufio.NewReader(os.Stdin) for { line, _, err := in.ReadLine() if err != nil { log.Fatal(err) } var reply Reply err = client.Call("Listener.GetLine", line, &reply) if err != nil { log.Fatal(err) } log.Printf("Reply: %v, Data: %v", reply, reply.Data) } }
rpc.Dialclient.Call
❯ go run simple_server.go Receive: hi Receive: haha ❯ go run simple_client.go hi 2019/07/14 18:19:14 Reply: {hi}, Data: hi haha 2019/07/14 18:19:15 Reply: {haha}, Data: haha
net/rpc/jsonrpc
net/rpcnet/rpc/jsonrpc
import "net/rpc/jsonrpc" func main() { addy, err := net.ResolveTCPAddr("tcp", "0.0.0.0:12345") if err != nil { log.Fatal(err) } inbound, err := net.ListenTCP("tcp", addy) if err != nil { log.Fatal(err) } listener := new(Listener) rpc.Register(listener) for { conn, err := inbound.Accept() if err != nil { continue } jsonrpc.ServeConn(conn) } }
客户端部分也只是改动rpc.Dial部分:
func main() { client, err := jsonrpc.Dial("tcp", "localhost:12345") // 只改动这一行 if err != nil { log.Fatal(err) } in := bufio.NewReader(os.Stdin) for { line, _, err := in.ReadLine() if err != nil { log.Fatal(err) } var reply Reply err = client.Call("Listener.GetLine", line, &reply) if err != nil { log.Fatal(err) } log.Printf("Reply: %v, Data: %v", reply, reply.Data) } }
json-rpc是基于TCP协议实现的,目前它还不支持HTTP方式。运行效果和上面的一样:
❯ go run simple_jsonrpc_server.go Receive: hi Receive: haha ❯ go run simple_jsonrpc_client.go hi 2019/07/14 20:22:02 Reply: {hi}, Data: hi haha 2019/07/14 20:22:03 Reply: {haha}, Data: haha
请求的json数据对象在内部对应两个结构体:客户端是clientRequest,服务端是serverRequest。大抵是这样
type serverRequest struct { Method string `json:"method"` Params *json.RawMessage `json:"params"` Id *json.RawMessage `json:"id"` } type clientRequest struct { Method string `json:"method"` Params [1]interface{} `json:"params"` Id uint64 `json:"id"` }
所以我们可以基于这个格式用其他语言拼消息。简单一点,在命令行试试:
❯ echo -n "hihi" |base64 # 参数需要用base64编码 aGloaQ== ~/strconv.code/rpc master* ❯ echo -e '{"method": "Listener.GetLine","params": ["aGloaQ=="], "id": 0}' | nc localhost 12345 {"id":0,"result":{"Data":"hihi"},"error":null}
看到了吧,可以拿到对应的结果。其中id不是必须的,但是可以基于id在并发高或者异步调用中对应某一次调用。
gRPC
jsonrpc虽然可以支持跨语言但是不支持HTTP传输,而且性能不高,所以在实际生产环境中都不会用标准库里面的方式,而是选择Thrift、gRPC等方案。
gRPC是Google开源的高性能、通用的开源RPC框架,其主要面向移动应用开发并基于HTTP/2协议标准而设计,基于ProtoBuf序列化协议开发,支持Python、Golang、Java等众多开发语言。
ProtoBuf协议
Protobuf是Protocol Buffers的简称,它是Google公司开发的一种数据描述语言,类似于XML、JSON等数据描述语言,它非常轻便高效,很适合做数据存储或 RPC 数据交换格式。由于它一次定义,可生成多种语言的代码,非常适合用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。首先安装它和Go语言的生成工具:
❯ brew install protobuf ❯ protoc --version libprotoc 3.7.1 go get -u github.com/golang/protobuf/{proto,protoc-gen-go}
然后按照最新的proto3协议写一个描述文件
syntax = "proto3"; package simple; // 请求 message SimpleRequest { string data = 1; } // 响应 message SimpleResponse { string data = 1; } // rpc方法 service Simple { rpc GetLine (SimpleRequest) returns (SimpleResponse); }
其中描述了请求SimpleRequest(只有一个字符串参数data)、响应SimpleResponse(只有一个字符串参数data)和rpc方法。Simple服务只有一个GetLine方法,请求是SimpleRequest,响应SimpleResponse。然后基于.proto文件生成数据操作代码:
❯ mkdir src/simple ❯ protoc --go_out=plugins=grpc:src/simple simple.proto ❯ ll src/simple total 8.0K -rw-r--r-- 1 xiaoxi staff 7.0K Jul 14 21:43 simple.pb.go
src/simplesimple.pb.go
体验 gRPC
首先需要安装 gRPC
❯ go get -u google.golang.org/grpc
src/simple
package main import ( "fmt" "log" "net" pb "./src/simple" "golang.org/x/net/context" "google.golang.org/grpc" ) type Listener int func (l *Listener) GetLine(ctx context.Context, in *pb.SimpleRequest) (*pb.SimpleResponse, error) { rv := in.Data fmt.Printf("Receive: %v\n", rv) return &pb.SimpleResponse{Data: rv}, nil } func main() { addy, err := net.ResolveTCPAddr("tcp", "0.0.0.0:12345") if err != nil { log.Fatal(err) } inbound, err := net.ListenTCP("tcp", addy) if err != nil { log.Fatal(err) } s := grpc.NewServer() listener := new(Listener) pb.RegisterSimpleServer(s, listener) s.Serve(inbound) }
pb "./src/simple"
*pb.SimpleRequestpb.SimpleResponse
package main import ( "bufio" "log" "os" pb "./src/simple" "golang.org/x/net/context" "google.golang.org/grpc" ) func main() { conn, err := grpc.Dial("localhost:12345", grpc.WithInsecure()) if err != nil { log.Fatal(err) } c := pb.NewSimpleClient(conn) in := bufio.NewReader(os.Stdin) for { line, _, err := in.ReadLine() if err != nil { log.Fatal(err) } reply, err := c.GetLine(context.Background(), &pb.SimpleRequest{Data: string(line)}) if err != nil { log.Fatal(err) } log.Printf("Reply: %v, Data: %v", reply, reply.Data) } }
grpc.Dial("localhost:12345", grpc.WithInsecure())pb.NewSimpleClientsimpleClientSimpleClientXXXClientservice SimpleSimple
reply, err := c.GetLine(context.Background(), &pb.SimpleRequest{Data: string(line)})rpc GetLine (SimpleRequest) returns (SimpleResponse)context.Background()
❯ go run grpc_server.go Receive: hi Receive: Haha Receive: vvv ❯ go run grpc_client.go hi 2019/07/15 07:57:48 Reply: data:"hi" , Data: hi Haha 2019/07/15 07:57:51 Reply: data:"Haha" , Data: Haha vvv 2019/07/15 07:57:53 Reply: data:"vvv" , Data: vvv
代码地址
完整代码可以在 这个地址
找到。
延伸阅读
- https://books.studygolang.com/NPWG_zh/Text/chapter-rpc.html
- https://golang.org/pkg/net/rpc/
- https://developers.google.com/protocol-buffers/docs/proto3
- https://github.com/grpc/grpc-go