单Reactor单线程
单Reactor多线程
多Reactor多线程
主从Reactor One Thread One Loop
由于也采用了主从Reactor模式,所以性能不差,但为了服务器的实现更简单,放弃了线程池的实现。
本项目共分为两大模块:Reactor服务器模块和基于Reactor服务器模块实现的HTTP服务器模块。
下面的项目分解只是简单的说明了一下各模块的功能,项目源码中有详细的注释讲解,所以强烈建议搭配项目源码一起食用。
服务器模块共有以下子模块
recv并不能够保证读取到一个完整的协议数据,所以必须要将读取到的数据先暂存起来,然后上层检查数据完整性,若完整则拿走数据,不完整则一直等读取到一个完整的协议数据时再拿走数据,那么这时就需要一个缓冲区能暂时存放recv读取到的数据。并且写入数据时,也不能直接调用send,因为fd是要被epoll监控的,但用户又不知道什么时候调用,所以用户可以直接将数据写入缓冲区中,当fd上的写事件触发时,会自动将缓冲区中的数据send到fd中。
本模块就实现的是这样的一个缓冲区。
缓冲区结构如下:
封装系统调用socket,使对于socket的各项操作更加方便。
Channel模块是对一个fd进行事件监控管理以及事件回调管理的!
功能大概有:
开启/关闭fd的事件监控(读、写);
关闭fd的所有事件监控;
判断fd的事件监控是否被开启了;
设置事件触发后的回调函数(读事件、写事件、错误事件、关闭事件、任意事件);
调用已经触发的事件回调函数。
但要注意,关于fd的开启/关闭事件监控并不是真正在Channel模块执行的,而是在Epoller模块执行的。Channel模块只是将fd的相关监控操作和相关事件回调整合在了一起。
Epoller模块是对epoll系列操作进行的封装,让对fd的事件监控操作更加简单。
通过传入一个Channel指针,获取到fd需要监控的事件,然后Epoller模块就把这些事件进行监控,而当有事件触发时,Epoller模块就把已经触发的事件通过Channel传出,再由Channel内部调用事件回调。
功能大概有:
TimerWheel是一个定时任务管理模块。
大致思想就是:将任务封装到TimerTask的析构函数中,然后用shared_ptr管理起来放入TimerWheel中的vector里,每隔一秒就清空一下vector里的元素,此时调用析构函数,就会调用定时任务了。
每隔一秒,step_就前进一步,step_走到哪里,就清空哪里,然后当最后一个shared_ptr调用析构函数时,就会调用定时任务。
step_的每秒移动是根据timerfd技术来实现的。
创建一个timerfd,让内核每隔一秒写入一次,然后用Channel管理timerfd,注册一个读事件,在读事件里++step_,这样内核每隔一秒写入一次,就触发一次读事件,就会++step_一次。
EventLoop模块就是副Reactor模块,封装了Epoller模块和TimerWheel模块,并且一个EventLoop就是一个线程。
大致功能有:
关于任务队列,要详细说一下:
对于一个连接,用户所有关于连接的操作都是线程不安全的,比如在某个事件回调执行过程中,用户开辟了一个线程池,这个线程池都是共享这个连接的,那么假设有若干个线程同时对定时任务进行操作,就会出现线程安全问题。所以用户所有的对于连接的操作都是非线程安全的,但是又不能给每个连接的接口都添加锁,这样效率就太低了。于是就有了一个解决办法,在EventLoop模块里创建一个任务队列,所有的连接的接口在调用时都进行一下判断(接口内部判断),若是副Reactor线程就直接执行接口,若是其它线程,就将该任务压入队列中,由副Reactor线程统一执行。这样就避免了多线程对于连接访问的线程安全问题。
上面功能的第四点是在同一函数中执行的,那么就会出现一种情况,任务队列中有任务了,但此时没有事件触发,epoll_wait被阻塞,最终导致任务队列中的任务得不到及时执行。所以这里用了eventfd技术解决。eventfd用Channel管理起来,注册一个读事件,然后在将任务添加到任务队列后往eventfd里写入数据,此时就会触发读事件,epoll_wait不会被阻塞,任务队列中的任务也就能够被及时执行了。
Any模块是模仿C++17中的any类实现的。
TCP服务器并不知道上层要运用什么协议,也就无法用一个特定类型保存上层的上下文信息,所以用一个Any类来保存上层的上下文信息。
实现思路
要实现一个类,能够存放任意类型的数据,那么该类必定不能是模版类,模版类不能自动推演类型,并且模版类在实例化之后就只能存放单一类型的数据了。但是函数模版可以自动推演类型,于是就想到将类的构造函数设置成模版函数,成员变量为void *指针,但是void *太不安全了。于是又想到,在Any类的内部创建一对父子类,子类是模版类,成员变量为父类指针,在Any的构造函数中new一个子类对象用父类指针管理,就能够实现简易的Any类。
Connection模块是子模块中最复杂的模块,是对Buffer、Socket、Channel、Any、模块的整合,还关联了EventLoop模块。
大致功能就是:
Connection模块所有的对外提供的接口在调用时都要判断是否和副Reactor线程是同一个线程,是则直接执行,不是则压入队列。但是对于关闭连接的操作,无需进行判断,应该直接压入队列,关闭连接必须要在所有的事件触发函数执行完之后,在队列中执行。
假设有一种场景:非活跃连接销毁时间是10s,1、2、3、4、5号都有事件触发,1号事件执行了20s,那么timerfd就超时了20次,假设2、3、4中有一个就是timerfd事件,然后指针走了20下,再然后后面还没来得及执行的事件的连接就被销毁了,此时再去执行触发事件就会发生错误。所以关闭连接的操作必须要在触发事件全部调用完之后,在任务队列中执行。
Acceptor模块也就是主Reactor模块,负责获取新连接,内部有一个EventLoop和一个Channel来管理监听套接字。
该模块将EventLoop和线程强绑定在了一起。为什么非要这么做呢?
因为EventLoop模块在初始化的时候获取当前线程ID,那么用户可能在一个线程内部创建好几个EventLoop,然后再将这几个EventLoop分配给其它线程,这时虽然一个EventLoop占一个线程,但此时EventLoop内部的线程ID和实际所处的线程ID是不一样的。
将LoopThread模块封装成一个线程池,更加方便了服务器对于LoopThread数量的掌控。
是对所有模块的整合,但主要的成员也就是一个主Reactor(一个EventLoop和一个Acceptor)、一个LoopThreadPool。
主要功能有:
该模块提供了一些工具函数,比如字符串分割函数、读文件、写文件、编码、解码等。
该模块存放了解析后的Http请求报文数据,并且还提供了一些方法能够快速获取Request数据。
该模块存放了解析后的Http响应报文数据,并且还提供了一些方法能够快速获取Response数据。
该模块是接收Request的上下文模块,服务端接收到的数据有可能并不是一个一条完整的Http报文,所以需要该模块来记录下接收Http报文的过程(上下文)。
对上面所有模块的整合,并且设置了不同的Http请求与回调方法的映射。