浅谈RPC框架在UE4中的开发应用
1.简述
有网络编程基础的或是有源码阅读的小伙伴经常会看到或在实际开发使用RPC这个框架,目前流行的开源 RPC 框架也比较多,我在UE4C++中也经常会使用它,今天谈一谈什么是RPC框架。
RPC(Remote Procedure Call Protocol)–远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。RPC协议假定某些传输协议的存在,如TCP或UDP,为通信程序之间携带信息数据。在OSI网络通信模型中,RPC跨越了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。
RPC采用C/S模式。请求程序就是一个客户机,而服务提供程序就是一个服务器。首先,客户机调用进程发送一个有进程参数的调用信息到服务进程,然后等待应答信息。在服务器端,进程保持睡眠状态直到调用信息的到达为止。当一个调用信息到达,服务器获得进程参数,计算结果,发送答复信息,然后等待下一个调用信息,最后,客户端调用进程接收答复信息,获得进程结果,然后调用执行继续进行。
这里很好理解,一个 RPC 的核心功能主要有 5 个部分组成,分别是:客户端、客户端 Stub、网络传输模块、服务端 Stub、服务端等。
- 客户端(Client):服务调用方。
- 客户端存根(Client Stub):存放服务端地址信息,将客户端的请求参数数据信息打包成网络消息,再通过网络传输发送给服务端。
- 服务端存根(Server Stub):接收客户端发送过来的请求消息并进行解包,然后再调用本地服务进行处理。
- 服务端(Server):服务的真正提供者。
- Network Service:底层传输,可以是 TCP 或 HTTP。
2.RPC 核心功能
如果想要自己实现一个 RPC,最简单的方式要实现三个技术点,分别是:
- 服务寻址
使用 Call ID 映射。在本地调用中,函数体是直接通过函数指针来指定的,但是在远程调用中,函数指针是不行的,因为两个进程的地址空间是完全不一样的。
所以在 RPC 中,所有的函数都必须有自己的一个 ID。这个 ID 在所有进程中都是唯一的。
客户端在做远程过程调用时,必须附上这个 ID。还需要分别维护一个函数和Call ID的对应表。
当客户端需要进行远程调用时,它就查一下这个表,找出相应的 Call ID,然后把它传给服务端,服务端也通过查表,来确定客户端需要调用的函数,然后执行相应函数的代码。
总结:Call ID 映射:可以直接使用函数字符串,也可以使用整数 ID。映射表一般就是一个哈希表。
- 数据流的序列化和反序列化
客户端传给远程函数时,在本地调用中只需要把参数压到栈里,然后让函数自己去栈里读就行。
但是在远程过程调用时,客户端跟服务端是不同的进程,不能通过内存来传递参数。
这时候就需要客户端把参数先转成一个字节流,传给服务端后,再把字节流转成自己能读取的格式。
只有二进制数据才能在网络中传输,序列化和反序列化的定义是:
将对象转换成二进制流的过程叫做序列化
将二进制流转换成对象的过程叫做反序列化
这个过程叫序列化和反序列化。同理,从服务端返回的值也需要序列化反序列化的过程。
总结:序列化反序列化:可以自己写,也可以使用 Protobuf 或者 FlatBuffers 之类的。
- 网络传输
远程调用往往用在网络上,客户端和服务端是通过网络连接的。
所有的数据都需要通过网络传输层需要把 Call ID 和序列化后的参数字节流传给服务端,然后再把序列化后的调用结果传回客户端。
只要能完成这两者的,都可以作为传输层使用。因此,它所使用的协议其实是不限的,能完成传输就行。
尽管大部分 RPC 框架都使用 TCP 协议,但其实 UDP 也可以。
TCP 的连接是最常见的,简要分析基于 TCP 的连接:通常 TCP 连接可以是按需连接(需要调用的时候就先建立连接,调用结束后就立马断掉),也可以是长连接(客户端和服务器建立起连接之后保持长期持有,不管此时有无数据包的发送,可以配合心跳检测机制定期检测建立的连接是否存活有效),多个远程过程调用共享同一个连接。
总结:网络传输库:可以自己写 Socket,或者用 Asio,ZeroMQ,Netty 之类。
3.在UE4中的应用
不管在我前文提到的Carla ,AirSim还是其他项目,都或多或少的使用了RPC框架协议在进行本地C/S的通信,优点上文已经提及,UE4源码中也更是充分调用了。
有兴趣可以直接阅读源码,或官方文档进行学习吗,UE通常自身使用RPC进行C/S的链接,这里不再赘述,而是学习这个思想并拓展到自己的程序或插件中。以下以RpcLib为例:
4.RpcLib
这里我们使用RpcLib来快速学习并嵌入自己的程序或插件中。
首先rpclib是一个用于c++的RPC库,提供客户端和服务器的实现,没有代码生成步骤需要集成到您的构建中,只有c++。(c++11以上,所以过旧的编译器可能编译不过)
Server
#include <iostream>
#include "rpc/server.h"
void foo() {
std::cout << "foo was called!" << std::endl;
}
int main(int argc, char *argv[]) {
// Creating a server that listens on port 8080
rpc::server srv(8080);
// Binding the name "foo" to free function foo.
// note: the signature is automatically captured
srv.bind("foo", &foo);
// Binding a lambda function to the name "add".
srv.bind("add", [](int a, int b) {
return a + b;
});
// Run the server loop.
srv.run();
return 0;
}
当调用srv.run()时,rpclib启动服务器循环,该循环侦听传入的连接并尝试将调用分派到绑定函数。函数是从调用run的线程中调用的。还有一个async_run,它会生成工作线程并立即返回。
Client
#include <iostream>
#include "rpc/client.h"
int main() {
// Creating a client that connects to the localhost on port 8080
rpc::client client("127.0.0.1", 8080);
// Calling a function with paramters and converting the result to int
auto result = client.call("add", 2, 3).as<int>();
std::cout << "The result is: " << result << std::endl;
return 0;
}
github地址
clone到本地之后,仅保留include和lib文件夹,就可以调用并封装自己的API了。可以在AirSim中学习它的调用方式,我会后续记录上传一些示例。