【Golang | RPC】利用json编解码器实现RPC

环境:
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序列化得到的结果
图1:request
图2:response

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目录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

田土豆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值