拿到一个项目首先先跑通,然后再慢慢来看代码,关于怎么将这个项目跑通,上一篇已经讲过,感兴趣的小伙伴可以移步下面的链接,或者其他博主的教程。
C++入门项目:Linux下C++轻量级Web服务器 跑通|运行|测试(小白进)_c++web服务器-CSDN博客
跑通后 ,我们先来看看这个服务器是怎么实现的。要知道怎么实现,首先就要知道服务器的工作流程是怎么样的 ,背后的原理是什么,这样更容易理解整个项目。
web服务器的工作流程
大致流程就是服务器会一直监听web端,如果有客户端发起请求,则创建线程处理用户的链接,与客户端(web端)建立连接,然后根据web端的需求来响应。那么具体的实现细节分解为几个部分来实现,可分为以下5个部分来实现(不是下面这个图,是下面的5个标题)
整个服务器的框架如下图所示:(来自于原作者)
上面的框架图就是该服务器项目的整体框架:总共就是3部分I/O处理、逻辑单元处理、存储处理。(一定要用面向对象的思想来理解为什么这么划分)。I/O处理就是服务器监听客户端,建立连接、读写网络数据(主线程负责监听),逻辑处理单元负责处理接收到的请求,执行业务逻辑,包括数据处理、计算、决策等操作(也就是工作线程负责处理连接请求,如:日志的输出、处理非活动链接、处理http请求(Http请求通常用的比较多的是get和post,不同的需求使用的请求不一样)。存储部分就是数据库存储客户端的信息数据等以及日志记录服务器的运行状态(例如链接了多少客户端等)。各个模块之间通过消息队列进行通信。知道大概结构后,再来一个一个细究。
1、web端和服务器端建立连接
参照项目原作者的号:两猿社(最新版Web服务器项目详解 - 00 项目概述)
这里涉及到I/O多路复用、边缘触发模式(ET)、proactor模式、线程池、get请求、post请求等知识点
采用epoll的边缘触发模式同时监听多个文件描述符,采用同步I/O模拟proactor模式处理事件,主线程负责监听客户端是否发起请求
当web端发起http请求时,主线程接收请求报文,然后将任务插入请求队列,由工作线程通过竞争从请求队列中获取任务
通过http类中的主从状态机对请求报文进行分析,根据请求报文对客户端进行http响应,然后由主线程给客户端发送响应报文。
5种I/O模型:
阻塞IO:调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的去检查这个函数有没有返回,必须等这个函数返回才能进行下一步动作
非阻塞IO:非阻塞等待,每隔一段时间就去检测IO事件是否就绪。没有就绪就可以做其他事。非阻塞I/O执行系统调用总是立即返回,不管时间是否已经发生,若时间没有发生,则返回-1,此时可以根据errno区分这两种情况,对于accept,recv和send,事件未发生时,errno通常被设置成eagain
信号驱动IO:linux用套接口进行信号驱动IO,安装一个信号处理函数,进程继续运行并不阻塞,当IO时间就绪,进程收到SIGIO信号。然后处理IO事件。
IO复用:linux用select/poll函数实现IO复用模型,这两个函数也会使进程阻塞,但是和阻塞IO所不同的是这两个函数可以同时阻塞多个IO操作。而且可以同时对多个读操作、写操作的IO函数进行检测。知道有数据可读或可写时,才真正调用IO操作函数
异步IO:linux中,可以调用aio_read函数告诉内核描述字缓冲区指针和缓冲区的大小、文件偏移及通知的方式,然后立即返回,当内核将数据拷贝到缓冲区后,再通知应用程序。
I/O多路复用:I/O多路复用(异步阻塞IO)总共涉及到3中模式(select、poll、epoll),本项目中采用的epoll
select、poll、epoll的区别;
⽂件描述符集合的存储位置
对于 select 和 poll 来说,所有⽂件描述符都是在⽤户态被加⼊其⽂件描述符集合的,每次调⽤都需要将整个集合拷⻉到内核态;epoll 则将整个⽂件描述符集合维护在内核态,每次添加⽂件描述符的时候都需要执⾏⼀个系统调⽤。系统调⽤的开销是很⼤的,⽽且在有很多短期活跃连接的情况下,由于这些⼤量的系统调⽤开销,epoll 可能会慢于 select 和 poll。⽂件描述符集合的表示⽅法
select 使⽤线性表描述⽂件描述符集合,⽂件描述符有上限(1024);poll使⽤链表来描述;epoll底层通过红⿊树来描述,并且维护⼀个就绪列表,将事件表中已经就绪的事件添加到这⾥,在使epoll_wait调⽤时,仅观察这个list中有没有数据即可。
遍历⽅式
select 和 poll 的最⼤开销来⾃内核判断是否有⽂件描述符就绪这⼀过程:每次执⾏ select 或 poll 调⽤时,它们会采⽤遍历的⽅式,遍历整个⽂件描述符集合去判断各个⽂件描述符是否有活动;epoll 则不需要去以这种⽅式检查,当有活动产⽣时,会⾃动触发 epoll 回调函数通知epoll⽂件描述符,然后内核将这些就绪的⽂件描述符放到就绪列表中等待epoll_wait调⽤后被处理。
触发模式
select和poll都只能⼯作在相对低效的LT模式下,⽽epoll同时⽀持LT和ET模式。
适⽤场景
当监测的fd数量较⼩,且各个fd都很活跃的情况下,建议使⽤select和poll;当监听的fd数量较多,且单位时间仅部分fd活跃的情况下,使⽤epoll会明显提升性能。参照:TInyWebServer面试题_tinywebserver面经-CSDN博客
边缘触发模式(ET):
LT:⽔平触发模式,只要内核缓冲区有数据就⼀直通知,只要socket处于可读状态或可写状态,就会⼀直返回sockfd;是默认的⼯作模式,⽀持阻塞IO和⾮阻塞IO
ET:边沿触发模式,只有状态发⽣变化才通知并且这个状态只会通知⼀次,只有当socket由不可写到可写或由不可读到可读,才会返回其sockfd;只⽀持⾮阻塞IO
事件处理模式:
proactor模式(半同步proactor)):主线程处理连接和读写,返回就绪事件,并将处理好的数据从内核态拷贝到用户态
reactor\proactor模型的区别
- Reactor 是⾮阻塞同步⽹络模式,主线程(I/O处理单元)只负责监听文件描述符上是否有事件发生,有的话立即通知工作线程(逻辑单元 ),读写数据、接受新连接及处理客户请求均在工作线程中完成。通常由同步I/O实现。
- Proactor 是异步⽹络模式, 主线程和内核负责处理读写数据、接受新连接等I/O操作,工作线程仅负责业务逻辑,如处理客户请求。通常由异步I/O实现。
线程池: 线程池其实就是一种多线程处理形式,处理过程中可以将任务添加到队列中,然后在创建线程后自动启动这些任务。
线程池的定义:
template<typename T>
class threadpool{
public://构造函数 初始化参数threadpool( connection_pool *connpool,int thread_number=8, int max_requests=100000);//析构函数~threadpool();//向请求队列中插入任务请求bool append(T* request);private://工作线程运行的函数//它不断从工作队列中取出任务并执行之static void *worker(void *arg);void run();private://线程数
int m_thread_number;//队列中允许最大的请求数
int m_max_requests;//描述线程池的数组,大小为m_thread_number
pthread_t *m_threads;// 请求队列
std::list<T *> m_threads;//保护队列请求的互斥锁
locker m_queuelocker;//是否有任务需要处理 信号量
sem m_queuestat;//是否结束线程
bool m_stop;//数据库连接池
connection_pool *m_connPool;};
各成员函数的定义:
//构造函数中创建线程池
template<typename T>
threadpool<T>::threadpool(connection_pool *connPool, int thread_number, int max_requests) : m_thread_number(thread_number), m_max_requests(max_requests), m_stop(false), m_threads(NULL),m_connPool(connPool){if(thread_number<=0||max_requests<=0)throw std::exception();//线程id初始化m_threads=new pthread_t[m_thread_number];if(!m_threads)throw std::exception();for(int i=0;i<thread_number;++i){//循环创建线程,并将工作线程按要求进行运行if(pthread_create(m_threads+i,NULL,worker,this)!=0){delete [] m_threads;throw std::exception();}//将线程进行分离后,不用单独对工作线程进行回收if(pthread_detach(m_threads[i])){delete[] m_threads;throw std::exception();}}
}//向任务队列添加任务
template<typename T>
bool threadpool<T>::append(T* request){ m_queuelocker.lock();//根据硬件,预先设置请求队列的最大值if(m_workqueue.size()>m_max_requests){m_queuelocker.unlock();return false;}//添加任务m_workqueue.push_back(request);m_queuelocker.unlock();//信号量提醒有任务要处理m_queuestat.post();return true;
}//线程处理
template<typename T>
void* threadpool<T>::worker(void* arg){//将参数强转为线程池类,调用成员方法threadpool* pool=(threadpool*)arg;pool->run();return pool;
}//run执行任务
template<typename T>
void threadpool<T>::run()
{while(!m_stop){ //信号量等待m_queuestat.wait();//被唤醒后先加互斥锁m_queuelocker.lock();if(m_workqueue.empty()){m_queuelocker.unlock();continue;}//从请求队列中取出第一个任务//将任务从请求队列删除T* request=m_workqueue.front();m_workqueue.pop_front();m_queuelocker.unlock();if(!request)continue;//从连接池中取出一个数据库连接request->mysql = m_connPool->GetConnection();//process(模板类中的方法,这里是http类)进行处理request->process();//将数据库连接放回连接池m_connPool->ReleaseConnection(request->mysql);}
}
那么在客户端和服务器之间进行请求--响应时,有两种最常用到的请求方式是:
- GET
- POST
get请求:最常用于向服务器查询某些信息,例如查询操作、搜索操作、读操作等
post请求:通常用于向服务器发送应该被保存的数据,例如本项目中的登录注册请求
HTTP请求报文由请求行(request line)、请求头部(header)、空行和请求数据四个部分组成。
请求行,用来说明请求类型,要访问的资源以及所使用的HTTP版本。
GET说明请求类型为GET,/562f25980001b1b106000338.jpg(URL)为要访问的资源,该行的最后一部分说明使用的是HTTP1.1版本。请求头部,紧接着请求行(即第一行)之后的部分,用来说明服务器要使用的附加信息。
HOST,给出请求资源所在服务器的域名。
User-Agent,HTTP客户端程序的信息,该信息由你发出请求使用的浏览器来定义,并且在每个请求中自动发送等。
Accept,说明用户代理可处理的媒体类型。
Accept-Encoding,说明用户代理支持的内容编码。
Accept-Language,说明用户代理能够处理的自然语言集。
Content-Type,说明实现主体的媒体类型。
Content-Length,说明实现主体的大小。
Connection,连接管理,可以是Keep-Alive或close。
空行,请求头部后面的空行是必须的即使第四部分的请求数据为空,也必须有空行。
请求数据也叫主体,可以添加任意的其他数据。
服务器 响应报文:
HTTP响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。
状态行,由HTTP协议版本号, 状态码, 状态消息 三部分组成。
第一行为状态行,(HTTP/1.1)表明HTTP版本为1.1版本,状态码为200,状态消息为OK。消息报头,用来说明客户端要使用的一些附加信息。
第二行和第三行为消息报头,Date:生成响应的日期和时间;Content-Type:指定了MIME类型的HTML(text/html),编码类型是UTF-8。空行,消息报头后面的空行是必须的。
响应正文,服务器返回给客户端的文本信息。空行后面的html部分为响应正文。
HTTP有5种类型的状态码,具体的:
1xx:指示信息--表示请求已接收,继续处理。
2xx:成功--表示请求正常处理完毕。
200 OK:客户端请求被正常处理。
206 Partial content:客户端进行了范围请求。
3xx:重定向--要完成请求必须进行更进一步的操作。
301 Moved Permanently:永久重定向,该资源已被永久移动到新位置,将来任何对该资源的访问都要使用本响应返回的若干个URI之一。
302 Found:临时重定向,请求的资源现在临时从不同的URI中获得。
4xx:客户端错误--请求有语法错误,服务器无法处理请求。
400 Bad Request:请求报文存在语法错误。
403 Forbidden:请求被服务器拒绝。
404 Not Found:请求不存在,服务器上找不到请求的资源。
5xx:服务器端错误--服务器处理请求出错。
500 Internal Server Error:服务器在执行请求时出现错误。
2、连接数据库
- 单例模式创建数据库连接池,避免频繁建立连接,用于后续web端登录和注册校验访问服务器数据库
单例模式: 单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。
3、实现Web端的登录和注册
web访问的欢迎界面为GET请求,登录和注册界面是POST请求。
欢迎界面有新用户(0)和已有账号(1)两个选项,若选择新用户,会跳转注册(3)界面,注册成功或选择已有账号,跳转登录(2)界面,注册或登录失败会提示失败,成功和失败为0,1
4、记录服务器的运行状态(日志记录设计)
同步的方式下,工作线程直接写入日志文件
异步会另外创建一个写线程,工作线程将要写的内容push进请求队列,通过写线程写入文件
日志文件支持按日期分类,和超过最大行数自动创建新文件
5、处理非连接活动(定时器设计)
由于非活跃连接占用了连接资源,严重影响服务器的性能,通过实现一个服务器定时器,处理这种非活跃连接,释放连接资源。
利用alarm函数周期性地触发SIGALRM信号,该信号的信号处理函数利用管道通知主循环执行定时器链表上的定时任务.
先写到写到这,后面有时间再写!! 可以去原作者的公主号:两猿社 看