RPC(Remote Procedure Call),即远程过程调用,它允许像调用本地服务一样调用远程服务。是一种服务器-客户端(Client/Server)模式。
那“远程过程调用”,就是:可以跨过一段网络,调用另外一个网络节点上的方法。以上就是对远程过程调用的简单理解。
RPC 调用分以下两种:
异步和同步的区分在于是否等待服务端执行完成并返回结果。
知道什么是RPC以后就会发现,RPC需要解决一些问题:
这三个问题的解决方案也是RPC的核心功能:服务寻址、数据编解码和网络传输。
如果是本地调用,被调用的方法在同一个进程内,操作系统或者是虚拟机可以去地址空间去找;但是在远程调用中,这是行不通的,因为两个进程的地址空间是完全不一样的,肯定也无法知道远端的进程在那。
如果要想实现远程调用,我们需要对服务消费者和服务提供者两者进行约束:在远程过程调用中所有的函数都必须有一个 ID
,这个 ID 在整套系统中是唯一存在确定的。服务消费者在做远程过程调用时,发送的消息体中必须要携带这个 ID。服务消费者和服务提供者分别维护一个函数和 ID 的对应表。当服务消费者需要进行远程调用时,它就查一下这个表,找出对应的 ID,然后把它传给服务端,服务端也通过查表,来确定客户端需要调用的函数,然后执行相应函数的代码就行。
服务寻址的实现方式有很多种,常见的是:服务注册中心。要调用服务,首先你需要一个服务注册中心去查询对方服务都有哪些实例,然后根据负载均衡策略择优选一。
对计算机网络稍微有一点熟悉的同学都知道,数据在网络中传输都是二进制的:01010101010101010,类似这种,只有二进制数据才能在网络间传。选择好的序列化协议特别重要,一个好的序列化协议能减少序列化数据带来的性能损耗。常见的RPC序列化协议如下:
XML(Extensible Markup Language)是一种常用的序列化和反序列化协议,具有跨机器,跨语言等优点。狭义web service就是基于SOAP消息传递协议(一个基于XML的可扩展消息信封格式)来进行数据交换的。
JSON(Javascript Object Notation)起源于弱类型语言Javascript, 是采用"Attribute-value"的方式来描述对象协议。与XML相比,其协议比较简单,解析速度比较快。
Protocol Buffers 是google提供的一个开源序列化框架,是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。同 XML 相比, Protobuf 的主要优点在于性能高。它以高效的二进制方式存储,比 XML 小 3 到 10 倍,快 20 到 100 倍。
以上每种协议都有其优点和适用场景,需要根据具体的需求和环境来选择合适的协议。
提起网络传输大家脑海里肯定马上就能想到 TCP/IP四层模型、OSI 七层模型,那通常 RPC 会选择那一层作为传输协议呢?
在回答这个问题前,先来看下 RPC 需要网络传输实现什么样的功能。客户端的数据经过序列化后,就需要通过网络传输到服务端。网络传输层需要把前面说的函数 ID 和序列化后的参数字节流传给服务端,服务端处理完然后再把序列化后的调用结果传回客户端。
原则上只要能实现上面这个功能的都可以作为传输层来使用,具体协议没有限制。我们先来看下 TCP 协议,TCP 连接可以是按需连接,需要调用的时候就先建立连接,调用结束后就立马断掉,也可以是长连接,客户端和服务器建立起连接之后保持长期持有,不管此时有无数据包的发送,可以配合心跳检测机制定期检测建立的连接是否存活有效。
由此可见 TCP 的性能确实很好,因此市面上大部分 RPC 框架都使用 TCP 协议,但也有少部分框架使用其他协议,比如 gRPC 用的是 HTTP2 来实现。
忽略服务端向注册中心注册服务的流程,下面是客户端和服务端之间进行一次RPC调用的完整过程。
这里面有一个词语:
存根(Stub)
。这里存根的作用我认为和Linux内核里面的库打桩机制有点类似。在Linux中,一个"桩"(stub)就是一个程序或函数的临时替代品,"桩"可以模拟出类似于真实的程序或函数的行为。所以,在RPC中,客户端存根和服务器存根的作用是隐藏RPC底层机制的复杂性,让开发者可以像调用本地函数一样调用远程函数。
服务端代码:
type Args struct {
A, B int
}
type Compute int
func (c *Compute) Add(args *Args, reply *int) error {
*reply = args.A + args.B
return nil
}
func main() {
compute := new(Compute)
rpc.HandleHTTP() // 注册 HTTP 路由
// 注册 RPC 服务
if err := rpc.Register(compute); err != nil {
log.Fatal("Register error:", err)
}
listen, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatal("Listen error:", err)
}
if err = http.Serve(listen, nil); err != nil {
log.Fatal("Serve error:", err)
}
}
rpc库对注册的方法有一定的限制,方法必须满足签名func (t *T) MethodName(argType T1, replyType *T2) error{}
:
客户端代码:
type Args struct {
A, B int
}
func main() {
client, err := rpc.DialHTTP("tcp", "localhost:8080")
if err != nil {
log.Fatal("dialing:", err)
}
args := &Args{3, 5}
// 同步调用
var reply1 int
if err = client.Call("Compute.Add", args, &reply1); err != nil {
log.Fatal("Compute error:", err)
}
fmt.Printf("同步调用的sum: %d\n", reply1)
// 异步调用
var reply2 int
divCall := client.Go("Compute.Add", args, &reply2, nil)
_ = <-divCall.Done // 接收调用结果
fmt.Printf("异步调用的sum: %d\n", reply2)
}
运行结果如下:
PS D:\GolandProjects\RPC\client> go run .\client.go
同步调用的sum: 8
异步调用的sum: 8
服务端代码:
type Args struct {
A, B int
}
type Compute int
func (c *Compute) Add(args *Args, reply *int) error {
*reply = args.A + args.B
return nil
}
func main() {
compute := new(Compute)
if err := rpc.Register(compute); err != nil {
log.Fatal("Register error:", err)
}
listen, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatal("Listen error:", err)
}
rpc.Accept(listen)
}
客户端代码:
type Args struct {
A, B int
}
func main() {
client, err := rpc.Dial("tcp", "localhost:8080")
if err != nil {
log.Fatal("dialing:", err)
}
args := &Args{6, 8}
// 同步调用
var reply1 int
if err = client.Call("Compute.Add", args, &reply1); err != nil {
log.Fatal("Compute error:", err)
}
fmt.Printf("同步调用的sum: %d\n", reply1)
// 异步调用
var reply2 int
divCall := client.Go("Compute.Add", args, &reply2, nil)
_ = <-divCall.Done // 接收调用结果
fmt.Printf("异步调用的sum: %d\n", reply2)
}
运行结果:
PS D:\GolandProjects\RPC\client> go run .\client.go
同步调用的sum: 14
异步调用的sum: 14