前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Tars-C++ 揭秘篇:Tars-RPC收发包管理

Tars-C++ 揭秘篇:Tars-RPC收发包管理

原创
作者头像
路小饭
修改2019-01-11 10:25:20
2.4K0
修改2019-01-11 10:25:20
举报

本篇文章从第十节开始,承接于上篇文章:Tars-C++ 揭秘篇:Tars协议解析

收发包的管理在整个RPC中占据了十分重要的地位,如何保证在各种网络状况下内容不丢失,同时内容还能被高效、正确解析,是一件比较有意思的事情。

Tars RPC在收发包管理上基本思路是利用缓存,下面的文章从主要逻辑上对如何使用缓存进行说明。

10.1 服务端收包管理

Tars服务端在接收请求时,为了兼顾效率、严谨,对收到的字符进行了“分层”处理,如下图:

10.1服务端收包管理.png
10.1服务端收包管理.png
  • 第一层,从套接字里读取请求,放入char类型的buffer中,长度为32 * 1024
  • 第二层,因为请求的长度很可能大于32 * 1024,程序会从一直从套接字里循环读取,直到读取的字节数小于等于零,所以需要将buffer内容追加到string类型的_recvbuffer中。这便出现了第一次字符串的拷贝过程,即将buffer内容拷贝到_recvbuffer中
  • 第三层,对_recvbuffer进行“分包”,确保每个包是一个完整的请求。这里的包是由“内容长度+内容“(iHeaderLen+ro)组成的,所以分包的逻辑也比较简单,先读取前4个字节内容转为整形长度iHeaderLen,根据这个长度截取后面的内容ro即可。分包后_recvbuffer可能有剩余不够组成一个完整包,这时候会继续等待数据。这里出现了第二次字符串拷贝,将_recvbuffer中的部分内容拷贝到ro中
  • 第四层,将第三层分好的包体内容ro转移(std::move)到结构体tagRecvData.buffer中,注意这里为了提高效率,没有采用字符串拷贝,而是使用了std::move语义
  • 第五层,对第四层中的tagRecvData.buffer内容进行反序列化,如果想深入了解Tars协议序列化和反序列化,请参考Tars-C++ 揭秘篇:Tars协议解析。注意这里使用的是char*的方式,同样避免了字符串的拷贝

10.2 服务端发包管理

相比服务端收包流程,发包稍稍复杂,为了简化,我们从ResponsePacket序列化完的字符串说起。即说明TC_EpollServer::Handle::sendResponse后面的流程。

  • 简单情况:发送的数据量比较小,原生::send函数能够完整发送所有数据
  • 复杂情况:::send一次发送不完数据,需要保存在std::vector< TC_Slice > _sendbuffer中进行后续处理。

具体处理逻辑如下所示:

10.2服务端发包管理.png
10.2服务端发包管理.png

简单说明下图中变量。buffer为string类型,是要发送的序列化完的内容;tcpSend调用的是原生的::send方法;bytes是tcpSend后实际发送的数据长度;TC_Slice是用来分割buffer的数据结构; _sendbuffer是vector类型的TC_Slice,用来管理buffer数据;kChunkSize是分割大小。 图里一共有三个NetThread::Connection::send函数,processPipe里有一个,processNet里有两个,注意他们是重载函数,是不同的函数,我在后面标注了他们在文件(tc_epoll_server.cpp)中的行数

  • 第一种场景: 在processPipe(第一个虚线框图)中数据通过tcpSend一次就全部发送成功。走绿色箭头,bytes < buffer.size() 为N的路径
  • 第二种场景:假设服务刚启动,进来第一笔请求,开始发送结果,具体处理流程如下:
  • 在processPipe中走绿色箭头,bytes < buffer.size() 为Y,表示buffer的数据没有发送完,剩余的内容按照kChunkSize大小分割为TC_Slice,然后依次放入到_sendbuffer中。注意,这时processPipe就结束了,_sendbuffer的发送放在了第二笔请求的processNet(第二个虚线框图)中处理
  • 如第一条所说,第二次请求到达时,会触发processNet函数,走红色箭头,除了处理本次请求内容,还会处理上次没发送完的response数据,即_sendbuffer。这时会把_sendbuffer再次分割成一组组的vector< iovec > vecs,最后通过tcpWriteV中的::writev函数发送
  • 通过tcpWriteV依然有可能会有剩余数据,这时候就需要根据发送的数据量去调整_sendbuffer,走的是粉红色的虚线箭头
  • 第二次请求处理后,也会产生response,会再次进入processPipe中,这时候会判断_sendbuffer是否为空,为空走绿色箭头逻辑,不为空,走蓝色箭头逻辑,直接分割为TC_Slice,放入_sendbuffer中,等待第三次请求到达时候处理

