一、RPC简介
RPC(Remote Procedure Call)远程过程调用是一种协议,用于在网络中不同计算机上运行的程序之间进行通信。它允许一个程序像调用本地服务一样调用另一个计算机上的服务,而无需了解底层网络细节。RPC通常用于分布式系统中,使得不同机器上的程序能够无缝协作。
二、RPC工作原理
RPC的工作原理是将调用方的函数调用封装成网络请求,并发送到远程计算机上的服务端。服务端接收到请求后,解包并执行对应的函数,然后将结果返回给调用方。整个过程对调用方来说是透明的,就像是在本地调用函数一样。
三、RPC类型
RPC有多种实现方式,常见的包括:
同步RPC:客户端发起调用后,会阻塞等待服务端返回结果。这种方式简单直接,但可能导致客户端长时间无响应。
异步RPC:客户端发起调用后,不会立即等待结果,而是继续执行其他任务。当服务端返回结果时,会通过某种机制通知客户端。这种方式可以提高系统的并发性能。
双向流RPC:客户端和服务端可以同时发送和接收数据流,实现更复杂的交互模式。
四、远程过程调⽤带来的新问题
在远程调⽤时,我们需要执⾏的函数体是在远程的机器上的,也就是说,add 是在另⼀个进程中执⾏的。这就带
来了⼏个新问题:
1. Call ID 映射。
我们怎么告诉远程机器我们要调⽤ add,⽽不是 sub 或者Foo 呢?在本地调⽤中,函数体是直接通过函数指针来指定的,我们调⽤add,编译器就⾃动帮我们调⽤它相应的函数指针。但是在远程调⽤中,函数指针是不⾏的,因为两个进程的地址空间是完全不⼀样的。所以,在RPC 中,所有的函数都必须有⾃⼰的⼀个 ID。这个 ID 在所有进程中都是唯⼀确定的。客户端在做远程过程调⽤时,必须附上这个ID。然后我们还需要在客户端和服务端分别维护⼀个 {函数 <–> Call ID}的对应表。两者的表不⼀定需要完全相同,但相同的函数对应Call ID 必须相同。当客户端需要进⾏远程调⽤时,它就查⼀下这个表,找出相应的Call ID,然后把它传给服务端,服务端也通过查表,来确定客户端需要调⽤的函数,然后执⾏相应函数的代码。
2. 序列化和反序列化。
客户端怎么把参数值传给远程的函数呢?在本地调⽤中,我们只需要把参数压到栈⾥, 然后让函数⾃⼰去栈⾥读就⾏。但是在远程过程调⽤时,客户端跟服务端是不同的进程,不能通过内存来传递参数。甚⾄有时候客户端和服务端使⽤的都不是同⼀种语⾔(⽐如服务端⽤ C++,客户端⽤ Java 或者 Python)。这时候就需要客户端把参数先转成⼀个字节流,传给服务端后,再把字节流转成⾃⼰能读取的格式。这个过程叫序列化和反序列化。同理,从服务端返回的值也需要序列化反序列化的过程。
3. ⽹络传输。
远程调⽤往往⽤在⽹络上,客户端和服务端是通过⽹络连接的。所有的数据都需要通过⽹络传 输,因此就需要有⼀个⽹络传输层。⽹络传输层需要把 Call ID 和序列化后的参数字节流传给服务端,然后再把序列化后的调⽤结果传回客户端。只要能完成这两者的,都可以作为传输层使⽤。因此,它所使⽤的协议 其实是不限的,能完成传输就⾏。尽管⼤部分RPC 框架都使⽤ TCP 协议,但其实 UDP 也可以,⽽ gRPC ⼲ 脆就⽤了 HTTP2。Java 的 Netty也属于这层的东⻄。
解决了上⾯三个机制,就能实现 RPC 了,具体过程如下:
client 端解决的问题:
1. 将这个调⽤映射为Call ID。这⾥假设⽤最简单的字符串当Call ID的⽅法
2. 将Call ID,a和b序列化。可以直接将它们的值以⼆进制形式打包
3. 把2中得到的数据包发送给ServerAddr,这需要使⽤⽹络传输层
4. 等待服务器返回结果
4. 如果服务器调⽤成功,那么就将结果反序列化,并赋给total
server 端解决的问题
1. 在本地维护⼀个Call ID到函数指针的映射call_id_map,可以⽤dict完成
2. 等待请求,包括多线程的并发处理能⼒
3. 得到⼀个请求后,将其数据包反序列化,得到Call ID
4. 通过在call_id_map中查找,得到相应的函数指针
5. 将a和rb反序列化后,在本地调⽤add函数,得到结果
6. 将结果序列化后通过⽹络返回给Client
在上⾯的整个流程中,估计有部分同学看到了熟悉的计算机⽹络的流程和 web 服务器的定义。
所以要实现⼀个 RPC 框架,其实只需要按以上流程实现就基本完成了。