RPC 编程(三):引入 jsonrpc 包通过 JSON 对 RPC 传输数据进行编解码
自定义编解码接口实现原理
上篇教程我们介绍了 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
然后新开一个终端窗口,运行客户端调用代码:
返回响应数据符合我们的预期,服务端也会打印客户端请求数据:
No Comments