详解高并发web服务器

总体概述:

高并发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:返回视频页面

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值