引言

RPC是远程过程调用的缩写(Remote Procedure Call),通俗地说就是调用远处的一个函数。使得应用程序之间可以进行通信,而且也遵从server/client模型。使用的时候客户端调用server端提供的接口就像是调用本地的函数一样。
RPC原理图

参考文献

net/rpc实现rpc代码:https://github.com/mailjobblog/dev_go/tree/master/220113_rpc/1.rpc
jsonrpc实现rpc代码:https://github.com/mailjobblog/dev_go/tree/master/220113_rpc/2.rpcjson

RPC实现

Go原生net/rpc实现

Go语言的RPC包的路径为net/rpc,也就是放在了net包目录下面。因此我们可以猜测该RPC包是建立在net包基础之上的。

代码实现

server.go

func main() {
	_ = rpc.Register(new(HelloService))
	// _ = rpc.RegisterName("HelloService", new(HelloService)) // 自定义名称注册

	listener, err := net.Listen("tcp", ":8888")
	if err != nil {
		log.Fatal("ListenTCP error:", err)
	}

	conn, err := listener.Accept()
	if err != nil {
		log.Fatal("Accept error:", err)
	}
	rpc.ServeConn(conn)
}

type HelloService struct {}

func (h *HelloService) Length(res string, reply *int) error {
	*reply = len(res)
	return nil
}

rpc.Register函数调用会将对象类型中所有满足RPC规则的对象方法注册为RPC函数,所有注册的方法会放在“HelloService”服务空间之下。然后我们建立一个唯一的TCP链接,并且通过rpc.ServeConn函数在该TCP链接上为对方提供RPC服务。

其中Length方法必须满足Go语言的RPC规则

  • 方法只能有两个可序列化的参数
  • 其中第二个参数是指针类型,
  • 并且返回一个error类型
  • 必须是公开的方法。

client.go

func main() {
	client, err := rpc.Dial("tcp", "127.0.0.1:8888")
	if err != nil {
		log.Fatal("dialing:", err)
	}

	res := "test HelloService"
	var reply int
	err = client.Call("HelloService.Length", res, &reply)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(reply)
}

Go使用jsonrpc实现

标准库的RPC默认采用Go语言特有的 gob 编码,因此从其它语言调用Go语言实现的RPC服务将比较困难。

Go语言的RPC框架有两个比较有特色的设计:一个是RPC数据打包时可以通过插件实现自定义的编码和解码;另一个是RPC建立在抽象的io.ReadWriteCloser接口之上的,我们可以将RPC架设在不同的通讯协议之上。这里我们将尝试通过官方自带的net/rpc/jsonrpc扩展实现一个跨语言的PPC。

代码实现

server.go

func main() {
	_ = rpc.Register(new(HelloService))

	listener, err := net.Listen("tcp", ":8888")
	if err != nil {
		log.Fatal("ListenTCP error:", err)
	}

	for {
		conn, err := listener.Accept()
		if err != nil {
			log.Fatal("Accept error:", err)
		}
		// 用rpc.ServeCodec函数替代了rpc.ServeConn函数
		// 传入的参数是针对服务端的json编解码器
		go rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
	}
}

type HelloService struct{}
func (h *HelloService) Length(res string, reply *int) error {
	*reply = len(res)
	return nil
}

client.go

func main() {
	// 手工调用net.Dial函数建立TCP链接
	conn, err := net.Dial("tcp", "127.0.0.1:8888")
	if err != nil {
		log.Fatal("net.Dial:", err)
	}

	// 针对客户端的json编解码器
	client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))

	res := "test HelloService"
	var reply int
	err = client.Call("HelloService.Length", res, &reply)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(reply)
}

知识拓展

查看服务器接收的客户端数据

为了查看到客户端调用时发送的数据格式。我们可以用 nc 命令 nc -l 8888 在同样的端口启动一个TCP服务。

然后再次执行一次RPC调用(client.go)将会发现nc输出了以下的信息:

{"method":"HelloService.Length","params":["test HelloService"],"id":0}

这是一个json编码的数据,其中method部分对应要调用的rpc服务和方法组合成的名字,params部分的第一个元素为参数,id是由调用端维护的一个唯一的调用编号。

请求的json数据对象在内部对应两个结构体:客户端是clientRequest,服务端是serverRequest。

clientRequest 和 serverRequest结构体的内容基本是一致的:

type clientRequest struct {
    Method string         `json:"method"`
    Params [1]interface{} `json:"params"`
    Id     uint64         `json:"id"`
}
type serverRequest struct {
    Method string           `json:"method"`
    Params *json.RawMessage `json:"params"`
    Id     *json.RawMessage `json:"id"`
}
用linux命令模拟客户端请求服务端

在获取到RPC调用对应的json数据后,我们可以通过直接向架设了RPC服务(server.go)的TCP服务器发送json数据模拟RPC方法调用:

$ echo -e '{"method":"HelloService.Length","params":["test HelloService"],"id":0}' | nc 127.0.0.1 8888

返回的结果也是一个json格式的数据:

{"id":0,"result":17,"error":null}

返回的json数据也是对应内部的两个结构体:客户端是clientResponse,服务端是serverResponse。两个结构体的内容同样也是类似的:

type clientResponse struct {
    Id     uint64           `json:"id"`
    Result *json.RawMessage `json:"result"`
    Error  interface{}      `json:"error"`
}
type serverResponse struct {
    Id     *json.RawMessage `json:"id"`
    Result interface{}      `json:"result"`
    Error  interface{}      `json:"error"`
}

因此无论采用何种语言,只要遵循同样的json结构,以同样的流程就可以和Go语言编写的RPC服务进行通信。这样我们就实现了跨语言的RPC。