(一)TCP网络编程的本质:三个半事件
1.连接的建立,包括服务端接受(accept) 新连接和客户端成功发起(connect) 连接。TCP 连接一旦建立,客户端和服务端是平等的,可以各自收发数据。
2.连接的断开,包括主动断开(close 或shutdown) 和被动断开(read(2) 返回0)。
3.消息到达,文件描述符可读。这是最为重要的一个事件,对它的处理方式决定了网络编程的风格(阻塞还是非阻塞,如何处理分包,应用层的缓冲如何设计等等)。
3.5 消息发送完毕,这算半个。对于低流量的服务,可以不必关心这个事件;另外,这里“发送完毕”是指将数据写入操作系统的缓冲区,将由TCP 协议栈负责数据的发送与重传,不代表对方已经收到数据。
这其中,最主要的便是第三点: 消息到达,文件描述符可读。下面我们来仔细分析(顺便分析消息发送完毕):
(1)消息到达,文件可读:
内核接收-> 网络库可读事件触发–> 将数据从内核转至应用缓冲区(并且回调函数OnMessage根据协议判断是否是完整的数据包,如果不是立即返回)–>如果完整就取出读走、解包、处理、发送(read decode compute encode write)
(2)消息发送完毕:
应用缓冲区–>内核缓冲区(可全填)—>触发发送完成的事件,回调Onwrite。如果内核缓冲区不足以容纳数据(高流量的服务),要把数据追加到应用层发送缓冲区中内核数据发送之后,触发socket可写事件,应用层–>内核;当全发送至内核时,又会回调Onwrite(可继续写)
(二)事件循环类图
- 实心菱形是组合(强依赖),空心菱形是聚合(弱依赖)聚合与组合
- 白色类图对外可见,灰色类对外不可见
- Poller是IO复用的抽象类,他有3个纯虚函数,两个子类内部分别封装了poll和epoll,这个类也是muduo库唯一使用面向对象思想封装的类。Poller生存期由Eventloop控制
- Channel是对IO事件注册与响应的封装。他的update负责注册或更新IO可读可写等事件(Channel中的update回调Eventloop中的updateChannel,它再调Poller中的updateChannel,相当于将channel注册到Poller当中,或者说将文件描述符中可读可写事件注册到Poller中)。handleEvent函数负责对所发生的事件进行处理
- 一个Eventloop包含多个Channel(聚合关系,Eventloop不负责Channel的生存期的控制),也就是他可以用来捕捉多个通道的可读可写事件。Channel不拥有文件描述符,Channel对象销毁的时候不close(fd_)。fd_关联FileDescriptor, 一个Eventloop有多个FileDescriptor,一个Channel对应一个FileDescriptor。FileDescriptor是被Socket所拥有的,FileDescriptor的生存期由Socket来控制
- TcpConnection, Acceptor(被动,关注监听套接字的可读事件)和Connetcor(主动,关注监听套接字的可写和错误事件)包含Channel,且他们控制Channel的生存周期。Channel中的handleEvent回调handleRead和handlewrite…
- TcpServer和TcpClient分别包含一个Acceptor和Connector。1个TcpServer或TcpClient包含多个TcpConnection, 且他们不负责控制TcpConnection生命周期
时序图:
(三)函数梳理
-
创建事件通知描述符eventfd函数使用了flag: EFD_CLOEXEC,使用FD_CLOEXEC实现close-on-exec,关闭子进程无用文件描述符
-
Evevtloop.h最后一个变量std::vector pendingFunctors_;比较不好理解,它是一个任务容器,存放的是将要执行的回调函数。准备这么一个容器的原因(主要为了线程安全),以及runInLoop和queueInLoop函数是如何把事件添加到这个变量里的,eventfd如何通知IO线程来处理pendingFunctors_里的函数的参考muduo网络库学习(四)事件驱动循环EventLoop介绍的很详细!
以下内容参考muduo网络库学习笔记(11):有用的runInLoop()函数
runloop函数:
代码片段1:EventLoop::runInLoop()
文件名:EventLoop.cc
// 在IO线程中执行某个回调函数,该函数可以跨线程调用
void EventLoop::runInLoop(const Functor& cb)
{
if (isInLoopThread())
{
// 如果是当前IO线程调用runInLoop,则同步调用cb
cb();
}
else
{
// 如果是其它线程调用runInLoop,则异步地将cb添加到队列
queueInLoop(cb);
}
}
queueinloop函数:
代码片段2:EventLoop::queueInLoop()
文件名:EventLoop.cc
void EventLoop::queueInLoop(const Functor& cb)
{
// 把任务加入到队列可能同时被多个线程调用,需要加锁
{
MutexLockGuard lock(mutex_);
pendingFunctors_.push_back(cb);
}
// 将cb放入队列后,我们还需要在必要的时候唤醒IO线程来处理
// 必要的时候有两种情况:
// 1.如果调用queueInLoop()的不是IO线程,需要唤醒
// 2.如果在IO线程调用queueInLoop(),且此时正在调用pending functor,需要唤醒
// 即只有在IO线程的事件回调中调用queueInLoop()才无需唤醒
if (!isInLoopThread() || callingPendingFunctors_)
{
wakeup();
}
}
I.第一种情况易理解:调用queueInLoop的线程不是当前IO线程时,则需要唤醒当前IO线程,
才能及时执行doPendingFunctors()。
II.第二种情况,调用queueInLoop()的线程是当前IO线程,比如在doPendingFunctors()中执行
functors[i]() 时又调用了queueInLoop()。此时doPendingFunctors() 执行functors[i]()
过程中又添加了任务,故循环回去到poll的时候需要被唤醒返回,进而继续执行doPendingFunctors()。
loop函数
代码片段3:EventLoop::loop()部分
文件名:EventLoop.cc
while (!quit_)
{
activeChannels_.clear();
pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
for (ChannelList::iterator it = activeChannels_.begin();
it != activeChannels_.end(); ++it)
{
currentActiveChannel_ = *it;
currentActiveChannel_->handleEvent(pollReturnTime_);
}
// 执行pending Functors_中的任务回调
// 这种设计使得IO线程也能执行一些计算任务,避免了IO线程在不忙时长期阻塞在IO multiplexing调用中
doPendingFunctors();
}
eventfd的好处也可见这篇文章最后(优于pipe和socketpair)