天天看点

tars服务端(二):网络io模型和线程模型

一.网络io模型

tars服务端的网络io模型是用epoll(边缘触发)做io多路复用 + 非阻塞socket,即Reactor。

如果把线程也纳入考量,那么

  • 有多个网络线程,每个网络线程都有自己的epoll实例,即每个网络线程一个Reactor
  • 多线程与io:很关键的一点,每个socket fd的read,write,close都只由同一个网络线程操作;这样做的好处很明显,避免了很多竞争条件
  • 由于每个fd只由一个网络线程处理,所以网络线程中不能出现耗时比较久的操作(例如长时间的阻塞),否则将阻塞该线程其他的fd事件

总结起来,每个网络线程都是一个基于epoll的循环:

tars服务端(二):网络io模型和线程模型

另外,tars还将epoll监听的文件描述符划分为几种类型:

enum                                                                                                                   
 {                                                                                                                      
     ET_LISTEN = 1,     //socket listening                                                                                                 
     ET_CLOSE  = 2,     //暂时没用                                                                                                
     ET_NOTIFY = 3,     //handle线程通知网络线程用的                                                                                                
     ET_NET    = 0,     //socket read,write                                                                                                        
 };             
           

Reactor模式还需要注意:

  1. 怎么保证不漏掉io事件?
  2. write到一半,tcp发送队列满了,要怎么处理?不可能一直在原地等待重试

我们以单个网络线程,TCP链接为例来看一下整个io流程:

tars服务端(二):网络io模型和线程模型
  • 主线程调用NetThread::bind和NetThread::createEpoll创建socket和epoll。之后网络io部分都在网络线程的run()中执行
  • 灰色部分,由于边缘触发非阻塞模式,在进行accept,read和send时都需要由一个循环包住,直到返回EAGAIN为止,防止漏掉IO事件(解决上面提到的第1个问题)
  • 绿色部分,在第一次把socket fd注册到epoll时,就可以同时注册EPOLLIN,EPOLLOUT,而且之后无需取消这两个事件。因为是边缘触发模式,所以不用担心会频繁触发EPOLLIN,EPOLLOUT事件。
  • 蓝色部分,在accept,read,send结束后,又回到_epoller.wait继续等待网络io事件
  • 黄色部分,在accept到一个新链接之后,都会创建一个Connection实例来接管这个链接的读写。Connection还有一个重要的功能,当该链接的socket发送队列已满(即send返回EAGAIN),把未发送出去的包缓存在发送缓存_sendbuffer中(解决了上面提到的第2个问题)
  • 发送的逻辑比较复杂:
  1. 首先需要判断Connection的发送缓存是否为空。如果为空,则直接write;如果不为空,不能直接写,原因是会打破数据顺序(缓存里的数据应先发送),而且此时socket很大概率也还是处于不可写状态,所以应该把发送包push到缓存,等待socket可写;
  2. 当epoll监听到socket可写时,如果缓存区有未发送的数据,则把缓存发送完;否则回到_epoller.wait()
  • 此流程没有把socket的close画出来,关闭链接主要有几种情况:(1)read返回0,对端关闭;(2)read,write出错 (3)服务端主动关闭。在关闭链接的时候,还需要考虑如果缓存中还有数据没发出去,应该怎么处理?在链接管理的时候再来看tars是如何解决的。

二.线程模型:N收发,M处理

在tars服务端(一):server的启动流程中,有粗略的讲过tars服务端的网络线程和处理线程,这一节会详细的分析N网络线程-M业务线程的细节。

上节讲到,每个网络线程都是一个基于epoll循环的Reactor,而且一个socket fd只能由一个网络线程进行io操作。如果再加上多个业务线程则变成Reactors + thread pool模式:

tars服务端(二):网络io模型和线程模型

按上图标示的顺序:

  1. 所有的监听端口都是在第一个网络线程中进行socket创建,bind,listen;该socket创建后的fd也只由第一个网络线程accept
  2. 当accept到一个客户端socket fd后,会创建一个新的Connection实例来接管这个fd的读写
  3. 新的Connection按照分配策略被分配到一个网络线程后,该Connection的读,写,关闭等IO都只能由这一个线程操作。每个网络线程会有一个ConnectionList来管理该线程的所有Connection
  4. 属于同个Adapter(即同个端口)的链接收到的所有请求都插入到Adapter所属的接收队列中
  5. 每个Adapter都有属于自己的一组M个业务线程去处理接收队列中的请求
  6. 业务线程在处理完请求后,如果需要回包,会根据分配策略把包push到对应的网络线程的发送队列中。注意,这里的分配策略和第3步选择网络线程时的分配策略是一样的,所以两次选出来的网络线程都是同一个,这样才能保证同个socket的收发包都是在同个网络线程中。
  7. 每个网络线程处理自己的发送队列,把包发给客户端

这里有一点需要注意:在配置了网络线程数后,网络线程的个数就是确定的了,例如这里的N个;而业务线程是和Adapter的个数有关的,每个Adapter都有自己的接收队列和一组业务线程。假设一个Server有2个Adapter,每个Adapter都有M个业务线程,则总的业务线程数就是2*M个。

继续阅读