tomcat源码阅读之Tribes.RpcChannel
一、RpcChannel简介:
1、RPC即远程过程调用,它的提出旨在消除通信细节、屏蔽繁杂且易错的底层网络通信操作,像调用本地服务一般地调用远程服务,让业务开发者更多关注业务开发而不必考虑网络、硬件、系统的异构复杂环境。
2、假设一个集群环境中有node1, node2, node3, node4四个集群节点,现在node1调用RPC接口向node2, node3, node4进行服务请求,其过程如下:
a) 先将待传递的数据放到NIO集群通信框架中,由于使用的是NIO模式,线程无需阻塞直接返回, 由于与集群其他节点通信需要花销若干时间,为了提高CPU使用率当前线程应该放弃CPU的使用权进行等待操作;
b) Node2, node3, node4分别收到请求消息后,根据请求消息封装成Response对象返回;
c) 假如NIO集群通信框架首先接收到node2节点的响应消息,将Response对象保存至响应数组;
d) 由于改RPC消息响应模式为RpcResponseType.ALL_REPLY,而此时集群通信框架只收到了node2的响应消息,还有node3和node4的响应消息尚未收到,因此线程持续阻塞中;
e) 此时收到了node4的响应消息,和node2一样,将Response对象保存到响应数组,最后收到node3的响应消息时,将Response对象保存到响应数组;
f) 现在所有节点的响应都已经收集完毕,是时候通知刚刚被阻塞的那条线程了,原来的线程被notify醒后拿到所有节点的响应Response[]进行处理,至此完成了整个集群RPC过程。
3、在多线程环境下,由于多个线程都调用了RPC接口,此时收到的响应消息并不知道是哪个线程发送的,因此在发送之前生成一个UUID标识,此标识要保证同socket中唯一,再把UUID与线程对象关系对应起来,可使用Map数据结构实现,UUID的值作为key,线程对应的锁对象为value。
4、RpcCallback接口:
public interface RpcCallback { public Serializable replyRequest(Serializable msg, Member sender); public void leftOver(Serializable msg, Member sender); }
接口里面的方法是预留提供给上层具体逻辑处理的入口,replyRequest方法用于处理响应逻辑,leftOver方法用于残留请求的逻辑处理,残留响应是指有时我们在接收到第一个响应后就唤起线程。如果node1调用node2的RPC方法,node2收到请求消息后,会调用node2上的replyRequest处理请求消息,处理完后封装成Response对象返回给集群通信框架,集群通信框架根据响应模式决定什么时候返回Response对象给node1,node1收到Response对象后进行处理;如果响应模式是FIRST_REPLY或者MAJORITY_REPLY时(也就是收到第一条响应或者超过半数的响应时),还有部分响应消息不在Response里面,这个时候集群通信框架会调用node1节点上的leftover方法处理;
5、RPCChannel基于Tribes集群通信框架,是整个RPC的抽象,它实现通信框架的ChannelListener接口,实现了该接口就能在messageReceived方法中处理接收到的消息。RPCChannel.send方法也是调用的Channel.send方法实现的;
二、自定义RpcChannel Demo:
自定义一个RPC,它要实现RpcCallback接口,分别对请求处理和残留响应处理,这里请求处理仅仅是简单返回“hello,response for you!”作为响应消息,残留响应处理则是简单输出“receive a leftover message!”。代码如下:
public class MyRPC implements RpcCallback { @Override public Serializable replyRequest(Serializable msg, Member sender) { RpcMessage mapmsg = (RpcMessage) msg; mapmsg.message = "hello,response for you!"; return mapmsg; } @Override public void leftOver(Serializable msg, Member sender) { System.out.println("receive a leftover message!"); } public static void main(String[] args) { MyRPC myRPC = new MyRPC(); byte[] rpcId = new byte[] {1, 1, 1, 1}; byte[] key = new byte[] {0, 0, 0, 0}; String message = "hello"; int sendOptions = Channel.SEND_OPTIONS_SYNCHRONIZED_ACK | Channel.SEND_OPTIONS_USE_ACK; RpcMessage msg = new RpcMessage(rpcId, key, (Serializable) message); RpcChannel rpcChannel = new RpcChannel(rpcId, channel, myRPC); RpcResponse[] resp = rpcChannel.send(channel.getMembers(), msg, RpcResponseType.FIRST_REPLY, sendOptions, 3000); while (true) Thread.currentThread().sleep(1000); } }
三、UML图:
1、RpcMessage定义通信消息协议,实现Externalizable接口自定义序列化和反序列化;message用于存放响应消息,uuid标识用于关联线程,rpcId用于标识RPC实例,reply表示是否回复消息;其writeExternal方法用于将RpcMessage序列化,readExternal用于将二进制的字节流反序列化;
2、NoRpcChannelReply表示响应消息,继承于RpcMessage类,其成员与RpcMessage完全相同,只是在构造函数里面将reply变量指定为True;
3、当调用RpcChannel.send方法后,线程会阻塞住,唤醒线程的条件有四种:接收到第一个响应就唤起线程、接收到集群中大多数节点的响应就唤起线程、接收到集群中所有节点的响应才唤起线程、无需等待响应的无响应模式,代码中的定义如下:
public class RpcResponseType { public static final int FIRST_REPLY = 1; public static final int MAJORITY_REPLY = 2; public static final int ALL_REPLY = 3; public static final int NO_REPLY = 4; }
4、Response表示响应对象,用于封装接收到的消息,Member在通信框架是节点的抽象,这里用来表示来源节点。Message表示响应的消息,也就是RpcMessage类型的对象;
5、RpcCollectorKey只是对字节数组类型的id的封装,RpcCollector 表示RPC响应集,用于存放同个UUID的所有响应,其成员变量responses存储了响应数组,key与UUID的意义相同,表示该key下的所有的response响应,options表示线程唤醒类型(FIRST_REPLY, MAJORITY_REPLY, ALL_REPLY, NO_REPLY);destcnt为整型变量,表示总共需要收到的响应数量,如果线程唤醒条件为MAJORITY_REPLY时,response数组中的数量必须大于destcnt的一半以上,如果线程唤醒条件为ALL_REPLY,则response数组中的数量等于destcnt时才会唤醒线程;在isComplete方法里面就是根据线程唤醒条件判断的:
6、RpcChannel是整个RPC的核心抽象,它实现通信框架的ChannelListener接口,实现了该接口就能在messageReceived方法中处理接收到的消息。它还实现了send方法,这两个方法都是基于成员变量Channel来实现的:
Send方法通过调用channel.send将消息发送到其他集群节点上,并阻塞线程等到response响应数组收到的响应数量满足唤醒时才将Response对象数组返回给调用方;
RPC请求发送出去后,其他集群节点收到消息后触发messageReceived方法,在这个方法里面首先调用replyRequest对请求消息进行处理,处理完后将reply设置为true并发送给调用方,而调用方将其封装成Response对象并添加到response数组中:
而调用方接收到响应消息后,将其封装成Response对象并添加到response数组中,此时如果满足线程唤醒条件则唤醒线程:
通过send方法可以看到在发送RPC请求Key添加到responseMap中,发送后阻塞线程,线程唤醒后将该key从responseMap中删除,messageReceived中处理响应消息时,如果在responseMap中找不到该Key,则说明该消息属于残留请求,此时应该调用leftOver来处理该消息;