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.ClientCodec 和 rpc.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 调用响应


Vote Vote Cancel Collect Collect Cancel

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

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