C++11muduo网络库——NetWings
https://github.com/Kiseshi111/NetWings
本项目是参考 muduo 实现的基于 Reactor 模型的多线程网络库。使用 C++ 11 编写去除 muduo 对 boost 的依赖, 它采用非阻塞IO模型,基于事件驱动和回调,是一个oneloop per thread + threadpool框架, 大量使用了智能指针,bind,function等技术,对socket和epoll进行了封装,使其便于使用。
目录
- 一、概述
- 二、线程
- 三、主线
一、概述
1、Multi-Reactor概述
-
muduo网路库架构采用的是Reactor模式,而且将该模式分为多个类,并支持线程池实现多线程并发处理。
-
Reactor是一种服务器设计模式,事件处理模型,事件驱动的反应堆模式。
-
反应堆就是:事件来了就执行,但可能有很多类型的事件,所以需要提前注册好不同类型的事件处理函数以便事件到来时方便执行。
-
事件到来则由
epoll_wait
获取同时到来的多个事件,然后根据不同的类型将事件分发给不同的事件处理器,就是提前注册的事件处理函数 -
Reactor作为事件驱动的模式,是通过到来的事件来自动调用相应函数的,函数是由Reactor去调用的,而不是在主函数中调用的,所以大量运用了回调函数。
-
该模式的核心思想为将所有要处理的IO事件注册到一个IO多路复用器上(通常为epoll系统调用,我们也只使用epoll),同时主线程阻塞在多路复用器上,如
epoll_wait
,一旦有IO事件就绪或到来,则返回并将注册好的相应的IO事件分到对应的处理器上。 -
简略流程:
1.注册事件和对应事件处理器
2.多路复用器等待事件到来
3.事件到来,事件分发器分发事件到处理器
4.事件处理器处理事件,然后注册新的事件
2、Multi-Reactor架构的三大核心模块介绍
2.1 概述
- muduo库有三个核心组件支撑一个Reactor实现持续的监听一组fd,并根据每个fd上发生的事件调用相应的处理函数。
- 三个组件分别是Channel类,Poller,EpollPoller类和EventLoop类。
2.2 Channel类
2.2.1 Channel类概述
- Channel类其实相当于一个文件描述符fd的保姆
- 作为一个IO事件分发器,封装了发生IO事件的文件描述符,关心的事件类型,以及事件相应的回调函数。
- Channel对fd和事件进行了一层封装。平常我们写网络编程的相关函数,基本就是创建套接字,绑定地址,转变为可监听状态,然后接受连接。
- 但是得到了一个初始化好的socket还不够,我们需要监听这个socket上的事件并且处理事件的。
- 比如Reactor模型中使用了epoll监听该socket上的事件,我们还需要将需要被监视的套接字和监视的事件注册到epoll对象中。
- 而Channel类将文件描述符fd和感兴趣的事件event(需要监听的事件)封装到了一起,而事件监听相关的代码放到了Poller和EpollPoller类中。
- Channel扮演了一个IO事件分发器的作用,Acceptor中的Channel主要处理连接事件,而每个TcpConnection中会有一个Channel,来检测fd的可读,关闭,错误消息等。触发相应的回调函数。
- 一般网络库会把监听的socket单独放在一个线程,然后连接到来的多个socket放在其他几个线程,所以就有了两个创建Channel的地方。
- 一个是在Acceptor构造中创建用于监听socket的通道,一个是新连接到来,TcpConnection中创建的Channel。构造函数中传入了当前所属的事件循环和文件fd。
- 既然在两个地方创建了Channel,所以回调的设置也不同,Acceptor中Channel回调指向了Acceptor的handle(处理)函数,进而指向了TcpServer的newConnetion,而TcpConnection中的Channel则是回调到了TcpConnection中。
acceptChannel.setReadCallback(std::bind(&Acceptor::handleRead, ......)); channel->setReadCallback(std::bind(&TcpConnection::handleRead, ......)); channel->setWriteCallback(std::bind(&TcpConnection::handleWrite, ......)); channel->setCloseCallback(std::bind(&TcpConnection::handleClose, ......)); channel->setErrorCallback(std::bind(&TcpConnection::handleError, ......));
2.2.2 Channel重要成员变量
const int fd
:这个Channel对象管理的文件描述符,Poller监听的对象。int events
:fd所感兴趣的事件类型的集合int revents
:事件监听器实际监听到的该fd发生的事件类型的集合,当事件监听器监听到了一个fd发生了什么事件,通过set_revents来设置revent的值。static const int NoneEvent
:事件状态,相当于封装了epoll_ctl。EventLoop *loop
:这个fd属于哪个EventLoop对象。ReadEventCallback readcallback
:各种事件发生时的回调函数。std::weak_ptr<void> tie
:bool tied
:解决TcpConnection和Channel生命周期问题。
2.2.3 Channel重要方法
void setReadCallback()
:向Channel对象注册各类事件的回调函数- 一个fd在发生可读可写这类操作时,就需要调用相应处理函数来处理。
- 外部通过调用这类函数就可以将事件处理函数放进Channel类中,当需要调用的时候就可以直接拿出来调用了。
void enableReading()
:设置fd相应的事件状态(ReadEvent),相当于epoll_ctl。- 外部通过这几个函数来告知Channel你所监管的fd都对哪些事件类型感兴趣,并把这个fd及其感兴趣的事件注册到事件监听器(IO多路复用模块)上。
- 这些函数中都有update方法,本质上就是调用了epoll_ctl();
void setRevents()
:当事件监听器监听到某个fd发生了什么事件,通过这个函数可以将这个fd实际(real)发生的事件封装进这个Channel中。void handleEvent()
:当调用了epoll_wait后,可以得知事件监听器上哪些Channel(fd)发生了哪些事件,继而要调用Channel对应的处理函数。- 此函数让每个发生了事件的Channel调用自己保管的事件处理函数。
- 每个Channel会根据自己fd实际发生的事件(revents)和感兴趣的事件(events)来选择调用readcallback这类函数。
void handleEventWithGuard()
:真正的事件处理函数。void tie()
:防止当channel被手动remove掉,channel还在执行回调操作,成员变量weak_ptr<void> tie
作为引用参数传入。void update()
:通过channel所属的EventLoop,调用poller的相应方法,注册fd的events事件。
2.3 Poller/EpollPoller类
2.3.1 Poller/EpollPoller概述
- 负责监听文件描述符事件是否触发以及返回发生事件的文件描述符以及具体事件的模块就是Poller。
- 所以一个Poller对象对应一个事件监听器,在Multi-Reactor模型中,有多少Reactor就有多少Poller。
- muduo提供了epoll和poll两种IO多路复用的方法来实现事件监听,不过我们只使用epoll。
- 调用一次poll方法就能给你返回事件监听器的监听结果(发生事件的fd及其发生的事件)
- 这个Poller是个抽象虚类,由EpollPoller和PollPoller继承实现,与监听文件描述符和返回监听结果的具体方法也基本上是在这两个派生类中实现。
- EpollPoller就是封装了用epoll方法实现的与事件监听有关的各种方法。
- PollPoller则是封装了poll方法实现的与事件监听有关的各种方法。我们不使用。
2.3.2 Poller重要成员变量
ChannelMap channels:
类型为unordered_map
,负责记录文件描述符到Channel的映射,管理了所有注册在Poller上的Channel。EventLoop *ownerLoop
:所属的EventLoop对象。
2.3.3 Poller重要方法
- 所有IO复用保留统一接口
virtual Timestamp poll();
virtual void updateChannel();
virtual void removeChannel();
2.3.4 EpollPoller重要成员变量
int epoll_fd
:就是用epoll_create方法返回的epoll句柄。EventList events
:用于存放epoll_wait返回的所有发生事件的文件描述符。
2.3.5 EpollPoller重要重要方法
- 重写基类抽象方法
Timestamp poll()
:- 这个函数作为Poller的核心,当外部调用poll方法的时候,该方法底层是通过epoll_wait来获取这个事件监听起上发生事件的fd及其对应发生的事件。
- 我们自动每个fd都是由一个Channel封装的,通过map channels可以根据fd找到封装这个fd的Channel。
- 将事件监听器监听到该fd发生的事件写进这个Channel中的revents成员变量中,然后把这个Channel装进activeChannels中。
- 这样,当外界调用完poll之后就能拿到事件监听器的监听结果,也就是activeChannels中的成员(活跃事件或者说监听到的事件的fd),以及每个fd都发生了什么事件。
void updateChannel()
:通过channel的update函数传递到EventLoop中,再传递到Poller中。其中调用了update()
。void removeChannel()
:同上。
void fillActiveChannels()
:填写活跃的连接,即监听到的连接。void update()
:更新channel通道,就是调用epoll_ctl,add/mod/del。
2.4 EventLoop类
2.4.1 EventLoop概述
- 作为一个网络服务器,需要有持续监听,持续获取监听结果,持续处理监听结果对应的事件的能力;
- 也就是我们需要循环的去调用poll方法来获取实际发生事件的Channel集合,然后调用这些Channel里保管的不同类型事件的处理函数(调用HandlerEvent方法)。
- EventLoop就是负责实现循环,负责驱动循环的模块,Channel和Poller相当于EventLoop的手下,EventLoop整合封装了两者并向上提供了更方便的接口来使用。
2.4.2 概览Poller,Channel,EventLoop在Reactor架构中的角色
- EventLoop起到一个驱动循环的功能,Poller负责从事件监听器上获取监听结果。
- 而Channel类则在其中起到了将fd及其相关属性封装的作用,将fd及其感兴趣的事件和发生的事件以及不同事件的回调函数封装在一起,这样在各个模块中传递更加方便。
2.4.3 One Loop Per Thread概述
- 每一个EventLoop都绑定了一个线程,每一个线程负责循环监听一组fd的集合。
- 每个EventLoop对象都是一个reactor,所以每个EventLoop必然存在于子线程,那么我们的主线程如何在接受到事件连接后把accept到的fd传递给子线程呢?
- 把线程所属的EventLoop对象的指针传过去,然后注册回调。
- 综上EventLoop实际上就是一个管理类,它管理子线程内的连接,即channel对象和IO多路复用对象。那它一定也有一个进行事件循环的函数,来进行正常的事件循环。
2.4.4 EventLoop重要成员变量
std::atomic_bool looping
:标识loop是否运行。std::atomic_bool quit
:标识loop是否退出。const pid_t threadId
:标识当前EventLoop的所属id。Timestamp pollRetureTime
:Poller返回发生事件的Channel的时间点int wakeupFd
:当mainLoop获取一个新用户的Channel需通过该成员变量唤醒subLoop处理Channel。ChannelList activeChannels
:返回Poller检测到当前有事发生的所有Channel列表。std::atomic_bool callingPendingFunctors
:标识当前loop是否有需要执行的回调操作std::vector<Functor> pendingFunctors
:存储loop需要执行的所有回调操作。
2.4.5 EventLoop重要方法
void loop()
:- 每个EventLoop对象都唯一绑定了一个线程,这个线程其实一直执行这个函数里面的while循环,这个while循环的大致逻辑就是调用poll方法获取事件监听器上的监听结果。
- 接下来在loop里就会调用监听结果中每一个Channel的处理函数HandlerEvent。每一个Channel的处理函数会根据Channel类中封装的实际发生的事件,执行Channel类中封装的各事件处理函数。
- EventLoop的主要功能就是持续循环的获取监听结果并且根据结果调用处理函数。
void quit()
:退出事件循环。void runInLoop()
:在当前loop中执行。void queueInLoop()
:把上层注册的回调函数cb放入队列中,唤醒loop所在的线程执行cb。void wakeup()
:通过eventfd唤醒loop所在的线程。- EventLoop的方法通知Poller的方法
void updateChannel()
:void removeChannel()
:
void handleRead()
:给eventfd返回的文件描述符wakeupFd绑定的事件回调,当wakeup()时,即有事件发生,调用handleRead()读wakeupFd,同时唤醒阻塞的epoll_wait。void doPendingFunctors()
:执行上层回调。
3、其他类
3.1 Acceptor类
- 接受新用户的连接并分发连接给SubReactor(SubEventLoop)。
3.1.1 Acceptor概述
- Acceptor封装了服务器监听套接字fd以及相关处理方法,主要是对其他类的方法调用进行了封装。
3.1.2 Acceptor重要成员变量
Socket acceptSocket:
服务器监听套接字的文件描述符。Channel acceptChannel
:把acceptSocket及其感兴趣的事件和事件对应的处理函数都封装进去。EventLoop *loop
:监听套接字的fd由这个EventLoop负责循环监听以及处理相应事件。一般是主EventLoop。NewConnectionCallback newConnectionCallback
:TcpServer构造函数中将newConnection函数注册给了这个成员变量。- 这个newConnection函数的功能是公平的选择一个subEventLoop,并把已经接受的连接分发给这个subEventLoop
3.1.3 Acceptor重要成员方法
void listen()
:该函数底层调用了linux的函数listen,开启对acceptSocket的监听,同时将acceptChannel及其感兴趣的事件(可读事件)注册到主EventLoop的事件监听器上。- 就是让主EventLoop事件监听器去监听acceptSocket。
void handleRead()
:这是一个私有方法,这个方法是要注册到acceptChannel上的,同时handleRead方法内部还调用了成员变量newConnectionCallback保存的函数。- 当主EventLoop监听到acceptChannel上发生了可读事件时(新用户连接事件),就是调用这个handleRead方法。
- 当监听的fd有事件发生了,该函数就接受新连接,并且以负载均衡的方式选择一个subEventLoop,并把这个新连接分发到这个subEventLoop上。
void setNewConnectionCallback()
:设置新连接的回调函数。
3.2 Socket类
Socket类是对socket文件描述符的封装,也包括一些对socket操作的工具函数。
3.2.1 Socket成员变量
int socketFd
:服务器监听套接字的文件描述符。
3.2.2 Socket方法
int fd()
:获取文件描述符。void bindAddress()
:调用bind绑定服务器IP端口。void listen()
:监听套接字。int accept()
:调用accept接受新用户连接请求。void setTcpNoDelay()
:调用setsockopt来设置一些socket选项。void shutdownWrite()
:使用shutdown设置SHUT_WR,关闭写端。
3.3 TcpConnection类
3.3.1 TcpConnection概述
- 这个类主要封装了一个已建立的TCP连接,以及控制该TCP连接的方法(连接建立/关闭/销毁),以及该连接发生的各种事件(读/写/错误/连接)对应的处理函数,和这个TCP连接的服务端和客户端的套接字地址信息等。
- Acceptor用于mainEventLoop中,对服务器监听套接字fd及其相关方法进行封装(监听/接受连接/分发连接给subEventLoop)
- TcpConnection用于subEventLoop中,对服务器监听套接字fd及其相关方法进行封装(读消息事件/发送消息事件/连接关闭事件/错误事件)。
3.3.2 TcpConnection重要变量
std::unique_ptr<Socket> socket
:用于保存已连接套接字的文件描述符。std::unique_ptr<Channel> channel
:封装了socket及其各类事件的处理函数(读/写/错误/关闭等事件处理函数 )。这个Channel保存的各类事件的处理函数是在TcpConnection对象的构造函数中注册的。EventLoop *loop
:该Tcp连接的Channel注册到了哪一个subEventLoop上,这个loop就是哪一个subEventLoop。Buffer inputBuffer
:Buffer类,是该TCP连接对应的用户接收缓冲区。Buffer outputBuffer
:Buffer类,用于暂存那些暂时发送不出去的待发送数据。因为TCP发送缓冲区是有大小限制的,若到达高水位线,则无法把发送的数据通过send直接拷贝到TCP发送缓冲区,而是暂存在这个outputBuffer中,等TCP发送缓冲区有空间了,触发可写事件了,再把outputBuffer中的数据拷贝到TCP发送缓冲区中。enum state
:标识了当前TCP连接的状态- connected:已连接
- connecting:正在连接
- disconnecting:正在断开连接
- disconnected:已经断开连接
ConnectionCallback connectionCallback
:用户会自定义连接建立关闭后的处理函数,收到消息后的处理函数,消息发送完毕后的处理函数,连接关闭后的处理函数,然后这些函数会注册给相应成员变量保存。
3.3.3 TcpConnection重要成员方法
- 在一个已经建立好的TCP连接上主要会发生四类事件:可读事件,可写事件,连接关闭事件,错误事件。
- 当事件监听器监听到一个连接发生了以上的事件,那么就会在EventLoop中调用这些事件对应的处理函数。
void handleRead()
:负责处理TCP连接的可读事件,它会将客户端发送来的数据拷贝到用户缓冲区中(intputBuffer),然后再调用connectionCallback保存的连接建立后的处理函数。void handleWrite()
:负责处理TCP连接的可写事件。void handleClose()
:负责处理TCP连接的关闭事件,就是将这个TcpConnection对象中的channel从事件监听器中移除。然后调用connectionCallback和closeCallback保存的回调函数。void handleError()
:负责处理TCP连接的错误事件。
void send()
:发送数据。void shutdown()
:关闭连接。void connectEstablished()
:连接建立。void connectDestroyed()
:连接销毁。void setState()
:设置连接状态。void sendInLoop()
:当前Loop线程中发送数据。void shutdownInLoop()
:关闭当前Loop线程写端
3.4 Buffer类
3.4.1 Buffer类概述
Buffer类其实就是封装了一个用户缓冲区,以及向这个缓冲区读写数据等一系列控制方法。
3.4.2 为什么要有缓冲区
- 非阻塞网络编程中应用层buffer是必须的,非阻塞IO的核心思想是避免阻塞在read或write或其他IO系统调用上,这样可以最大限度复用thread-of-control(控制线程),让一个线程能服务于多个socket连接。
- IO线程只能阻塞在IO复用函数上,如
select/poll/epoll_wait
。这样一来,应用层的缓冲就是必须的,每个Tcp socket都要有inputBuffer和outputBuffer
- IO线程只能阻塞在IO复用函数上,如
- TcpConnection必须要有outputBuffer:使程序在write操作上不会产生阻塞,当write操作后,操作系统一次性没有接受完时,网络库把剩余数据放入outputBuffer中,然后注册POLLOUT事件,一旦socket变得可写,则立刻调用write进行写入数据。即应用层buffer到操作系统buffer。
- TcpConnection也必须要有intputBuffer:当发送方send数据后,接受方收到的数据不一定是完整的数据,网络库在处理socket可读事件的时候,必须一次性把socket里的数据读完,否则会反复触发POLLIN事件,造成busy-loop
- 当连接到达文件描述符上限时,此时没有可供你保存新连接套接字的文件描述符了,那么新来的连接就会一直放在accept队列中,于是其可读事件就会一直触发读事件,因为一直不读,也没办法读,这就是busy-loop
- 所以网络库为了应对数据不完整的情况,收到的数据先放到inputBuffer里。即操作系统buffer到应用层buffer。
3.4.3 Buffer类设计
-
这个类用两个游标标记了可读缓冲区和可写缓冲区(空闲)的起始位置。
-
初始状态
readIndex
和writeIndex
在同一位置(8),kCheapPrepend
定义prependable
初始大小,kInitalSize
定义writable
初始大小 -
若向Buffer写入200字节则
writeIndex
游标则会向后移动。 -
若从BUffer中读入了50字节,
readIndex
则会向后移动50字节,writeIndex
保持不变。
- 若一次性读完全部数据(
writeIndex
-readIndex
),则两游标都返回原位以备新一轮使用。
-
Buffer可以自动增长,初始值为1024,在写入字节超过最大长度时,会进行扩容。
比如一次性写入1000字节,若writeable(可写缓冲区)不够,则需加上
readIndex
-kCheapPrepare
(已读缓冲区)进行判断。若依然不够,则进行扩容。此时
readIndex
游标将回到初始位置(8),而writeIndex
的位置则是readable
(新可读缓冲区,即前可读缓冲区150+ 新写入长度1000)+ 8 -
在将这1150字节可读缓冲区读完后,
readIndex
和writeIndex
依然返回kCheapPrepend
(8),size则是保持在了1158,writeable
可写缓冲区为1150,这样就实现了一次扩容。
3.4.4 为什么要预留prepareable
- ** 什么是粘包**
- 首先我们要了解一下粘包这个概念:
- 客户端发送的多个数据包被当作一个数据包接收,就称作粘包,也称数据的无边界性,read/recv函数不知道数据包的开始或结束标志,只把它们当作连续的数据流来处理。
- 而UDP则是把内容一个个的发送过去,不管客户端能否完全接收到内容,它都会发送,而内容大于设定大小时,多余部分会被丢掉。
- 所以只有TCP才会出现粘包,UDP则不会出现粘包。
- 为什么产生粘包
- 主要原因:TCP称为流失协议,数据流会杂糅在一起,接收端不清楚每个消息的界限,不知道每次应该读取多少字节的数据。
- 次要原因:TCP为了提高传输效率会有一个nagle优化算法,当多次send的数据字节都非常少,nagle算法就会将这些数据合并成一个TCP段再发送,这就无形中产生了粘包。
- 如何解决粘包
- 这里就提到prepareable字段的作用了。
- 它为Buffer提供了一个8字节的预留空间,可以用作存放数据包的字节长度,以便接收端判断数据包的边界。
3.4.5 Buffer成员变量
static const size_t kCheapPrepend
:预留区大小。static const size_t kInitialSize
:初始化buffer大小。std::vector<char> buffer
:buffer缓冲区。size_t readerIndex
:可读区起始位置。size_t writerIndex
:可写区起始位置。
3.4.6 Buffer重要成员方法
void append()
:将data数据添加到缓冲区size_t retrieveAsString()
:获取缓冲区中长度为len的数据,并以string返回。size_t retrieveAllString()
:获取缓冲区所有数据,并以string返回。void ensureWritableBytes()
:在向缓冲区写入长度为len的数据之前,先调用此函数,会检查你的缓冲区可写空间是否能装下长度为len的数据,如果不能,则动态扩容。size_t readFd()
:客户端发来数据,readFd从该Tcp接收缓冲区中将数据读出来并放到Buffer中。size_t writeFd()
:服务端要向这条Tcp连接发送数据,通过该方法将Buffer中的数据拷贝到Tcp发送缓冲区中。void makeSpace()
:扩容操作。size_t readableBytes()
:获取可读区域字节数。size_t writableBytes()
:获取可写区域字节数。size_t prependableBytes()
:获取预留区域字节数。
3.5 TcpServer类
- TcpServer主要的功能是管理新连接到来时创建的TcpConnection,是直接提供给用户使用的类。
3.5.1 TcpServer重要成员变量
EventLoop *loop
:mainEventLoop中的baseloop,用户自定义的loop。std::unique_ptr<Acceptor> acceptor
:Acceptor对象,用于监听新连接事件。std::shared_ptr<EventLoopThreadPool>
:线程池对象。ConnectionCallback connectionCallback
:各种类型事件的回调函数。ConnectionMap connections
:保存所有连接的Map。
3.5.2 TcpServer重要成员函数
void setThreadInitCallback()
:设置各种类型事件的回调函数。void setThreadNum()
:设置底层subLoop的个数。void start()
:开启服务器监听。void newConnection()
:当有新用户连接时,acceptor会执行找个回调操作,负责将mainLoop接收到的请求连接通过回调轮询分发给subLoop去处理。void removeConnection()
:移除连接。
4、工具类
4.1 noncopyable不可拷贝基类
- 将拷贝构造函数和拷贝赋值函数用delete修饰
4.2 Thread线程类
- 对线程的封装,使用的是pthread
4.2.1 Thread成员变量
bool started
:线程开启标识。bool joined
:线程退出标识。std::shared_ptr<std::thread> thread
:线程类型智能指针。pid_t tid
:线程号。ThreadFunc func
:线程回调函数。std::string name
:线程名。static std::atomic_int numCreated
:线程序号。
4.2.2 Thread成员函数
void start()
:开启线程。void join()
:终止线程。bool started()
:返回线程是否开启。pid_t tid()
:返回线程号。const std::string &name()
:返回线程名。static int numCreated()
:返回线程序号。void setDefaultName()
:设置默认线程名。
4.3 EventLoopThread事件循环线程类
- 封装了当前EventLoop所对应的线程。
4.3.1 EventLoopThread重要成员变量
EventLoop *loop
:当前线程对应的事件循环。Thread thread
:当前线程。ThreadInitCallback callback
:保存线程初始化回调函数。
4.3.2 EventLoopThread成员方法
EventLoop *startLoop()
:启用底层线程Thread类对象thread中的start()创建线程,然后绑定事件循环,返回新线程中的EventLoop指针。void threadFunc()
:该方法在单独的新线程里运行,先创建一个独立的EventLoop对象,然后执行EventLoop的loop(),这样就开启了底层Poller的poll()。
4.4 EventLoopThreadPool线程池类
- 线程池负责管理subEventLoop。
- 负责对线程的创建,结束,可以用轮询的方式获取线程。
4.4.1 EventLoopThreadPool重要成员变量
EventLoop *baseloop
:mainLoop对应的事件循环。std::vector<std::unique_ptr<EventLoopThread>> threads
:线程集合。std::vector<EventLoop *> loops
:事件循环集合。
4.4.2 EventLoopThreadPool重要成员方法
EventLoop *getNextLoop()
:以轮询方式分配Channel给subLoop,返回获取到的loop。void start()
:开启线程池,初始创建mainEventLoop对应的线程,以及加入后续创建的subEventLoop对应的线程。
4.5 Timestamp时间戳类
- 封装当前时间以及格式化输出。
4.6 CurrentThread当前线程类
- 该类获取线程标识,即tid
4.7 InetAddress地址类
- 封装socket地址类型。
二、线程
1、One Loop Pre Thread
- One Loop Pre Thread的含义就是,一个EventLoop和一个线程唯一绑定,
被这个EventLoop管辖的一切操作都必须在这个EventLoop绑定的线程中执行。
- 比如负责新连接建立的操作都要在mainEventLoop线程中运行,已建立的连接分发到某个subEventLoop上,这个已建立连接的任何操作,都必须在这个subEventLoop线程上运行,不能到别的subEventLoop去执行。
1.1 eventfd()
- eventfd是一种系统调用,可以用来实现事件通知。
- eventfd包含一个64位无符号整型计数器,创建eventfd时会返回一个文件描述符,进程可以通过这个文件描述符进行read/write来读取或改变计数器的值,从而实现进程间通信。
#include<sys/eventfd.h> int eventfd(unsigned int initval, int flags);
initval
:创建eventfd时,64位计数器的初始值。flags
:eventfd文件描述符的标志。EFD_CLOEXEC
:返回的eventfd文件描述符在fork后exec其他程序时会自动关闭这个文件描述符。EFD_NONBLOCK
:设置返回的eventfd为非阻塞。EFD_SEMAPHORE
:将eventfd作为一个信号量来使用
1.1.1 read
- 读取计数器的值
- 如果计数器中的值大于0
- 设置了
EFD_SEMAPHORE
标志位,则返回1,且计数器中的值减去1。 - 没有设置
EFD_SEMAPHORE
标志位,则返回计数器中的值,且计数器设置为0。
- 设置了
- 如果计数器中的值为0
- 设置了
EFD_NONBLOCK
标志位就直接返回-1。 - 没有设置
EFD_NONBLOCK
标志位就会一直阻塞直到计数器中的值大于0。
- 设置了
1.1.2 write
- 向计数器中写入值
- 如果写入的值和小于
0xFFFFFFFFFFFFFFFE
,则写入成功。 - 如果写入的值和大于
0xFFFFFFFFFFFFFFFE
- 设置了
EFD_NONBLOCK
标志位就直接返回-1 - 如果没有设置
EFD_NONBLOCK
标志位,则会一直阻塞直到read操作执行。
- 设置了
1.2 保证EventLoop和线程唯一绑定
1.2.1 __thread线程局部存储
- __thread变量在每一个线程都会有一份独立实体,各个线程的值互不干扰。
__thread EventLoop *t_loopInThisThread = nullptr;
- 因为一般全局变量都是被同一个进程中的多个线程共享,但是我们不希望共享。
- 在EventLoop对象的构造函数中,如果当前线程没有绑定EventLoop对象,那么
t_loopInThisThread
为nullptr,然后就让该指针变量指向EventLoop对象的地址。 - 如果
t_loopInThisThread
不为nulptr,说明当前线程已经绑定了一个EventLoop对象了,这时候EventLoop对象构造失败。EventLoop::EventLoop() : wakeupFd(createEventfd()) , wakeupChannel(new Channel(this, wakeupFd)) , ...... { if (t_loopInThisThread) { LOG_FATAL("Another EventLoop %p exists in this thread %d\n", t_loopInThisThread, threadId); } else { t_loopInThisThread = this; } ...... }
1.3 EventLoop线程只执行自己的操作
- 比如负责新连接建立的操作都要在mainEventLoop线程中运行,已建立的连接分发到某个subEventLoop上,这个已建立连接的任何操作,都必须在这个subEventLoop线程上运行,不能到别的subEventLoop去执行。
1.3.1 EventLoop构造
createEventfd()
返回一个eventfd文件描述符,并且该文件描述符设置为非阻塞和子进程不拷贝模式。该eventfd文件描述符赋给了EventLoop对象的成员变量wakeupFd。- 然后将wakeupFd用Channel封装起来,得到wakeupChannel。
int createEventfd() { int evtfd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC); if (evtfd < 0) { LOG_FATAL("eventfd error:%d\n", errno); } return evtfd; }
- 在EventLoop构造函数的内部
// 给Channel注册一个读事件处理函数 wakeupChannel->setReadCallback( std::bind(&EventLoop::handleRead, this)); // 然后将wakeupChannel注册到事件监听器上监听其可读事件,当监听器监听到wakeupChannel的可读事件时就会调用EventLoop::handleRead()函数 wakeupChannel->enableReading();
1.3.2 Loop执行
- 当mainEventLoop接受一个新连接请求,并把新连接封装成一个TcpConnection对象,并且希望在subEventLoop线程中执行
TcpConnection::connectEstablished()
函数。 - 那我们怎么在mainEventLoop线程中通知subEventLoop线程来执行该函数(即执行分派任务)。
EventLoop::runInLooo()
函数接受一个可调用的函数对象Functor cb
,如果当前cpu正在运行的线程就是该EventLoop对象绑定的线程,那么就同步调用cb回调函数,否则就说明这是跨线程调用,需要异步将cb传给queueInLoop()
函数。void EventLoop::runInLoop(Functor cb) { // 在当前EventLoop中执行回调 if (isInLoopThread()) { cb(); } // 在非当前EventLoop线程中执行cb,就需要唤醒EventLoop所在线程执行cb else { queueInLoop(cb); } }
queueInLoop()
函数其实就是主线程用来给子线程传递回调的。- 我们希望这个cb能在某个EventLoop对象所绑定的线程(subEventLoop)上运行,所以把cb这个可调用对象保存在EventLoop对象的
pendingFunctors
这个数组中。void EventLoop::queueInLoop(Functor cb) { { std::unique_lock<std::mutex> lock(mutex); pendingFunctors.emplace_back(cb); } // callingPendingFunctors为true的意思是,如果EventLoop正在处理当前的PendingFunctors函数时有新的回调函数加入,我们也要继续唤醒loop所在线程,如果不唤醒,那么新加入的函数就不会得到处理,会因为下一轮的epoll_wait而继续阻塞住。 if (!isInLoopThread() || callingPendingFunctors) { // 唤醒Eventloop所在线程 wakeup(); } }
- 在wakeup函数中,向wakeupFd写数据,这样会触发EventLoop读事件,当前loop线程就会被唤醒,epoll_wait就会返回,EventLoop::loop中阻塞的情况被打断。
void EventLoop::wakeup() { uint64_t one = 1; ssize_t n = write(wakeupFd, &one, sizeof(one)); ...... }
EventLoop::loop()
肯定是运行在其所绑定的EventLoop线程中,在该函数内会调用doPendingFunctors()
函数,该函数就是把其所绑定的EventLoop对象中的pendingFunctors数组中保存的可调用对象拿出来执行。void EventLoop::loop() { looping = true; quit = false; while (!quit) { ...... doPendingFunctors(); } looping = false; }
三、主线
- TCP网络编程的本质其实是处理三个半事件:
- 连接的建立:服务器被动接收连接(accept)和客户端主动发起连接(connect)。
- 连接的断开:包括主动断开(close,shutdown)和被动断开(read返回0)。
- 消息到达:文件描述符可读。
- 消息发送完毕:算半个,发送完毕是指数据写入操作系统缓冲区(内核缓冲区),将由TCP协议栈负责数据的发送与重传。
- 不代表对方已经收到数据。
1、Echo服务器
- 要实现网络库的代码,首先要知道该库对外是怎么提供服务的。
class EchoServer { public: EchoServer(......) { // 将用户自定义连接事件处理函数注册进TcpServer里,TcpServer发生连接事件时会执行该函数 server.setConnectionCallback(......); server.setMessageCallback(......); // 设置合适的subLoop(subReactor)线程数量 server.setThreadNum(......); } void start() { server.start(); } private: // 用户自定义连接事件处理函数 // 连接建立或断开的回调函数 void onConnection(......) { if (//新连接建立请求) { printf("Connection UP"); } //关闭连接请求 else { printf("Connection DOWN"); } } // 可读写事件回调 void onMessage(......) { ...... } EventLoop *loop; TcpServer server; }; int main() { // mainEventLoop主事件循环,负责循环监听处理新用户连接事件的事件循环器 EventLoop loop; // InetAddress是对sockaddr_in进行封装 InetAddress addr(......); // 构造回显服务器对象 EchoServer server(&loop, addr, "EchoServer"); // 启动回显服务器 server.start(); // 开启事件循环 loop.loop(); return 0; }
- 建立事件循环器EventLoop:
EventLoop loop;
- 建立服务器对象:
TcpServer server;
- 向TcpServer注册各类事件的用户自定义处理函数:
setConnectionCallback;
- 启动server:
server.start();
- 开启事件循环:
loop.loop();
- 建立事件循环器EventLoop:
2、连接的建立
2.0 用户开始使用
- 用户首先在回显服务器上自定义连接事件的处理函数
onConnection
void onConnection(const TcpConnectionPtr &conn) { ...... }
2.0.1 TcpServer::setConnectionCallback()
ConnectionCallback
是一个通过TcpConnectionPtr
封装的回调类型。using ConnectionCallback = std::function<void(const TcpConnectionPtr &)>;
- 所以可以在TcpServer中为用户自定义连接事件设置回调函数。
void setConnectionCallback(const ConnectionCallback &cb) { connectionCallback = cb; }
2.1 TcpServer::TcpServer()构造
- 当我们创建一个TcpServer对象时,TcpServer构造函数做的最主要的事就是在类的内部实例化了一个Acceptor对象,并往这个Acceptor对象注册了一个回调函数
TcpServer::newConnection()
。TcpServer::TcpServer(......) : acceptor(new Acceptor(......)) , ...... { acceptor->setNewConnectionCallback( std::bind(&TcpServer::newConnection, ......)); }
2.1.1 TcpServer::newConnection()
- 该函数的功能就是将建立好的连接进行封装,封装成一个TcpConnection对象。
- 有一个新用户连接,Acceptor会执行这个回调,负责将mainLoop接收到的请求连接通过回调分发给subLoop去处理
void TcpServer::newConnection(......) { // 轮询算法 选择一个subLoop 来管理connfd对应的channel EventLoop *ioLoop = threadPool->getNextLoop(); // 生成一条TCP连接对象,这个TCP连接对象封装了该TCP连接即将分发给哪一个subEventLoop,即ioLoop。 TcpConnectionPtr conn(new TcpConnection(......)); // 下面的回调都是用户设置给TcpServer中的TcpConnection的 conn->setConnectionCallback(connectionCallback); conn->setMessageCallback(messageCallback); conn->setWriteCompleteCallback(writeCompleteCallback); conn->setCloseCallback( std::bind(&TcpServer::removeConnection, ......)); // 调用TcpConnection::connectEstablished()将TcpConnection::channel注册到刚刚选择的subEventLoop上 ioLoop->runInLoop( std::bind(&TcpConnection::connectEstablished, ......)); }
2.1.2 TcpConnection::connectEstablisher()
- 此函数是在subEventLoop线程中执行的。
void TcpConnection::connectEstablished(......) { // 设置这个TcpConnection的状态为已连接 setState(......); channel->tie(shared_from_this()); //channel封装的是建立好连接的客户端fd,将客户端fd及其可读事件注册到subEventLoop的事件监听器上 channel->enableReading(); // 新连接建立,调用用户提供的连接事件处理函数 connectionCallback(shared_from_this()); }
2.2 Acceptor::Acceptor()构造
- 当我们在TcpServer构造函数实例化Acceptor对象时,Acceptor的构造函数中实例化了一个Channel对象,即acceptChannel。
Acceptor::Acceptor(......) : acceptChannel(......) , ...... { ...... acceptChannel.setReadCallback( std::bind(&Acceptor::handleRead, ......)); }
- 该Channel对象封装了服务器监听套接字文件描述符,但是尚未注册到mainEventLoop的事件监听器上。
- 接着Acceptor构造函数将
Acceptor::handleRead()
方法注册进acceptChannel中,这就意味着,日后如果事件监听器监听到acceptChannel发生可读事件,将会调用Acceptor::handleRead()
函数。
2.2.1 Acceptor::handleRead()
- 当acceptChannel发生可读事件发生时,即新用户连接时,Acceptor会执行这个
handleRead()
函数。 - 在TcpServer构造函数中就已经通过
acceptor->setNewConnectionCallback
设置过回调函数。class Acceptor : noncopyable { public: ...... void setNewConnectionCallback(const NewConnectionCallback &cb) { NewConnectionCallback_ = cb; } ...... };
void Acceptor::handleRead() { InetAddress peerAddr; //调用linux函数accept()接受新客户连接 int connfd = acceptSocket.accept(&peerAddr); ...... //NewConnectionCallback实际指向TcpServer::newConnection NewConnectionCallback(connfd, peerAddr); ...... }
2.3 整体流程
2.3.1 两条代码主线
💜用户自定义处理函数
- 2.0 用户开始使用:自定义连接事件处理函数
onConnectionn
。 - 2.0.1
TcpServer::setConnectionCallback()
:TcpServer中设置用户自定义连接事件onConnection的回调函数。 - 2.1.1
TcpServer::newConnection()
:为新连接分发好subEventLoop后,给该连接设置回调函数。 - 2.1.2
TcpConnection::connectEstablisher()
:在连接建立好后,执行回调。
💚TcpServer构造
- 2.1
TcpServer::TcpServer()
:Acceptor对象通过设置新连接回调函数将TcpServer::newConnection
绑定。- 2.2
Acceptor::Acceptor()
:TcpServer构造后,Acceptor构造函数将Acceptor::handleRead()
读回调注册进acceptorChannel。 - 2.1.1
TcpServer::newConnection()
:在分配好subEventLoop后,执行该事件循环对应的TcpConnection连接的connectEstablished
函数。
- 2.2
- 2.2.1
Acceptor::handleRead()
:当事件监听器监听到acceptorChannel发生读事件,则会调用handleRead()读回调。
2.3.2 主要流程
- 在TcpServer对象创建完毕后,用户调用
TcpServer::start()
方法,开启TcpServer。void TcpServer::start() { // 启动底层的loop线程池 threadPool->start(threadInitCallback); loop->runInLoop(std::bind(&Acceptor::listen, ......)); }
- 主要调用了
Acceptor::listen()
函数(底层Linux函数listen)来监听服务器套接字。void Acceptor::listen() { listenning = true; acceptSocket.listen(); acceptChannel.enableReading(); }
- 以及将 acceptor注册到mainEventLoop的事件监听器上监听它的可读事件(新用户连接事件)。
- 接着调用loop,循环获取事件监听器的监听结果,并根据监听结果调用注册在事件监听器上的Channel对象的事件处理函数。
3、消息读取
3.1 新连接初始化
- 在连接建立完成,并且mainEventLoop接受新连接请求之后,这条Tcp连接就被封装成了TcpConnection对象,主要封装了连接套接字的fd(socket),连接套接字的channel等。
- 在TcpConnection构造时,下面四个方法会被注册进这个channel里。
//handleRead前半部分处理读取消息逻辑,后半部分通过调用messageCallback处理读消息后的逻辑 void handleRead(Timestamp receiveTime); //handleWrite前半部分处理写消息逻辑,当一条消息完整写入Tcp发送缓冲区中时,则调用writeCompleteCallback void handleWrite(); //handleClose前半部分处理连接关闭逻辑,后半部分调用connectionCallback和closeCallback void handleClose(); //handleError处理连接错误事件 void handleError();
- 在TcpConnection中,通过
set......Callback
方法来设置相应事件的回调函数。//上层分别对应了用户自定义的 连接后/关闭后事件处理函数onConnection void setConnectionCallback(const ConnectionCallback &cb) { connectionCallback = cb; } // 接收到消息后的函数onMessage void setMessageCallback(const MessageCallback &cb) { messageCallback = cb; } // 写完后的事件处理函数onWriteComplete void setWriteCompleteCallback(const WriteCompleteCallback &cb) { writeCompleteCallback = cb; } // 处理关闭连接的函数(网络库提供)removeConnection void setCloseCallback(const CloseCallback &cb) { closeCallback = cb; } // 这些回调TcpServer也有 用户通过写入TcpServer注册,TcpServer再将注册的回调传递给TcpConnection,TcpConnection再将回调注册到Channel中 ConnectionCallback connectionCallback; MessageCallback messageCallback; WriteCompleteCallback writeCompleteCallback; CloseCallback closeCallback;
- 当TcpConnection对象建立完毕后,mainEventLoop的Acceptor会将这个TcpConnection对象中的channel注册到某一个subEventLoop中。
3.2 EventLoop循环监听
- 在TcpConnection对象中,已经封装好了套接字的fd以及channel。而这个channel会被当前subEventLoop事件循环中的Poller对象加入
ChannelList
监听集合进行事件监听。//vector中的每一个Channel封装着 //一个fd //fd感兴趣的事件 //事件监听器监听到该fd实际发生的事件 using ChannelList = std::vector<Channel *>;
- 当Channel有事件发生时,Poller会调用
poll
方法,也就是底层epoll_wait
获取事件监听结果,来调用每一个发生事件的Channel的handleEvent事件处理函数。 - 该方法会根据每一个Channel的感兴趣事件以及实际发生的事件调用提前注册在Channel内的对应的事件处理函数。
void Channel::handleEvent(......) { ...... // 读 if (revents & (EPOLLIN | EPOLLPRI)) { if (readCallback) { readCallback(receiveTime); } } // 写 if (revents & EPOLLOUT) { if (writeCallback) { writeCallback(); } } //关闭 if ((revents & EPOLLHUP) && !(revents_ & EPOLLIN)) { if (closeCallback) { closeCallback(); } } // 错误 if (revents & EPOLLERR) { if (errorCallback) { errorCallback(); } } }
3.3 消息读取处理
- handleEvent中的
readCallback
事件处理函数其实保存的是TcpConnection::handleRead
。 - 该方法首先调用
Buffer.readFd
,底层为linux函数readv实现。将Tcp接受缓冲区数据拷贝到用户定义的inputBuffer缓冲区中。void TcpConnection::handleRead(......) { ...... ssize_t n = inputBuffer.readFd(channel->fd(), ......); // 有数据到达 if (n > 0) { // 表明已建立连接的用户有可读事件发生了,调用用户传入的回调操作onMessage messageCallback(shared_from_this(), &inputBuffer, receiveTime); } else if (n == 0) { handleClose(); } else { ...... handleError(); } }
3.3.1 Buffer::readFd()
- Buffer缓冲区是有大小的,但是从fd上读取数据的时候,却不知道数据的最终大小。如果一次性读出来可能导致Buffer装不下而溢出。
- 通过readFd的设计能够让用户一次性把所有TCP缓冲区的所有数据全部读出来并放到用户自定义的缓冲区Buffer中。
ssize_t Buffer::readFd(int fd, ......) { // 栈额外空间,用于从套接字往出读时,当buffer暂时不够用时暂存数据,待buffer重新分配足够空间后,在把数据交换给buffer。 char extrabuf[65536] = {0}; /* struct iovec { ptr_t iov_base; // iov_base指向的缓冲区存放的是readv所接收的数据或是writev将要发送的数据 size_t iov_len; // iov_len在各种情况下分别确定了接收的最大长度以及实际写入的长度 }; */ // 使用iovec分配两个连续的缓冲区 struct iovec vec[2]; // 这是Buffer底层缓冲区剩余的可写空间大小,不一定能完全存储从fd读出的数据 const size_t writable = writableBytes(); // 第一块缓冲区,指向可写空间 vec[0].iov_base = begin() + writerIndex; vec[0].iov_len = writable; // 第二块缓冲区,指向栈空间 vec[1].iov_base = extrabuf; vec[1].iov_len = sizeof(extrabuf); //如果第一个缓冲区writable大于等于64K,那就只采用一个缓冲区,而不使用栈空间extrabuf的内容 const int iovcnt = (writable < sizeof(extrabuf)) ? 2 : 1; const ssize_t n = readv(fd, vec, iovcnt); ...... // Buffer的可写缓冲区已经够存储读出来的数据了 else if (n <= writable) { writerIndex += n; } // extrabuf里面也写入了n-writable长度的数据 else { writerIndex = buffer.size(); // 对buffer扩容 并将extrabuf存储的另一部分数据追加至buffer append(extrabuf, n - writable); } return n; }
- extrabuf是在栈上开辟的空间,函数结束后会自动释放,而且速度也比在堆上开辟要快。
4、消息发送
- 当用户调用了
TcpConnection::send(buf)
函数时,相当于要求网络库把buf数据发送给该Tcp连接的客户端。TcpConnection::send(buf)
内部实际是调用了linux函数write。- 如果Tcp发送缓冲区能一次性容纳buf,那这个write函数将buf数据全部拷贝到发送缓冲区中。
- 如果Tcp发送缓冲区不能一次性容纳buf,这时候write函数将buf数据尽可能的拷贝到Tcp发送缓冲区中,并将errno设置为EWOULDBLOCK。
- 剩余没拷贝到Tcp发送缓冲区中的buf数据会被放在
TcpConnection::outputBuffer
中,并且向事件监听器上注册该TcpConnection::channel
的可写事件。 - 事件监听器监听到该Tcp连接的可写事件,就会调用
TcpConnection::handleWrite
函数把TcpConnection::outputBuffer
中的剩余数据发送出去。 - 在
TcpConnection::handleWrite
函数中,通过调用Buffer::writeFd
函数将outputBuffer的数据写入到Tcp发送缓冲区。void TcpConnection::handleWrite() { //监听到可写事件 if (channel->isWriting()) { //将outputBuffer中readable部分写入Tcp发送缓冲区 ssize_t n = outputBuffer.writeFd(channel->fd(), ......); ...... //若完全写完 if (outputBuffer.readableBytes() == 0) { channel->disableWriting(); ...... } } ...... }
- 如果Tcp发送缓冲区不能完全容纳全部剩余的未发送数据,那就尽可能的将数据拷贝到Tcp发送缓冲区中,结束该函数,继续保持可写事件的监听。一次次调用handleWrite,直到数据写完。
- 当数据全部拷贝到Tcp发送缓冲区之后,就会调用用户自定义的写完后的事件处理函数 ,并且移除该TcpConnection在事件监听器上的可写事件。
//写完成 if (writeCompleteCallback) { // TcpConnection对象在其所在的subloop中,加入用户自定义回调函数 loop->queueInLoop( std::bind(writeCompleteCallback, shared_from_this())); } if (state == Disconnecting) { // 在当前所属的loop中把TcpConnection删除掉 shutdownInLoop(); }
5、连接断开
5.1 连接被动断开
- 当
TcpConnection::handleRead
函数内部的readv返回0时,就说明没有数据可读或读到了文件尾部,服务端就知道客户端断开连接了。 - 接着就调用
TcpConnection::handleClose
。void TcpConnection::handleClose() { ...... setState(Disconnected); //将channel从事件监听器上移除 channel->disableAll(); TcpConnectionPtr connPtr(shared_from_this()); // 执行用户自定义的连接事件处理函数 connectionCallback(connPtr); // 执行关闭连接的回调,执行的是TcpServer::removeConnection回调方法 closeCallback(connPtr); }
5.1.1 TcpServer::removeConnection
- 和handleClose一样运行在subEventLoop中
void TcpServer::removeConnection(const TcpConnectionPtr &conn) { loop->runInLoop( std::bind(&TcpServer::removeConnectionInLoop , ......); }
5.1.2 TcpServer::removeConnectionInLoop
- 该函数运行在mainEventLoop中,主要从TcpServer对象中删除某条数据.
- TcpServer对象中有一个connections成员变量,这是一个
unordered_map
,负责保存Tcp连接的名字到TcpConnection对象的映射。 - 因为这个Tcp连接要关闭了,所以也要把这个TcpConnection对象从connections中删除,然后再调用
TcpConnection::connectDestroyed
函数。void TcpServer::removeConnectionInLoop(const TcpConnectionPtr &conn) { ...... connections.erase(conn->name()); EventLoop *ioLoop = conn->getLoop(); //在该TcpConnection所属的subEvenntLoop中执行connectDestroyed ioLoop->queueInLoop( std::bind(&TcpConnection::connectDestroyed, conn)); }
5.1.3 TcpConnection::connectDestroyed
- 该函数的执行又跳回到subEventLoop线程中,将Tcp连接的监听描述符从事件监听器中移除。
- 另外subEventLoop中的Poller类对象还保存着这条Tcp连接的channel,所以调用
channel.remove
将这个Tcp连接的channel对象从Poller内删除。void TcpConnection::connectDestroyed() { if (state == Connected) { setState(Disconnected); 把channel的所有感兴趣的事件从poller中删除掉 channel->disableAll(); connectionCallback(shared_from_this()); } 把channel从poller中删除掉 channel->remove(); }
5.2 服务器主动关闭导致连接断开
5.2.1 TcpServer::~TcpServer
- 当服务器主动关闭时,调用
TcpServer::~TcpServer
析构函数。 - 不断循环的让这个TcpConnetion对象所属的subEventLoop线程执行
TcpConnection::connectDestroyed
函数。 - 同时在mainEventLoop的TcpServer::~TcpServer函数中调用
item.second.reset
释放保管TcpConnection对象的共享智能指针,以达到释放TcpConnection对象的堆内存空间的目的。TcpServer::~TcpServer() { for(auto &item : connections) { TcpConnectionPtr conn(item.second); //当conn出了其作用域,即可释放智能指针指向的TcpConnection对象,即引用计数减为0,这个TcpConnection对象的堆内存就会被释放。 item.second.reset(); // 销毁连接 conn->getLoop()->runInLoop( std::bind(&TcpConnection::connectDestroyed, conn)); } }
- TcpServer::connections是一个
unordered_map<string,TcpConnectionPtr>
,其中TcpConnectionPtr是一个指向TcpConnection的shared_ptr。using ConnectionMap = std::unordered_map<std::string, TcpConnectionPtr>; ...... using TcpConnectionPtr = std::shared_ptr<TcpConnection>;
- 一开始,每一个TcpConnection对象都被一个共享智能指针TcpConnectionPtr持有,当执行了
TcpConnectionPtr conn(item.second)
时,这个TcpConnection对象就被conn和这个item.second共同持有,但是这个conn的生存周期很短,只要离开了当前for循环,conn就会被释放。 - 接着调用item.second.reset释放掉TcpServer中保存的该TcpConnection对象的智能指针,但是还剩conn依然持有这个TcpConnection对象,因此当前TcpConnection对象还不会被析构。
- 接下来调用
conn->getLoop()->runInLoop(function)
,这个function函数会在loop绑定的线程,也就是subEventLoop上执行connectDestroyed。
5.2.2 TcpConnection::connectDestroyed
- 在创建TcpConnection对象时,Acceptor都要将这个对象分发给subEventLoop来管理,这个TcpConnection对象的一切函数都要在其管理的subEventLoop线程中运行。
- 所以connectDestroyed方法必须在这个TcpConnection对象所属的subEventLoop绑定的线程中执行。
- mainEventLoop线程将当前for循环跑完后,共享智能指针conn离开代码块,因此被析构,但是TcpConnection对象还不会被释放,因为还有一个共享智能指针指向这个TcpConnection对象(item.second),我们看不到这个智能指针,这个智能指针在connectDestroyed中,是一个隐式的this。
- 当这个函数执行完后,智能指针就彻底被释放了。至此,就没有任何智能指针指向这个TcpConnection对象了,TcpConnection对象就被彻底析构了。
5.2.3 TcpConnection数据未发完
- 我们需要在触发TcpConnection关闭机制后,让TcpConnection先把数据发送完再关闭。
- 先了解一下
shared_from_this
是什么。enable_shared_from_this
是一个模板类template<class T> class enable_shared_from_this;
- 我们继承
enable_shared_from_this
的目的,就是使用shared_from_this
函数shared_ptr<T> shared_from_this();
- 把当前类对象作为参数传给其他函数时,如果直接传递this指针,我们将不会得知调用者会如何使用。
- 若是在类内用
shared_ptr<TcpConnection> p1(this);
的方式显示初始化一个指向自身的智能指针,外部的其他智能指针并不知情,他们不共享引用计数, - 在类外这样构造的智能指针,不知道类内部有相同的指针,会造成两个非共享的shared_ptr指向一个对象,所以会造成两次析构。
shared_ptr<TcpConnection> x(new TcpConnection);
- 我们在类内使用shared_from_this就不会出现这样的情况
shared_ptr<TcpConnection> p2 = shared_from_this();
- 在
TcpConnection::connectEstablished
中,我们有这样一段代码。connectionCallback(shared_from_this());
- 该函数返回一个shared_ptr。
using ConnectionCallback = std::function<void(const TcpConnectionPtr &)>; ...... using TcpConnectionPtr = std::shared_ptr<TcpConnection>;
- 接下来这个shared_ptr就作为Channel的
Channel::tie()
函数的参数。std::weak_ptr<void> tie; ...... //tie可以解决TcpConnection和Channel的生命周期时长问题,从而保证了Channel对象能够在TcpConnection销毁前销毁 void Channel::tie(const std::shared_ptr<void> &obj) { tie = obj; tied = true; }
- 当事件监听器返回监听结果,就要对每一个发生事件的channel对象调用他们的handleEvent函数。
- 在这个函数中会先把tie这个weak_ptr提升为强共享智能指针,这个强共享智能指针会指向当前的TcpConnection对象,就算外面调用删除析构了所有指向该TcpConnection对象的智能指针,只要
handleEventWithGuard
函数没执行完,这个TcpConnection对象都不会被析构释放堆内存。void Channel::handleEvent(Timestamp receiveTime) { if (tied) { std::shared_ptr<void> guard = tie.lock(); if (guard) { handleEventWithGuard(receiveTime); } // 如果提升失败了 就不做任何处理 说明Channel的TcpConnection对象已经不存在了 } else { handleEventWithGuard(receiveTime); } }
- 而
handleEventWithGuard
函数里就有负责处理消息发送事件的逻辑,当handleEventWithGuard
函数调用完毕,这个guard智能指针就会被释放。