10.3 客户端发包管理

上面在整理服务端流程时,因为把不同场景放在一张图里,看起来比较复杂。在客户端这儿,我们尝试按场景进行说明。先从客户端发送第一笔请求开始说起。如下图所示。

10.3客户端发包管理-第一次发送请求.png
10.3客户端发包管理-第一次发送请求.png
  • 在ET_C_NOTIFY == pFDInfo->iType(第一个虚线框图)中,客户端发送第一笔请求,经历了将RequestPacket进行序列化的过程,序列化完的结构是“iHeaderLen + 字符串内容 ”, 放在了msg->sReqData中。
  • msg->sReqData通过TcpTransceiver::send进行发送,iRet是实际发送的字节数。iSize是msg->sReqData的长度。
  • 当只发送了一部分sReqData,即0 < iRet < iSize时,剩余的数据会放到TC_Buffer _sendBuffer中
  • 当整个请求发送失败,即iRet < 0时,会把这个请求msg整体放到_timeoutQueue中
  • 这时请求过程就结束了,剩余的数据不论是_sendBuffer,或者_timeQueue中的msg都会在处理response时一并处理
  • 假设现在客户端已经接收了服务端的response,然后会继续进入handleOutputImp(第二个虚线框图)函数处理剩下的请求数据。
  • 如果_sendBuffer不为空,先从_sendBuffer中取出数据再次发送给服务端,如果还剩下数据会调整_sendBuffer大小,然后再继续处理_timeQueue中未发送的请求
  • 如果_sendBuffer为空,直接去处理_timeQueue中未发送的请求

下面是客户端开始发送第二次请求。

10.3客户端发包管理-第二次发送请求.png
10.3客户端发包管理-第二次发送请求.png
  • 跟第一次请求类似,经过序列化后得到sReqData,这时候会判断_sendBuffer是否为空。(其实在代码逻辑中,每次请求都要判断_sendBuffer是否为空。因为第一次请求_sendBuffer肯定是空的,为了简单就没有在“10.3客户端发包管理-第一次发送请求.png”中画这部分逻辑)
  • 如果_sendBuffer为空,继续进入到TcpTransceiver::send环节,逻辑与第一次发送请求相同
  • 如果_sendBuffer不为空,直接把msg消息体放入未发送队列_timeoutQueue中,等待第二次response到达时在CommunicatorEpoll::handleOutputImp中处理

10.4 客户端收包管理

客户端的收包管理比较简单,使用一个TC_Buffer类型_recvBuffer作为缓存。如下图所示:

10.4客户端收包管理.png
10.4客户端收包管理.png
  • 第一层,从套接字里接收到的数据都放在_recvBuffer中
  • 第二层,将_recvBuffer按照"iHeaderLen + ResponsePacket rsp"方式分割为一个个独立的包,并完成rsp的反序列化工作。注意rsp的内容与_recvBuffer是拷贝关系
  • 第三层,将一个个独立包(ResponsePacket rsp)放到从_timeoutQueue中取出的msg中,这里也是一个拷贝过程,就完成了客户端的收包管理

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 10.1 服务端收包管理
  • 10.2 服务端发包管理
  • 10.3 客户端发包管理
  • 10.4 客户端收包管理
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档