网络模式概述
在网络编程早期的时候使用 Process Per Connection,即一个进程一个连接;后面又使用更轻量的线程去替代进程,即 thread pre connection,为了解决线程不断的创建销毁的问题,使用了线程池尽可能的节约资源。但依然存在很多问题,比如一个连接如果没有可读数据,read系统调用就会阻塞,即使是使用了non-blocking,也需要一直轮询检测是否有可读数据。
I/O 多路复用这时候就派上了用场,epoll、poll、select等系统调用会阻塞直到有感兴趣的事件发生,就可以用一个线程监视所有连接。
Rector模式(one loop per thread),主循环负责监听是否有事件发生,一旦有感兴趣的事件发生,执行对应的回调函数。Reactor模式在执行回调函数的时候不能阻塞过久,否则客户端容易失去响应,一般采用非阻塞IO。
为了解决同步中出现的阻塞问题,Proactor应运而生,是一种完全的异步框架。
由于项目中使用的是多Reactor模式,就不过多阐述Proactor,但目前协程 + Proactor应该会成为一种趋势
Reactor模型实现
Reactor模式由EventLoop类实现,框架如图,主要包括以下几个类
- Channel,负责注册和响应一个文件描述符fd的感兴趣的事件,不负责关闭该fd,生命期由拥有者例如EventLoop控制。
- poller,一个EventLoop使用一个poller,通过调用底层的epoll相关函数监听关心的事件,生命期和EventLoop相同
- timers, 对定时任务的包装,包括超时时间,超时回调函数等,生命期由TimerQueue控制
- TimerQueue,定时器需要快速选择出超时任务,因此需要选择有序数据结构,又希望可以快速删除节点任务,因此这里选用了底层基于红黑树的set。
主要的类EventLoop是主体是一个循环(one loop per thead),在监测到事件发生的时候执行事件,如图;
一开始程序阻塞在Epoll类的poll成员函数,即等待关心的事件发生,一旦事件发生,执行activeChannel中的任务,即感兴趣的事件的回调函数,最后执行用户的任务。
可以想到的是,如果一直没有事件发生,那么用户任务就一直无法执行。为了解决这个问题,需要把程序从poll阻塞唤醒:关心一个自定义的fd的可读事件,当向EventLoop中添加任务的时候,写入该fd数据,这样就不会阻塞太久导致用户任务迟迟无法执行。
Tcp网络库
网络库部分TcpServer组成大致如图,包括以下类
- InetAddress,包装sockaddr_in,封装与之相关的系统调用
- Socket,包装sockfd,在析构函数中关闭fd,即利用RAII管理资源,并且封装与之相关的系统调用
- Buffer,输入输出缓冲区,在非阻塞编程中接收和发送信息必不可少。底层利用2个指针在一段内存上移动控制可读可写区域。
- TcpConnection,负责管理一个连接,一个TcpConnection活动在给定的loop中,通过设置各种读写回调函数来实现对消息的接收和发送,消息存放在inputBuffer和outputBuffer中,在接收到消息的后,把inputBuffer传给回调函数处理,在发送的时候,把发送的数据传递给outputBuffer。
- Acceptor,用于accept新TCP连接,并通过回调通知使用者。
- EventLoopThreadPool,多Reactor模型
最后TcpServer是供用户直接使用的,生命期由用户控制。通过acceptor接收新连接的时候调用回调函数产生TcpConnection。每一个连接对应一个TcpConnection实例。消息的发送和接收都通过这个实例完成。
如果只是以sockfd做为连接的标识,容易出现串话的可能性,即前一个关闭了一个连接A的sockfd,后面又马上创建了连接B,使用了和A相同的sockfd,如果在其他程序处理的时候完成了这种偷梁换柱,比如接收了A的消息,正在处理消息,准备发送消息的时候,还以为现在的B是之前的A,就把消息发给了B。
把连接直接抽象成一整个类隐含一个好处,避免串话的可能性,因为当连接结束后,这个TcpConneciton就会被析构,那么下一个连接过来的时候就会产生新的连接,之前的消息什么就都没有了,就不会出现串话的可能性
下图为一个Tcpserver产生连接发送数据到最后销毁连接的大致流程:
首先是设置线程数目,然后启动EventLoop线程池,调用Acceptor的成员函数listen进行监听,当客户端连接的时候,调用Acceptor的回调函数,在TcpServer中会产生TcpConnection实例,这个实例就代表这一条连接;之后通过EventLoop进行数据的发送和接收。在读取到0字节的时候关闭连接,Tcpserver在回调函数中移除该连接的实例,实例会最终被析构
TcpClient和TcpServer基本一致,除了connector在连接建立之后就会析构,而Acceptor生命期和Tcpserver一样。