引言

RPC是远程过程调用的缩写(Remote Procedure Call),通俗地说就是调用远处的一个函数。使得应用程序之间可以进行通信,而且也遵从server/client模型。使用的时候客户端调用server端提供的接口就像是调用本地的函数一样。
RPC原理图

参考文献

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))
// _ = 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

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)
}
// 用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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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输出了以下的信息:

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。