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之事件驱动EventLoop一文中,有一点没有明确指出:
EventLoop的IO线程是基于非阻塞IO模型+IO复用。
在这种编程模型下,我们要避免阻塞在read()/write()/accept()/connect或者其他IO系统调用上,只有这样才可以最大程度上复用IO线程。
这样一来,应用层缓冲是必需的。
以TCP通信为例,在数据接收时,考虑以下场景:发送方发送两条10k字节的消息,接收方接收到数据的情况可能是:
- 一次性收到20k数据
- 分两次收到,第一次5k,第二次15k
- 分三次收到,第一次6k,第二次8k,第三次6k
- 类似其他可能
有了接收缓冲后,我们可以把接收到的数据先放到缓冲中去。
在协议层根据事先定义的通信协议判断是否接收到了一条完整的消息,当收到一条完整的消息后,再通知应用层去处理。
同样地,在使用TCP发送数据发送时,考虑以下场景:应用层需要发送10k字节的数据,而在执行write时,由于操作系统维护的缓冲区空间不足,可能无法立即接纳所有待写入的数据,那剩下的数据该如何处理?一个常见但不高明的处理方式是:
s32 nRet = 0;
s32 nSentLen = 0; //已发送长度
s32 nRemainLen = dwMsgLen; //剩下未发送长度
u32 dwTimeOut = 10000;
u64 dwStartTime = GETTIME64();
while (nSentLen < dwMsgLen)
{
SLEEP(1);
nRet = send(dwHandle, pMsgBody + nSentLen, nRemainLen, nFlag);
if (SOCKET_ERROR == nRet || nRet > nRemainLen)
{
return 0;
}
nSentLen += nRet;
nRemainLen -= nRet;
}
但我们知道,上面的代码的效率并不高。
如果有了发送缓冲后,我们可以把操作系统内核暂时无法接受的数据先存入发送缓冲中,然后关注可写事件,一旦可写就立即从发送缓冲区中取出数据发送之。
如果还是没有写完那就继续关注可写事件,等待下次可写时,继续写入。当发送缓冲区中的所有数据写完时,立即停止关注可写事件(避免不断被触发busy loop)。
如果在发送缓冲区中的数据没有写完时,又来了新的数据,这部分数据则被追加到发送缓冲区中。
整体结构图
下图是cpp-tbox中应用层缓冲的代码模型:
Buffer类是对缓冲区的抽象。
BufferedFd是对应用层缓冲区的抽象,应用层缓冲包含接收缓冲和发送缓冲,即对应两个Buffer类的实例。
FdEvent是对IO事件的抽象,前文已介绍,不再赘述。
这里通过设置FdEvent的回调接口,在接收数据时,把IO线程接收到的数据通过回调函数写入接收缓冲。
在发送数据时,把不能一次性发送完的数据,在可写事件发生后,让IO线程通过回调函数读取发送缓冲区数据发送出去。
Buffer
下图是对Buffer的一个简略画法:
* buffer_ptr_ buffer_size_
* | |
* v V
* +----+----------------+----------------+
* | | readable bytes | writable bytes |
* +----+--------------