MIT 6.824 - Lab 1 (4): Go RPC
Lab 1 虽然是个单机器多线程、多进程的程序,但主节点和工作节点的交互依然通过 RPC 实现,Go 本身也提供了开箱即用的 RPC 功能,下面将通过一个简单的求和服务来了解在 Go 中如何实现一个 RPC 服务。
定义请求体和响应体
请求体和响应体都非常简单,SumRequest 中包含要求和的两个数字,SumReply 中存放求和的结果:
1 | package pb |
服务端
首先定义服务类 SumService 和提供的方法:
1 | type SumService struct { |
SumService 只有一个 Sum 方法,接收 SumRequest 和 SumReply 两个参数,求和后将结果放回到 SumReply 中(Sum 的方法签名必须是这样的形式,即两个入参和一个 error 类型的出参,具体规则见下文描述)。
然后进行服务注册:
1 | sumService := &SumService{} |
通过 rpc.Register 这个方法可以知道,一个服务类及其提供的方法必须满足以下条件才能注册成功:
- 服务类必须是公共的
- 服务类提供的方法必须是公共的
- 服务类提供的方法入参必须是两个,一个表示请求,一个表示响应(从编码的角度来说方法入参是两个,但是实际代码是判断是否等于3个,因为在这种场景下定义的方法的第一个入参类似于
Java中的this) - 服务类提供的方法的第一个参数类型必须是公共的或者是
Go内置的数据类型 - 服务类提供的方法的第二个参数类型也必须是公共的或者是
Go内置的数据类型,且必须是指针类型 - 服务类提供的方法的出参个数只能是1个
- 服务类提供的方法的出参类型必须是
error
而 rpc.HandleHTTP() 表示通过 HTTP 作为客户端和服务端间的通信协议,当客户端发起一个 RPC 调用时,本质上是将要调用的方法和参数包装成一个 HTTP 请求,服务端收到 HTTP 请求后,解码出要调用的本地方法名称和入参,然后调用本地方法,在本地方法调用完成后再将结果写入到 HTTP 响应中,客户端收到响应后,再解析出远程调用的结果。
rpc.HandleHTTP() 本质上是个 HTTP 路由注册,实际上是调用 Handle(pattern string, handler Handler) 方法,当请求路由匹配 pattern 时,会调用对应的 handler 执行,对于 Go RPC 来说,固定路由路径是 /_goRPC_。
所以,在完成 HTTP 路由注册后,还需要配合开启一个 HTTP 服务,这样才能接受远程服务调用:
1 | listener, err := net.Listen("tcp", ":1234") |
http.Serve 方法中对于每一个客户端的连接,最终会分配一个 goroutine 来调用 Handler 的 ServeHTTP(ResponseWriter, *Request) 方法来处理,对于 Go 的 RPC 包来说,则可以实现该方法来处理 RPC 请求:
1 | // ServeHTTP implements an http.Handler that answers RPC requests. |
客户端
对于客户端来说,发起远程方法调用前需要先和服务端建立连接:
1 | client, err := rpc.DialHTTP("tcp", ":1234") |
该方法同时返回了一个 RPC 客户端类,内部同时负责对 RPC 请求的编码和解码。
然后通过 client.Call 来发起远程调用:
1 | sumRequest := &pb.SumRequest{ |
这里的调用一共有三个参数,第一个是被调用的远程方法名,需要是 类名.方法名 的形式,后两个则是远程方法的约定入参。
完整的代码可参考 go-rpc-demo。