muduo库对send的处理:
muduo库的send()函数重载了三个:
void send(const void* message, int len);
void send(const StringPiece& message); //短字符优化子string类
void send(Buffer* message); // this one will swap data
分别发送不同的类型,实际上它们有调用关系的,我们挑一种分析即可,发送字符数组:
//调用发送字符串的send,发送字符缓冲区
void TcpConnection::send(const void* data, int len)
{
send(StringPiece(static_cast<const char*>(data), len));
}
转化为发送StringPiece类型:
void TcpConnection::send(const StringPiece& message)
{
if (state_ == kConnected)
{
//如果在I/O线程,直接调用
if (loop_->isInLoopThread())
{
sendInLoop(message);
}
else
{
//如果不在,跨线程调用,需要传递缓冲区message过去,有一定的开销
loop_->runInLoop(
boost::bind(&TcpConnection::sendInLoop,
this, // FIXME
message.as_string()));
//std::forward<string>(message)));
}
}
}
可以看出如果处于已连接状态,且当前线程是I/O线程,有EventLoop,那么直接调用sendInLoop()发送,如果不是I/O线程,需要跨线程传入I/O线程,不过还是执行sendInLoop()函数,有一定的开销。
这个函数是这样实现的:void TcpConnection::sendInLoop(const StringPiece& message)
{
sendInLoop(message.data(), message.size());
}
又调用下一层:
void TcpConnection::sendInLoop(const StringPiece& message)
{
sendInLoop(message.data(), message.size());
}
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;
}
//没有关注可写事件,并且缓冲区数目为0,直接发送
// 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(boost::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;
}
}
}
}
//没有错误,并且还有未写完的数据(说明内核缓冲区满,要将未写完的数据添加到output buffer中
assert(remaining <= len);
if (!faultError && remaining > 0)
{
//如果超过高水位标志,逻辑可能有问题,回调highWaterMarkCallback,不过还是要写
size_t oldLen = outputBuffer_.readableBytes();
if (oldLen + remaining >= highWaterMark_
&& oldLen < highWaterMark_
&& highWaterMarkCallback_)
{
loop_->queueInLoop(boost::bind(highWaterMarkCallback_, shared_from_this(), oldLen + remaining));
}
//把剩余数据追加到outputbuffer,并注册POLLOUT事件
outputBuffer_.append(static_cast<const char*>(data)+nwrote, remaining);
if (!channel_->isWriting())
{
channel_->enableWriting();
}
}
}
函数有两个大情况:
情况1:当我们调用send()时,如果没有关注可写事件并且缓冲区无缓存,那我们直接立即写入。这是为什么呢?因为muduo这就是一般异步网络库的通用方法,缓存应用层传过来的数据,在I/O设备可写的情况下尽量写入数据。如果关注了POLLOUT事件,意味着我们要等待POLLOUT事件发生才能写入。如果缓冲区有数据,意味者我们必须先写完缓冲区数据,所以都不能立即写。写入之后,可能没有写完需要注册writeCompleteCallback事件(这个函数是用户注册的写完数据的回调函数)。如果写出错,且errno!=EWOULDBLOCK,也就是EAGAIN,那就表示出错了。
writeComplateCallback()函数表示数据发送完毕回调函数,即所有的用户数据都已拷贝到内核缓冲区时调用该回调函数,Buffer被清空也要调用该回调函数,可以理解为lowWaterMarkCallback()函数。上层用户只管调用conn->send()函数发送数据,网络库负责将用户数据拷贝到内核缓冲区。通常情况下编写大流量的应用程序,才需要关注writeCompleteCallback()。大流量的应用程序不断生成数据,然后send(),如果对等方接收不及时,收到通告窗口的影响,发送缓冲区会不足这个时候,就会将用户数据添加到应用层的发送缓冲区,可能会撑爆outputBuffer。解决方法就是调整发送频率,只需要关注writeCompleteCallback,当所有用户数据都拷贝到内核缓冲区后,上层用户会得到通知,就可以继续发送数据。这需要上层用户的配合。对于低流量的应用程序,通常不需要关注writeCompleteCallback()。
highWaterMarkCallback()是高水位标回调函数,outputBuffer撑到一定程度时最好回调该函数。这意味着对等方接收不及时,导致outputBuffer不断增大,很可能因为我们没有关注completeWaterMarkCallback()。在这个回调函数中用户就可以断开这个连接,避免内存不足。
讲到这里,不得不再提一下muduo库所谓的“三个半事件”:
1.连接建立:服务器accept(被动)接收连接,客户端connect(主动)发起连接。
2.连接断开:主动断开(close、shutdown),被动断开(read返回0)
3.消息到达:文件描述符可读
4.消息发送完毕:这算半个事件。对于低流量的服务,可以不关心这个事件。这里的发送完毕是数据写入操作系统缓冲区,将由TCP协议栈法则数据的发送与重传,不代表对方已经接数据。高流量服务可能导致内核缓冲区满了,数据还会追加到应用层缓冲区。
上面我们进行实际上就是最后的这“消息发送完毕”的半个事件直接写入套接字实际上就是直接写入操作系统缓冲区,然后写入数据有剩余的情况下,我们把数据追加到Buffer中。至于数据到底有没有发送到对端,这让TCP去负责,所以就是半个事件。
此外,还有一个函数,我们上面那个函数末尾进行了enableWriting,注册了关注POLLOUT事件,事件发生后会调用TcpConnection的handleWrite()函数:
//内核缓冲区有空间了,回调该函数
void TcpConnection::handleWrite()
{
loop_->assertInLoopThread();
if (channel_->isWriting()) //关注了可写事件
{
//把outputBuffer缓冲区内容写入fd
ssize_t n = sockets::write(channel_->fd(),
outputBuffer_.peek(), //返回可读下标,
outputBuffer_.readableBytes());
if (n > 0)
{
outputBuffer_.retrieve(n); //写了n个字节,移动下标n个
if (outputBuffer_.readableBytes() == 0) //如果全部写完
{
channel_->disableWriting(); //数据发送完毕,停止关闭POLLOUT事件,以免出现busy loop
if (writeCompleteCallback_) //回调writeCompleteCallback
{
//彻底写完调用writeCompleteCallback
loop_->queueInLoop(boost::bind(writeCompleteCallback_, shared_from_this()));
}
if (state_ == kDisconnecting)
{
shutdownInLoop(); //发送完毕关闭连接
}
}
}
else
{
LOG_SYSERR << "TcpConnection::handleWrite";
// if (state_ == kDisconnecting)
// {
// shutdownInLoop();
// }
}
}
else
{
LOG_TRACE << "Connection fd = " << channel_->fd()
<< " is down, no more writing";
}
}
缓冲区有空间回调handleWrite()函数,发送数据完了会关闭连接,在这里由于数据发送完毕,调用了channel_->disableWriting()函数通知管制POLLOUT,会关闭连接。
muduo库对shutdown的处理:
一般情况下shutdown()函数也不是直接关闭的:
void TcpConnection::shutdownInLoop()
{
loop_->assertInLoopThread();
if (!channel_->isWriting()) //如果不再关注POLLOUT事件,否则不能关闭,那就仅仅是将状态更改为kDisconnecting,不能关闭连接
{
// we are not writing
socket_->shutdownWrite();
}
}
如果不再关注POLLOUT事件,才立即关闭连接,调用socket的shutdown()函数。否则不会关闭。
muduo库对SIGPIPE的处理:
对一个对端已经关闭的socket调用两次write, 第二次将会生成SIGPIPE信号, 该信号默认结束进程.
具体的分析可以结合TCP的”四次握手”关闭. TCP是全双工的信道, 可以看作两条单工信道, TCP连接两端的两个端点各负责一条. 当对端调用close时, 虽然本意是关闭整个两条信道, 但本端只是收到FIN包. 按照TCP协议的语义, 表示对端只是关闭了其所负责的那一条单工信道, 仍然可以继续接收数据. 也就是说, 因为TCP协议的限制, 一个端点无法获知对端已经完全关闭.
对一个已经收到FIN包的socket调用read方法, 如果接收缓冲已空, 则返回0, 这就是常说的表示连接关闭. 但第一次对其调用write方法时, 如果发送缓冲没问题, 会返回正确写入(发送). 但发送的报文会导致对端发送RST报文, 因为对端的socket已经调用了close, 完全关闭, 既不发送, 也不接收数据. 所以, 第二次调用write方法(假设在收到RST之后), 会生成SIGPIPE信号, 导致进程退出.
服务器可是不能退出的,所以要忽略该信号。那么muduo库是怎么处理的呢?
为了避免客户端每次都需要写一个signal()函数去捕获SIGPIPE信号,muduo库利用了RAII机制,在EventLoop.cc中有这样的定义:
class IgnoreSigPipe
{
public:
IgnoreSigPipe()
{
::signal(SIGPIPE, SIG_IGN);
// LOG_TRACE << "Ignore SIGPIPE";
}
};
IgnoreSigPipe initObj;
可见,每当客户使用EventLoop时,由于该全局对象的初始化,SIGPIPE信号被自动设置为忽略了,好省事。