环境:
Golang:go1.18.2 linux/amd64
上文【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序列化得到的结果
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目录