本文旨在讲述muduo网络库数据的发送流程。
首先明确一点,Muduo中的epoll使用了水平触发方式,而不是边缘触发。
TcpConnection首先需要增加应用层的缓冲区
Buffer outputBuffer_;
我们看到在TcpConnection类内,提供了两个数据发送函数,分别是TcpConnection::send,TcpConnection::sendInLoop。前者也是调用后者,保证数据的发送在IO线程内的EventLoop执行。(本博客系列都是基于Muduo的示例教程代码分析,个人认为这样更能理解整个网络库的整体流程)
void TcpConnection::send(const std::string& message)
{
if (state_ == kConnected) {
if (loop_->isInLoopThread()) {
sendInLoop(message);
} else {
loop_->runInLoop(
boost::bind(&TcpConnection::sendInLoop, this, message));
}
}
}
void TcpConnection::sendInLoop(const std::string& message)
{
loop_->assertInLoopThread();
ssize_t nwrote = 0;
// if no thing in output queue, try writing directly
//如果channel没有在写,并且buffer没有数据
if (!channel_->isWriting() && outputBuffer_.readableBytes() == 0) {
//直接先尽力写
nwrote = ::write(channel_->fd(), message.data(), message.size());
if (nwrote >= 0) {
//如果写的长度小于消息长度
if (implicit_cast<size_t>(nwrote) < message.size()) {
LOG_TRACE << "I am going to write more data";
} else if (writeCompleteCallback_) {
//如果写完了,回调writeCompleteCallback_
loop_->queueInLoop(
boost::bind(writeCompleteCallback_, shared_from_this()));
}
} else {
nwrote = 0;
if (errno != EWOULDBLOCK) {
LOG_SYSERR << "TcpConnection::sendInLoop";
}
}
}
assert(nwrote >= 0);
//如果消息没有写完,那么将消息添加到Buffer中
if (implicit_cast<size_t>(nwrote) < message.size()) {
outputBuffer_.append(message.data()+nwrote, message.size()-nwrote);
if (!channel_->isWriting()) {
channel_->enableWriting();
}
}
}
我们可以看到sendInLoop()会先尝试直接发送数据,如果一次发送完毕启用WriteCallback;如果只发送了部分数据,则把剩余的数据放入outputBuffer_,并开始关注writable事件,以后在handlerWrite()中发送剩余的数据。
channel::enableWriting的代码如下,通知poller关心本channel的可读事件
void enableWriting() { events_ |= kWriteEvent; update(); }
上面我们只是写了一部分的数据,剩下来的数据什么时候写呢?当socket变得可写时,Channel会调用TcpConnection::handleWrite(),这里我们继续发送outputBuffer_中的数据。 这里socket可写,主要是指发送缓冲区可写字节数,低于低水位标志。
我们来看看TcpConnection::handleWrite(),当可读时,就将Buffer中数据写入,如果写完了,需要关闭对套接字读事件的关心(因为是水平触发,如果不关闭,将一直触发)。然后执行writeCompleteCallback_。在TcpConnection::sendInLoop中,当数据一次发送完成后,也调用了writeCompleteCallback_。这个就是用户的写完数据的回调函数。
void TcpConnection::handleWrite()
{
loop_->assertInLoopThread();
//如果是可写状态
if (channel_->isWriting()) {
//将Buffer中数据写入
ssize_t n = ::write(channel_->fd(),
outputBuffer_.peek(),
outputBuffer_.readableBytes());
//如果写成功
if (n > 0) {
outputBuffer_.retrieve(n);
if (outputBuffer_.readableBytes() == 0) {
channel_->disableWriting();
if (writeCompleteCallback_) {
loop_->queueInLoop(
boost::bind(writeCompleteCallback_, shared_from_this()));
}
if (state_ == kDisconnecting) {
shutdownInLoop();
}
} else { //如果一次没有写完,下次还需要继续写
LOG_TRACE << "I am going to write more data";
}
} else {
LOG_SYSERR << "TcpConnection::handleWrite";
}
} else {
LOG_TRACE << "Connection is down, no more writing";
}
}
网络程序中,数据的发送可能过快,在本地堆积,所以Muduo为了解决数据发送的困难,提供了两个回调函数,分别是writeCompleteCallback_和HighWaterMarkCallback_,可以称为低水位回调和高水位回调。如果当前缓冲区中数据大小超出上限值,那么调用HighWaterMarkCallback_。高水位回调将停止从用户接收数据。WriteCompleteCallback函数为发送缓冲区为空时调用,在这个函数重启开启接收数据。
最后总结发送流程
1、调用TcpConnection::send
2、调用TcpConnection::sendInLoop
1、如果目前可以发送(channel没有在写,并且缓冲区没有数据),先尝试发送
2、如果没有完全发送,将数据添加到buffer中,添加对channel对应的socket写事件关心,等待socket可写,然后调用 handlewrite
3、如果发送完全,调用用户回调函数
3、如果没有发送完全,并且socket可读,调用HandleWrite。继续发送,如果发送完全,就取消对socket可写事件关系。如果没有发送完全,等待下一次的发送。
本文详细分析了Muduo网络库中TcpConnection的发送数据流程,包括使用水平触发的epoll,send函数的工作机制,以及如何在socket可写时通过handleWrite()继续发送数据。Muduo通过writeCompleteCallback_和HighWaterMarkCallback_解决数据发送过快的问题,确保网络程序的稳定运行。

4295

被折叠的 条评论
为什么被折叠?



