简介
muduo是基于Reactor模式的网络库,其核心是个事件循环EventLoop,用于响应IO事件。muduo不考虑移植性,不跨平台,只支持Linux,只支持TCP,其网络模型为非阻塞IO + one event loop per thread。
本篇文章源码的地址为:https://github.com/freshman94/NetLib
TCP网络编程本质论
基于事件的非阻塞网络编程是编写高性能并发网络服务程序的主流模式,头一次使用这种方式编程通常需要转换思维模式。把原来“主动调用recv来接受数据,主动调用accept来接受新连接,主动调用send来发送数据”的思路转换为“注册一个收数据的回调,网络库收到数据会调用我,直接把数据提供给我。注册一个接受连接的回调,网络库接受了新连接会回调我,直接把新的连接对象传给我。需要发送数据时,只管往连接中写,网络库会负责无阻塞地发送。”
陈硕先生认为,TCP网络编程最本质的是处理三个半事件:
- 连接的建立,包括服务端接受新连接和客户端主动发起连接。TCP连接一旦建立,客户端和服务端是平等的,可以各自收发数据。
- 连接的断开,包括主动断开(close、shutdown)和被动断开(read返回0)。
- 消息到达,文件描述符可读。这是最为重要的一个事件,对它的处理方式决定了网络编程的风格(阻塞还是非阻塞,如何处理分包,应用层的缓冲如何设计等等)。
- 消息发送完毕,这算半个。这里的“发送完毕”是指将数据写入操作系统的缓冲区,将由TCP协议栈负责数据的发送与重传,不代表对方已经收到数据。
何为one event loop per thread?
首先,先简要解释一下reactor模式。Reactor模式应用于同步I/O的场景。我们以读操作为例来看看Reactor中的具体步骤:
读取操作:
-
应用程序注册读就需事件和相关联的事件处理器
-
事件分离器等待事件的发生
-
当发生读就需事件的时候,事件分离器调用第一步注册的事件处理器
-
事件处理器首先执行实际的读取操作,然后根据读取到的内容进行进一步的处理
举个例子来说明:刚开店做生意,老板为了给顾客一个美好的印象,给顾客最好的服务,一对一;这种方式类似于多线程的处理方式,一个事件到来,就会有一个线程为其服务。很显然这种方式在人少的情况下会有很好的用户体验,每个客人都感觉自己享有了最好的服务。
随着经营的生意越来越好,顾客多了,不能服务员也多吧,那样得支出的成本也太大了,得改变这种服务模式,但又不能让顾客感到这里的服务下降了,怎么办呢?这时老板想出了一个办法:当客人点菜的时候,服务员就可以去招呼其他客人了,等客人点好了菜,直接招呼一声“服务员”,马上就有个服务员过去服务。在用了这个新方法后,老板进行了一次裁员,只留了一个服务员!这就是用单个线程来做多线程的事,也就是Reactor模式,这个服务员就是一个Reactor。
上面的这种模式称为reactor + thread pool。全部的IO工作都在一个reactor线程完成,而计算任务交给线程池。如果计算任务彼此独立,而且IO的压力不大,那这种方案是非常适用的。
如果IO的压力比较大,一个Reactor处理不过来,这时可以采用reactors in threads模型。在这种模型中,有一个main Reactor负责accept连接,然后把连接挂在某个sub Reactor中,这样该连接的所有操作都在那个sub Reactor所处的线程中完成,这就是one loop per thread,每个线程中都有一个EventLoop处理连接请求。
实现中采用固定大小的Reactor pool,池子的大小通常根据CPU数目确定,这样程序的总体处理能力不会随连接数增加而下降。另外由于一个连接完全由一个线程管理,那么请求的顺序性有保证。
实现网络库
实现网络库的核心类包括:
- Channel
- Epoll
- EventLoop
- EventLoopThread
- EventLoopThreadPool
- TcpConnection
- TcpServer
Channel类
Channel是一个IO channel,负责注册与响应IO事件,它是EventLoop和TcpConnection的成员。每个Channel只属于一个EventLoop,对于TcpConnection也是一样。
Channel类的核心成员变量为:
EventLoop* loop_;
const int fd_;
__uint32_t events_;
__uint32_t revents_;
loop_就是Channel所属的EventLoop,fd_是Channel负责处理的文件描述符,会对其进行IO事件颁发,但它并不拥有这个fd_,不会在析构时关闭这个fd_。events_表示epoll关心的事件。revents_表示目前活动的事件(received events)
Channel的构造函数的声明为:
Channel(EventLoop* loop, int fd);
Channel中负责注册IO事件的代码为:
const __uint32_t Channel::NoneEvent = 0;
const __uint32_t Channel::ReadEvent = EPOLLIN | EPOLLPRI;
const __uint32_t Channel::WriteEvent = EPOLLOUT;
void enableReading() { events_ |= ReadEvent; update(); }
void disableReading() { events_ &= ~ReadEvent; update(); }
void enableWriting() { events_ |= WriteEvent; update(); }
void disableWriting() { events_ &= ~WriteEvent; update(); }
void disableAll() { events_ = NoneEvent; update(); }
void Channel::update(){
addedToLoop_ = true;
loop_->updateChannel(this);
}
update函数其内部是通过调用epoll_ctrl来控制IO事件的注册。
Channel会把不同的IO事件分发给不同的回调,注册异步回调函数的代码为:
void setReadCallback(Callback cb){ readCallback_ = std::move(cb);}
void setWriteCallback(Callback cb){ writeCallback_ = std::move(cb);}
void setCloseCallback(Callback cb){ close