Go原生方法实现RPC
2022-01-13
go
go
字数统计: 1.3k(字)
阅读时长: 6(分)
引言
RPC是远程过程调用的缩写(Remote Procedure Call),通俗地说就是调用远处的一个函数。使得应用程序之间可以进行通信,而且也遵从server/client模型。使用的时候客户端调用server端提供的接口就像是调用本地的函数一样。
参考文献
net/rpc实现rpc代码:https://github.com/mailjobblog/dev_go/tree/master/220113_rpc/1.rpc
jsonrpc实现rpc代码:https://github.com/mailjobblog/dev_go/tree/master/220113_rpc/2.rpcjson
RPC实现
Go原生net/rpc实现
Go语言的RPC包的路径为net/rpc,也就是放在了net包目录下面。因此我们可以猜测该RPC包是建立在net包基础之上的。
代码实现
server.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func main () { _ = rpc.Register(new (HelloService)) listener, err := net.Listen("tcp" , ":8888" ) if err != nil { log.Fatal("ListenTCP error:" , err) } conn, err := listener.Accept() if err != nil { log.Fatal("Accept error:" , err) } rpc.ServeConn(conn) } type HelloService struct {}func (h *HelloService) Length(res string , reply *int ) error { *reply = len (res) return nil }
rpc.Register函数调用会将对象类型中所有满足RPC规则的对象方法注册为RPC函数,所有注册的方法会放在“HelloService”服务空间之下。然后我们建立一个唯一的TCP链接,并且通过rpc.ServeConn函数在该TCP链接上为对方提供RPC服务。
其中Length方法必须满足Go语言的RPC规则 :
方法只能有两个可序列化的参数
其中第二个参数是指针类型,
并且返回一个error类型
必须是公开的方法。
client.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func main () { client, err := rpc.Dial("tcp" , "127.0.0.1:8888" ) if err != nil { log.Fatal("dialing:" , err) } res := "test HelloService" var reply int err = client.Call("HelloService.Length" , res, &reply) if err != nil { log.Fatal(err) } fmt.Println(reply) }
Go使用jsonrpc实现
标准库的RPC默认采用Go语言特有的 gob 编码,因此从其它语言调用Go语言实现的RPC服务将比较困难。
Go语言的RPC框架有两个比较有特色的设计:一个是RPC数据打包时可以通过插件实现自定义的编码和解码;另一个是RPC建立在抽象的io.ReadWriteCloser接口之上的,我们可以将RPC架设在不同的通讯协议之上。这里我们将尝试通过官方自带的net/rpc/jsonrpc扩展实现一个跨语言的PPC。
代码实现
server.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 func main () { _ = rpc.Register(new (HelloService)) listener, err := net.Listen("tcp" , ":8888" ) if err != nil { log.Fatal("ListenTCP error:" , err) } for { conn, err := listener.Accept() if err != nil { log.Fatal("Accept error:" , err) } go rpc.ServeCodec(jsonrpc.NewServerCodec(conn)) } } type HelloService struct {}func (h *HelloService) Length(res string , reply *int ) error { *reply = len (res) return nil }
client.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func main () { conn, err := net.Dial("tcp" , "127.0.0.1:8888" ) if err != nil { log.Fatal("net.Dial:" , err) } client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn)) res := "test HelloService" var reply int err = client.Call("HelloService.Length" , res, &reply) if err != nil { log.Fatal(err) } fmt.Println(reply) }
知识拓展
查看服务器接收的客户端数据
为了查看到客户端调用时发送的数据格式。我们可以用 nc 命令 nc -l 8888 在同样的端口启动一个TCP服务。
然后再次执行一次RPC调用(client.go)将会发现nc输出了以下的信息:
1 { "method" : "HelloService.Length" , "params" : [ "test HelloService" ] , "id" : 0 }
这是一个json编码的数据,其中method部分对应要调用的rpc服务和方法组合成的名字,params部分的第一个元素为参数,id是由调用端维护的一个唯一的调用编号。
请求的json数据对象在内部对应两个结构体:客户端是clientRequest,服务端是serverRequest。
clientRequest 和 serverRequest结构体的内容基本是一致的:
1 2 3 4 5 6 7 8 9 10 type clientRequest struct { Method string `json:"method"` Params [1 ]interface {} `json:"params"` Id uint64 `json:"id"` } type serverRequest struct { Method string `json:"method"` Params *json.RawMessage `json:"params"` Id *json.RawMessage `json:"id"` }
用linux命令模拟客户端请求服务端
在获取到RPC调用对应的json数据后,我们可以通过直接向架设了RPC服务(server.go)的TCP服务器发送json数据模拟RPC方法调用:
1 $ echo -e '{"method":"HelloService.Length","params":["test HelloService"],"id":0}' | nc 127.0.0.1 8888
返回的结果也是一个json格式的数据:
1 { "id" : 0 , "result" : 17 , "error" : null }
返回的json数据也是对应内部的两个结构体:客户端是clientResponse,服务端是serverResponse。两个结构体的内容同样也是类似的:
1 2 3 4 5 6 7 8 9 10 type clientResponse struct { Id uint64 `json:"id"` Result *json.RawMessage `json:"result"` Error interface {} `json:"error"` } type serverResponse struct { Id *json.RawMessage `json:"id"` Result interface {} `json:"result"` Error interface {} `json:"error"` }
因此无论采用何种语言,只要遵循同样的json结构,以同样的流程就可以和Go语言编写的RPC服务进行通信。这样我们就实现了跨语言的RPC。