前言
此博客记录对于TinyWebServer项目的学习,并根据自己的理解做出些许更改。
原项目地址:https://github.com/qinguoyi/TinyWebServer
服务器基本规范
服务器框架
服务器程序虽然种类繁多,但是基本框架都是一样的。
I/O处理单元是服务器管理客户连接的模块。它通常要完成以下工作: 等待并接受新的客户连接,接收客户数据,将服务器响应数据返回给客户端。但是,数据的收发不一定在I/О处理单元中执行,也可能在逻辑单元中执行,具体在何处执行取决于事件处理模式(reactor模式中数据的读写就在逻辑单元中执行,proactor模式中数据的读写在I/O处理单元中执行)。
一-个逻辑单元通常是一个进程或线程。它分析并处理客户数据,然后将结果传递给IO处理单元或者直接发送给客户端(具体使用哪种方式取决于事件处理模式)。服务器通常拥有多个逻辑单元,以实现对多个客户任务的并行处理。
网络存储单元可以是数据库、缓存和文件,甚至是一台独立的服务器。但它不是必须的,比如ssh、telnet等登录服务就不需要这个单元。
请求队列是各单元之间的通信方式的抽象。IO处理单元接收到客户请求时,需要以某种方式通知一个逻辑单元来处理该请求。同样,多个逻辑单元同时访问一个存储单元时,也需要采用某种机制来协调处理竞态条件
两种事件处理模式:Reactor和Proactor
服务器程序通常需要处理三类事件:I/O事件、信号和定时事件。
Reactor
Reactor是这样一种模式,它要求主线程(I/O处理单元,下同)只负责监听文件描述上是否有事件发生,有的话就立即将该事件通知工作线程(逻辑单元,下同)。除此之外,主线程不做任何其他实质性的工作。读写数据,接受新的连接,以及处理客户请求均在工作线程中完成。
Proactor
Proactor模式将所有I/O操作都交给主线程和内核来处理,工作线程仅仅负责业务逻辑。Proactor模式一般使用异步IO模型实现。
使用同步IO方式模拟出 Proactor模式的一种方法。其原理是﹔主线程执行数据读写操作,读写完成之后,主线程向工作线程通知这一“完成事件”。那么从工作线程的角度来看,它们就直接获得了数据读写的结果,接下来要做的只是对读写的结果进行逻辑处理。
高效的并发模式:半同步/半反应堆模式
并发编程的目的是让程序“同时”执行多个任务。如果程序是计算密集型的,并发编程并没有优势,反而由于任务的切换使效率降低。但如果程序是IO密集型的,比如经常读写文件,访问数据库等,则情况就不同了。由于IO操作的速度远没有CPU的计算速度快,所以让程序阻塞于IO操作将浪费大量的CPU时间。如果程序有多个执行线程,则当前被IO操作所阻塞的执行线程可主动放弃CPU(或由操作系统来调度),并将执行权转移到其他线程。这样一来,CPU就可以用来做更加有意义的事情(除非所有线程都同时被IO操作所阻塞),而不是等待IO操作完成,因此CPU的利用率显著提升。
在并发模式中,“同步”指的是程序完全按照代码序列的顺序执行,“异步”指的是程序的执行需要系统事件(例如中断、信号等)驱动。
半同步/半反应堆模式,属于半同步/半异步模式。
图中,异步线程只有一个,由主线程来充当。它负责监听所有socket 上的事件。如果监听socket 上有可读事件发生,即有新的连接请求到来,主线程就接受之以得到新的连接socket,然后往epoll内核事件表中注册该socket上的读写事件。如果连接socket上有读写事件发生,即有新的客户请求到来或有数据要发送至客户端,主线程就将该连接socket插人请求队列中。所有工作线程都睡眠在请求队列上,当有任务到来时,它们将通过竞争(比如申请互斥锁)获得任务的接管权。这种竞争机制使得只有空闲的工作线程才有机会来处理新任务,。主线程插入请求队列中的任务是就绪的连接socket,说明其事件处理模式为Reactor。
半同步/半反应堆模式也可以使用模拟的Proactor事件处理模式,即由主线程来完成数据的读写。在这种情况下,主线程一般会将应用程序数据、任务类型等信息封装为一个任务对象,然后将其(或者指向该任务对象的一个指针)插人请求队列。工作线程从请求队列中取得任务对象之后,即可直接处理之,而无须执行读写操作了。
缺点:
1.主线程和工作线程共享请求队列。主线程往请求队列中添加任务,或者工作线程从请求队列中取出任务,都需要对请求队列加锁保护,从而白白耗费CPU时间。
2.每个工作线程在同一时间只能处理一个客户请求。如果客户数量较多,而工作线程较少,则请求队列中将堆积很多任务对象,客户端的响应速度将越来越慢。如果通过增加工作线程来解决这一问题,则工作线程的切换也将耗费大量CPU时间。
WebServer框架
webserver整体框架如图所示。
webserver实现
webserver首先需要完成各模块的初始化,创建网络编程,开启epoll监听,然后调用相应功能完成主线程的逻辑功能,循环实现形成事件回环。
const int MAX_FD = 65536; //最大