目录
定位
HTTP协议以及HTTP服务器
高并发服务器
单Reactor单线程
单Reactor多线程
多Reactor多线程
模块划分
SERVER模块划分
Buffer 模块
Socket模块
Channel 模块
Connection模块
Acceptor模块
TimerQueue模块
Poller模块
EventLoop模块
TcpServer模块
SERVER子块之间的连接
连接模块
EventLoop 模块
Acceptor模块
定位
在这个项目中,我们要实现的是一个高并发服务器组件,项目中通过组件的方式提供不同的应用层协议支持,以让使用者能够快速搭建指定应用层协议的一个高并发服务器。 当然,实际在项目中我们只提供了HTTP协议的支持。
要注意的是,我们的服务器只是一个服务器组件,不提供具体的业务逻辑,业务逻辑需要组建的使用者根据需求自行设置。
那么我们的项目的重点就是实现两个模块或者说组件,一个是服务器的组件,一个是HTTP协议的组件。
在这里我们先了解服务器组件的模块划分。
HTTP协议以及HTTP服务器
HTTP协议(Hyper Text Transfer Protoccol) ,超文本传输协议,他是一个应用层协议,是一种简单的请求-响应式协议,一般都是有客户端向服务器建立连接,并且发起请求,而服务器则根据客户端的请求将特定的资源返回给客户端或者完成特定的功能,而一般响应返回之后,连接就关闭了,当然HTTP也支持长连接。
详细的HTTP的讲解在我们的Linux系统与网络专栏中有讲解,如果想要深入了解,可以前往
应用层协议 —— https-****博客
HTTP协议是基于TCP协议的,所以我们所说的搭建一个HTTP服务器,其实是搭建一个TCP服务器,然后在服务器使用HTTP协议来解析请求,完成业务逻辑,以及构建响应返回。
简单来说,HTTP服务器其实就是以下四个步骤:
1 搭建一个TCP服务器,接收TCP连接
2 接收HTTP请求,进行请求的解析处理与响应
3 将处理结果组织成一个HTTP响应,通过TCP服务器返回给客户端
高并发服务器
实现一个HTTP服务器是十分简单的,但是实现一个高性能的支持高并发的服务器并不简单。而要实现一个高性能服务器,目前最常用的方式就是使用 Reactor 模型来搭建。
Reactor 模式简单理解就是 多路复用,仅用一个线程就能监听多个连接,哪个连接有数据输入或者说哪个连接接收到了发数据,就去处理该连接的事件。 他是一种事件驱动模式,只有在事件到来时,才会驱动服务器去处理。
而在Reactor线程监听到了对应的事件之后,就可以将数据分发给线程池进行业务逻辑的处理,这样就不需要多个线程一直在死循环等待IO就绪,能将系统资源让出来。
我们一般监听的事件就是 读事件,而读事件又分为两种,一种是读数据的事件就绪,另一种是监听套接字的读事件,其实就是底层有新的连接就绪,等待上层去获取。
Reactor 模型有三种:
单Reactor单线程
单Reactor单线程其实指的就是只由一个线程完成所有的连接的事件监控以及事件触发之后直接由该线程处理。 说白了就是单线程。那么在单线程情况下,不管是连接建立请求,还是数据处理的请求,都是在这一个线程完成的。
在这种模式下,所有的操作都在同一个线程内完成。
这种模式的优点就是设计简单,不会出现线程安全问题,所有的操作都是船形的。
但是他的缺点也很明显,由于是单线程,他无法发挥多核CPU的优势,性能上限不高。
但是并不是说他就一无是处,他适合那些客户端很少,且业务处理速度很快的场景。
而如果业务处理很慢,那么他的串行化就会导致新连接来不及处理。
单Reactor多线程
在这种模式下,我们使用一个 Reactor 线程专门用来监听事件,如果是新连接到来,那么在Reactor线程中进行连接的获取以及将新连接添加到多路复用模型中进行事件监控,而如果是数据到来,那么还是由Reactor线程进行数据的IO,然后将完整的请求交给线程池中的业务线程进行业务处理。 最终业务线程把数据请求完之后,将响应交给Reactor线程进行响应操作。
在这个工作模式下,能够利用多核CPU的优势,让不同线程并行推进进度,提高效率。
他的缺点也很明显,首先,多线程访问任务队列要做共享数据安全的保护。 同时,我们的Reactor线程承担了所有的IO事件(获取请求和响应请求)和连接事件,那么意味着,如果我们的服务器是一个高并发的服务器,也就是说,每一个时刻都有很多的客户端发送连接请求,而由于在这个模型中,IO和连接事件都是由一个Reactor线程处理的,也就是说,IO和连接事件的处理是串行的,那么也就意味着,IO的时候,我们是来不及进行新连接的获取的,由于IO的时间很长,最终可能导致大量连接来不及处理,无法满足高并发场景。
多Reactor多线程
上面的单Reactor多线程模型的最大的缺点就是高并发场景下,新连接可能来不及处理。而多Reactor多线程模型就是解决这个问题的。
首先,我们只需要让一个单独的Reactor线程,专门用来获取新连接,这样就不会出现新连接来不及处理的问题。 而对于已经建立的连接的IO事件的监控,则交给其他的Reactor线程去完成。而这些Reactor线程读到完整的请求之后,还是将数据放到任务队列中,让我们的工作线程进行处理。
我们一般把这个负责获取新连接的Reactor线程称为主Reactor线程,而把这些负责具体的连接的事件监控的Reactor线程称为从属Reactor线程,最后,还是把负责业务处理的线程称之为工作线程。
但是上面的模型有一些很明显的缺点:
1 由于我们需要管理从属Reactor线程池和工作线程池,我们需要管理的共享资源都需要加锁,而加锁会导致效率的降低。
2 由于线程过多,CPU调度时线程切换的成本高。
那么要怎么解决呢?
一般在主从Reactor模式里面,从属Reactor线程之下不设置工作线程,而是直接由从属线程进行业务处理。
那么这么做的好处是什么呢?首先,线程的数量显著减少了,CPU线程切换的次数或者说成本就降低了。
同时,每一个连接对应的事件都只在一个Reactor线程中进行,那么对于一个连接的事件处理可以在线程中通过执行顺序的控制,保证一个连接的事件操作的安全。
而在我们的项目中要使用的就是主从Reactor模型,我们不需要实现工作线程池,直接在从属线程中进行事件的监控和处理。
从属Reactor线程要做三件事: 1、IO事件的监控 ;2、IO事件的处理;3、业务处理
模块划分
我们的项目中需要两个大模块,一个就是服务器模块,该模块用于实现一个主从Reactor模型的TCP服务器。 另一个就是协议模块,他用来给我们的服务器提供应用层的协议支持。在我们这个项目中就只提供HTTP协议的支持。
SERVER模块划分
我们的SERVER模块就是服务器模块,他需要管理所有的连接,以及管理我们所创建的所有的线程或者进程,我们需要保证他们安全高效的运行,最终达到高性能服务器的目标。
而具体的管理可以划分为三个部分:
1 对新连接的监听管理
2 对已建立的连接的管理
3 对超时连接的管理
对新连接的监听管理,其实就是管理我们的监听套接字,以及新连接建立之后的一系列操作,这需要由我们的主Reactor线程完成。
而队以建立的连接的管理则是监听已建立的连接的IO事件以及进行具体的业务处理,理所当然由从属Reactor线程完成。
对超时连接的管理其实就是对已建立的连接大的管理,也需要在从属Reactor线程中进行,为什么要进行超时管理呢?我们知道TCP协议是有自己的超时管理的,但是他的超时时间太长,是以小时为单位的,而我们要实现的服务器并不想让一些非活跃的连接来长期占用我们的资源,毕竟我们是要应对高并发的场景,如果一个连接不活跃,不如尽早把他的资源回收了来交给新的连接。
那么基于以上的管理思想,我们又可以将SERVER模块划分为以下的子模块:
Buffer 模块
Buffer模块其实就是一个用户态的缓冲区,那么对应每一个连接都需要配备两个缓冲区,接收缓冲区和发送缓冲区。 为什么我们有内核的缓冲区了,还需要提供用户级缓冲区? 这一点我们在网络的学习中应该也十分熟练了,因为我们从内核缓冲区读取出来的数据不一定就是一个完整的报文,那么我们就需要将已经读出来的数据继续保存。 于此同时,我们为什么要有发送缓冲区呢? 因为如果我们直接向内核缓冲区写的话,可能内核缓冲区的写事件没有就绪,那么也就会阻塞在写入操作,所以对用户的要发送的数据我们需要使用一个缓冲区来进行保存,使用缓冲区的原因也有一点就是不需要让用户在写入失败时再来考虑什么时候能写,而是把这些工作交给了我们的底层的事件监听以及事件处理来做。
而我们的缓冲区模块需要对外提供以供其他的模块调用调用的接口其实就只有两个,一个就是往缓冲区中写入数据,一个是从缓冲区中读取数据。
Socket模块
Socket 模块其实就是对socket的封装,简化我们对套接字的操作,同时,我们还可以提供一些集成的功能,比如提供接口直接创建一个服务端的监听套接字,或者直接创建一个客户端连接服务端的通信套接字。
那么Socket的的操作其实就是对socket的操作的一些封装,让用户用起来更简单。
而Socket重点的接口或者常用的接口其实就是两个继承功能,以及发送和接受数据。
Channel 模块
Channel模块主要负责套接字所监听的事件的管理,以及就绪的事件的处理的回调函数的管理。
每一个连接都需要对应一个 Channel 模块,Channel 模块中保存了该连接的套接字所需要监听的事件,同时Channel对象中也保存了各类事件的处理的回调方法。
那么具体有哪些事件呢?其实很简单,我们可以看一下 epoll 可以为我们监视的事件的类型,我们大致将其分为四类
第一类就是 读事件,EPOLLIN和EPOLLPRI都是数据到来,我们需要进行读取。 而EPOLLRDHUP则表示对端已经关闭套接字,准备关闭连接了,这时候其实我们也需要尽快将数据从内核缓存区读走。 这三个我们都归为读事件
第二类就是写事件,EPOLLOUT
第三类是 挂断事件,EPOLLHUP,这个挂断和前面的EPOLLRDHUP的区别就是,EPOLLRDHUP是对端挂断,也就是对端关闭连接,而EPOLLHUP则是本端主动挂断,这时候我们也需要进行处理,比如将new出来的Channel对象释放,以及从其他的结构中拿走
第四类就是错误事件,错误事件发生之后我们其实也是需要释放对象和将对象从其他的模块中拿走。
第三类和第四类时间虽然有一定的区别,但是实际上我们的处理方法是类似的。
那么在与其他模块的关联上,重点接口就是四类事件的监控,以及四类事件的处理
Connection模块
Connection 模块是对一个通信套接字的整体的管理,他内部其实会管理 Buffer,Socket,Channel对象,以及超时连接的处理等。每当我们Accept获取到一个新链接的时候,其实就需要创建一个Connection对象,而Connection 对象进行创建的时候,会对内部的三个子模块进行初始化设置,比如 Channel 内部的四个回调函数,就是有Connection传进去的。
同时Connection模块也需要包含几个上层设置的回调函数: 连接建立时需要执行的回调函数,连接关闭时执行的回调函数,有一个新数据时执行的回调函数,以及事件就绪时的回调函数
而Connection模块还需要提供协议切换/升级的功能,这个功能其实就是对协议上下文以及Connection的四个上层回调方法的重新设置。
他的主要接口其实分为三个部分,一类是对外提供的接口,供其他模块调用,一类是封装内部模块的操作的接口,还有一类是由上层模块设置的事件回调,当然这一类其实是算在他的成员,严格意义上来说并不是他的接口,不过为了理解他与上层模块的联系,我们还是将其罗列出来。
Acceptor模块
Acceptor模块是对监听套接字的Socket和Channel模块的一个整体管理,他实现的是对监听套接字的管理。
他的主要操作就是获取新连接,然后通过内部设置的回调方法将新连接封装成一个Connection对象,而他的可读事件的处理其实就是 accept 并创建Connection对象交给上层。
TimerQueue模块
TimerQueue简单理解就是对超时连接进行管理,更好的理解就是用于设置定时任务,以及管理定时任务。相当于一个定时任务的管理器,我们可以往里面添加一些定时任务,同时也可以刷新定时人物的超时事件,以及取消定时任务等等。
这个模块在我们项目中主要的作用就是对Connection对象的超时管理,也就是对超时连接的资源释放。
Poller模块
Poller模块就是对epoll及其操作的封装,是直接进行事件监控的模块,需要完成监控事件的添加,移除,以及对描述符移除监控,以及获取就绪事件等。
EventLoop模块
EventLoop模块其实就是我们的主从Reactor模型中的从属Reactor模块,它是对Poller,TimerQueue,Socket模块的整体封装,负责所有的描述符的事件监控。
每一个EventLoop模块其实都需要对应一个线程,每一个连接创建出来之后,都需要关联或者说绑定一个EventLoop线程,从此之后,对于该连接的所有操作都需要在这个EventLoop线程中完成,以保证操作的线程安全。 不能出现说其中一个线程正在进行操作,另一个线程就把连接关闭了。 把对同一个连接的所有操作放在同一个线程中执行,他们就是串行化的,能避免出现线程安全问题。
EventLoop需要进行实际的监控和事件处理,那么他需要关联一个Poller模块,每一个EventLoop模块内部都需要一个Poller来进行事件的监控,每一个连接和对应的EventLoop也需要互相关联,EvenetLoop中才能调用Connection中的Channel模块进行事件的处理。
同时,EventLoop模块还需要保证内部的所有的连接的活跃性,所以他还需要包含一个TimerQueue模块,来进行超时连接的资源释放工作。
那么EventPool需要对外提供的重要接口其实大部分都是对子模块的封装,比如添加事件监控,移出事件监控,更新/修改时间监控(取消某个时间的监控都算在更新事件监控中)。 以及添加定时任务,刷新定时任务,取消定时任务, 还有对四类就绪事件以及任意事件的处理。
同时,由于同一时间可能会有很多事件就绪,IO事件完成之后,还需要进行一些其他的操作,而EventLoop为了提高效率,高优先级的先把所有的IO事件处理完成,而将对连接的其他的操作,比如数据处理,从用户缓冲区读取和发送数据等等操作放入一个任务队列中,而后对任务队列中的所有的任务按顺序执行。
重要接口 :
TcpServer模块
TcpServer模块是所有管理模块的整合,它内部主要就是Acceptor模块和我们的从属Reactor线程池,也就是EventLoop线程池,这个模块是提供给用户使用的,提供一些接口让用户能够简易搭建一个服务器,以及进行一些设置。
这个模块是在最后所有的小模块都设计完了,再对Acceptor和EventLoopPool模块进行一个封装。重点工作其实还是在其他的子模块,他只是对其他模块提供的功能的封装。
SERVER子块之间的连接
连接模块
首先,最重要的就是 Connection模块与他的子模块的关系,因为Conection是我们整个项目中设计最复杂的模块,同时也是功能最复杂的模块,我们就拿他的几个重要的接口来举例:
首先对于四个向外提供的机构来说,关闭连接,释放资源这一点,其实TcpServer模块是会对所有的Connection对象进行管理的,在实现关闭连接释放资源的时候,是会调用TcpServer给Connection设置的一个回调函数的。
而对于发送数据,Connection提供的发送数据是将数据拷贝到发送缓冲区,因为我们并不能保证调用发送时,我们套接字的内核缓冲区一定就能够进行写入,一定要等到写事件就绪才会真正的发送
而协议切换,主要有两个方面,一个就是我们Connection中会保存数据处理的上下文,这是跟协议强相关的,比如我们使用Http协议的话,我们的上下文中就会保存Http协议的报头中的各个字段,来表示当前处理到哪一个阶段了,那么如果这一次读取没有读到一个完整的报文,我们可以将这个进度保存在上下文中,那么下一次读事件再到来,有新数据接受的时候,我们就可以直接从上下文保存的进度开始继续读取,提高效率。那么协议在进行切换的时候,就需要将上下文数据进行替换。 除了上下文的替换之外,与协议强相关的还有TcpServer给Connection设置的四个回调方法,这四个回调方法就是不同协议该如何处理的区别所在,尤其是接收新数据之后的回调,当然这些方法是由用户设置,协议切换时,用户也需要传入新的数据和新的上下文结构数据。
而启动和取消超时释放策略,测试依赖于TimerQueue所提供的接口来完成的,当启动超时策略时我,我们就会添加/刷新(有可能上次取消超时策略之前设置的定时任务还在时间轮中)一个当前连接的定时关闭任务,而取消超时策略就是将Connection的定时任务通过TimerQueue提供的接口取消掉。同时,一个连接是否需要启动超时策略也是由TcpServer或者用户来设置的。
而Connection中的接收数据的方法,其实是作为Channel中的读事件的回调执行的,由Connection提供,而接收数据最终也是将数据缓存到Connection中的Buffer缓冲区中。
而发送数据也是由Channel中的写事件触发的,写事件就绪时,会调用由Connection设置的写事件回调,将数据从Buffer缓冲区写到内核缓冲区中。
关闭套接字接口也是由Channel事件触发的,但是这个关闭套接字接口其实只会将该套接字的监听移除,而整个连接的释放其实会在所有数据处理完之后进行
刷新活跃度的接口其实就是通过调用TimerQueue中提供的接口来完成的。
剩下的就是用户设置给TcpServer,而TcpServer设置给Connection 的四个回调方法。 建立连接回调,会在Acceptor获取到一个新连接,并将其封装成一个Connection,建立完连接时进行调用。
而用户设置的接收数据的回调,会在我们的Channel的读事件回调中进行一次调用,其实就是在Connection提供的读接口中。
而关闭连接回调也是在Connection设置给Channel的关闭连接事件的回调中调用。
任意事件回调也是一样的,会在Connection提供的,设置给Channel的任意事件回调用进行调用,也就是在刷新定时器之后进行一次调用。
EventLoop 模块
其实对连接的所有的操作都会由EventLoop对应的线程来完成,EventLoop是操作的真正的执行者,Connection等模块其实并不会真正执行操作,只有交给EventLoop线程执行才会保证线程安全。
而在这里我们就重点关注三个小模块,分别是TimerQueue模块,Channel模块以及Poller模块。
首先对于事件的管理,其实EventLoop中对于事件的处理的操作都是封装了Poller的接口,而Channel在监控的事件发生变化时,也会调用EventLoop中的接口进行操作,最终本质还是调用了Poller的接口操作。
而事件就绪之后,调用处理方法,其实不是直接调用,因为未来我们是使用线程池来进行事件监控和获取的,取出这个事件的并不一定就是关联的EventLoop线程,所以未来我们进行操作时,如果是EventLoop获取的,那么可以直接执行,而如果不是,那么会放到EventLoop线程的任务队列中。
而EventLoop对于连接的定时任务的管理也是使用TimerQueue提供的接口完成的。
Acceptor模块
Acceptor模块就是我们的主Reactor线程,他负责接受新连接,同时要将新连接封装成一个Conencton对象,添加到EventLoop的事件循环模块中。
以上就是我们的项目中的服务器组件的基本的模块划分、各个模块的主要功能,以及模块之间的大致关系。
下一篇文章会介绍几个项目中会用到的重要的知识点或者技术。