MyTynyWeb项目介绍
-
参考文章
https://zhuanlan.zhihu.com/p/269247362
https://blog.nowcoder.net/n/16c82edbfd0041f8ae1ca44e5363f57f
项目介绍
-
实现功能
这个项目主要实现了一个web服务器,支持基于http报文传输的用户登录和注册功能,以及大文件如图片和视频传输的功能。
-
主要结构
主要由一个web服务器的三大基本部分构成,首先是io处理单元,由主线程实现,用来监听到达用户连接请求,以及已连接用户的读写请求任务,第二大部分消息队列,主线程将待处理请求对象插入消息队列,第三部分则是工作线程,这里通过线程池来实现,从消息队列中获取并发用户请求并处理,从而减小了反复建立和销毁线程的开销。请求任务处理主要包括对http报文的解析和响应逻辑,这里通过状态机编程完成,另外利用数据库来存储用户登录信息,这里还设计了map缓存来保存查询用户名和密码,减小了对数据库查询。最后设计了基于升序链表的定时器来对非活跃连接进行处理。
服务器并发模型
并发模型
服务器的并发模型是半同步模型,主线程是异步线程,监听客户端请求以及向已连接用户读写事件。工作线程是同步线程,处理任务。
什么是同步?什么是异步?
同步是按顺序执行,异步则受信号,中断等驱动的。
reactor模式和proactor模式
Reactor和Proactor模式的主要区别就是真正的读取和写入操作是有谁来完成的,Reactor中需要工作线程读取或者写入数据,Proactor模式中,工作线程不需要进行实际读写过程。
五大IO模型
网络IO涉及用户空间和内核空间,一般会经历两个阶段:
- 一阶段:等待数据准备就绪,即等待网络数据被copy到内核缓冲区
- 二阶段:将数据从内核缓冲区copy到用户缓冲区
常见的IO模型有五种:
-
阻塞IO
等待「内核数据准备好」和「数据从内核态拷贝到用户态」,直到完成。用户在发起IO请求后,不能做任何事情,对CPU的资源利用率不够。
-
非阻塞IO
判断「内核数据准备好」,没有立即返回,准备好则继续执行「数据从内核态拷贝到用户态」后再返回。但是为了判断内核数据是否准备好,仍需要不断地轮询、重复请求,消耗了大量的CPU的资源。一般很少直接使用这种模型。
-
IO多路复用
主要作用是可以避免同步非阻塞IO模型中轮询等待的问题,可达到在同一个线程内同时处理多个IO请求的目的。
-
信号驱动IO:
会给对应的socket注册一个信号函数,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用IO读写操作来进行实际的IO请求操作。
-
异步IO:
linux中,可以调用aio_read函数告诉内核描述字缓冲区指针和缓冲区的大小、文件偏移及通知的方式,然后立即返回,当内核将数据拷贝到缓冲区后,再通知应用程序。
IO复用技术
I/O多路复用(multiplexing)的本质是通过一种机制(系统内核缓冲I/O数据),让单个进程可以监视多个文件描述符,一旦某个描述符就绪(读就绪或写就绪),就通知程序进行相应的读写操作。
select函数
-
原理
-
以数组形式的监听文件描述符集并拷贝到内核中,分别监听读、写、异常动作。
-
内核态采用轮询方式,遍历查询所有fd的状态
-
返回所有fd,用户需要轮询所有fd,判断其触发事件。
-
-
优势
其最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。
-
缺点
- select通过线性表描述文件描述符集合,文件描述符有上限,一般是1024,每次调用select,都需要把fd_set集合从用户态拷贝到内核态,如果fd_set集合很大时,那这个开销也很大,比如百万连接却只有少数活跃连接时这样做就太没有效率。
- 需要遍历select返回结果,来判断监听事件的发生。
poll函数
poll的机制与select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询,poll是链表结构来描述文件描述符,突破了文件描述符上限,最大可以打开文件的数目。
epoll函数
- 原理
- 监听文件描述符集合,并以红黑树形式放在内核里保存,减少了监听文件描述符在内核和用户态之间拷贝带来的开销,并且没有数量限制。
- epoll是基于事件机制的,每个fd的回调函数(中断函数),将发生事件描述符返回用于存储准备就绪的链表。所以在内核态不用轮询查询文件描述符集合的状态。
- 将就绪链表中文件描述符返回给数组,用户层不用轮询遍历。
应用场景分析
- 当监测的fd数目较小,且各个fd都比较活跃,建议使用select或者poll。
- 当监测的fd数目非常大,成千上万,且单位时间只有其中的一部分fd处于就绪状态,这个时候使用epoll能够明显提升性能。
什么是ET(边缘触发)、LT(水平触发)
可写事件和可读事件?
可读事件,当内核接收缓存区数据不为空,可写事件、当内核写缓存区有空余空间。
ET和LT模式
-
实现原理
ET模式下,每次通知用户事件后,会将就绪表中的文件描述符移去。
-
通知方式
对于LT模式,只要socket处于可读或者可写状态,那么就一直通知用户可读写事件。对于ET模式,只有从不可读到可读,不可写到可写,才会通知一次事件。因此ET模式的通知会相对较少。
-
编程
ET模式下因为ET模式下,当可读事件就绪,用户可能不会一次性把所有数据读取,所以容易遗漏事件,应用需要添加业务逻辑来处理。
EPOLLONESHOT模式
- 一个线程读取某个socket上的数据后开始处理数据,在处理过程中该socket上又有新数据可读,此时另一个线程被唤醒读取,此时出现两个线程处理同一个socket。
- 我们期望的是一个socket连接在任一时刻都只被一个线程处理,通过epoll_ctl对该文件描述符注册epolloneshot事件,一个线程处理socket时,其他线程将无法处理,当该线程处理完后,需要通过epoll_ctl重置epolloneshot事件。
线程池相关
线程池的实现
-
线程池
初始化多个线程,并用线程描述符数组来存储线程池标识。
-
消息队列
指针链表,指针里存储指向http报文类对象的地址。
线程池的同步机制有哪些?
主要采用了互斥锁和信号量,互斥锁。
- 互斥锁用来保障对消息队列的消息添加和删除的同步
- 信号量用来同步消息队列中消息数量。
线程池中的工作线程是一直等待吗?
将线程池中的工作线程都设置为阻塞等待在请求队列是否不为空的条件上。
你的线程池工作线程处理完一个任务后的状态是什么?
这里要分两种情况考虑
-
当处理完任务后如果请求队列为空时,则这个线程重新回到阻塞等待的状态。
-
当处理完任务后如果请求队列不为空时,那么这个线程将处于与其他线程竞争资源的状态,谁获得锁谁就获得了处理事件的资格。
如果同时1000个客户端进行访问请求,线程数不多,怎么能及时响应处理每一个呢?
将访问请求插入消息队列,线程竞争获取请求任务进行处理,线程池减小每次获取任务建立连接,然后处理完后的开销。
设计模式
单例模式设计数据库连接池类
class A {
//设置静态成员函数
static A* GetInstance(){
static A a;
return &a;
};
private:
int data;
}
int mian() {
A* a=GetInstance();
return;
}
HTTP报文解析
基本步骤
-
读取请求报文到缓存区
如果是reactor模式,那么由线程池里的工作线程完成读取工作,如果是proactor模式,那么利用主线程读取来完成。
-
解析报文,状态机编程实现
主要有三种状态,检查请求行状态,检查消息头状态、检查消息体状态。从缓存区读取一行数据,然后依据当前状态进行报文解析。首先进入请求行状态,如果获取完整行解析行,进入检查消息头状态,解析消息行,当遇到空行,判断消息体长度字段,如果不为0,跳转到检查消息体状态。
-
依据请求报文信息,处理登录注册逻辑,以及返回文件的映射。
用了状态机,为什么要用状态机?
场景比较适合,有利于理清编程逻辑,解析报文的一个过程,就是循环读取报文的一行,然后在不同状态间进行转移。
HTTPs了解吗?
基本概述
https=http+TLS/SSL
TLS/SSL协议位于应用层协议和TCP之间,构建在TCP之上,由TCP协议保证数据传输版的可靠性,任何数据到权达TCP之前,都经过TLS/SSL协议处理。
为什么要用https加密传输?
https是加密传输协议,可以保障客户端到服务器端的传输数据安全。用户通过http协议访问网站时,浏览器和服务器之间是明文传输,这就意味着用户填写的密码、帐号、交易记录等机密信息都是明文,随时可能被泄露、窃取、篡改,被第三者加以利用。安装SSL证书后,使用https加密协议访问网站,可激活客户端浏览器到网站服务器之间的"SSL加密通道"(SSL协议),实现高强度双向加密传输,防止传输数据被泄露或篡改。
https的ssl连接过程
-
客户端提交https请求
-
服务器响应客户,并把证书公钥发给客户端
-
客户端验证证书公钥的有效性
-
有效后,会生成一个会话密钥
-
用证书公钥加密这个会话密钥后,发送给服务器
-
服务器收到公钥加密的会话密钥后,用私钥解密,回去会话密钥
-
客户端与服务器双方利用这个会话密钥加密要传输的数据进行通信
为什么要使用mmap映射?
用户态对常规文件的读取,需要先查询页面缓存,如果发送缺页中断,将缺失文件映射到内存,然后再从页缓存拷贝到用户空间。mmap优势在于,直接把磁盘文件与进程虚拟地址做了映射,这样就可以直接跳过内核的页缓存,只使用一次数据拷贝。
数据库登录注册
登录注册
注册登录基本步骤
- 从数据库载入用户名和密码信息到缓存map
- 报文解析,提取用户名和密码
- 注册和登录检验,返回跳转网页报文。
用户状态保存
为什么需要状态保存?
每次用户登录跳转到上次浏览页面,那么必须有一方,服务器/用户端浏览器需要保存这一状态
cookie
-
保存在客户端
-
跟踪用户访问状态
Cookie可以让服务端程序跟踪每个客户端的访问,但是每次客户端的访问都必须传回这些Cookie,这无形地增加了客户端与服务端的数据传输量。
-
安全性
但是由于cookie 是存在用户端,最关键是用户可以是可见的,并可以随意的修改,很不安全。
session(会话)
session意为会话,主要作用是记录了用户会话相关的数据,如:身份信息、登陆状态、用户的个性配置、权限列表、其他的一些通用数据(比如购物车)。通常我们把通用的、频繁存取的、小数据量的 跟用户相关的 数据放入SEESION。
-
原理
当客户端第一次访问服务器时,服务器创建一个session,同时生成一个唯一的会话key,即sessionID。接着sessionID及session分别作为key和value保存到缓存中,也可以保存到数据库中,然后服务器把sessionID以cookie的形式发送给客户端浏览器,浏览器下次访问服务器时直接携带上cookie中的sessionID,服务器再根据sessionID找到对应的session进行匹配。
-
以cookie形式实现sessionID发送
-
session由服务端产生
- sessionid返回给客户端保存至本地服务端需要一定的空间存储session,且一般为了提高响应速度,都是存储在内存中sessionID会自动由浏览器带上。
token
-
服务器端不存储用户的登录信息
-
访问流程
客户端使用用户名跟密码请求登录;
服务端收到请求,去验证用户名与密码;
验证成功后,根据传过来的唯一标识userId(也可以是mac地址等唯一标识),服务端会通过一些算法,如常用的HMAC-SHA256算法,然后加一个密钥,生成一个token,然后通过BASE64编码一下之后将这个token发送给客户端;
客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里;(通过js代码写入Local Storage,通过js获取,并不会像cookie一样自动携带)
客户端每次向服务端请求资源的时候需要带着服务端签发的 Token;
服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据。 -
通常用于用户授权。
用的mysql啊,redis了解吗?用过吗?
mysql是关系型数据库,主要用于存放持久化数据,将数据存储在硬盘中,读取速度较慢。
redis是NOSQL,即非关系型数据库,也是缓存数据库,即将数据存储在缓存中,缓存的读取速度快,能够大大的提高运行效率,但是保存时间有限。
区别
- 从类型上来说,mysql是关系型数据库,redis是缓存数据库
- mysql用于持久化的存储数据到硬盘,功能强大,但是速度较慢
- redis用于存储使用较为频繁的数据到缓存中,读取速度快
基于升序链表的定时器
为什么要用定时器?
本项目是为了方便释放那些超时的非活动连接,关闭被占用的文件描述符占用资源。
定时器结果
-
定时器
这里只涉及一种定时事件,这里将该定时事件与连接资源封装为一个定时器类。具体包括连接资源、超时时间和回调函数,将定时器封装成类,并以升序双向链表
-
定时事件
定时事件,是指固定一段时间之后触发中断,这里是删除非活动的的注册事件,并关闭对应的socket,连接次数减一。
-
连接资源
客户端套接字以及定时器。
工作原理
利用alarm函数周期性地触发SIGALRM信号,信号处理函数利用soketpair通知主循环,然后进入升序定时器链表,将超时的定时器的资源进行释放,并移除定时器。
优化
最坏的添加定时器,则是从头遍历到尾部。可以考虑用最小堆优化,这样添加只有logn。

浙公网安备 33010602011771号