1. 前言
![](https://i-blog.csdnimg.cn/direct/7bc6085ea7a942ddbffef07551d134e1.png)
这是EpollServer
中处理读取数据的逻辑,该代码存在一些问题
- 我们从
fd
读上来的数据都放在同一个buffer
中了,如果有多个fd
,数据不就混在一起了吗? - 通过非阻塞循环读取,确实能将本轮数据全部读上来,但如何保证读上来的数据是一个完整的报文?
因此,我们对每个fd
都要建立一个单独的缓冲区,同时要引入协议
2. Reactor
Reactor
是什么,我们暂时不管,先编写代码
不管是listensockfd
,还是普通sockfd
,今天我们不做区分,统称为"连接"
"连接"分为两种,监听sockfd
和普通sockfd
,用宏进行定义,同时提供属性的接口
每当"连接"底层事件就绪时,希望自动调用对应的处理方法,将数据放到自身的缓冲区中;因此,在每个连接中添加一批方法和缓冲区
![](https://i-blog.csdnimg.cn/direct/13d8dc3d64ff454796eb73ef154783e0.png)
在Rector
中以unordered_map
的数据结构将Connection
管理起来,键值为sockfd
要提供能添加连接的接口,添加连接中,要完成三个任务:
- 创建
Connection
对象,并设置好属性 - 将
Connection
对象添加到数据结构中 - 托管给
epoll
对于epoll
的操作,我们也进行封装,使用模板方法类的设计模式,对外提供接口
![](https://i-blog.csdnimg.cn/direct/b88b7cfc83b04458ad96707f41f6b176.png)
![](https://i-blog.csdnimg.cn/direct/635d7f8f53014eb6a328cde09226d632.png)
接下来,我们需要一个专门用来监听的"连接",将它单独设计为一个模块,为Listener
![](https://i-blog.csdnimg.cn/direct/57f4724e46d44755a392dc8f21675281.png)
在Main
函数中,将listensockfd
添加到Reactor
中,开始进行事件的派发
![](https://i-blog.csdnimg.cn/direct/743173dc073f4a07ad547f8d6731dc0e.png)
进行事件派发时,将EPOLLERR/EPOLLHUP
等事件统一交给EPOLLIN,EPOLLOUT
进行处理
当EPOLLIN
事件到来,在每个Connection
中不是有对事件的处理方法吗?直接回调
EPOLLOUT
也是同理
![](https://i-blog.csdnimg.cn/direct/e7aa6eebc61543a2947e642968dbcc6a.png)
但每个"连接"的处理方法还没有,因此,AddConnection
中,还要进行处理方法的注册
将两种套接字的处理方法都放在Reactor
中,AddConnection
时,根据"连接"的类型,注册不同的方法
![](https://i-blog.csdnimg.cn/direct/f321104f5fdc44c3af9e7b043d4b9718.png)
在Main函数中,就要提前注册好Listener
和Normal
的处理方法
![](https://i-blog.csdnimg.cn/direct/b0e718cc76ce4748bfe615dd9bbeaa86.png)
![](https://i-blog.csdnimg.cn/direct/75f754e9faa04e2d9447b514e8be26b0.png)
我们的Listener
就开始监听,当有连接到来,自动回调到Accepter
所有ET
模式下的fd
都要设置为非阻塞,listensockfd
和sockfd
都要设置为非阻塞
连接到来,进行非阻塞循环读取
成功获取一个连接,由于需要调用Reactor
中的AddConnection
方法,因此,每个Connection
都需要知道自身所处的Reactor
,添加一个Reactor
的地址即可
![](https://i-blog.csdnimg.cn/direct/3aeabe347b8141b58a8be0db0f84294c.png)
![](https://i-blog.csdnimg.cn/direct/3540e7296bfd4ee897bc8a55a4efe774.png)
将新的连接添加到Reactor
中,当连接底层读事件就绪,就会自动回调到处理读事件的方法
对于读,由于sockfd
设置为ET
模式,也是非阻塞循环读取,将读到的数据全部放到连接自身的缓冲区中
![](https://i-blog.csdnimg.cn/direct/c8ca1d5db69c4daaa2bd675ee3d655a0.png)
如何保证读到的是一个完整的报文?连接只负责IO,如何处理读到的数据则交给报文解析模块,因此,Normal
内部需要有解析报文的方法
当底层数据读完了,执行报文解析方法
报文解析在cal_server
代码中就有,直接拿来用
对于写,关心的是底层发送缓冲区有无空间,一个新获得的sockfd
默认发送缓冲区为空
因此,写事件默认就是就绪的,直接发送数据即可
当写条件不满足时,也就是发送缓冲区满了并且数据没有发完,我们再开启对写事件的关心
如何得知写条件不满足呢?调用send
接口失败,根据errno
判断
等到写事件就绪,Reactor
中会自动调用到发送数据的接口,替我们将剩余的数据发送了
需要注意的是:
- 如果默认就开启对写事件的关心,底层就会有大量写事件就绪,因此,对于写事件我们按需开启关心
- 当数据发完了,需要关闭对写事件的关系
- 如果设置了对写事件的关心,
epoll
对首次设置关心写事件时会默认就绪一次
![](https://i-blog.csdnimg.cn/direct/c80ec735879e4bc9b8d2ca86699cdc4b.png)
![](https://i-blog.csdnimg.cn/direct/64c6b361dda4479baa44f240b6a42b74.png)
![](https://i-blog.csdnimg.cn/direct/274433f5317d4872a9c2ebf17c3941a8.png)
最后就是异常事件的处理了,当读/写出错,自动调用处理异常方法
![](https://i-blog.csdnimg.cn/direct/74725b75418349df85729537597d9782.png)
3. 总结
![](https://i-blog.csdnimg.cn/direct/02d3162637154dc0a612d01692b609b9.png)
Reactor
类似于一种Connection
的容器,当底层事件就绪,进行事件派发,让不同的Connection
的去处理事件
Reacotr
是基于事件驱动的网络服务器设计的主流模式