上期写了简历项目链接简历项目烂大街怎么办?教你最有谱的摆烂,有位读者照做以后,拿下了主管面,在群里宣传以后,最近多了不少小伙伴来催我更新服务器项目相关知识点。
这份总结是我之前秋招的时候,根据每次面试的问题,不断查漏补缺总结而成,迭代了很多次。每次遇到新的问题,自己在网上边查边总结,当时主要是自己看嘛,也没有什么版权问题,但是现在要发在微信公众号这个公开平台,就需要追本溯源的查一查当初内容来源,尊重原作者的成果。我会尽量确认出处,如有侵权烦请告知!
说在前面的话
每个人服务器项目实现的功能不同,可以延伸的方向也会不同,第一节关于项目本身细节问题仅限于个人改进后的项目,大家只需参考与自己重合的内容即可。
面试官的问题千奇百怪,通常问你的时候不太可能就像下文标题一样,非常精准的问你某某知识点。通常可能会抛出一些笼统的问题,这个时候就需要你快速定位一下,他到底感兴趣的是什么。所以我们需要以不变应万变,引导面试官到你准备好的内容上面来。
同时需要强调一点, 学无止境,本篇内容只能算是抛砖引玉,给大家一些借鉴,并不能止步于此,大家还是要继续做一些更深更细的学习~
当然我也知道,这只是我个人的面试问题,不一定非常全面,后面我会开源本篇笔记,如果后面有同学被问到了关于这个项目新的问题,可以更新上来,一同维护!
一、项目细节
项目介绍
这个项目主要的目的是对浏览器的链接请求进行解析处理,处理完之后给浏览器客户端返回一个响应,如文字图片视频等。服务器后端的处理方式使用socket通信,利用多路IO复用,可以同时处理多个请求,请求的解析使用预先准备好的线程池,使用reactor模式,主线程负责监听IO,获取io请求后把请求对象放入请求队列,交给工作线程。睡眠在请求队列上的工作线程被唤醒进行数据读取以及逻辑处理。利用==状态机==思想解析 Http 报文,支持 GET/POST 请求,支持长/短连接;
使用基于小根堆的定时器关闭超时请求,解决超时,连接系统资源占用问题。
架构概述
主线程:
-
在主线程中,epoll监听套接字, 处理就绪套接字上的外部IO事件,包括已连接客户的写请求(发送报文),或者新客户的连接请求。
-
将就绪IO套接字发过来的请求封装成一个requestData对象,对象里面包括了就绪文件描述符是哪一个,发过来的报文数据是啥,这些数据的处理函数是啥等等。
-
并且设置requestData中的timer为NULL,也就是处理完一个requestData就delete掉,默认是短连接。如果报文解析到长连接,则会在后面补上timer。然后将requestData放在线程池的任务队列里面等待工作线程的处理。
-
主线程还有一个while循环,利用定时器堆管理定时器结点,删除超时事件。
工作线程:
-
工作线程采用条件变量和锁的形式从任务队列里面取任务,用requestData自己的处理函数分析http报文,发送http响应。
-
如果分析到报文里面有keep-alive选项,则requestData不会销毁,而是用清空的方式保留。
-
keep-alive长连接不是永久保留的,而是设置了一个定时器(也就是keep-alive中的timer成员)超时,超出时间以后,就把他关闭掉,这里超时时间设定为500ms。
-
然后就把它加入到定时器最小堆中
-
最后由于一开始fd是epolloneshot模式,还需要再epoll ctl设置一下fd的状态,使得他可以再次被监听。
其中线程池中工作线程取用任务队列里面的任务,在工作线程调用requestData中的handleRequest进行使用状态机解析了HTTP请求
屏蔽掉SIGPIPE信号
在整个epoll监听循环开始之前,需要先屏蔽掉SIGPIPE信号。
默认读写一个关闭的socket会触发sigpipe信号。该信号的默认操作是关闭进程,这明显是我们不想要的。所以我们需要重新设置sigpipe的信号回调操作函数,比如忽略操作等,可以防止调用它的默认操作。
//处理sigpipe信号
void handle_for_sigpipe(){
struct sigaction sa; //信号处理结构体
memset(&sa, '\0', sizeof(sa));
sa.sa_handler = SIG_IGN;//设置信号的处理回调函数 这个SIG_IGN宏代表的操作就是忽略该信号
sa.sa_flags = 0;
if(sigaction(SIGPIPE, &sa, NULL))//将信号和信号的处理结构体绑定
return;
}
端口复用
用setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)消除bind时"Address already in use"错误,即设置SO_REUSEADDR 端口复用
epoll监管和EPOLLONESHOT
epoll监管套接字的时候用边沿触发+EPOLLONESHOT+非阻塞IO
EPOLLONESHOT事件
即使可以使用边缘触发模式,一个socket上的某个时间还是可能被触发多次。比如一个线程在读取完某个socket上的数据后开始处理这些数据,而在数据的处理过程中,socket上又有了新数据可以读(EPOLLIN再次被触发),此时另外一个线程被唤醒来读取这些新的数据。就会出现两个线程同时操作一个socket的局面。一个socket连接在任意时刻都只被一个线程处理,可以使用epoll EPOLLONESHOT实现。
对于注册了EPOLLONESHOT事件的文件描述符有,操作系统最多出发其注册的一个刻度、可写或异常事件,且只触发一次。除非我们使用epoll_ctl函数重置该文件描述符上注册的EPOLLONESHOT事件。
这样一个线程在处理某个socket时,其他线程是不可能有机会操作该socket,但反过来要注意,注册了EPOLLONESHOT事件的socket一旦被某个线程处理完毕,该线程就应该立即重置socket上的EPOLLONESHOT事件,以确保这个socket下一次可读时,其EPOLLIN事件能被触发,进而可以让其他线程有几回处理这个socket。
多线程和线程池
使用多线程充分利用多核CPU,并使用线程池避免线程频繁创建、销毁加大系统开销。
- 创建一个线程池来管理多线程,线程池中主要包含任务队列 和工作线程集合,将任务添加到队列中,然后在创建线程后,自动启动这些任务。使用了一个固定线程数的工作线程,限制线程最大并发数。
- 多个线程共享任务队列,所以需要进行线程间同步,工作线程之间对任务队列的竞争采用条件变量和互斥锁结合使用
- 一个工作线程先加互斥锁,当任务队列中任务数量为0时候,阻塞在条件变量,当任务数量大于0时候,用条件变量通知阻塞在条件变量下的线程,这些线程来继续竞争获取任务
- 对任务队列中任务的调度采用先来先服务算法
线程池的线程数量最直接的限制因素是CPU处理器的个数。
如果CPU是四核的,那么对于CPU密集的任务,线程池的线程数量最好也为4,或者+1防止其他因素导致阻塞。
如果是IO密集的任务,一般要多于CPU的核数,因为线程间竞争的不是CPU资源而是IO,IO的处理一般比较慢,多于核数的线程将为CPU争取更多的任务,不至于在县城处理IO的时候造成CPU空闲导致资源浪费。
解析HTTP请求
-
采用reactor事件处理模式,主线程只负责监听IO,获取io请求后把请求对象放入请求队列,交给工作线程,工作线程负责数据读取以及逻辑处理。
proactor模式将所有IO读写操作 都交给主线程和内核来处理,工作线程仅仅负责业务逻辑。
-
在主线程循环监听到读写套接字有报文传过来以后,在工作线程调用requestData中的handleRequest进行使用状态机解析了HTTP请求
-
http报文解析和报文响应 解析过程状态机如上图所示。
在一趟循环过程中,状态机先read一个数据包,然后根据当前状态变量判断如何处理该数据包。当数据包处理完之后,状态机通过给当前状态变量传递目标状态值来实现状态转移。那么当状态机进行下一趟循环时,将执行新的状态对应的逻辑。
</
-