RPC 编程(三):引入 jsonrpc 包通过 JSON 对 RPC 传输数据进行编解码

go-jsonrpc

自定义编解码接口实现原理

上篇教程我们介绍了 Go 语言内置的数据序列化工具 —— Gob,但是 Gob 只能在 Go 语言内部使用,不支持跨语言 RPC 调用,如果要实现这一功能,就需要对 RPC 接口的编解码实现进行自定义。

Go 的 net/rpc 实现很灵活,它在数据传输前后实现了编码解码器的接口定义,这意味着,开发者可以自定义数据的传输方式以及 RPC 服务端和客户端之间的交互行为。PRC 客户端和服务端提供的编码解码器接口如下:

type ClientCodec interface { 
    WriteRequest(*Request, interface{}) error
    ReadResponseHeader(*Response) error 
    ReadResponseBody(interface{}) error
    Close() error
}

type ServerCodec interface {
    ReadRequestHeader(*Request) error 
    ReadRequestBody(interface{}) error 
    WriteResponse(*Response, interface{}) error
    Close() error
}

接口 ClientCodec 定义了 RPC 客户端如何在一个 RPC 会话中发送请求和读取响应。客户端程序通过 WriteRequest() 方法将一个请求写入到 RPC 连接中,并通过 ReadResponseHeader()ReadResponseBody() 读取服务端的响应信息。当整个过程执行完毕后,再通过 Close() 方法来关闭该连接。

接口 ServerCodec 定义了 RPC 服务端如何在一个 RPC 会话中接收请求并发送响应。服务端程序通过 ReadRequestHeader()ReadRequestBody() 方法从一个 RPC 连接中读取请求信息,然后再通过 WriteResponse() 方法向该连接中的 RPC 客户端发送响应。当完成该过程后,通过 Close() 方法来关闭连接。

通过实现上述接口,我们可以自定义数据传输前后的编码解码方式,而不仅仅局限于 Gob。实际上,Go 标准库提供的 net/rpc/jsonrpc 包,就是一套实现了 rpc.ClientCodecrpc.ServerCodec 接口的 JSON-RPC 模块。

基于 jsonrpc 包对传输数据进行编解码

接下来,我们就来演示如何基于内置 jsonrpc 包通过 JSON 对 RPC 传输数据进行编解码。

参数定义

我们创建一个 utils.go 来定义请求和响应类,以便在 RPC 客户端和服务端中使用:

package main

type Item struct {
	Id   int `json:"id"`    	Name string `json:"name"`
}

type Response struct {
	Ok  bool `json:"ok"`
	Id  int `json:"id"`
	Message string `json:"msg"`
}

这里我们对参数字段进行额外的描述,这样,jsonrpc 包会在序列化 JSON 时,将该聚合字段命名为指定的字符串。

PRC 服务端

首先我们创建一个 server.go 来定义 RPC 服务端代码:

package main

import (
	"log"
	"net"
	"net/rpc"
	"net/rpc/jsonrpc"
)

// 服务端的rpc处理器
type ServiceHandler struct {}

func (serviceHandler *ServiceHandler) GetName(id int, item *Item) error {
	log.Printf("receive GetName call, id: %d", id)
	item.Id = id
	item.Name = "学院君"
	return nil
}

func (serviceHandler *ServiceHandler) SaveName(item Item, resp *Response) error {
	log.Printf("receive SaveName call, Item: %v", item)
	resp.Ok = true
	resp.Id = item.Id
	resp.Message = "存储成功"
	return nil
}

func main()  {
	// 初始化 RPC 服务端
	server := rpc.NewServer()

	// 监听端口 8080
	listener, err := net.Listen("tcp", ":8080")
	if err != nil {
		log.Fatalf("监听端口失败:%v", err)
	}
	defer listener.Close()

	log.Println("Start listen on port localhost:8080")

	// 初始化服务处理器
	serviceHandler := &ServiceHandler{}
	// 注册处理器
	err = server.Register(serviceHandler)
	if err != nil {
		log.Fatalf("注册服务处理器失败:%v", err)
	}

	// 等待并处理客户端连接
	for {
		conn, err := listener.Accept()
		if err != nil {
			log.Fatalf("接收客户端连接请求失败: %v", err)
		}

		// 自定义 RPC 编码器:新建一个 jsonrpc 编码器,并将该编码器绑定给 RPC 服务端处理器
		go server.ServeCodec(jsonrpc.NewServerCodec(conn))
	}
}

注意到,这里我们修改了服务端启动逻辑,重点关注这段代码:

go server.ServeCodec(jsonrpc.NewServerCodec(conn))

这里通过协程启动 RPC 服务端,并且每次拿到新的客户端连接 conn 后,通过 jsonrpc.NewServerCodec(conn) 对其进行封装,以便在处理接收到的请求数据和发送响应数据时通过 JSON 对数据进行编码和解码,然后将这个编解码器通过 server.ServeCodec 分配给 RPC 服务端,从而完成对数据编解码工具的自定义。

上上篇教程中,默认是通过 server.serveConn 方法启动的 RPC 服务端:

func (server *Server) ServeConn(conn io.ReadWriteCloser) {
	buf := bufio.NewWriter(conn)
	srv := &gobServerCodec{
		rwc:    conn,
		dec:    gob.NewDecoder(conn),
		enc:    gob.NewEncoder(buf),
		encBuf: buf,
	}
	server.ServeCodec(srv)
}

可以看到,这里没有指定编解码器,使用的是默认的 Gob 对数据进行编解码。

RPC 客户端

接下来我们创建一个 client.go 来定义客户端调用服务端代码的逻辑:

package main

import (
	"log"
	"net"
	"net/rpc/jsonrpc"
	"time"
)

func main() {
	conn, err := net.DialTimeout("tcp", "localhost:8080", 30 * time.Second) // 30秒超时时间
	if err != nil {
		log.Fatalf("客户端发起连接失败:%v", err)
	}
	defer conn.Close()

	client := jsonrpc.NewClient(conn)
	var item Item
	client.Call("ServiceHandler.GetName", 1, &item)
	log.Printf("ServiceHandler.GetName 返回结果:%v\n", item)

	var resp Response
	item = Item{2, "学院君2"}
	client.Call("ServiceHandler.SaveName", item, &resp)
	log.Printf("ServiceHandler.SaveName 返回结果:%v\n", resp)
}

这里我们以同步方式发起客户端请求,和服务端类似,我们通过 jsonrpc.NewClient(conn) 封装连接实例,这样,在发起请求和接收响应时,底层就会通过 JSON 格式对数据进行编码和解码。

测试 JSON-RPC 调用

最后我们来简单测试下,JSON-RPC 的调用,先打开一个终端窗口,启动 RPC 服务端:

go run server.go utils.go

然后新开一个终端窗口,运行客户端调用代码:

客户端 JSON-RPC 调用请求

返回响应数据符合我们的预期,服务端也会打印客户端请求数据:

服务端 JSON-RPC 调用响应

上一篇: RPC 编程(二):默认的编解码工具 Gob 使用介绍

下一篇: JSON 处理篇(上):JSON 编解码基本使用入门