# 介绍
RPC,远程调用过程的称呼,它调用远程计算机上的服务就像调用本地服务一样。RPC可以很好的解耦系统,比如我们常见的WebService就是基于Http协议的一种PRC,PRC总体架构如下:
# 关键技术
-
服务发布和订阅:在服务端我们使用Zookeeper提供注册服务地址,在客户端我们从Zookeeper获取可用的服务地址。
-
通信:在通信方面我们可以使用Netty作为通信方式。
-
Spring:使用Spring的配置服务,加载Bean,扫描注解。
-
动态代理:客户端使用了代理模式透明化服务调用。
-
消息编解码:使用了Protostuff来序列化和反序列化消息
# 核心流程
- 服务消费方(client)的调用以本地调用服务的方式调用。
- client stub接收到调用后负责将方法、参数等组装从能够进行网络传输的消息体。
- server stub找到服务地址,并将消息发送给服务端。
- server stub根据接收到的消息进行解码,根据解码结果调用相应的本地服务。
- 本地服务将执行结果返回给server stub。
- server stub将返回结果打包成消息发送给消费方。
- client stub接收到消息后进行解码。
- 服务消费方最终得到需求的请求结果。
流程如下图所示:
# 消息编解码
首先我们看下消息的数据结构:(接口名称+ 方法名+参数类型和参数值+超时时间+requestID)。
-
接口名称
在我们定义接口的时候取的名字:HelloWorldServer,如果不传的话,服务端不知道你要调用哪个接口了。
-
方法名
一个接口会有很多个方法,所以我们在调用的时候不仅传接口名称,还有传方法名。
-
参数类型和参数值
是对应方法中的参数类型和参数值的。
-
超时时间
设置超时时间,一旦请求超过这个时间,就认为是失败了。
-
requestID
标识唯一的请求方式。
-
服务端返回的消息体
一般包括以下内容。返回值+状态 code+requestID
-
序列化
一般使用 Protobuf、Thrift、Avro 等成熟的序列化解决方案来搭建 RPC 框架。
# 通信过程
如果我们使用Netty作为通信方式的话,一般会使用channel.writeAndFlush()方法来发送消息二进制串,这个方法是从发出消息到接收消息结果的整个过程都是一个异步的。对应当前线程来说,将请求发送出去之后,线程就一直往下执行了,至于服务端返的结果,是服务端处理完成之后在以消息的信息发送给客户端。这样我们就会碰到以下二个问题:
第一:怎么让当前的线程暂停,等结果返回之后再往下执行。
第二:如果有多个线程同步进行远程方法的调用,这时候建立在client server之间的socket连接上会有很多双方方式的消息传递,前后顺序也可能是随机的,server处理完成之后,将结果消息发送给client,client收到很多消息,怎么知道哪个消息的结果是之前哪个线程发送的呢?
如下图所示:
线程A和线程B同时向client server发送请求requestA和requestB,socket先后将requestB和requestA发送至server,而server可能将requestB先返回,尽管requestB请求到达时间更晚,我们需要一种机制保证requestA丢给ThreadA,requestB丢给ThreadB。
-
RequestID生成AtomicLong
Client线程每次通过socket调用一次远程接口前,生成一个唯一的ID,既requestID,requestID必须保证在一个socket里是唯一的,一般AtomicLong从0开始累计生成唯一的ID;
-
把回调对象callback存到全局的concurrentHashMap
将处理结果的回调对象callback,存到全局的concurrentHashMap里面。put(requestID,callback)
-
synchronized获取回调对象callback的锁并自旋wait
当线程调用channel.writeAndFlush()发送消息后,紧接着执行callback的get()方法试图获取返回的结果,在get内部,使用synchronized获取回调对象的callback的锁,在检测是否已经获取到结果,如果没有则调用callback的wait方法,释放callback上的锁,让当前线程处于等待状态。
-
监听消息的线程收到消息,找到callback上的锁并唤醒。
服务端接收到请求并处理后,将 response 结果(此结果中包含了前面的 requestID)发送给客户端,客户端 socket 连接上专门监听消息的线程收到消息,分析结果,取到 requestID,再从前面的 ConcurrentHashMap 里面 get(requestID),从而找到 callback 对象,再用 synchronized 获取 callback 上的锁,将方法调用结果设置到 callback 对象里,再调用 callback.notifyAll()唤醒前面处于等待状态的线程。
public Object get() { synchronized (this) { // 旋锁 while (true) { // 是否有结果了 If (!isDone){ wait(); //没结果释放锁,让当前线程处于等待状态 }else{//获取数据并处理 } } } } private void setDone(Response res) { this.res = res; isDone = true; synchronized (this) { //获取锁,因为前面 wait()已经释放了 callback 的锁了 notifyAll(); // 唤醒处于等待的线程 } }