cpp-tbox项目链接 https://gitee.com/cpp-master/cpp-tbox
更多精彩内容欢迎关注微信公众号:码农练功房
往期精彩内容:
Linux应用框架cpp-tbox之弱定义
Linux应用框架cpp-tbox之日志系统设计
Linux应用框架cpp-tbox之事件驱动EventLoop
Linux应用框架cpp-tbox之事件驱动Event
Linux应用框架cpp-tbox之线程池
Linux应用框架cpp-tbox之应用层缓冲
Linux应用框架cpp-tbox之串口通信
Linux应用框架cpp-tbox之TCP通信(上篇)
Linux应用框架cpp-tbox之TCP通信(下篇)
Linux应用框架cpp-tbox之UDP通信
Linux应用框架cpp-tbox之HTTP
RPC(远程过程调用)是一种用于实现分布式系统中不同进程或不同计算机之间通信的技术。它允许我们像调用本地函数一样调用远程计算机上的函数,使得分布式系统的开发变得更加简单和高效。
常见的RPC协议有XML-RPC、gRPC、JSON-RPC等。
虽然RPC主要用于不同计算机之间的通信,但是其底层在实现上并不一定需要使用网络通信。除了网络通信,我们还可以使用unix domain socket
、串口、管道、消息队列等其他通信方式 。
cpp-tbox实现了JSON-RPC,在需要使用rpc通信的场景下,开箱即用,十分方便。
传输层
谈到通信,那就不得不提到传输层。
我们这里提及的传输层,是一个更加广义的概念,只要是用来传输应用层消息的都可以看作是传输层。
比如,现在我的消息是json格式的:
- 如果把这个消息通过HTTP传输过去,那么HTTP就可以看作是传输层。
- 如果把这个消息通过UDP传输过去,那么UDP协议就可以看作是传输层。
- 如果把这个消息通过TCP传输过去,那么TCP协议就可以看作是传输层。
- 如果把这个消息通过串口传输过去,那么串口就可以看作是传输层。
而谈到传输层,那就又不得不提到传输层的数据格式。
对于不同的传输层,我们需要设计不同的传输层的数据格式,以便在接收到传输层数据后,解出应用层消息。
对此cpp-tbox提供了三种传输层数据格式:
HeaderStreamProto适用于流式协议,提供了含头部信息的流协议,传输层数据格式如下:
+--------+--------+---------------+
| Head | Length | JSON |
+--------+--------+---------------+
| 2B | 4B | Length |
+--------+--------+---------------+
可知HeaderStreamProto采用魔幻数+消息长度+消息
的格式对数据包做了界定。
在override的onRecvData方法中,需要实现分包逻辑:
ssize_t HeaderStreamProto::onRecvData(const void *data_ptr, size_t data_size)
{
TBOX_ASSERT(data_ptr != nullptr);
if (data_size < kHeadSize)
return 0;
util::Deserializer unpack(data_ptr, data_size);
uint16_t header_magic = 0;
uint32_t content_size = 0;
unpack >> header_magic >> content_size;
if (header_magic != header_code_) {
LogNotice("head code mismatch");
return -2;
}
if (content_size + kHeadSize > data_size) //! 不够
return 0;
const char *str_ptr = static_cast<const char*>(unpack.fetchNoCopy(content_size));
Json js;
bool is_throw = tbox::CatchThrow([&] { js = Json::parse(str_ptr, str_ptr + content_size); });
if (is_throw) {
LogNotice("parse json fail");
return -1;
}
onRecvJson(js);
return unpack.pos();
}
PacketProto适用于已经有分包机制的传输协议,如:UDP, MQTT, HTTP。
因为消息在传输层已经分好包了(即保证了data_ptr指向的是一个json字节流),在override的onRecvData方法中便可以直接构造出json对象:
ssize_t PacketProto::onRecvData(const void *data_ptr, size_t data_size)
{
TBOX_ASSERT(data_ptr != nullptr);
if (data_size < 2)
return 0;
const char *str_ptr = static_cast<const char*>(data_ptr);
const size_t str_len = data_size;
Json js;
bool is_throw = tbox::CatchThrow([&] { js = Json::parse(str_ptr, str_ptr + str_len); });
if (is_throw) {
LogNotice("parse json fail");
return -1;
}
onRecvJson(js);
return str_len;
}
RawStreamProto同样适用于流式协议,与HeaderStreamProto不同的是,RawStreamProto是通过对JSON的特征标志字符[]、{},进行计数来界定JSON数据包的,算是讨了个巧。
上面提及的是三种传输层数据格式,那这几种Proto是如何通过传输层收发数据的呢?对于发送数据,我们需要设置Proto的发送回调函数,以下是Proto通过TCP发送数据的一个示例:
proto.setSendCallback([&] (const void* data_ptr, size_t data_size) {
tcp_client.send(data_ptr, data_size); //! 将数据交给tcp_client去发送
});
而Proto接收数据,则是在传输层的接收数据函数中被调用,以下是Proto通过TCP接收数据的一个示例:
tcp_client.setReceiveCallback([&] (network::Buffer &buff) {
while (buff.readableSize() > 0) {
//! 将buff中的数据交给proto进行解析
auto ret = proto.onRecvData(buff.readableBegin(), buff.readableSize());
if (ret > 0) { //! 正常解析
buff.hasRead(ret);
} else if (ret < 0) { //! 有错误
tcp_client.stop();
tcp_client.start();
} else
break;
}
}, 0);
至此消息的发送、接收都已经封装好了,应用层开发者不必过于在于底层通信细节,彻底从中解放出来了。
应用层
应用层的设计十分简单,应用层对于Rpc对象,总体类图如下:
Rpc对象则将消息的传输功能,委托给Proto对象实现。
jsonrpc的消息格式由应用层定义,对于请求:
{
"jsonrpc": "2.0",
"method": "methodName",
"params": [param1, param2, ...],
"id": 1
}
jsonrpc
:指定JsonRPC版本,通常为"2.0"。method
:指定要调用的远程方法名。params
:包含要传递给远程方法的参数列表。id
:请求的唯一标识符,用于将请求和响应进行匹配。
对于响应:
// 成功的JsonRPC响应示例
{
"jsonrpc": "2.0",
"result": "Hello, world!",
"id": 1
}
// 失败的JsonRPC响应示例
{
"jsonrpc": "2.0",
"error": {"code": -32601, "message": "Method not found"},
"id": 1
}
result
:包含远程方法调用的结果值。error
:包含错误信息,如果请求执行过程中发生错误。
服务提供方,可以通过addService接口,来注册提供的服务:
rpc.addService("ping", [&] (int id, const Json &js_params, int &errcode, Json &js_result) {
int ping_count = 0;
util::json::GetField(js_params, "count", ping_count);
LogDbg("got ping_count: %d", ping_count);
js_result = js_params;
return true; //! 表示在函数返回后立即发送回复
});
对于服务请求方,可以通过request接口,来向服务提供方来请求服务:
//! 发送请求(需要回复的)
void request(const std::string &method, const Json &js_params, RequestCallback &&cb);
void request(const std::string &method, RequestCallback &&cb);
响应结果会通过异步回调方式,返回给服务请求方。当然如果服务请求方不需要回复,则可以调用notify接口:
//! 发送通知(不需要回复的)
void notify(const std::string &method, const Json &js_params);
void notify(const std::string &method);
整个应用层代码十分简单,具体实现不展开,不到200行,感兴趣的朋友可以直接看源码。
总结
- rpc其实就是封装了一下通信过程,没有什么神奇的。