首页
学习
活动
专区
圈层
工具
发布

Go 语言网络编程系列(八)—— RPC 编程篇:使用 JSON 对传输数据进行编解码

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

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

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

代码语言:javascript
代码运行次数:0
复制
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
}
代码语言:javascript
代码运行次数:0
复制

接口 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 传输数据进行编解码。

1、参数定义

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

代码语言:javascript
代码运行次数:0
复制
代码语言:javascript
代码运行次数:0
复制
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 时,将该聚合字段命名为指定的字符串。

2、PRC 服务端

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

代码语言:javascript
代码运行次数:0
复制
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))
  }
}

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

代码语言:javascript
代码运行次数:0
复制
go server.ServeCodec(jsonrpc.NewServerCodec(conn))

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

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

代码语言:javascript
代码运行次数:0
复制
代码语言:javascript
代码运行次数:0
复制
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 对数据进行编解码。

3、RPC 客户端

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

代码语言:javascript
代码运行次数:0
复制
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)
}
代码语言:javascript
代码运行次数:0
复制

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

4、测试 JSON-RPC 调用

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

代码语言:javascript
代码运行次数:0
复制
go run server.go utils.go
代码语言:javascript
代码运行次数:0
复制

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

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

下一篇
举报
领券