今天来学习 Go 语言的远程过程调用 RPC( Remote Procedure Call)。
在分布式计算,远程过程调用是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一个地址空间的子程序,而程序员就像调用本地程序一样,无需额外地为这个交互作用编程。RPC是一种服务器-客户端模式,经典实现是一个通过发送请求-接受回应进行信息交互的系统。
—— From WikiPedia
RPC 可以让客户端相对直接地访问服务端的函数,这里说的「相对直接」表示我们不需要在服务端自己写一些比如 web 服务的东西来提供接口,并且在两端手动做各种数据的编码、解码。
本文包括两部分,第一部分介绍 Golang 标准库的 net/rpc
,第二部分动手实现一个玩具版 PRC 框架来加深理解。
这一部分参考 《Go语言高级编程》4.1 RPC入门。未尽之处可移步阅读原文。
Go 标准库的 net/rpc
实现了基本的 RPC,它使用一种 Go 语言特有的 Gob 编码方式,所以服务端、客户端都必须使用 Golang,不能跨语言调用。
对于服务端, net/rpc
要求用一个导出的结构体来表示 RPC 服务,这个结构体中所有符合特定要求的方法就是提供给客户端访问的:
type T struct {
}
func (t *T) MethodName(argType T1, replyType *T2) error
服务端通过 rpc.Dial
(对 TCP 服务)连接服务端,然后用使用 Call 调用 RPC 服务中的方法:
rpc.Call("T.MethodName", argType T1, replyType *T2)
例如,用 net/rpc
实现一个 Hello World。
服务端
首先构建一个 HelloService
来表示提供的服务:
// server.go
// HelloService is a RPC service for helloWorld
type HelloService struct {
}
// Hello say hello to request
func (p *HelloService) Hello(request string, reply *string) error {
*reply = "Hello, " + request
return nil
}
接下来注册并开启 RPC 服务,我们可以基于 HTTP 服务:
// server.go
func main () {
// 用将给客户端访问的名字和HelloService实例注册 RPC 服务
rpc.RegisterName("HelloService", new(HelloService))
// HTTP 服务
rpc.HandleHTTP()
err := http.ListenAndServe(":1234", nil)
if err != nil {
log.Fatal("Http Listen and Serve:", err)
}
}
也可以使用 TCP 服务,替换上面的第 8~12 行代码:
// TCP 服务
listener, err := net.Listen("tcp", ":1234")
if err != nil {
log.Fatal("ListenTCP error:", err)
}
conn, err := listener.Accept()
if err != nil {
log.Fatal("Accept error:", err)
}
rpc.ServeConn(conn)
注意,这里服务端只 Accept 一个请求,在客户端请求过后就会自动关闭。如果需要一直保持处理,可以把后半部分代码换成:
for {
conn, err := listener.Accept()
if err != nil {
log.Fatal("Accept error:", err)
}
go rpc.ServeConn(conn)
}
客户端
package main
import (
"fmt"
"log"
"net/rpc"
)
func main() {
// HTTP
// client, err := rpc.DialHTTP("tcp", "localhost:1234")
//TCP
client, err := rpc.Dial("tcp", "localhost:1234")
if err != nil {
log.Fatal("dialing:", err)
}
var reply string
err = client.Call("HelloService.Hello", "world", &reply)
if err != nil {
log.Fatal(err)
}
fmt.Println(reply)
}
先启动服务端:
$ go run helloworld/server/server.go
在另一个终端调用客户端,即可得到结果:
$ go run helloworld/client/client.go
Hello, world
之前的代码服务端、客户端的注册、调用 RPC 服务都是写死的。所有的工作都放到了一块,相当不利于维护,需要考虑重构 HelloService 服务和客户端实现。
服务端
首先,用一个 interface 抽象服务接口:
// HelloServiceName is the name of HelloService
const HelloServiceName = "HelloService"
// HelloServiceInterface is a interface for HelloService
type HelloServiceInterface interface {
Hello(request string, reply *string) error
}
// RegisterHelloService register the RPC service on svc
func RegisterHelloService(svc HelloServiceInterface) error {
return rpc.RegisterName(HelloServiceName, svc)
}
在实例化服务时,注册用:
RegisterHelloService(new(HelloService))
其余的具体服务实现没有改变。
客户端
在客户端,考虑将 RPC 细节封装到一个客户端对象 HelloServiceClient
中:
// HelloServiceClient is a client for HelloService
type HelloServiceClient struct {
*rpc.Client
}
var _ HelloServiceInterface = (*HelloServiceClient)(nil)
// DialHelloService dial HelloService
func DialHelloService(network, address string) (*HelloServiceClient, error) {
c, err := rpc.Dial(network, address)
if err != nil {
return nil , err
}
return &HelloServiceClient{
Client: c}, nil
}
// Hello calls HelloService.Hello
func (p *HelloServiceClient) Hello(request string, reply *string) error {
return p.Client.Call(HelloServiceName + ".Hello", request, reply)
}
具体调用时,就不用去暴露处理 RPC 的细节了:
client, err := DialHelloService("tcp", "localhost:1234")
if err != nil {
log.Fatal("dialing:", err)
}
var reply string
err = client.Hello("world", &reply)
if err != ni