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窗口