muduo 网络库简要梳理
网络模型: one loop per thread + thread pool 的 reactor模型。
每个线程最多可以有一个自己的EventLoop,多个线程间也可以共享一个EventLoop。
使用主 EventLoop 连接线程 + 从 EventLoop工作线程 + 非阻塞IO模式。工作线程从线程池分配,如果线程池大小为0,则共用一个线程。
1 关键技术
1 每个线程最多可以有一个EventLoop,并启动一个loop事件循环。
2 EventLoop主线程职责:监听新连接、定时器、信号事件。
3 EventLoop工作线程职责:处理连接的读写操作。
4 EventLoop使用runInLoop(Func)函数,完成线程安全操作,即如果是当前 EventLoop线程Func则直接调用,如果是其他线程则加入 EventLoop 事件中。可以避免多线程操作频繁的加锁。
TcpServer处理流程
1 初始化 EventLoop ,并通过updateChannel、runAt等等增加socket、定时事件。
2 EventLoop启动loop循环, 监听事件(新的客户端连接、数据读写、定时器、信号事件等),并处理激活的事件以及悬挂其他线程的事件。
3 TcpServer 通过 accept 检测 socket新连接,并为每一个连接创建一个TcpConnectionPtr( 可能在主线程或工作线程),用来管理每个连接的读写。
如图,一个EventLoop添加两个TcpServer,也可以添加更多,Reactor模型为 单Reactor + 多工作线程。
简化类图
2 TCP新建连接
流程: 当有新连接到达,Channel::handleEvent处理请求,最终调用newConnection,TcpServer为该连接创建一个TcpConnection实例,调用connectEstablished将事件注册到Channel中。
TcpConnection是处理新连接的核心,也是muduo最核心最复杂的类,表示一次新的连接,管理该socket的所有读写消息,连接断开时该对象也随之销毁。TcpConnection生命周期比较模糊,因为用户可以持有,TcpConnection所在线程可能是TcpServer的loop,也有可能是线程池分配的loop
3 TCP断开连接
服务端半关闭机制
1 shutdown仅仅shutdownWrite,state为kConnected->kDisconnecting.
2 此时客户端会收到字节0,按正常逻辑客户端会关闭连接(潜在风险,如果客户端未关闭,则服务端一直处于半关闭状态,消耗系统资源)
3 服务端会收到字节为0的消息,此时调用handleClose完全关闭,handleClose通知TcpServer移除所持有的TcpConnectionPtr。
处理流程如下,调用removeConnectionInLoop移除TcpConnectionPtr的一个引用计数,在调用connectDestroyed将state调整kDisconnecting->kConnected,并在loop中移除该channel,handleClose()走完所有流程TcpConnection将进入析构销毁。
4 收发数据
引入二级缓冲区
对于每一个客户端连接都会分配两个Buffer。
接收缓冲区(inputBuffer):读取数据只需要关注数据是否完整,通过引入接收缓冲区解决掉粘包/拆包问题。
发送缓冲区(outputBuffer):发送数据要比读取数据困难,1 关注什么时候writable事件。 2 考虑发送速度数据高于接收数据速度,造成本地内存堆积。
muduo引入低水位WriteCompleteCallback、高水位HighWaterMarkCallback回调。发送缓冲区清空调用低水位回调,发送缓冲区超出上限调用高水位回调。
发送消息
1 判断发送缓冲区有无数据,如果没有数据且当前无消息发送,则直接调用sockets::write发送消息,否则等待。
2 发送消息后判断发送的消息是否有剩余,如果没有剩余且发送缓冲区已清空,则调用低水位回调。
3 如果有剩余,将剩余数据追加到发送缓冲区outputBuffer,使能此IOsocket可写。并判断outputBuffer总大小是否超过高水位门限,
如果超过调用高水位回调。
接收消息
1 Channel::handleEvent判断为接收消息后,TcpConnection::handleRead处理消息。
2 先将消息读入到inputBuffer,在回调给用户,这里的数据对于用户来说可能存在粘包/拆包现。
如果让用户只关心消息到达而不是数据到达,则需要引入一个中间件解决,
由于buffer设计存在预留区,可以在预留区存入消息长度通过buf->peek()获取到,从而方便的处理粘包/拆包。
5 接口
公开接口
Buffer:数据读写,
InetAddress:封装IPv4
EventLoop:事件循环(Reactor),每个线程只能有一个EventLoop实体,负责IO和定时器时间分配。
EventLoopThread:启动一个线程,在其中运行EventLoop::loop()
TcpConnection:网络库最核心最复杂的类,封装一次TCP连接,
TcpClient:网络客户端,发起连接重试功能
TcpServer:网络服务端,接收客户端连接
上述类中,TcpConnection生命周期由shared_ptr管理(即用户和库共同控制),Buffer生命周期由TcpConnection控制
其余生命周期由用户控制。
内部接口
Channel:selectable IO channel,负责注册与响应IO事件,处理IO事件。每个Channel对象只负责一个文件描述符(fd)的IO事件分发,但他不拥有这个fd。
是Acceptor、Connector、EventLoop、TimerQueue、TcpConnection的成员,生命周期由后者控制。
Socket:RAII,封装了一个file descriptor,并在析构时关闭fd,是Acceptor、TcpConnection的成员,生命期由后者控制。
SocketsOps:封装各类Sockets系统调用。
Pooler:PollPooler/EPollPooler的基类,是 EventLoop 的成员,只供EventLoop在IO线程调用,因此无需加锁,生命期由后者控制。
PollPooler/EPollPooler--只负责IO多路分发,不负责事件的分发(channel)
Connector:发起TCP连接,是TcpClient的成员,生命期由后者控制。
Acceptor:接受TCP连接,是TcpServer的成员,生命期由后者控制。
TimerQueue:用Timefd实现定时,是是EventLoop的成员,生命期由后者控制。
EventLoopThreadPool:用于创建IO线程池,用于把TcpConnection分配到某个EventLoop线程上,是TcpServer的成员,生命期由后者控制。
Buffer设计
结构:内部使用 std::vector<char> buffer_ ,易于使用,方便管理,对外表现为动态增长的连续空间,初始默认1024B。
通过readIndex,writeIndex元素,分别指向读位置与写位置(读位置结尾),将内存分为三部分(prependable,readable,writable)
数据操作 :每写一次数据,writeIndex向后滑动相应位置,每读一次数据,readIndex向后滑动响应位置。
挪移与扩容:Buffer自适应大小,当写入时writable数据不够用,先检测writable+prependable是否可用,如果可用,则先向前挪移在写入,否则扩大vector容量再写入。