为什么非阻塞网络编程中应用层Buffer是必须的?
在 陈硕的《Linux 多线程服务端编程》中一书提到:为什么非阻塞网络编程中应用层buffer是必需的?
非阻塞IO的核心思想是避免阻塞在 read()
和 write
或者其他IO系统调用上,这样可以最大限度地复用,让一个线程能否服务于多个socket连接,IO线程只能阻塞在IO 复用地函数上,如 select / poll / epoll_wait
。
-
Tcp Connection 必须要有output buffer
-
Tcp Connection 必须要有input buffer
Tcp 是一个无边界地字节流协议,接收方必须要处理"收到的数据不构成一条完整地消息" 和 "一次收到两条消息地数据" 等情况。
epoll LT 模式下接收数据
对于 epoll 模型,可读事件标志是 EPOLLIN。当可读事件触发后,我们调用 recv 函数从 clientfd 上收取数据(这里不考虑出错的情况),对于使用 epoll 的 LT 模式(水平触发模式),我们每次可以只收取部分数据;但是对于 ET 模式(边缘触发模式),我们必须将本次收到的数据全部收完。
epoll LT 模式下发送数据
epoll 的 LT 模式,通常都不会去注册监听该 clientfd 的可写事件。
这是因为,只要对端正常收数据,一般不会出现 TCP 窗口太小导致 send 或 write 函数无法写的问题。因此大多数情况下,clientfd 都是可写的,如果注册了可写事件,会导致一直触发可写事件,而此时不一定有数据需要发送。
所以,如果有数据要发送一般都是调用 send 或者 write 函数直接发送,如果发送过程中, send 函数返回 -1,并且错误码是 EWOULDBLOCK
表明由于 TCP 窗口太小数据已经无法写入时,而仍然还剩下部分数据未发送,此时我们才注册监听可写事件,并将剩余的服务存入自定义的发送缓冲区中,等可写事件触发后再接着将发送缓冲区中剩余的数据发送出去,如果仍然有部分数据不能发出去,继续注册可写事件,当已经无数据需要发送时应该立即移除对可写事件的监听。
void TcpConnection::sendMessage(const void* data, size_t len)
{
int32_t nwrote = 0;
size_t remaining = len;
bool faultError = false;
if (state_ == kDisconnected)
{
LOGW("disconnected, give up writing");
return;
}
// 当前未监听可写事件,且发送缓冲区中没有遗留数据
if (!channel_->isWriting() && outputBuffer_.readableBytes() == 0)
{
//直接发送数据
nwrote = sockets::write(channel_->fd(), data, len);
if (nwrote >= 0)
{
remaining = len - nwrote;
}
else // nwrote < 0
{
nwrote = 0;
//错误码不等于EWOULDBLOCK说明发送出错了
if (errno != EWOULDBLOCK)
{
LOGSYSE("TcpConnection::sendInLoop");
if (errno == EPIPE || errno == ECONNRESET)
{
faultError = true;
}
}
}
}
//发送未出错且还有剩余字节未发出去
if (!faultError && remaining > 0)
{
//将剩余部分加入发送缓冲区
outputBuffer_.append(static_cast<const char*>(data) + nwrote, remaining);
if (!channel_->isWriting())
{
//注册可写事件
channel_->enableWriting();
}
}
}
不能全部发出去监听可写事件后,可写事件触发后处理逻辑:
// 可写事件触发后会调用handleWrite()函数
void TcpConnection::handleWrite()
{
//将发送缓冲区中的数据发送出去
int32_t n = sockets::write(channel_->fd(), outputBuffer_.peek(), outputBuffer_.readableBytes());
if (n > 0)
{
//发送多少从发送缓冲区移除多少
outputBuffer_.retrieve(n);
//如果发送缓冲区中已经没有剩余,则移除监听可写事件
if (outputBuffer_.readableBytes() == 0)
{
//移除监听可写事件
channel_->disableWriting();
if (state_ == kDisconnecting)
{
shutdown();
}
}
}
else
{
//发数据出错处理
LOGSYSE("TcpConnection::handleWrite");
handleClose();
}
}
实际开发中,如果一段数据反复发送都不能完全发送完(例如对端先不收,后面每隔很长时间再收一个字节),我们可以设置一个最大发送次数或最大发送总时间,超过这些限定,我们可以认为对端出了问题,应该立即清空发送缓冲区并关闭连接。
网友评论