04.1 RPC
4.1 RPC
RPC( Remote Procedure Call ) 远程过程调用,是分布式系统中不同节点之间流行的通信方式。在互联时代 RPC 和 IPC (Inter-process communication, 进程间通讯) 成为了不可或缺的基础构件。
4.1.1 Hello World
创建 server
:
type HelloService struct{}
func (h *HelloService) Hello(request string, reply *string) error {
*reply = "hello " + request
return nil
}
func main() {
rpc.Register(new(HelloService))
listener, err := net.Listen("tcp", ":9090")
if err != nil {
log.Fatalf("listen tcp error: %v", err)
}
conn, err := listener.Accept()
if err != nil {
log.Fatalf("accept error: %v", err)
}
rpc.ServeConn(conn)
}
Hello(request string, reply *string) error
: Hello 方法需要满足 Go 语言的 RPC 规则- 只能有两个可序列化的参数
- 第二个参数为指针类型
- 返回值为 error 类型
rpc.Register
: 将对象中所有满足 RPC 规则的函数注册为 RPC 函数net.Listen
: 监听 TCP ,通过 TCP 提供 RPC 服务
创建 client
:
func main() {
client, err := rpc.Dial("tcp", ":9090")
if err != nil {
log.Fatalf("failed to dial: %v", err)
}
var reply string
err = client.Call("HelloService.Hello", "world", &reply)
if err != nil {
log.Fatalf("rpc error: %v", err)
}
fmt.Println("Result: ", reply)
}
client.Call
: 第一个参数为 RPC 服务名及调用的方法名,后面的两个参数为调用方法定义的参数
4.1.2 更安全的 RPC 接口
在设计 RPC 的应用中,开发人员一般有三种角色:
- 服务端 RPC 方法开发
- 客户端 RPC 方法调用
- 服务端和客户端 RPC 接口规范设计
在上面的例子中为了简化将三个角色的工作整合在一起,虽然看似实现简单,但是不易于后续的扩展和维护。于是下面就针对上述的例子进行重构。
首先将 RPC 接口规范分为三个部分:
- 服务名称
- 服务方法列表
- 注册服务函数
定义接口:
package rpc_objects
type HelloInterface interface {
Hello(request string, reply *string) error
}
按照三个部分来重构 sever
代码:
const HelloServiceName = "path/example/HelloService"
type HelloService struct{}
func (h *HelloService) Hello(request string, reply *string) error {
*reply = "Hello " + request
return nil
}
func RegisterHelloService(svc rpc_objects.HelloInterface) error {
return rpc.RegisterName(HelloServiceName, svc)
}
func main() {
RegisterHelloService(new(HelloService))
listener, err := net.Listen("tcp", ":9090")
if err != nil {
log.Fatalf("failed to listen tcp: %v", err)
}
for {
conn, err := listener.Accept()
if err != nil {
log.Fatalf("failed to accept conn: %v", err)
}
go rpc.ServeConn(conn)
}
}
为了避免名称冲突,为服务名添加了包路径前缀(RPC 服务的抽象路径,并非完全等价于 package 路径);注册服务通过 RegisterHelloService
函数传入 HelloInterface
接口来完成,这样不仅可以防止服务名称传入错误还可以保证方法满足 RPC 接口的定义,也为后续的扩展提供了便利(只需实现接口即可)。
同时也实现了支持多个 TCP 连接,为每个连接创建新的协程进行处理。
重构 client
:
const HelloServiceName = "path/example/HelloService"
var _ rpc_objects.HelloInterface = (*HelloServiceClient)(nil)
type HelloServiceClient struct {
*rpc.Client
}
func (h *HelloServiceClient) Hello(request string, reply *string) error {
return h.Client.Call(HelloServiceName+".Hello", request, reply)
}
func DialHelloService(network string, addr string) (*HelloServiceClient, error) {
client, err := rpc.Dial(network, addr)
if err != nil {
return nil, err
}
return &HelloServiceClient{
Client: client,
}, nil
}
func main() {
client, err := DialHelloService("tcp", ":9090")
if err != nil {
log.Fatalf("failed to dial tcp: %v", err)
}
var reply string
err = client.Hello("kesa", &reply)
if err != nil {
log.Fatalf("call Hello error: %v", err)
}
fmt.Println("Result: ", reply)
}
说明如下:
HelloServiceClient
: 新增类型,实现了HelloInterface
接口,这样客户端可以直接调用对应的方法即可DialHelloService
: 封装连接 TCP 方法,并返回HelloServiceClient
类型var _ rpc_objects.HelloInterface = (*HelloServiceClient)(nil)
: 确保HelloServiceClient
实现了HelloInterface
接口
这样客户端将不会出现调用服务名称错误或参数类型不匹配的问题了。
4.1.3 跨语言 RPC
标准的 RPC 库默认采用 gob 编码,对于其他语言应用调用不够友好。在互联网微服务时代,每个 RPC 以及服务使用者都可能采用不同的实现语言,因此跨语言是互联网时代 RPC 的一个首要条件。
Go 的 RPC 框架可以在 RPC 数据打包时使用自定义的编码和解码方式,并且 RPC 建立在 io.ReadWriteCloser
接口上,故可以将 RPC 服务架设在不同的协议上面。
接下来通过 jsonrpc
实现跨语言的 RPC, 首先定义类型 Args
及 Multiply
方法:
package rpc_objects
type Args struct {
M, N int
}
func (Args) Multiply(args *Args, reply *int) error {
*reply = args.M * args.N
return nil
}
server
:
func main() {
calc := new(rpc_objects.Args)
rpc.Register(calc)
listener, err := net.Listen("tcp", ":9090")
if err != nil {
log.Fatalf("failed to listen tcp: %v", err)
}
for {
conn, err := listener.Accept()
if err != nil {
log.Fatalf("failed to accept conn: %v", err)
}
go rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
}
}
ServeCodec
: 和ServeConn
类似,但是使用了 json 格式进行 request 的解码和 response 的编码
client
:
func main() {
conn, err := net.Dial("tcp", ":9090")
if err != nil {
log.Fatalf("failed to dial tcp: %v", err)
}
client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
args := &rpc_objects.Args{M: 2, N: 3}
var reply int
err = client.Call("Args.Multiply", args, &reply)
if err != nil {
log.Fatalf("failed to call rpc: %v", err)
}
fmt.Printf("%d * %d = %d", 2, 3, reply)
}
- 首先建立 TCP 连接
- 基于连接使用 json 编码/解码器创建 RPC 客户端
在确保客户端可以正常调用 RPC 服务后,我们启动一个普通的 TCP 服务来查看客户端发送的数据格式, nc( netcat ,网络工具中的瑞士军刀,能通过 TCP 和 UDP 在网络中读写数据,与其他工具结合和重定向)
$ nc -l 9090
{"method":"Args.Multiply","params":[{"M":2,"N":3}],"id":0}
其中:
method
: RPC 服务及方法组成的名称params
: 参数列表id
: 调用端维护的唯一调用编号
请求的 json 数据对象在内部对应两个结构体 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"`
}
在获取到了 json 格式的请求后,模拟客户端发送给服务端
$ echo -e '{"method":"Args.Multiply","params":[{"M":2,"N":3}],"id":0}'| nc localhost 9090
{"id":0,"result":6,"error":null}
id
为 request 中对应的 id,result
: 为返回结果,error
为错误信息。
对于同步调用,id
不是必须的,但对于异步调用时,若返回顺序和调用顺序不一致,可以通过id
来识别对应的调用
返回的 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"`
}
因此无论何种语言,只要遵循同样的数据结构即可实现跨语言的 RPC 通信。
4.1.4 HTTP 上的 RPC
Go 的 RPC 框架支持在 HTTP 协议上提供服务,下面实现一个简单的基于 HTTP 协议的 RPC 服务
server
:
func main() {
rpc.Register(new(rpc_objects.Args))
http.HandleFunc("/jsonRPC", func(w http.ResponseWriter, r *http.Request) {
var conn io.ReadWriteCloser = struct {
io.Writer
io.ReadCloser
}{
Writer: w,
ReadCloser: r.Body,
}
rpc.ServeRequest(jsonrpc.NewServerCodec(conn))
})
http.ListenAndServe(":9090", nil)
}
在处理函数中基于 http.ResponseWriter
和 http.Request
类型的参数构造了 io.ReadWriteCloser
类型的 conn
(使用匿名 struct), 并基于 conn
使用服务端 json
编解码器,使用 rpc.ServeRequest
处理 RPC 请求
$ curl -X POST 'http://localhost:9090/jsonRPC' -d '{"method":"Args.Multiply","params":[{"M":2,"N":3}],"id":0}'
{"id":0,"result":6,"error":null}
这样就实现了 HTTP 协议上的 RPC 服务了
示例代码参见code