发送数据是要比接收数据更加麻烦的事情,因为它是一个主动发生的事情,考虑下面情况:
水平触发模式(Level-Triggered);当socket可写时,会不停的触发socket可写的事件,如何处理?
也就是说,如果发送缓冲不满,将不停触发socket可写事件,也就是说,poll/epoll调用不停返回,也就进入busy loop了。怎么解决这个问题,以前有一个回答:“写的时候不用poll/epoll管理,也就是说直接write,如果返回EAGAIN才将描述符添加到loop中”
muduo的做法就验证了上述回答,将在接下来的文章中细表。
在之前的学习中,已经知道用Channel表示一个loop中被管理的事件,不过目前只是对描述符可读进行表示:
1.TimerQueue用它来读timerfd(2)—–>定时器事件
2.EventLoop用它来读eventfd(2)——>跨线程唤醒
3.TcpServer/Acceptor用它来读listening socket——>有新连接时监听套接字描述符可读事件
4.TcpConnection用它来读普通的TCP socket——>已连接套接字收到数据时的可读事件
而对于一个writable事件来说,正如上文所说,由于level trigger的特性,可能一直触发造成busy loop,因此我们只在特定情况下管理可读事件的描述符,并且写完数据后要将可读事件移出poll/epoll,所以在Channel中需要添加:
void enableWriting();//添加writeable事件
void disableWriting();//移除writeable事件
bool isWriting();
通过上述三个函数打开和关闭写通道,并且获取当前状态,可以很好的控制防止busy loop。另外需要注意的就是打开关闭通道的时机了。
从一个示例中来看使用TcpConnection发送数据的过程。
void onConnection(const muduo::TcpConnectionPtr& conn)
{
if (conn->connected())
{
printf("onConnection(): new connection [%s] from %s\n",
conn->name().c_str(),
conn->peerAddress().toHostPort().c_str());
//发送数据
conn->send(message1);
conn->send(message2);
conn->shutdown();
}
else
{
printf("onConnection(): connection [%s] is down\n",
conn->name().c_str());
}
}
在onConnection回调中,服务器向socket写入两次数据,客户端将收到数据,下面是TcpConnection::send
的实现:
void TcpConnection::send(const std::string& message)
{
if (state_ == kConnected) {
if (loop_->isInLoopThread()) {
sendInLoop(message);//本线程直接调用
} else {
//用runInLoop进行跨线程调用
loop_->runInLoop(
boost::bind(&TcpConnection::sendInLoop, this, message));
}
}
}
再来看TcpConnection::sendInLoop
void TcpConnection::sendInLoop(const std::string& message)
{
loop_->assertInLoopThread();
ssize_t nwrote = 0;
// if no thing in output queue, try writing directly
// 发送缓冲没有内容,直接写数据。否则就不发送了,先处理outbuffer不然会造成数据乱序
// 先尝试直接写数据,如果数据没有发完再用poll去管理描述符
if (!channel_->isWriting() && outputBuffer_.readableBytes() == 0) {
nwrote = ::write(channel_->fd(), message.data(), message.size());
if (nwrote >= 0) {
if (implicit_cast(nwrote) < message.size()) {//数据没有写完
LOG_TRACE << "I am going to write more data";
}
} else {//出错
nwrote = 0;
if (errno != EWOULDBLOCK) {
LOG_SYSERR << "TcpConnection::sendInLoop";
}
}
}
assert(nwrote >= 0);
if (implicit_cast(nwrote) < message.size()) {//数据没写完发
outputBuffer_.append(message.data()+nwrote, message.size()-nwrote);//往发送缓冲添加数据
if (!channel_->isWriting()) {
//使用epoll/poll管理,将outputbuffer_里面剩余的数据管理起来
channel_->enableWriting();
}
}
}
正如本文最开始提到的解决办法,来看muduo的做法:
1.如果outputBuffer_发送缓冲没有数据时,我们直接调用wirte(2)
2.如果数据没有写完,将剩余的待写数据放到发送缓冲outputBuffer_
中,并且打开写通道enableWriting()
3.这个时候writeable事件返回,将调用Channel::writeCallback()
4.在TcpConnection的构造函数中设置回调:
//该回调当将发送事件添加到loop中并返回时调用,处理outbuffer中的数据。
channel_->setWriteCallback(
boost::bind(&TcpConnection::handleWrite, this));
再看TcpConnection::handleWrite
//发送outputBuffer_中的数据
void TcpConnection::handleWrite()
{
loop_->assertInLoopThread();
if (channel_->isWriting()) {
ssize_t n = ::write(channel_->fd(),
outputBuffer_.peek(),
outputBuffer_.readableBytes());
if (n > 0) {
outputBuffer_.retrieve(n);
if (outputBuffer_.readableBytes() == 0) {
//数据发送完毕,关闭channel否则会busyloop
channel_->disableWriting();
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";
}
}
1.Linux多线程服务端编程 使用muduo C++ 网络库
2.http://blog.csdn.net/jnu_simba/article/details/15027289
3.http://blog.csdn.net/zhangxiao93/article/details/52849349?locationNum=3&fps=1