TCP连接发送数据和接收数据的处理方式略有不同,并没有让IO复用函数全权负责,这样做减少了程序从poll函数返回的次数,提高了服务器的效率。
具体做法是,当消息产生可以像某个socket写入的时候,我们可以调用runInLoop,让IO线程在执行doPendlingFunctor的时候替我们做发送的操作。具体来看TcpConnection模块的代码。
void TcpConnection::send(const std::string& message)
{
if (state_ == kConnected)
{
if (loop_->isInLoopThread())
sendInLoop(message);
else
boost::bind(&TcpConnectoin::sendInLoop,this,message));
}
}
但是和事件的接受一样,发送不可能是一帆风顺的,不可能每次发送都保证可以把所有信息都发出去,也正因为如此我们用到了发送缓冲,具体可见https://blog.csdn.net/qq_33113661/article/details/88533686。
muduo的做法是,如果当前发送缓冲里没有数据,我们便直接把数据写入socket,如果数据全部被写入socekt,那我们这次发送操作便圆满结束,不需要动用poll去监听socket上的可写时间。相对的,如果这次只写入了一部分数据,muduo并不会去尝试重复向socket重复写入,因为这样做几乎一定会得到一个EAGAIN,而剩下的这部分数据,我们把它存入socket的输出缓冲,并且向poll注册该socket的可写事件,等下次从poll返回的时候再去调用相应的handlerWrite去处理这些数据。而如果打一开始socket的发送缓冲中就有数据没有发送出去,那我们就直接把这次的数据append到后边,等着handlerWrite去帮我们处理。
void TcpConnection::sendInLoop(const void* data, size_t len)
{
loop_->assertInLoopThread();
ssize_t nwrote = 0;
size_t remaining = len;
bool faultError = false;
if (state_ == kDisconnected)
{
LOG_WARN << "disconnected, give up writing";
return;
}
// if no thing in output queue, try writing directly
if (!channel_->isWriting() && outputBuffer_.readableBytes() == 0)
{
nwrote = sockets::write(channel_->fd(), data, len);
if (nwrote >= 0)
{
remaining = len - nwrote;
if (remaining == 0 && writeCompleteCallback_)
{
loop_->queueInLoop(std::bind(writeCompleteCallback_, shared_from_this()));
}
}
else // nwrote < 0
{
nwrote = 0;
if (errno != EWOULDBLOCK)
{
LOG_SYSERR << "TcpConnection::sendInLoop";
if (errno == EPIPE || errno == ECONNRESET) // FIXME: any others?
{
faultError = true;
}
}
}
}
assert(remaining <= len);
if (!faultError && remaining > 0)
{
size_t oldLen = outputBuffer_.readableBytes();
if (oldLen + remaining >= highWaterMark_
&& oldLen < highWaterMark_
&& highWaterMarkCallback_)
{
loop_->queueInLoop(std::bind(highWaterMarkCallback_, shared_from_this(), oldLen + remaining));
}
outputBuffer_.append(static_cast<const char*>(data)+nwrote, remaining);
if (!channel_->isWriting())
{
channel_->enableWriting();
}
}
}
上述代码中可以看到有关于writeCompleteCallback和highWaterMarkCallback两个回调函数的使用,这里来介绍一下这两个函数。
现在考虑一个代理服务器有C和S两个链接,S向C发送数据,经由代理服务器转发,现在S的数据发送的很快,但是C的接受速率却较慢,如果本代理服务器不加以限制,那S到来的数据迟早会撑爆这C连接的发送缓冲区,解决的办法就是当C的发送缓冲中堆积数据达到了某个标志的时候,调用highWaterMarkCallback去让S的连接停止接受数据,等到C发送缓冲的数据被发送完了,调用writeCompleteCallback再开始接受S连接的数据。这样就确保了数据不会丢失,缓冲不会被撑爆。
知道了这两个回调的作用,我们来看她们的应用方法。这两个函数都是使用queueInLoop向EventLoop的pendingfunction队列中添加相应的函数对象,当然,这两个添加的操作也肯定是在EventLoop执行doPendingFunctors的时候执行的,这里曾有一个困扰我许久的问题——为什么要用queueInLoop而不是用runInLoop呢?如果我们要做的只是打开或者关闭某个连接的读写,那用runInloop可能不会出现什么问题,但是请你设想一下,假如有人偏偏要在writeCompleteCallback中向所在的socket写入1byte的信息呢?结合runInLoop和queueInLoop以及eventLoop的loop函数还有上边的send和sendInloop想一想。
答案是会stackoverflow,函数会递归调用把栈撑爆。因为要知道,run和queue的区别在于,在IO线程中调用run这个函数会立刻执行,而调用queue的话,会等下次从poll返回之后执行doPend的时候才执行,发送的过程是要调用send的,send又调用了sendInLoop,而当你在sendinLoop中把这1byte发送出去之后,我们正好又触发了writeComplete,循环开始,爆栈了。可以说有点隐晦了,以上也是我在给陈硕先生发了邮件询问,得到指点之后才得到的答案,在这里再一次感谢陈硕先生。
最后来看看handlerWrite,这是帮我们处理发送缓冲中积攒的数据的函数,是planB,因为为了保证服务器效率和数据有序的发送,我们不得不向poll注册这个soceket的写时间,poll返回后由管理连接的channel调用handlerWrite。
handlerWrite要做的事情就是再次尝试往socket里写入缓冲区的数据,如果能写完就取消监听写事件,否则就继续监听。
这里还有另一种情况,当我们打算关闭一个连接,但是他的应用层buffer里还有数据没有发送出去的时候,我们要实现先发送buffer的数据再关闭连接的功能,这里就涉及到了这一点。我们调用关闭连接的函数后,连接的状态会被设置为kDisconnecting,当handleWrite把buffer里的数据都写完了,而且连接还处于这个状态的时候,我们调用shutdownInloop,继续执行剩下的关闭操作。
void TcpConnection::handleWrite()
{
loop_->assertInLoopThread();
if (channel_->isWriting())
{
ssize_t n = sockets::write(channel_->fd(),
outputBuffer_.peek(),
outputBuffer_.readableBytes());
if (n > 0)
{
outputBuffer_.retrieve(n);
if (outputBuffer_.readableBytes() == 0)
{
channel_->disableWriting();
if (writeCompleteCallback_)
{
loop_->queueInLoop(std::bind(writeCompleteCallback_, shared_from_this()));
}
if (state_ == kDisconnecting)
{
shutdownInLoop();
}
}
}
else
{
LOG_SYSERR << "TcpConnection::handleWrite";
}
}
else
{
LOG_TRACE << "Connection fd = " << channel_->fd()
<< " is down, no more writing";
}
}