RPC
RPC(Remote Procedure Call)就是某台主机A(一般为client)像调用本地的过程一样去调用另一台主机B(一般为server)上的某个过程。RPC代码可能长成这个样子:
//client void SampleFunc() { Request req; req.set_key("client"); vector<string> values = {"v1", "v2"}; req.set_values(values); Response response = remoteHost.RemoteFunc(req); cout << response.status() << endl; } //server Response RemoteFunc(Request req) { //... }
在client调用remoteHost.RemoteFunc()时,其实已经发生了一次网络传输。参数被封包后传递给了server,传递方式可能是HTTP请求,也可能直接用TCP。server接收到参数后调用相应的RemoteFunc函数,并在离开函数时再将结果封包后传回给client。整个过程被称为一次RPC调用。
RPC调用可能像上面代码一样是同步的,但也可以是异步的,client在发起请求后需要主动去查询response。
由此我们可以看出RPC需要做的事有:
- 在client和server端各自准备好用于网络通信的函数,也就是上面代码中的RemoteFunc;
- 在client和server端各自准备好用于网络通信的数据类型,也就是上面代码中的Request和Response;
- 把请求分发给相应的函数。
这里面有两个问题:
- 如何保证client调用的函数和server端准备的函数是一致的(也就是可调用的);
- 如何保证Request和Response类型在client和server端是一致的(也就是可被正确解析的)。
stub就是用于解决这两个问题的代码。
Stub
wiki里的解释是:分布式计算中的stub是在RPC调用中用于转换参数的代码。进行RPC调用的两端都要装相应的RPC函数库,而stub就是RPC函数库里用于保证数据在两端一致的代码。
数据在两端为什么会不一致呢?可能有以下原因:
- 编程语言不同,那么使用的数据类型肯定不同;
- 位宽不同,一边是32位,一边是64位,整数长度不一致;
- 大小端不同,一边是大端,一边是小端;
- 结构体的内存布局不同,这个和整个编译环境有关;
- 字符串编码不同,一边是GBK,一边是UTF-8。
stub就是要选取一种中间形式,让两端的数据都可以正确可逆的与中间数据进行转换。常用的中间数据类型包括:纯字符串、XML、JSON、protobuf。
stub的代码可以手动去写,优点是可以处理非常复杂的数据类型,缺点是比较繁琐,如果数据类型需要修改,工作量很大。
一般来说stub都是通过现成的函数库自动生成的。这种方法需要用指定的语言(interface description language, IDL)完成一个描述文档,这个文档同时应用于client和server端。在使用时两端的stub编译器会将IDL文档翻译成指定的编程语言供两端的程序使用。如前面程序中使用的两种数据类型可以这样描述:
message Request{ required bytes key = 1; repeated bytes values = 2; } message Response { required int32 status = 1; optional bytes error_code = 2; }
当数据类型要进行修改的时候,只要改一下IDL文档,再用stub编译器进行编译就OK了。
除了数据,RPC过程也可以通过IDL进行定义:
service DefaultService {
rpc RemoteFunc(Request) returns(Response)
}