目录
0.简介
1. 各个类的作用:
TcpServer
Acceptor
TcpConnection
EventLoopThreadPool
EventLoopThread
EventLoop
Poller
Channel
2. 启动
3. 新连接到来
4. 数据收发
收数据
发数据
5. 关闭连接
主动关闭
被动关闭
参考文献
最近才拜读了《Unix网络编程卷1》的前15章,书很有用,但也很枯燥。Talk is cheap, show me your code. 于是找了muduo这个网络库来研究研究。
muduo基于reactor模式, 是一个多线程的c++网络库,只支持linux上的tcp协议. 实现上,采用 "one thread, one loop" , 到处充斥着回调函数, 看习惯了就好。服务端大致类图如下:
一般运行在主线程,由用户创建和销毁。功能包括监听套接口,创建事件循环线程池,创建并保存新连接TcpConnection, 以及和用户代码交互(通过xxxCallback 函数)。
主要负责监听连接请求,调用listen() 接口时,通过Channel::enableReading() 把socket的描述符加到poller(I/O复用器)中。当有新连接到达时,先调用系统函数accept,再回调函数 newConnectionCallback_ 让TcpServer去创建连接。
这个类同时被服务端和客户端使用。建立连接后,通过Channel::enableReading() 把socket的描述符加到poller(I/O复用器)中,然后进行正常的TCP 数据收发。基本上一个TcpConnection对应一个Channel.
管理所有客户端连接的线程池,每个线程都有唯一一个事件循环。可以调用setThreadNum设置线程的数目。
事件循环线程, 包含一个Thread对象,一个EventLoop对象。在构造函数中,把EventLoopThread::threadFunc 注册到Thread对象中(线程启动时会调用EventLoopThread::threadFunc),线程启动如下:
EventLoop* EventLoopThread::startLoop()
{
assert(!thread_.started());
thread_.start(); // 这个时候loop_为NULL, 然后启动线程
{
MutexLockGuard lock(mutex_);
while (loop_ == NULL)
{
cond_.wait(); // 如果loop_为NULL,就等待, 直到线程完全启动,调用下面的threadFunc(), loop_ 被创建
}
}
return loop_;
}
void EventLoopThread::threadFunc()
{
EventLoop loop;
if (callback_)
{
callback_(&loop);
}
{
MutexLockGuard lock(mutex_);
loop_ = &loop;
cond_.notify(); // loop_创建了,通知startLoop()函数
}
loop.loop();
//assert(exiting_);
loop_ = NULL;
}
以下三个是reactor模式的核心类:
线程启动后,事件循环就开始跑起来。首先调用Poller:poll(阻塞调用) 接口查看I/O 可读/可写情况,获得所有活跃的channel, 执行每个channel的handleEvent函数,这个handleEvent最终会调用到Accepter或TcpConnection的handleRead / handleWrite, 完成真正的读或写。然后调用doPendingFunctors 按顺序执行 队列中的函数。
void EventLoop::loop()
{
assert(!looping_);
assertInLoopThread(); // loop()函数的调用者必须是EventLoop对象的创建者
looping_ = true;
quit_ = false; // FIXME: what if someone calls quit() before loop() ?
LOG_TRACE << "EventLoop " << this << " start looping";
while (!quit_) // 如果不退出,就一直循环
{
activeChannels_.clear();
pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_); // 获得所有可用的channels
++iteration_;
if (Logger::logLevel() <= Logger::TRACE)
{
printActiveChannels();
}
// TODO sort channel by priority
eventHandling_ = true;
for (ChannelList::iterator it = activeChannels_.begin();
it != activeChannels_.end(); ++it)
{
currentActiveChannel_ = *it;
currentActiveChannel_->handleEvent(pollReturnTime_); // 执行handleEvent
}
currentActiveChannel_ = NULL;
eventHandling_ = false;
doPendingFunctors(); // 执行队列中的函数
}
LOG_TRACE << "EventLoop " << this << " stop looping";
looping_ = false;
}
这是muduo中唯一的抽象类,被PollPoller和EpollPoller实现,内部分别使用Linux的poll和epoll系统函数来做I/O轮询。这个类保存着某个EventLoop中的所有channel,能够对channel 增加/修改(updateChannel()), 删除(removeChannel())。当然了,它的最主要任务是轮询所有channel的可用情况。
Channel和文件描述符一一对应,所以,Accepter用它做监听"读",TcpConnection用它监听“读” / “写”。Channel保存了所关心的文件描述符(fd_)、关注的事件,poller返回的事件,还有几个回调函数(readCallback_, writeCallback_等):
EventLoop* loop_;
const int fd_; // 文件描述符,但不负责关闭该文件描述符
int events_; // 关注的事件
int revents_; // poll/epoll返回的事件
int index_; // 被 Poller 所用,用来对channel标号
bool logHup_;
boost::weak_ptr tie_;
bool tied_;
bool eventHandling_;
bool addedToLoop_;
ReadEventCallback readCallback_; // 读回调
EventCallback writeCallback_; // 写回调
EventCallback closeCallback_; // 关闭回调
EventCallback errorCallback_; // 出错回调
Channel类对象被传到Poller类中进行poll(), 返回时,成员变量revents_会被改写,handleEvent正是根据这个revents_来执行读/写/出错 操作。
服务端启动过程如下:
在这里有必要提一下EventLoop::runInLoop()这个函数。先判断调用者是否在创建EventLoop的那个线程,若是,则直接调用Functor,否则调用queueInLoop() 放到Functor列表中,并唤醒EventLoop(因为EventLoop::loop() 有可能阻塞在poll函数)。
void EventLoop::runInLoop(const Functor& cb)
{
if (isInLoopThread())
{
cb(); // 如果在同一线程,直接执行
}
else
{
queueInLoop(cb); // 否则排队
}
}
void EventLoop::queueInLoop(const Functor& cb)
{
{
MutexLockGuard lock(mutex_);
pendingFunctors_.push_back(cb); // 加锁,把Functor加到队列中
}
if (!isInLoopThread() || callingPendingFunctors_)
{
wakeup(); // 唤醒loop()
}
}
void EventLoop::wakeup()
{
uint64_t one = 1;
// 往wakeupFd_中写个“1”,EventLoop本身一直在监控wakeupChannel_的读入事件,
// 所以能使poll() 尽快返回,以便有机会执行刚append进来的Functor
ssize_t n = sockets::write(wakeupFd_, &one, sizeof one);
if (n != sizeof one)
{
LOG_ERROR << "EventLoop::wakeup() writes " << n << " bytes instead of 8";
}
}
Channel::enableReading 把events_加上可读属性,然后调用update() , 最终通过poller来添加或更新channel.
主线程在EventLoop::loop() 中不停查询可用的I/O. 当一个新的tcp连接到来时,Channel::handleEventWithGuard 会调用Acceptor::handleRead, 然后回调TcpServer::newConnection():
void Acceptor::handleRead()
{
loop_->assertInLoopThread();
InetAddress peerAddr;
//FIXME loop until no more
int connfd = acceptSocket_.accept(&peerAddr); // 调用系统函数accept获得连接描述符
if (connfd >= 0)
{
// string hostport = peerAddr.toIpPort();
// LOG_TRACE << "Accepts of " << hostport;
if (newConnectionCallback_)
{
newConnectionCallback_(connfd, peerAddr); // 回调
}
else
{
sockets::close(connfd);
}
}
}
void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr)
{
loop_->assertInLoopThread();
EventLoop* ioLoop = threadPool_->getNextLoop(); // 取出一个事件循环对象
char buf[64];
snprintf(buf, sizeof buf, "-%s#%d", ipPort_.c_str(), nextConnId_);
++nextConnId_;
string connName = name_ + buf;
LOG_INFO << "TcpServer::newConnection [" << name_
<< "] - new connection [" << connName
<< "] from " << peerAddr.toIpPort();
InetAddress localAddr(sockets::getLocalAddr(sockfd));
// FIXME poll with zero timeout to double confirm the new connection
// FIXME use make_shared if necessary
TcpConnectionPtr conn(new TcpConnection(ioLoop,
connName,
sockfd,
localAddr,
peerAddr));
connections_[connName] = conn;
conn->setConnectionCallback(connectionCallback_); //
conn->setMessageCallback(messageCallback_);
conn->setWriteCompleteCallback(writeCompleteCallback_);
conn->setCloseCallback(
boost::bind(&TcpServer::removeConnection, this, _1)); // FIXME: unsafe
// 最后一步,调用connectEstablished
ioLoop->runInLoop(boost::bind(&TcpConnection::connectEstablished, conn));
}
void TcpConnection::connectEstablished()
{
loop_->assertInLoopThread();
assert(state_ == kConnecting);
setState(kConnected);
channel_->tie(shared_from_this());
// 每个连接对应一个channel,打开描述符的可读属性
channel_->enableReading();
connectionCallback_(shared_from_this()); // 连接成功,回调客户注册的函数
}
至此,tcp连接工作已完成,可以开始收发数据了。
收数据逻辑比较简单。Poller检测到某个channel有读事件发生时,最后会调到TcpConnection::handleRead()函数,然后从socket里读入数据到buffer,再通过回调把这些数据返回给用户层。
void TcpConnection::handleRead(Timestamp receiveTime)
{
loop_->assertInLoopThread();
int savedErrno = 0;
ssize_t n = inputBuffer_.readFd(channel_->fd(), &savedErrno); // 读数据
if (n > 0)
{
// 返回给用户处理
messageCallback_(shared_from_this(), &inputBuffer_, receiveTime);
}
else if (n == 0)
{
// 如果数据长度为0,说明对端发起了关闭请求(调用了shutdown关闭写)
handleClose();
}
else
{
errno = savedErrno;
LOG_SYSERR << "TcpConnection::handleRead";
handleError();
}
}
readFd() 函数定义如下:
ssize_t Buffer::readFd(int fd, int* savedErrno)
{
// saved an ioctl()/FIONREAD call to tell how much to read
char extrabuf[65536];
struct iovec vec[2];
const size_t writable = writableBytes();
vec[0].iov_base = begin()+writerIndex_; // 这个是base的buffer
vec[0].iov_len = writable;
vec[1].iov_base = extrabuf; // 这个是extra buffer
vec[1].iov_len = sizeof extrabuf;
// when there is enough space in this buffer, don't read into extrabuf.
// when extrabuf is used, we read 128k-1 bytes at most.
const int iovcnt = (writable < sizeof extrabuf) ? 2 : 1;
const ssize_t n = sockets::readv(fd, vec, iovcnt); // 调用系统函数readv()
if (n < 0)
{
*savedErrno = errno;
}
else if (implicit_cast(n) <= writable)
{
writerIndex_ += n;
}
else
{
writerIndex_ = buffer_.size();
append(extrabuf, n - writable);
}
return n;
}
关于readv() 的用法,可以看看 https://blog.csdn.net/iEearth/article/details/46730669
网络库在处理“socket 可读”事件的时候,必须一次性把 socket 里的数据读完(从操作系统 buffer 搬到应用层 buffer),否则会反复触发 POLLIN 事件,造成 busy-loop.
与收数据不同,发数据是主动的,用户一般可用TcpConnection::send() 来发送数据,最终会调到TcpConnection::sendInLoop().
如果数据不能一次发完,则打开channel的写事件开关,分开几次发。
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
// 如果当前channel没有写事件发生,或者发送buffer已经清空,则直接发送
if (!channel_->isWriting() && outputBuffer_.readableBytes() == 0)
{
nwrote = sockets::write(channel_->fd(), data, len); // 系统调用
if (nwrote >= 0)
{
remaining = len - nwrote; // 把没发完的data size记着
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;
}
}
}
}
assert(remaining <= len);
if (!faultError && remaining > 0)
{
// 看看发送buffer里还有多少数据没发出
size_t oldLen = outputBuffer_.readableBytes();
if (oldLen + remaining >= highWaterMark_
&& oldLen < highWaterMark_
&& highWaterMarkCallback_)
{
// 如果总共要发送的数据大于tcp的高水位,则调整一下
loop_->queueInLoop(boost::bind(highWaterMarkCallback_, shared_from_this(), oldLen + remaining));
}
// 把新数据append到发送buffer
outputBuffer_.append(static_cast(data)+nwrote, remaining);
if (!channel_->isWriting())
{
// 监听channel的可写事件(因为还有数据未发完)
channel_->enableWriting();
}
}
}
当可写事件被触发,就可以继续发送了,调用的是TcpConnection::handleWrite:
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); // 调整发送buffer的内部index,以便下次继续发送
if (outputBuffer_.readableBytes() == 0)
{
// 如果已经发完,不再关注channel的可写事件
channel_->disableWriting();
if (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";
}
}
上述函数调用一次或多次之后,数据最终会被成功发完。
直接调用TcpConnection::shutdown(), 先把当前状态设为kDisconnecting, 最后会调到TcpConnection::shutdownInLoop:
void TcpConnection::shutdownInLoop()
{
loop_->assertInLoopThread();
if (!channel_->isWriting()) // 注意,这里先判断是否发送buffer还有东西,若有,暂时不关闭,
{
// we are not writing
// 否则关闭写功能
socket_->shutdownWrite();
}
}
void sockets::shutdownWrite(int sockfd)
{
if (::shutdown(sockfd, SHUT_WR) < 0) // 系统调用
{
LOG_SYSERR << "sockets::shutdownWrite";
}
}
如果还有数据未发完,暂时不关闭。然后在前文介绍过的handleWrite()中,等数据发完了,再一次调用shutdownInLoop(), 这时候channel_->isWriting() 返回fasle,连接得以关闭。
当对端调用shutdown()关闭连接时,本端会收到一个FIN, channel的读事件被触发,但inputBuffer_.readFd() 会返回0,然后调用handleClose():
void TcpConnection::handleClose() // 此函数主要做一些清理工作
{
loop_->assertInLoopThread();
LOG_TRACE << "fd = " << channel_->fd() << " state = " << stateToString();
assert(state_ == kConnected || state_ == kDisconnecting);
// we don't close fd, leave it to dtor, so we can find leaks easily.
setState(kDisconnected);
channel_->disableAll();
TcpConnectionPtr guardThis(shared_from_this());
connectionCallback_(guardThis);
// must be the last line
closeCallback_(guardThis);
}
这里我有点想不懂:handleClose()并没有调用shutdown(), 所以本端还能继续发送数据吗?虽然sendInLoop()如果发现state_等于kDisconnected 就立刻返回,但这相当于只要某一端关闭了写,另一端最终也不能写了,这好像不太符合tcp的协议。
至此,服务端整个工作流程已经大概介绍完毕,其实还有很多很多(是的,两个“很多”)细节值得推敲,希望后续能有时间继续研究。
muduo库源码剖析(一) reactor模式
muduo库源码剖析(二) 服务端
muduo库的源代码分析1--整体架构 https://blog.csdn.net/adkada1/article/details/54342275
muduo源码分析专栏:https://blog.csdn.net/column/details/tach4.html