环境:
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. 实践
/net/rpc/jsonrpc

2.1 服务端

2.1.1 首先新建项目RPC,并创建Server目录,新建main.go

[root@tudou workspace]# mkdir -p RPC/Server && cd RPC/Server && touch main.go
QueryGetAge
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
}
RegisterNameQueryService
func main() {
	// 注册服务方法
	if err := rpc.RegisterName("QueryService", new(Query)); err != nil {
		log.Println(err)
	}
	...
}
net.Listenjsonrpc.NewServerCodec()
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
net.Dialjsonrpc.NewClientCodec()Call
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. 原理分析
WriteRequestCallMethodparamclientRequestjson.EncoderEncodeclientRequest
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
}
DecodeclientResponseReadResponseHeaderReadResponseBody
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"}
net/rpc/jsonrpcProtobuf
5. 总结
net/rpc/jsonrpcclientRequestclientResponse

完整代码:
https://github.com/WanshanTian/GolangLearning
参考 GolangLearning/RPC/jsonRPC目录