TinywebServer项目-章节整理(16)
TinywebServer项目– 章节整理(16)
原项目地址(点击跳转)
博主添加注释后项目地址(点击跳转)
一、线程同步机制包装类
将线程互斥锁、条件变量、信号量通过类进行封装,保证线程同步
二、半同步半反应堆线程池
基于模板类,为连接任务构建工作线程数量一定的线程池,主线程将任务插入线程池的请求队列中,工作线程阻塞至请求队列上,竞争任务,并执行
工作线程的run
函数中,会根据服务器的事件处理模式,对任务进行处理:
Reactor模式下:
- 主线程仅负责套接字文件描述符的监听。线程池中的工作线程需要进行 IO 数据的读写,以及业务的逻辑处理
Proactor模式下:
- 使用同步 IO 模拟Proactor模式,主线程不仅需要负责套接字文件描述符的监听,而且需要负责 IO 数据的读写。线程池工作线程仅需要进行业务的逻辑处理
Reactor模式下:
- 若当前就绪事件为读事件,工作线程需要为连接任务,进行 IO 数据的读取,将客户端的请求报文,读取至缓冲区
m_read_buf
。并且进行业务的逻辑处理,对请求报文进行解析,并根据解析情况,生成响应报文至m_write_buf
中,若是一个完整无错的请求,并且客户端访问的资源存在且有访问权限,则使用向量缓冲区iovec
分别指向m_write_buf
以及通过mmap
函数对访问资源进行内存映射的地址- 若当前就绪事件为写事件,工作线程需要为连接任务进行 IO 数据的写操作,通过
writev
方法从向量缓冲区(多个缓冲区)收集数据写入通信套接字的写缓冲区发送给客户端。不需要进行额外的业务逻辑处理(没有数据需要进行分析处理)Proactor模式下:
- 工作线程仅需要对业务进行逻辑处理(数据分析处理),若当前就绪事件为读事件,工作线程需要对请求报文进行解析,并根据解析情况,生成响应报文至
m_write_buf
中,若是一个完整无错的请求,并且客户端访问的资源存在且有访问权限,则使用向量缓冲区iovec
分别指向m_write_buf
以及通过mmap
函数对访问资源进行内存映射的地址- 若当前就绪事件为写事件,不需要进行额外的业务逻辑处理(没有数据需要进行分析处理)
三、HTTP连接处理
1.http报文处理流程
浏览器端发出
http
连接请求,主线程创建http
对象接收请求,并且根据服务器事件处理模式,进行处理,若当前为同步IO模拟的Proactor模式,主线程还需要进行 IO 数据读取将请求报文读入m_read_buf
中,并且将该请求对象插入线程池中的任务队列,工作线程从任务队列中取出一个任务进行处理;若当前为Reactor模式,则直接将请求对象插入线程池中的任务队列,工作线程从任务队列中取出一个任务,进行处理工作线程取出任务之后,需要根据服务器事件处理模式,进行处理,若当前为同步IO模拟Proactor模式,工作线程则直接对主线程准备好的数据(
读入至m_read_buf
的数据),通过主、从状态机对请求报文进行解析;若当前为Reactor模式,工作线程则还需要进行 IO 数据读取,将请求报文读入m_read_buf
中。后通过主、从状态机对请求报文进行解析解析完成之后,完成资源文件的内存映射,并生成响应报文写入
m_write_buf
中,通过向量缓冲区iovec
对资源文件的内存映射以及m_write_buf
进行指向,并使用writev
方法从向量缓冲区指向的多个缓冲区收集数据,返回给浏览器端
2.主-从状态机报文解析
使用主从状态机模式对请求报文进行解析,从状态机负责读取报文的一行,主状态机负责对该行数据进行解析,主状态机内部调用从状态机,从状态机驱动主状态机
主状态机:
主状态机,根据请求报文的组成,请求行、请求头、空行、请求体(空行不为有效内容),分为三种状态,用于标识当前请求报文解析的位置
CHECK_STATE_REQUESTLINE
,解析请求行CHECK_STATE_HEADER
,解析请求头CHECK_STATE_CONTENT
,解析消息体,仅用于解析POST请求
从状态机:
从状态机,从请求报文中一行一行的读取解析,使用3种状态标识解析一行的读取状态
LINE_OK
,完整读取一行LINE_BAD
,报文语法有误LINE_OPEN
,读取的行不完整
(1)从状态机逻辑
在HTTP报文中,每一行的数据由\r\n
作为结束字符,空行则是仅仅是字符\r\n
。因此,可以通过查找\r\n
将报文拆解成单独的行进行解析,项目中便是利用了这一点
从状态机负责读取m_read_buf
中的数据,将每行数据末尾的\r\n
置为\0\0
,并更新从状态机在m_read_buf
中读取的位置m_checked_idx
,将m_check_idx
更新至下一行的开始位置,以此来驱动主状态机解析
从状态机从
m_read_buf
中逐字节读取,判断当前字节是否为\r
- 接下来的字符是
\n
,将\r\n
修改成\0\0
,将m_checked_idx
指向下一行的开头,则返回LINE_OK
,表示读取到一个完整的行- 接下来达到了
buffer
末尾,表示buffer
还需要继续接收,返回LINE_OPEN
,表示行数据尚且不完整- 否则,表示语法错误,返回
LINE_BAD
当前字节不是
\r
,判断是否是\n
(一般是上次读取到\r
就到了buffer
末尾,没有接收完整,再次接收时会出现这种情况)
- 如果前一个字符是
\r
,则将\r\n
修改成\0\0
,将m_checked_idx
指向下一行的开头,则返回LINE_OK
- 当前字节既不是
\r
,也不是\n
,表示接收不完整,需要继续接收,返回LINE_OPEN
(2)主状态机逻辑
主状态机初始状态是CHECK_STATE_REQUESTLINE
(请求行),通过调用从状态机来驱动主状态机,在主状态机进行解析前,从状态机(parse_line
)已经将每一行的末尾\r\n
符号改为\0\0
,以便于主状态机直接取出对应字符串(每次从请求报文中取出一行)进行处理
在状态机中有一个标识m_start_line
(初始为0),用于标识m_read_buf
中一行数据的开始,主状态机可以取出m_start_line
索引位置至下一个\0\0
中间的内容作为请求报文的一行数据进行处理解析
当从状态机将一行末尾\r\n
变为\0\0
,并且主状态机取出m_start_line
索引位置至\0\0
中间的一行数据,就会将m_start_line
通过m_check_idx
进行更新,指向下一行的开始位置
解析请求行:
CHECK_STATE_REQUESTLINE
- 主状态机的初始状态,调用
parse_request_line
函数解析请求行- 解析函数从
m_read_buf
中解析HTTP
请求行,获得请求方法、目标URL
及HTTP
版本号- 解析完成后主状态机的状态变为
CHECK_STATE_HEADER
请求头
解析请求头:
CHECK_STATE_HEADER
- 调用
parse_headers
函数解析请求头部信息,以及空行信息- 判断是空行还是请求头(通过判断获取的一行数据的首位是否是
\0
)
- 若是空行,进而判断
content-length
是否为0,如果不是0
,表明是POST
请求,则状态转移到CHECK_STATE_CONTENT
,否则说明是GET
请求,则此时已经获得了一个完整的GET
请求则报文解析结束- 若解析的是请求头部字段,则主要分析
connection
字段,content-length
字段,其他字段可以直接跳过,也可以根据需求继续分析connection
字段判断是keep-alive
还是close
,决定是长连接还是短连接content-length
字段,这里用于读取post
请求的消息体长度
解析请求体:
当前请求为POST
请求则还需要对请求报文的请求体部分进行解析
CHECK_STATE_CONTENT
- 仅用于解析
POST
请求,调用parse_content
函数解析消息体- 用于保存
POST
请求消息体,为后面的登录和注册做准备
3.功能逻辑单元d0_request
使用主-从状态机解析请求报文,若得到一个完整、正确的HTTP
请求时,就需要分析目标文件的属性,如果目标文件存在、对所有用户可读,且不是目录,则使用mmap
对该访问资源进行内存映射(可以把文件的某个部分直接映射到进程的地址空间中,这样可以通过指针直接访问文件内容,而无需调用读写文件的系统调用。这通常可以提高文件操作的性能,因为减少了内核与用户空间之间的数据拷贝),并告诉调用者获取文件成功
4.响应报文生成
根据主从状态机解析报文,以及功能逻辑单元的返回结果,为响应报文生成对应的信息,写入m_write_buf
中,并且使用iovec
向量缓冲区分别指向m_read_buf
以及访问资源内存映射的地址(若访问资源存在且访问合法)
5.响应报文的发送
使用writev
方法,从iovec
向量缓冲区指向的缓冲区,写入数据至该http
连接对应的套接字,写缓冲区,发送给浏览器端
四、定时器处理非活跃连接
1.相关概念
非活跃
- 是指客户端(这里是浏览器)与服务器端建立连接后,长时间不交换数据,一直占用服务器端的文件描述符,导致连接资源的浪费
定时事件
- 是指固定一段时间之后触发某段代码,由该段代码处理一个事件,如从内核事件表删除事件,并关闭文件描述符,释放连接资源
定时器
- 是指利用结构体或其他形式,将多种定时事件进行封装起来。具体的,这里只涉及一种定时事件,即定期检测非活跃连接,这里将该定时事件与连接资源封装为一个结构体定时器
定时器容器
- 是指使用某种容器类数据结构,将上述多个定时器组合起来,便于对定时事件统一管理。具体的,项目中使用升序链表将所有定时器串联组织起来
2.定时器定时方法
本项目中,服务器主循环accept
每一个http
连接之后,会为每一个http
连接创建一个定时器,并对每个连接进行定时。另外,利用升序时间链表容器将所有定时器串联起来,若主循环接收到定时通知,则在链表中依次执行定时任务
项目中使用的定时方法(定时器系统工作原理):
利用
alarm
函数周期性地触发SIGALRM
信号,信号处理函数利用管道通知主循环,主循环(使用 IO 复用系统在管道读端)接收到该信号后对升序链表上所有定时器进行处理,若该段时间内没有交换数据,则将该连接关闭,释放所占用的资源,若该端段时间内存在数据交换,则将该定时器的到期时间继续延长3个单位,并且调整该定时器在升序双向链表上的位置
3.信号通知流程
信号是软中断,Linux
下的信号采用的异步处理机制,信号处理函数和当前进程是两条不同的执行路线。具体的,当进程收到信号时,操作系统会中断进程当前的正常流程,转而进入信号处理函数执行操作,完成后再返回中断的地方继续执行
为避免信号竞态现象发生,信号处理期间系统不会再次触发它。所以,为确保该信号不被屏蔽太久,信号处理函数需要尽可能快地执行完毕
这里的解决方案是,信号处理函数仅仅发送信号通知程序主循环,将信号对应的处理逻辑放在程序主循环中,由主循环执行信号对应的逻辑代码
即统一事件源:
- 信号处理函数使用管道套接字将信号传递给主循环,信号处理函数往管道的写端写入信号值,主循环则从管道的读端读出信号值,使用
I/O
复用系统调用来监听管道读端的可读事件,这样信号事件与其他文件描述符都可以通过epoll
来监测,从而实现统一处理
4.定时器容器设计
项目中的定时器容器为带头尾结点的升序双向链表,具体的为每个连接创建一个定时器,将其添加到链表中,并按照超时时间升序排列。执行定时任务时,将到期的定时器从链表中删除
从实现上看,主要涉及双向链表的插入,删除操作,其中添加定时器的事件复杂度是O(n)
,删除定时器的事件复杂度是O(1)
5.定时任务处理函数
使用统一事件源,SIGALRM
信号每次被触发,主循环中调用一次定时任务处理函数,处理链表容器中到期的定时器
具体逻辑,如下:
遍历定时器升序链表容器,从头结点开始依次处理每个定时器,直到遇到尚未到期的定时器
- 因为链表是按照到期时间进行升序组织的,若当前定时器未到期,则后面的定时器更加没有到期
若当前时间小于定时器超时时间,跳出循环,即未找到到期的定时器
若当前时间大于定时器超时时间,即找到了到期的定时器,执行回调函数,然后将它从链表中删除,然后继续遍历
五、日志系统
1.相关概念
日志
- 由服务器自动创建,并记录运行状态,错误信息,访问数据的文件
同步日志
- 日志写入函数与工作线程串行执行,由于涉及到
I/O
操作,当单条日志比较大的时候,同步模式会阻塞整个处理流程,服务器所能处理的并发能力将有所下降,尤其是在峰值的时候,写日志可能成为系统的瓶颈生产者-消费者模型
- 并发编程中的经典模型。以多线程为例,为了实现线程间数据同步,生产者线程与消费者线程共享一个缓冲区,其中生产者线程往缓冲区中
push
消息,消费者线程从缓冲区中pop
消息阻塞队列
- 将生产者-消费者模型进行封装,使用循环数组实现队列,作为两者共享的缓冲区
异步日志
- 将所写的日志内容先存入阻塞队列,写线程从阻塞队列中取出内容,写入日志
单例模式
- 最简单也是被问到最多的设计模式之一,保证一个类只创建一个实例,同时提供全局访问的方法
2.整体概述
本项目中,使用单例模式(懒汉式)创建日志系统,对服务器运行状态、错误信息和访问数据进行记录,该系统可以实现按天分类,超行分类功能,可以根据实际情况分别使用同步和异步写入两种方式
其中异步写入方式,将生产者-消费者模型封装为阻塞队列,创建一个写线程,工作线程将要写的内容push
进队列,写线程从队列中取出内容,写入日志文件
懒汉式单例模式:
类的唯一实例对象直到第一次获取的时候才产生
- 使用的是局部静态变量的懒汉式单例模式,获取实例对象的静态函数接口中,使用
static
创建静态的局部类对象,**c++11
后这样的写法是线程安全的**
3.日志系统工作流程
获取日志类唯一实例对象,对日志系统进行初始化,按照当前时间
当前年份_当前月份_当前日_日志文件名
的方式生成日志文件,判断日志写入方式,异步\同步
- 异步,则还需要创建
string
类型的阻塞队列,以及创建写线程用于从阻塞队列中取出日志信息,异步写入日志文件写日志
- 判断是否分文件,根据当前月份的日判断,若目前时间月份的日与系统中上一个日志文件记录的日不一致,则需要创建新的日志文件;当然系统中存在一个成员变量
m_split_lines
标识一个日志文件记录的最大行数,若当前日志文件的记录日志行数量超过m_split_lines
,则需要在之前日志文件名称增加一个后缀,标识当前时间的第几份日志- 判断写入方式,若为异步,则将日志信息
push
至日志信息的阻塞队列中,之后由写线程从队列中取出内容,写入日志文件;若为同步,则将日志信息,直接写入日志文件
六、数据库连接池
1.概念及优势
数据库连接池中的资源为一组数据库连接,由程序动态地对池中的连接进行使用,释放。数据库连接池是一种技术,主要用于管理数据库连接,以提高数据库操作的效率和性能。它允许系统预先创建一定数量的数据库连接,并将这些连接存储在一个“池”中。当应用程序需要访问数据库时,它可以直接从池中取出一个现有的连接,使用完毕后再放回池中,而不是每次需要时都创建和销毁连接
主要优势:
资源重用:重复使用现有的数据库连接,减少创建和销毁连接的开销
提高性能:减少数据库连接的开销可以显著提高应用程序的性能
控制并发:连接池可以限制系统中数据库连接的最大数量,避免过多的并发连接导致数据库崩溃
简化连接管理:开发者只需关心如何从连接池中获取和返回连接,无需手动管理每个数据库连接的生命周期
为什么创建数据库连接池:
从一般流程中可以看出,若系统需要频繁访问数据库,则需要频繁创建和断开数据库连接,而创建数据库连接是一个很耗时的操作,也容易对数据库造成安全隐患
在程序初始化的时候,集中创建多个数据库连接,并把他们集中管理,供程序使用,可以保证较快的数据库读写速度,更加安全可靠。可以实现资源重用、提高性能、控制并发
2.数据库访问流程
连接数据库:
- 首先,需要通过数据库驱动或API建立与数据库的连接。在使用连接池的情况下,这通常意味着从连接池中获取一个可用的连接
执行查询或更新:
- 通过连接执行SQL命令。这些命令可以是数据查询(SELECT)、数据更新(INSERT、UPDATE、DELETE)或其他数据库操作(如事务控制)
- SQL命令可以直接编写,或者通过使用预编译的查询来提高性能和安全性(例如,使用预编译的语句可以防止SQL注入攻击)
处理结果:
- 如果执行的是查询操作,需要处理返回的数据。这通常涉及从数据库获取结果集,并在应用程序中对其进行迭代,以读取单行或多行数据
- 对于更新操作,可能需要检查操作的影响(例如,影响的行数)
关闭连接:
- 在数据访问完成后,及时关闭数据库连接是很重要的。这通常意味着将连接返回到连接池中,供后续使用
- 确保释放资源,如结果集和数据库连接对象,以避免资源泄露和其他潜在问题
异常处理:
- 在整个数据库操作过程中,应妥善处理可能出现的异常或错误,例如连接失败、SQL错误等
- 通常需要在代码中添加错误处理逻辑,以确保即使在出现错误的情况下,程序也能正常运行,同时保证所有资源都被正确清理
事务管理:
- 对于需要多个步骤共同完成的数据库操作,常常需要用到事务管理。事务确保这些步骤要么全部完成,要么全部不做,这是通过事务的提交(commit)和回滚(rollback)操作来控制的
3.实现
懒汉式单例模式:
- 使用局部静态变量的类对象的懒汉式单例模式,创建数据库连接池
RAII机制释放数据库连接:
- 使用
RAII
机制,将连接池中的数据库连接对象进行管理,当从连接池中连接队列中取出的数据库连接使用完毕,通过RAII
类对象的析构函数,将使用完成的数据库连接,归还至连接池的连接队列中
七、登录注册
1.载入数据库用户名密码表
在
webServer
服务器初始化时,会从数据库连接池中取出一个数据库连接,从数据库中将所有以及注册的用户信息,加载到本地服务器的map中,之后用于对登录用户的校验,以及新注册用户的用户名时候已经存在的注册校验,若新用户注册成功,会更新本地服务器的map,以及数据库中的用户信息
2.注册登录流程
通过请求报文URL,
/
后面的第一个字符判断(对html
中action
行为设置标志位)当前是登录or
注册
- 若为登录或者注册,会先将当前POST请求,请求体中的用户名以及密码获取
- 若为登录,则会直接使用请求体获取的用户名密码与服务器本地中的map进行校验,若map存在一致的用户名以及密码,则登录成功,浏览器端进行页面跳转至欢迎页面
- 若为注册,则会直接使用请求体获取的用户名密码与服务器本地中的map进行校验,若map不存在一致的用户名,则注册成功,则浏览器跳转至登录页面
3.页面跳转
根据html
提交表单中的action
行为标志位进行判断
/
- 首先解析,请求报文的URL,若URL中只有
/
,则浏览器端跳转至,判断页面,判断用户的需求,是登录账号,还是新用户注册新账号
/0
- 解析,请求报文的URL,若URL中有
/0
,判断/
后的第一个字符为0
,则浏览器端跳转至,注册页面
/1
- 解析,请求报文的URL,若URL中有
/0
,判断/
后的第一个字符为0
,则浏览器端跳转至,注册页面
/5
- 解析,请求报文的URL,若URL中有
/5
,判断/
后的第一个字符为5
,则浏览器端跳转至,显示图片页面
/6
- 解析,请求报文的URL,若URL中有
/6
,判断/
后的第一个字符为6
,则浏览器端跳转至,显示视频页面
/7
- 解析,请求报文的URL,若URL中有
/7
,判断/
后的第一个字符为7
,则浏览器端跳转至,显示关注页面
具体的,描述了GET和POST请求下的页面跳转流程