环境:
Golang:go1.18.2 linux/amd64
1. 简介
上文【Golang | RPC】Golang-RPC机制的理解里提到了使用json将客户端request和服务端response编码后得到的数据
// request
{"method":"QueryService.GetAge","params":["bar"],"id":0}
// response
{"id":0,"result":"The age of bar is 20","error":null}
下面就以socket连接为基础,用json编解码器具体实现RPC,并简要分析其原理
2. 实践
通过Go语言自带的包/net/rpc/jsonrpc
实现编解码。现有下面一种场景:服务端保存着用户的年龄信息,客户端输入姓名,经RPC后获得对应的年龄
2.1 服务端
2.1.1 首先新建项目RPC,并创建Server目录,新建main.go
[root@tudou workspace]# mkdir -p RPC/Server && cd RPC/Server && touch main.go
2.1.2 服务端使用map保存用户年龄信息,同时创建Query
结构体,该结构体实现了GetAge
方法
package main
import (
"fmt"
"log"
"net"
"net/rpc"
"net/rpc/jsonrpc"
)
// 用户信息
var userinfo = map[string]int{
"foo": 18,
"bar": 20,
}
// 实现查询服务,结构体Query实现了GetAge方法
type Query struct {
}
func (q *Query) GetAge(req string, res *string) error {
*res = fmt.Sprintf("The age of %s is %d", req, userinfo[req])
return nil
}
2.1.3 使用RegisterName
注册服务方法,并指定服务名为QueryService
func main() {
// 注册服务方法
if err := rpc.RegisterName("QueryService", new(Query)); err != nil {
log.Println(err)
}
...
}
2.1.4 使用net.Listen
创建socket开启监听,通过jsonrpc.NewServerCodec()
指定json进行编解码。使用for循环,每收到一个rpc客户端的请求,便开启一个goroutine
func main() {
...
// 开启监听,接受来自rpc客户端的请求
listener, _ := net.Listen("tcp", ":1234")
for {
conn, _ := listener.Accept()
// 使用json作为编解码器
go rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
}
}
2.1.5 运行服务
[root@tudou Server]# go build main.go && ./main
2.2 客户端
2.2.1 在RPC/Server同级目录下创建Client目录,新建main.go
[root@tudou workspace]# mkdir -p RPC/Client && cd RPC/Client && touch main.go
2.2.2 首先通过net.Dial
建立socket连接,然后通过jsonrpc.NewClientCodec()
指定json编解码器,最后使用Call
远程调用GetAge方法
package main
import (
"fmt"
"net"
"net/rpc"
"net/rpc/jsonrpc"
)
func main() {
// 建立socket连接
conn, _ := net.Dial("tcp", ":1234")
// 使用json作为编解码器
client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
// 远程调用GetAge方法
var res string
_ = client.Call("QueryService.GetAge", "foo", &res)
fmt.Println(res)
}
2.2.3 运行客户端,得到如下结果
[root@tudou Client]# go run main.go
The age of foo is 18
3. 原理分析
3.1 这里主要从客户端的角度分析json序列化和反序列化的过程
先看客户端如何将request数据序列化,从如下源码可以看出,通过WriteRequest
方法将Call
的请求参数(包括请求方法Method
、请求方法对应的参数param
以及请求id)封装到clientRequest
结构体内,并通过json.Encoder
结构体的Encode
方法将clientRequest
序列化,最后通过建立的socket连接发送给服务端
type clientRequest struct {
Method string `json:"method"`
Params [1]any `json:"params"`
Id uint64 `json:"id"`
}
func (c *clientCodec) WriteRequest(r *rpc.Request, param any) error {
c.mutex.Lock()
c.pending[r.Seq] = r.ServiceMethod
c.mutex.Unlock()
c.req.Method = r.ServiceMethod
c.req.Params[0] = param
c.req.Id = r.Seq
return c.enc.Encode(&c.req)
}
type clientCodec struct {
dec *json.Decoder // for reading JSON values
enc *json.Encoder // for writing JSON values
c io.Closer
req clientRequest
resp clientResponse
mutex sync.Mutex // protects pending
pending map[uint64]string // map request id to method name
}
3.2 客户端收到response后,需要进行反序列化,使用Decode
方法将反序列化后的结果保存在clientResponse
结构体内。主要使用了两个方法ReadResponseHeader
(进行错误校验),ReadResponseBody
(获取返回结果)
type clientResponse struct {
Id uint64 `json:"id"`
Result *json.RawMessage `json:"result"`
Error any `json:"error"`
}
func (c *clientCodec) ReadResponseHeader(r *rpc.Response) error {
...
if err := c.dec.Decode(&c.resp); err != nil {
return err
}
...
if c.resp.Error != nil || c.resp.Result == nil {
x, ok := c.resp.Error.(string)
if !ok {
return fmt.Errorf("invalid error %v", c.resp.Error)
}
if x == "" {
x = "unspecified error"
}
r.Error = x
}
return nil
}
func (c *clientCodec) ReadResponseBody(x any) error {
if x == nil {
return nil
}
return json.Unmarshal(*c.resp.Result, x)
}
3.3 指定端口1234,通过wireshark抓包分析,如下图1所示,这段请求报文的载荷正好对应客户端将request通过json序列化得到的结果;而图2的响应报文载荷对应服务端将response通过json序列化得到的结果
4. 思考
gob是Golang独有的序列化方式,而json序列化Golang可以用,python也可以用,那自然而言就能想到是不是可以用python写RPC服务端(客户端),用Golang写RPC客户端(服务端)。答案自然是可以的,于是笔者就在socket连接的基础上,浅尝了一下,发现问题并不简单~原因是:每种语言在json序列化反序列化的时候存在结构差异,导致对端不能正确解析。
比如以python作为RPC客户端时,其json序列化后可能是这样(相比Golang多了一个jsonrpc字段):
{"method": "hello", "params": [], "jsonrpc": "1.0", "id": 0}
以python作为RPC服务端时,其json序列化后可能是这样(相比Golang多了一个jsonrpc字段,同时没有error字段):
{"result": "this is python test", "id": 0, "jsonrpc": "2.0"}
Golang的net/rpc/jsonrpc
包将json序列化的结构是写死的,并不能自定义格式,因此在socket基础上通过json编解码器建立Golang和python的RPC连接就卡住了。最简单的解决方案就是使用统一的编解码器Protobuf
,待续。。。
5. 总结
- 使用Golang自带的包
net/rpc/jsonrpc
进行json编解码 - RPC客户端将请求数据以
clientRequest
结构体形式封装,并序列化;将响应报文反序列化,并以clientResponse
结构体形式保存
完整代码:
https://github.com/WanshanTian/GolangLearning 参考 GolangLearning/RPC/jsonRPC目录