总体概述:
高并发web服务器总共分为四个模块,第一个模块是解析http报文的模块,第二个模块是定时器处理非活跃连接的模块,第三个是日志系统模块,最后一个是注册登录模块,分别从这几个模块进行讲解。
一、http报文解析模块
1.线程池的创建
一个类模板,线程池中包含以下几个成员变量:
线程池中线程的个数、保存消息的队列、消息队列的最大个数、保存所有线程的数组、用于互斥访问消息队列的互斥锁、用于通知线程去消息队列中取任务的信号量。
线程池中包含的成员函数:
append:用于添加任务到消息队列中,每添加一个任务,就sem_post,将信号量加1,唤醒等待的信号量的线程
worker:静态成员函数,线程的主函数,通过传递进来的线程池,调用run函数
run:调用sem_wait,等待信号量,对消息队列加锁,取出任务,执行任务(其实就是解析http报文,发送http报文)。
2.网络通信
主线程执行socket、bind、listen,并创建一个epoll实例,然后将监听文件描述符listenfd添加到epoll实例中,循环调用epoll_wait等待事件发生,遍历所有的就绪事件
(1)如果返回的文件描述符等于listenfd,表示有新的连接进入,调用accept接收新的通信fd,初始化一个http报文解析类
(2)如果是EPOLLIN事件,读取数据,调用线程池的append,等待线程处理
(3)如果是EPOLLERR或者EPOLLHUP或者EPOLLRDHUP,表明出现异常,调用close关闭连接
(4)如果是信号,调用处理非活跃连接的事件。
3.http报文解析
http报文解析总共包括三个部分,第一部分解析请求行,第二部分解析请求头,第三部分解析请求体
(1)解析请求行
通过\r\n判断为一行结尾,有三个关键字,请求方式、url、http协议版本
请求方式有8种,get、post、head、put、delete、option、trace、connect。
url:用来定位所请求的资源在服务器上的位置
(2)解析请求头
请求头是以k-v形式存储的。
主要的关键字有以下几种
host:请求的域名
connect:可选为keep-alive或者close,选择为keep-alive表示为保持长连接,当发送其他http报文时,不需要重复建立和断开tcp连接
accept-encoding:可接受报文的编码格式
content-encoding:报文编码格式
content-type:请求体的类型
content-Length:请求体的长度
强制缓存字段:
Cache-Control:是一个相对时间,表明什么时候过期,优先级高于Expires
Expires:是一个绝对的过期时间
协商缓存:通过询问浏览器是否可以使用缓存,请求的响应码是304,一共有两种实现方式
第一种:
last-modified:标示响应资源的最后修改时间(在响应头部种)
If-Modified-Since:当响应头部中有last-modified,且过期了,则在请求时带上last-modified时间
第二种:
Etag:唯一标识响应资源
If-None-Match:当资源过期且响应头部有Etag,则在请求时带上Etag的值
Etag优先级更高。
(3)用post请求方式会带上请求体,比如用户名&密码
采用有限状态机解析http报文整体逻辑:
主状态机有以下几种状态:解析请求行、解析请求头、解析请求体,初始状态为解析请求体
从状态机有以下几种状态:成功读取一行,读取一行有错误,没有完整读取一行,需要继续读取,初始状态为成功读取一行
http请求的处理结果:
NO_REQUEST:请求不完整,需要继续读取请求报文数据
GET_REQUEST:获得完整请求
BAD_REQUEST:请求报文有语法错误
INTERNAL_ERROR:服务器内部错误
伪代码:
while((主状态机==解析请求体&&从状态机==完整读取一行)||从状态机==完成读取一行)
{
switch 主状态机:
{
case 解析请求行:
成功解析 从状态机=完整读取一行 主状态机==解析请求头
否则 返回bad request
case 解析请求头:
成功解析 从状态机=完整读取一行
if(是get请求) return GET_REQUEST
else 主状态机==解析请求体
否则 返回bad request
case 解析请求体:
成功解析 从状态机=line_open
否则 返回bad request
default:
返回 INTERNAL_ERROR
}
}
4.生成http回复报文
根据url中/后面的数字判断所请求的资源
具体是请求登录页面资源、注册页面资源、欢迎页面资源、图片页面资源、视频页面资源,请看最后的注册登录模块
(1)根据请求资源的路径,判断是否存在当前服务器中,如果不存在,返回404not found,如果存在,但没有访问权限(可以使用stat获取文件的权限),则返回403 forbid。如果请求报文有错,返回400。如果可以成功访问到文件,就使用mmap映射到内存。
(2)生成固定格式的报文
一是生成响应行,包括协议类型、状态码、状态码描述
二是生成响应头,请求头里面介绍的哪些
三是一个空行
四是响应体 即mmap映射的文件
(3)三是发送响应报文
首先记录需要发送的大小,使用wrtiev分块发送或者write直接发送都行,没发送多少字节,就将需要发送的大小减去已经发送的部分,直到为0。
二、采用定时器处理非活跃连接
非活跃连接是指一段时间之内,没有通过次文件描述符进行通信(如接受数据或者发送数据),这些文件描述符需要close,防止占用文件描述符资源和内存等。定时是指采用alarm函数,每隔一段时间发送SIGALRM信号,并注册一个定时事件处理函数,每当收到SIGALRM信号时,就调用事件处理函数。
1.注册事件处理函数
struct sigaction,常用的有以下三个结构体
(1)一个void (*sa_handler)(int) 类型的函数指针
(2)sigset_t类型的sa_mask,用于指定在信号处理函数执行期间需要屏蔽的信号
(3)int 类型的sa_flags,用于指定信号处理行为,包括以下几种值
sigaction(int signum,const struct sigaction *act,struct sigaction *oldact);
signum表示要捕捉的信号
此函数用于注册当捕捉到signum信号时,需要调用act参数所指的函数
alarm(unsigned int seconds); 用于每隔seconds秒时间发送一次SIGALRM信号
socketpair(int domain,int type,int protocol,int sv[2]);用于创建管道通信
2.信号处理逻辑
(1)采用socketpair创建一个管道,sv[0]用于读,sv[1]用于写,将sv[0]添加到epoll实例中,每次收到sigalrm信号时,就调用事件处理函数,往sv[1]中写一个数字,当使用epoll_wait检测到sv[0]有数据时,把数据读取出来,设置timeout为真,在完成所有的读写事件之后,再调用非活跃事件处理函数。
3.非活跃事件处理函数
连接资源类主要包含以下几个成员:通信的文件描述符,定时器类。
定时器类主要包含以下几个成员:超时时间、回调函数、连接资源、前向定时器、后向定时器
定时器的回调函数:调用epoll_ctl删除epoll实例中的通信文件描述符,然后调用close。
定时器的插入、删除
采用双向链表管理定时器,并且按过期时间升序排序,并采用一个数组管理所有的连接资源,数组的下标表示对应连接资源的通信文件描述符
1.插入一个新的定时器类
创建一个新的定时器类,设置过期时间为当前时间+没有通信时能保留的最大时间,直接插入到链表末尾,时间复杂度为O(1)。
2.删除一个定时器类
通过fd和管理连接资源的数组,找到定时器类,调用回调函数,关闭文件描述符,然后从链表中删除。时间复杂度为O(1)。
3.调整一个定时器类
当一个通信文件描述符发生读或写的时候,需要修改超时时间,通过fd和管理连接资源的数组,找到对应的定时器类,修改对应的超时时间,然后从当前双链表中删除,然后插入到双链表末尾。
4.非活跃事件处理函数
在处理完所有的读写事件之后,判断timeout是否为真,再执行(因为此事件为非必须事件,收到信号不是立马处理)。
从头结点开始遍历当前的链表,如果当前超时事件小于当前时间,则删除,否则,停止遍历,因为后面的时间都大于当前的时间。
三、日志系统模块
日志:由服务器创建,记录运行状态、错误信息,访问数据的文件。
消息队列,采用条件变量进行通信
对于添加消息操作:先加锁,然后push数据,调用pthread_cond_broadcast或者pthread_cond_signal,最后解锁
对于取出消息操作:先加锁,然后当队列中没有数据时,循环调用pthread_mutex_wait直到由数据就跳出循环,然后取出队首元素,进行解锁。
日志信息:
总共包括四种类型的日志,debug日志,普通信息日志,警告日志,错误日志
写日志的逻辑:
首先判断是同步写日志还是异步写日志,还需要判断当前的日期,如果不是同一天,需要重新创建一个新文件,还需要判断写入文件的行数是否已经超过最大行,如果已经超过最大行,也需要重新创建一个文件。当时异步写的时候,就把小日志任务放入消息队列中,否则直接调用fputs写,这样做的一个好处是当遇到一个大的日志需要写入时,不会造成阻塞。
四、注册登录模块
首先根据请求方式,判断是get还是post。
如果是get方式:判断url的\之后的数字
如果是0:表示请求的是注册页面,返回注册页面,根据url后的用户名和密码,与数据库中的用户名和密码比对,如果相等表示注册成功,返回欢迎页面,否则,返回错误页面
如果是1:表示请求的是登录页面,登陆成功或者失败逻辑参考上面
如果是post方式:判断url的\之后的数字
如果是2:表示请求的是注册页面,返回注册页面,此时需要从请求体中获取用户名和密码,与数据库中的用户名和密码比对,如果相等表示注册成功,返回欢迎页面,否则,返回错误页面
如果是3:表示请求的是登录页面,后续处理逻辑同上
如果是4:返回图片页面
如果是5:返回视频页面