1. 说明
web服务器的基本功能包括:处理IO事件、解析请求报文、生成响应报文。因此基本框架中需要I/O处理单元、业务处理单元以及沟通前两个单元的通信单元。
单元名称 | 功能描述 |
I/O处理单元 | 处理网络IO,对套接字读取/发送数据 |
业务处理单元 | 解析请求报文、生成响应报文 |
通信单元 | 用于各单元之间的通信 |
2. I/O处理单元
在web服务器中使用I/O处理单元处理网络连接,工作内容包括:
- 等待网络连接
- 接收客户端数据
- 向客户端发送响应数据
另外接收到的客户端数据后要发送给业务处理单元做逻辑处理,也要注意接收来自业务处理单元的处理后数据。
对于不同的事件处理模式,I/O处理单元的工作内容会有些许不同,比如在reactor模式,数据收发是在业务处理单元进行,proactor模式,数据收发又内核完成,需要使用异步IO,编程较为复杂。
也可以使用同步IO模拟proactor,由主线程作为I/O处理单元,在主线程中等待网络连接,存在网络读事件时,由主线程完成数据读取,读取后通知业务处理单元处理;存在网络写事件时,由主线程完成数据写入。
3. 业务处理单元
业务处理单元主要用于分析处理客户端数据,并将结果返还给I/O处理单元。
实现时,业务处理单元分为解析数据、生成响应两部分,如果解析出错直接关闭连接、解析不完全则通知I/O处理单元需要继续读取数据,解析完成后则进行响应处理。响应处理需要根据解析出的字段内容生成不同的响应报文,响应报文构造成功后,需要通知I/O处理单元发送数据。
4. 通信单元
I/O处理单元要把收到的客户端数据传给业务处理单元,业务处理单元要把处理后的数据传给I/O处理单元,这一传递工作即由通信单元完成。
通信单元是一种逻辑抽象,实际通常实现为池和请求队列的形式。构造一个工作队列,I/O处理单元接收到数据后,将数据以及必要信息封装成对象放入工作队列。线程池中的子线程被唤醒,获取工作队列中的对象,调用业务处理单元接口,实现I/O处理单元和业务处理单元之间的通信。业务处理单元处理数据,数据不完全时注册EPOLLIN事件,通知主线程继续读,数据处理完全时,注册EPOLLOUT事件,通知主线程写数据。
5. 程序结构
5.1 主线程
- 主线程开启监听socket,将监听socket绑定到epollfd
- 主线程无限循环调用epoll_wait,返回就绪事件,就绪事件主要分为三类
监听socket、普通socket读事件、普通socket写事件;
- 处理监听socket
调用accept函数接受连接,并将返回的客户端socket绑定到epollfd上;
初始化连接对象;
- 普通socket读事件
调用read/recv进行非阻塞读取,直到出错、客户端关闭连接(返回0)、TCP缓冲区无数据可读(返回-1,errno = EAGAIN);
读取数据成功,则将数据封装成对象,放入工作队列;
读取失败,关闭连接,移除监听;
- 普通socket写事件
写数据,直到出错、TCP缓冲区写满(返回-1,errno = EAGAIN)、数据写完
TCP缓冲区写满时,需要重新注册EPOLLOUT事件
数据写完,根据keep-alive字段,决定是否重新注册EPOLLIN
5.2 工作线程
- 线程池中有多个工作线程
- 工作线程循环调用sem_wait,初始都被阻塞
- 当主线程把封装对象放入工作队列,则调用sem_post
- 随机唤醒的工作线程对封装对象做业务处理,业务包括解析请求、生成响应
请求报文完全解析后才能进行生成响应步骤;
不需要让一个线程完成对一个socket的所有业务,但是不能让多个线程对一个socket操作;
- 解析请求
使用状态机思想解析请求报文,请求行、请求头、请求体三种状态
请求报文以“\r\n”标志一行结束,每次都要解析一行;
解析一行可能的结果:成功解析、出错、一行不完整;
成功解析时:存储相关信息、转换状态、准备请求资源(如文件等的映射工作);
出错:返回相关标志,生成响应时返回相关错误号
一行不完整:说明主线程读取时没有读到这完整的一行,重新注册EPOLLIN,通知主线程继续读即可
- 生成响应
根据解析请求时返回的标志,生成不同的状态码,如200----请求成功、400----请求出错、403----不可访问资源、404----未找到资源、500----内部错误等;
将响应写入缓冲区后,需要注册EPOLLOUT事件,通知主线程将响应写入TCP窗口
















