RPC(Remote Procedure Call),即远程过程调用。它允许像调用本地服务一样调用远程服务。它属于 C/S 模式,一般都是由客户端发起请求,服务端响应。RPC 是为了解决类似远程、跨内存空间、的函数/方法调用的。
我理解的 RPC 是一种思想,一种调用方式,网上经常说的 “为什么有了 HTTP 还要有 RPC?” 我认为完全就是一个不合理的问题,HTTP 是一个协议,跟 RPC 完全是两个东西。实现 RPC 其实也可以使用 HTTP 协议,正如前后端交互基本都是基于 RESTful API ,这也是一种远程调用。
RPC的核心有两个:通信协议和序列化。在 HTTP2 之前,一般采用自定义 TCP 协议的方式进行通信,HTTP2 出来后,也有采用该协议的,比如流行的 gRPC,也有自己造轮子的,如 thrift 。
下面我为你总结下 RPC 调用的流程:
- 客户端(Client)调用客户端存根(Client Stub),同时把参数传给客户端存根;
- 客户端存根将参数打包编码,并通过系统调用发送到服务端;
- 客户端本地系统发送信息到服务器;
- 服务器系统将信息发送到服务端存根(Server Stub);
- 服务端存根解析信息,也就是解码;
- 服务端存根调用真正的服务端程序(Sever);
- 服务端(Server)处理后,通过同样的方式,把结果再返回给客户端(Client)。
RPC 调用常用于大型项目,也就是我们现在常说的微服务,而且还会包含服务注册、治理、监控等功能,是一套完整的体系。
如何在 Go 语言中实现 RPC
Go 语言为我们内置了 net/rpc 包来让我们实现 rpc。
接下来我们用一个加法来演示 rpc 包的基本使用。
// 自定义服务 A
type ServiceA struct{}
// 方法的参数
type Args struct {
A, B int
}
// 我们要暴露的方法
func (s *ServiceA) Add(args *Args, resp *int) error {
*resp = args.A + args.B
return nil
}
func main() {
service := new(ServiceA)
rpc.RegisterName("AddSrv", service)
l, err := net.Listen("tcp", ":8081")
if err != nil {
log.Fatalln(err)
}
rpc.Accept(l)
}
go 语言的 rpc 包使用 gob 协议对传输数据进行序列化和反序列化,所以我们注册的服务有一点要求便是参数必须能被 encoding/gob 序列化。除此之外还有以下要求:
- 方法的类型是可导出的(公开的);
- 方法本身也是可导出的;
- 方法必须有 2 个参数,并且参数类型是可导出或者内建的;
- 方法必须返回一个 error 类型。
func (s *ServiceType)MethodName(arg argType,resp *respType)error
还有两点需要注意:
- 第一个参数 argType 是调用者(客户端)提供的;
- 第二个参数 replyType是返回给调用者结果,必须是指针类型。
在上面我们已经解决了服务端的问题,接下来我们演示客户端如何调用这一方法。
func call() {
time.Sleep(time.Second) // 先睡一秒保证服务端的服务已经启动
client, err := rpc.Dial("tcp", "localhost:8081")
if err != nil {
log.Println(err)
return
}
args := &Args{A: 10, B: 20}
resp := new(int)
err = client.Call("MathSrv.Add", args, resp)
if err != nil {
log.Println("call Add service failed err:", err)
return
}
log.Printf("%d + %d = %d\n", args.A, args.B, *resp)
}
在以上实例代码中,首先通过 rpc.Dial 函数建立 TCP 链接,需要注意的是这里的 IP、端口要和 RPC 服务提供的一致,确保可以建立 RCP 链接。
TCP 链接建立成功后,就需要准备远程方法需要的参数,也就是示例中的args 和 resp。参数准备好之后,就可以通过 Call 方法调用远程的RPC 服务了。Call 方法有 3 个参数,它们的作用分别如下所示:
- 调用的远程方法的名字,这里是MathSrv.Add,点前面的部分是注册的服务的名称,点后面的部分是该服务的方法;
- 客户端为了调用远程方法提供的参数,示例中是 args;
- 为了接收远程方法返回的结果,必须是一个指针,也就是示例中的 &resp ,这样客户端就可以获得服务端返回的结果了。
然后我们在 rpc.Accept 前开一个协程来跑客户端代码。
得到以下输出
2023/05/29 21:25:22 10 + 20 = 30
这代表我们调用成功了
go 语言为我们内置的 rpc 实现还有许多,如基于 HTTP 的 rpc,还有使用 JSON 序列化的 rpc 实现,上面只演示了一种,其他的大家可以自行了解。