引言
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
func main() {
_ = rpc.Register(new(HelloService))
// _ = rpc.RegisterName("HelloService", 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
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
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)
}
// 用rpc.ServeCodec函数替代了rpc.ServeConn函数
// 传入的参数是针对服务端的json编解码器
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
func main() {
// 手工调用net.Dial函数建立TCP链接
conn, err := net.Dial("tcp", "127.0.0.1:8888")
if err != nil {
log.Fatal("net.Dial:", err)
}
// 针对客户端的json编解码器
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输出了以下的信息:
{"method":"HelloService.Length","params":["test HelloService"],"id":0}
这是一个json编码的数据,其中method
部分对应要调用的rpc服务和方法组合成的名字,params
部分的第一个元素为参数,id
是由调用端维护的一个唯一的调用编号。
请求的json数据对象在内部对应两个结构体:客户端是clientRequest,服务端是serverRequest。
clientRequest 和 serverRequest结构体的内容基本是一致的:
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方法调用:
$ echo -e '{"method":"HelloService.Length","params":["test HelloService"],"id":0}' | nc 127.0.0.1 8888
返回的结果也是一个json格式的数据:
{"id":0,"result":17,"error":null}
返回的json数据也是对应内部的两个结构体:客户端是clientResponse,服务端是serverResponse。两个结构体的内容同样也是类似的:
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。