1. 前言

这是EpollServer
中处理读取数据的逻辑,该代码存在一些问题
- 我们从
fd
读上来的数据都放在同一个buffer
中了,如果有多个fd
,数据不就混在一起了吗? - 通过非阻塞循环读取,确实能将本轮数据全部读上来,但如何保证读上来的数据是一个完整的报文?
因此,我们对每个fd
都要建立一个单独的缓冲区,同时要引入协议
2. Reactor
Reactor
是什么,我们暂时不管,先编写代码
不管是listensockfd
,还是普通sockfd
,今天我们不做区分,统称为"连接"
"连接"分为两种,监听sockfd
和普通sockfd
,用宏进行定义,同时提供属性的接口
每当"连接"底层事件就绪时,希望自动调用对应的处理方法,将数据放到自身的缓冲区中;因此,在每个连接中添加一批方法和缓冲区

在Rector
中以unordered_map
的数据结构将Connection
管理起来,键值为sockfd
要提供能添加连接的接口,添加连接中,要完成三个任务:
- 创建
Connection
对象,并设置好属性 - 将
Connection
对象添加到数据结构中 - 托管给
epoll
对于epoll
的操作,我们也进行封装,使用模板方法类的设计模式,对外提供接口


接下来,我们需要一个专门用来监听的"连接",将它单独设计为一个模块,为Listener

在Main
函数中,将listensockfd
添加到Reactor
中,开始进行事件的派发

进行事件派发时,将EPOLLERR/EPOLLHUP
等事件统一交给EPOLLIN,EPOLLOUT
进行处理
当EPOLLIN
事件到来,在每个Connection
中不是有对事件的处理方法吗?直接回调
EPOLLOUT
也是同理

但每个"连接"的处理方法还没有,因此,AddConnection
中,还要进行处理方法的注册
将两种套接字的处理方法都放在Reactor
中,AddConnection
时,根据"连接"的类型,注册不同的方法

在Main函数中,就要提前注册好Listener
和Normal
的处理方法


我们的Listener
就开始监听,当有连接到来,自动回调到Accepter
所有ET
模式下的fd
都要设置为非阻塞,listensockfd
和sockfd
都要设置为非阻塞
连接到来,进行非阻塞循环读取
成功获取一个连接,由于需要调用Reactor
中的AddConnection
方法,因此,每个Connection
都需要知道自身所处的Reactor
,添加一个Reactor
的地址即可


将新的连接添加到Reactor
中,当连接底层读事件就绪,就会自动回调到处理读事件的方法
对于读,由于sockfd
设置为ET
模式,也是非阻塞循环读取,将读到的数据全部放到连接自身的缓冲区中

如何保证读到的是一个完整的报文?连接只负责IO,如何处理读到的数据则交给报文解析模块,因此,Normal
内部需要有解析报文的方法
当底层数据读完了,执行报文解析方法
报文解析在cal_server
代码中就有,直接拿来用
对于写,关心的是底层发送缓冲区有无空间,一个新获得的sockfd
默认发送缓冲区为空
因此,写事件默认就是就绪的,直接发送数据即可
当写条件不满足时,也就是发送缓冲区满了并且数据没有发完,我们再开启对写事件的关心
如何得知写条件不满足呢?调用send
接口失败,根据errno
判断
等到写事件就绪,Reactor
中会自动调用到发送数据的接口,替我们将剩余的数据发送了
需要注意的是:
- 如果默认就开启对写事件的关心,底层就会有大量写事件就绪,因此,对于写事件我们按需开启关心
- 当数据发完了,需要关闭对写事件的关系
- 如果设置了对写事件的关心,
epoll
对首次设置关心写事件时会默认就绪一次



最后就是异常事件的处理了,当读/写出错,自动调用处理异常方法

3. 总结

Reactor
类似于一种Connection
的容器,当底层事件就绪,进行事件派发,让不同的Connection
的去处理事件
Reacotr
是基于事件驱动的网络服务器设计的主流模式