1、收发数据
最最基础的他们需要有向对方发送数据以及接收对方所发送数据的能力,可能接口根据其使用的协议,方案可能千变万化,但其接口集合里头肯定会包含如下子集(不会完全相同,可能下面一个接口只是某方案某接口的一部分,或者几个接口共同完成一个接口的任务):
int sendTo(Address addr, byte[] data, int len);
int recvFrom(Address &addr, byte[] data, int *len);
总而言之,就是提供向某个地址发送一段二进制码流的能力。
2、理解数据
光拿到二进制数据是没用的,我们还需要从数据里头抽取信息。要做到这点,我们首先要约定双方的编解码规则,可能是口头的然后双方各自写代码编解码;也可能像TDR,protobuf这些写个规范化文档,让机器生成编解码代码;或者像XML,JSON,Msgpack这种schema free的协议,有统一的编解码库,自己写少量代码进行消息结构的组织;
无论哪种,我们都可以抽象为根据某个编解码协议进行编解码。
int decode(Protocol protocol, byte[] data, int len, Message *msg);
int encode(Protocol protocol, byte[] data, int *len, Message msg);
3、消息分发
每个模块都调用recvFrom,然后decode一下吗?这显然不适合的,很可能收到的不是自己感兴趣的消息,往往我们会做类似如下的设计
统一由一个分发模块来收消息,每个消息都会带一个消息类型字段,并提供一个注册接口:
int registerHandler(MessageType msgType, void (*callback)(Message msg));
分发模块负责解码,并通过消息类型调用到相应的处理函数。
4、请求应答
客户端向服务器发送了一个购买消息,跟着期望服务器发送个消息进行成功或者失败的确认信息,这就是典型的请求应答场景。
这场景下我们要关注什么呢?
首先是一个消息对,发送方要求对方回应,但对方可能由于各种原因无法回应(网络故障,进程挂死等等),所以我们需要发送消息后开始计时,在一定的时间内没回应就认为请求失败,然后做相应的处理;
其次如果我们这类请求可以同时发多个的话(上个响应还没回就发下个请求),我们还需要有个标识来关联请求和响应,比如请求带上一个不重复的序列号,要求响应也带上这个序列号。
1、oneway模式的rpc
这是最基本的模式,其实是把上一章的1~3标准化,自动化了。
oneway客户端调用obj->foo(1, 2)相当于encode+sendTo的打包;
而服务端的register相当于第三步的消息分发,两者都包含了个“注册”的动作。
2、twoway模式的rpc
对应的上一章4里头场景,其把超时和消息配对标准化,自动化了。
好了,结案陈词阶段:综上所述,RPC本质是一种消息处理模型而已。不用RPC,你往往也要做一套类似的东西。
误区1:“RPC消息”,总有人爱这么提,实际上世界上没有RPC消息,也没有非RPC消息,正如上面所说的,RPC只是一种消息处理模型,任何消息都可以用这种模型来处理,比如UDP消息,可以抽象为对端有个oneway的void UDP:OnMessage(char *buff, int len)服务。
误区2:把RPC和请求-应答模型等同,进而认为其有局限性,比如有时候需要多请求单应答,单请求多应答等等。。但RPC还有oneway模式的,而且参数也可以是任意类型。所以任意基于消息包的通讯都能简单的用RPC来抽象。至于流协议?TCP的底层IP协议也不就是消息包协议么?
所谓成也萧何败萧何,RPC最大的优点是使用别的进程提供的服务就像本地调用,其最大的缺点也恰恰在此。它把自己假装成一个本地函数,但。。它不是!主要区别是:
1、开销,显而易见的事情,但它把自己假装成本地函数却有可能让人忽视了这点。有个这样的案例:有个模块有个RPC接口是获取一个用户的某数据,有个童鞋想获取一批用户的数据,于是ta搞了个for循环。。
2、异常,一个本地函数,我总能通过修bug能让它越来越可靠,一个简单的加法操作,除了宇宙射线让存储器翻转这类小概率事件之外,我可以认为调用它都是可靠的。but,RPC是基于网络通讯,so,你懂的。但它又把自己假装成一个本地函数,让你容易忽视这点。