Webserver项目
1 项目介绍
- 使用多线程模型,利用信号量实现线程间加锁;
- 利用I0复用技术Epoll与线程池实现多线程的Reactor高并发模型;
- 利用RAII机制实现了数据库连接池,减少数据库连接建立与关闭的开销;
- 利用正则与状态机解析 HTTP请求报文实现处理静态资源的请求;
- 基于小根堆实现的定时器,关闭超时的非活动连接;
- 利用webbench进行服务器压力测试;
实现能够处理高并发的访问服务器的需求
- 相关知识点:
- I0复用技术
- RAII机制
- 数据库连接池
- 小根堆实现的定时器
- 测试
- 多线程
- 并发并行
- 信号量
- 锁
- HTTP相关
- c++特性,类的封装、继承、多态的应用
- c++11,14,17新特性
2 项目难点
-
首先是服务器⽹络框架等⼀些基本系统的搭建,这部分的难点主要就是技术的理解和选型,以及将⼀些开源的框架调整后应⽤到我的项⽬中去,难点在于理解拆分项目的功能实现
-
另⼀部分是为了提⾼服务器性能所做的⼀些优化,⽐如缓存机制、内存池等⼀些额外系统的搭建。这部分的难点主要是找出服务器的性能瓶颈,然后结合⾃⼰的想法去突破这个瓶颈,提⾼服务器的性能
3 项目中线程池怎么实现,有参考开源的线程池吗
-
线程池的实现
首先创建一组线程,其数量和CPU的核心数相当,比如是4核CPU就创建4个或者5个线程。当有任务来时,主线程会通过线程选择算法,比如我在项目中使用的比较常用和简单的round robin轮询算法来选择线程中的子线程来进行任务。相比较于每次遇到任务就创建线程,显然调用现有的线程对资源消耗小得多。
具体来说,主线程和所有子线程通过一个共享的工作队列来同步,子线程都睡眠在该工作队列上。当有新的任务到来时,主线程将任务添加到工作队列中。这将唤醒正在等待任务的子线程,不过只有一个子线程将获得新任务的”接管权“,它可以从工作队列中取出任务并执行之,而其他子线程将继续睡眠在工作队列上。 -
参考开源的线程池
github上的tinyC++webserver项目
-
相关问题:
- 为什么线程数量要和CPU相当?见问题8
- 线程池的作用:通过空间换取时间,通过已经建好的一定数量的线程来减少每次工作事件到来的时候的创建线程的资源消耗,从而达到处理高并发的需求
4 项目有用到锁吗
有用到,在项目中定义locker类,使用互斥锁pthread_mutex_lock(),类中构造函数pthread_mutex_init,初始化互斥量,析构函数pthread_mutex_destroy,释放互斥量的资源
5 有用到中断吗,常用的中断函数
没有
6 定时关闭怎么实现的
通过sigalarm信号
升序双链表保存定时器,定义一个结构包含客户端socket地址,socket文件描述符,读缓存和timer定时器
timer是一个双向链表,有回调函数指针
定时器链表
主函数定义匿名管道pipe,fd[1]表示写
监听读端
7 有测试过性能吗,怎么测试
使用webbench进行测试,在10000左右客户端连接时能够保证连接的有效和正确性。
8 怎么确定线程池中线程的数量,那你有测试过线程数量对于性能的影响吗
线程池中的线程数量最直接的限制因素是CPU的处理器的数量N :如果你的CPU是4-cores的,对于CPU密集型的任务来说,那线程池中的线程数量最好也设置为4(或者+1防止其他因素造成的线程阻塞);对于IO密集型的任务,一般要多于CPU的核数,因为线程间竞争的不是CPU的计算资源而是IO,IO的处理一般较慢,多于cores数的线程将为CPU争取更多的任务,不至于在线程处理IO的过程造成CPU空闲导致资源浪费。
9 RAII机制是什么
RAII(Resource Acquisition Is Initialization)资源获取即初始化:使用局部对象来管理资源的技术称为资源获取即初始化;这里的资源主要是指操作系统中有限的东西如内存、网络套接字等等,局部对象是指存储在栈的对象,它的生命周期是由操作系统来管理的,无需人工介入;
资源的使用一般经历三个步骤a.获取资源 b.使用资源 c.销毁资源,但是资源的销毁往往是程序员经常忘记的一个环节,所以程序界就想如何在程序员中让资源自动销毁呢
整个RAII过程总结四个步骤:比如智能指针就是一个典型对象
-
设计一个类封装资源
-
在构造函数中初始化
-
在析构函数中执行销毁操作
-
使用时声明一个该对象的类
10 你⽤了epoll,说⼀下为什么⽤epoll,还有其他多路复⽤⽅式吗?区别是什么?
- 文件描述符集合的存储位置:
对于 select 和 poll 来说,所有⽂件描述符都是在⽤户态被加⼊其⽂件描述符集合的,每次调⽤都需要将整个集合拷⻉到内核态;epoll 则将整个⽂件描述符集合维护在内核态,每次添加⽂件描述符的时候都需要执⾏⼀个系统调⽤。系统调⽤的开销是很⼤的,⽽且在有很多短期活跃连接的情况下,由于这些⼤量的系统调⽤开销,epoll 可能慢于 select 和 poll - 文件描述符集合的表示方法
select 使⽤线性表描述⽂件描述符集合,⽂件描述符有上限;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会明显提升性能
11 在项目中怎么用的epoll
有调用epoll_wait
12 在多线程编程需要注意哪些问题,怎样解决
多线程编程时,首先需要避免同时多条线程操作需要注意到避免死锁的问题
13 你觉得项目还有哪些可以改进
我觉得还可以添加内存池以提升服务器的处理事务的能力。
14 项目有使用到数据库吗,怎么使用的
15 项目是本地部署还是云服务器部署,云端部署的流程和区别有哪些
项目是在云端部署的,我是在阿里云租的服务器,云端服务器在系统环境配置方面很方便,并且服务器自带公网地址,能够很方便的测试服务器性能,但是需要在服务器中设置服务器中的端口给权限,否则外部请求无法
16 项目中有用到哪些类,你怎么定义拆解出这些类的
⾸先是封装,我在项⽬中将各个模块使⽤类进⾏封装,⽐如连接⽤ httpconnection/ftpconnection 类来封装,⽇志就⽤ log 类来封装,将类的属性私有化,⽐如请求的解析状态,并且对外的接⼝设置为公有,⽐如连接的重置,不对外暴露⾃身的私有⽅法,⽐如读写的回调函数等。还有⼀个就是,项⽬中每个模块都使⽤了各⾃的命名空间进⾏封装,避免了命名冲突或者名字污染
17 项目中用到哪些多态
使用了静态多态,在线程池的部分使用了模板技术,这样可以增加代码的复用性,使得线程池能够处理不同数据类型的任务。
18 项目有用到什么软件设计模式吗
单例模式:在线程池中有使用到
单例模式可以分为懒汉式和饿汉式,两者之间的区别在于创建实例的时间不同:
懒汉式:指系统运⾏中,实例并不存在,只有当需要使⽤该实例时,才会去创建并使⽤实例。(这种⽅式要考虑线程安全)
饿汉式:指系统⼀运⾏,就初始化创建实例,当需要时,直接调⽤即可。(本身就线程安全,没有多线程的问题)
首先我们需要定义一个静态的线程池变量以及静态的线程池方法,我们使用懒汉式来实现。
当一个线程进入getinstance函数时,要创建变量,但是被切走了,此时其他线程进入,就会导致线程安全的问题,因此需要进行加锁的操作。
19 能介绍下线程池吗,线程池的优势是什么
- 线程池是空间换时间,浪费服务器的硬件资源,换取运行效率。
- 池是一组资源的集合,这组资源在服务器启动之初就被完全创建好并初始化,这称为静态资源。
- 当服务器进入正式运行阶段,开始处理客户请求的时候,如果它需要相关的资源,可以直接从池中获取,无需动态分配。
- 当服务器处理完一个客户连接后,可以把相关的资源放回池中,无需执行系统调用释放资源。
20 能介绍几个线程相关的函数吗
-
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
- 一般情况下,main函数所在的线程我们称之为主线程(main线程),其余创建的线程称之为子线程。
- 程序中默认只有一个进程,fork()函数调用,2进行
- 程序中默认只有一个线程,pthread_create()函数调用,2个线程。
- 功能:创建一个子线程
- 参数:
- thread:传出参数,线程创建成功后,子线程的线程ID被写到该变量中。
- attr : 设置线程的属性,一般使用默认值,NULL
- start_routine : 函数指针,这个函数是子线程需要处理的逻辑代码
- arg : 给第三个参数使用,传参
- 返回值:
- 成功:0
- 失败:返回错误号。这个错误号和之前errno不太一样。
获取错误号的信息: char * strerror(int errnum);
-
void pthread_exit(void *retval);
- 功能:终止一个线程,在哪个线程中调用,就表示终止哪个线程
- 参数:
retval:需要传递一个指针,作为一个返回值,可以在pthread_join()中获取到。
-
pthread_t pthread_self(void);
- 功能:获取当前的线程的线程ID
-
int pthread_equal(pthread_t t1, pthread_t t2);
- 功能:比较两个线程ID是否相等
- 不同的操作系统,pthread_t类型的实现不一样,有的是无符号的长整型,有的是使用结构体去实现的。
-
int pthread_join(pthread_t thread, void **retval);
- 功能:
- 和一个已经终止的线程进行连接
- 回收子线程的资源
- 这个函数是阻塞函数,调用一次只能回收一个子线程
- 一般在主线程中使用
- 参数:
- thread:需要回收的子线程的ID
- retval: 接收子线程退出时的返回值
- 返回值:
- 0 : 成功
- 非0 : 失败,返回的错误号
- 功能:
-
int pthread_detach(pthread_t thread);
- 功能:分离一个线程。被分离的线程在终止的时候,会自动释放资源返回给系统。
- 不能多次分离,会产生不可预料的行为。
- 不能去连接一个已经分离的线程,会报错。
- 参数:需要分离的线程的ID
- 返回值:
- 成功:0
- 失败:返回错误号
- 功能:分离一个线程。被分离的线程在终止的时候,会自动释放资源返回给系统。
-
int pthread_cancel(pthread_t thread);
- 功能:取消线程(让线程终止)
- 取消某个线程,可以终止某个线程的运行,但是并不是立马终止,而是当子线程执行到一个取消点,线程才会终止。
- 取消点:系统规定好的一些系统调用,我们可以粗略的理解为从用户区到内核区的切换,这个位置称之为取消点。
- 功能:取消线程(让线程终止)
-
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
- 初始化互斥量
- 参数 :
- mutex : 需要初始化的互斥量变量
- attr : 互斥量相关的属性,NULL
- restrict : C语言的修饰符,被修饰的指针,不能由另外的一个指针进行操作。
pthread_mutex_t *restrict mutex = xxx;
pthread_mutex_t * mutex1 = mutex;
-
int pthread_mutex_destroy(pthread_mutex_t *mutex);
- 释放互斥量的资源
-
int pthread_mutex_lock(pthread_mutex_t *mutex);
- 加锁,阻塞的,如果有一个线程加锁了,那么其他的线程只能阻塞等待
-
条件变量的类型 pthread_cond_t
-
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
-
int pthread_cond_destroy(pthread_cond_t *cond);
-
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
- 等待(此时会解锁),调用了该函数,线程会阻塞。
-
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
- 等待多长时间,调用了这个函数,线程会阻塞,直到指定的时间结束。
-
int pthread_cond_signal(pthread_cond_t *cond);
- 唤醒一个或者多个等待的线程
-
int pthread_cond_broadcast(pthread_cond_t *cond);
- 唤醒所有的等待的线程
21 线程之间怎么通信
22 服务器一般需要处理哪些事件,有哪些事件处理模式,你为什么要选择reactor模式,reactor中各个模式的区别
-
一般需要处理
-
reactor、proactor模型的区别?
- Reactor 是⾮阻塞同步⽹络模式,感知的是就绪可读写事件。在每次感知到有事件发⽣(⽐如可读就绪事件)后,就需要应⽤进程主动调⽤ read ⽅法来完成数据的读取,也就是要应⽤进程主动将 socket 接收缓存中的数据读到应⽤进程内存中,这个过程是同步的,读取完数据后应⽤进程才能处理数据。
- Proactor 是异步⽹络模式, 感知的是已完成的读写事件。在发起异步读写请求时,需要传⼊数据缓冲区的地址(⽤来存放结果数据)等信息,这样系统内核才可以⾃动帮我们把数据的读写⼯作完成,这⾥的读写⼯作全程由操作系统来做,并不需要像 Reactor 那样还需要应⽤进程主动发起 read/write 来读写数据,操作系统完成读写⼯作后,就会通知应⽤进程直接处理数据。
-
Proactor这么好⽤,那你为什么不⽤?
在 Linux 下的异步 I/O 是不完善的, aio 系列函数是由 POSIX 定义的异步操作接⼝,不是真正的操作系统级别⽀持的,⽽是在⽤户空间模拟出来的异步,并且仅仅⽀持基于本地⽂件的 aio 异步操作,⽹络编程中的 socket 是不⽀持的,也有考虑过使⽤模拟的proactor模式来开发,但是这样需要浪费⼀个线程专⻔负责 IO 的处理。
⽽ Windows ⾥实现了⼀套完整的⽀持 socket 的异步编程接⼝,这套接⼝就是 IOCP ,是由操作系统级别实现的异步 I/O,真正意义上异步 I/O,因此在 Windows ⾥实现⾼性能⽹络程序可以使⽤效率更⾼的 Proactor ⽅案 -
reactor模式中,各个模式的区别
Reactor模型是⼀个针对同步I/O的⽹络模型,主要是使⽤⼀个reactor负责监听和分配事件,将I/O事件分派给对应的Handler。新的事件包含连接建⽴就绪、读就绪、写就绪等。reactor模型中⼜可以细分为单reactor单线程、单reactor多线程、以及主从reactor模式。- 单reactor单线程模型就是使⽤ I/O 多路复⽤技术,当其获取到活动的事件列表时,就在reactor中进⾏读取请求、业务处理、返回响应,这样的好处是整个模型都使⽤⼀个线程,不存在资源的争夺问题。但是如果⼀个事件的业务处理太过耗时,会导致后续所有的事件都得不到处理。
- 单reactor多线程就是⽤于解决这个问题,这个模型中reactor中只负责数据的接收和发送,reactor将业务处理分给线程池中的线程进⾏处理,完成后将数据返回给reactor进⾏发送,避免了在reactor进⾏业务处理,但是 IO 操作都在reactor中进⾏,容易存在性能问题。⽽且因为是多线程,线程池中每个线程完成业务后都需要将结果传递给reactor进⾏发送,还会涉及到共享数据的互斥和保护机制。
- 主从reactor就是将reactor分为主reactor和从reactor,主reactor中只负责连接的建⽴和分配,读取请求、业务处理、返回响应等耗时的操作均在从reactor中处理,能够有效地应对⾼并发的场合。
具体项目中:要求主线程只负责监听文件描述符上是否有事件发生,有的话就立即将该事件通知工作线程,将 socket 可读可写事件放入请求队列,交给工作线程处理。除此之外,主线程不做任何其他实质性 的工作。读写数据,接受新的连接,以及处理客户请求均在工作线程中完成。
23 介绍一下有限状态机,你怎么使用的
逻辑单元内部的一种高效编程方法:有限状态机(finite state machine)。
有的应用层协议头部包含数据包类型字段,每种类型可以映射为逻辑单元的一种执行状态,服务器可以根据它来编写相应的处理逻辑。这是一个简单的有限状态机,只不过该状态机的每个状态都是相互独立的,即状态之间没有相互转移。状态之间的转移是需要状态机内部驱动,通过while循环和switch函数来进行实现,比如
STATE_MACHINE()
{
State cur_State = type_A;
while( cur_State != type_C )
{
Package _pack = getNewPackage();
switch( cur_State )
{
case type_A:
process_package_state_A( _pack );
cur_State = type_B;
break;
case type_B:
process_package_state_B( _pack );
cur_State = type_C;
break;
}
}
}
该状态机包含三种状态:type_A、type_B 和 type_C,其中 type_A 是状态机的开始状态,type_C 是状态机的结束状态。状态机的当前状态记录在 cur_State 变量中。在一趟循环过程中,状态机先通过getNewPackage 方法获得一个新的数据包,然后根据 cur_State 变量的值判断如何处理该数据包。数据包处理完之后,状态机通过给 cur_State 变量传递目标状态值来实现状态转移。那么当状态机进入下一趟循环时,它将执行新的状态对应的逻辑。
24 你写的响应是什么,通过什么来写的
返回静态资源,或者一些简单情况
25 介绍一下内存映射和共享内存的区别
- 共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外)
- 共享内存效果更高
- 内存
所有的进程操作的是同一块共享内存。
内存映射,每个进程在自己的虚拟地址空间中有一个独立的内存。 - 数据安全
- 进程突然退出
共享内存还存在
内存映射区消失 - 运行进程的电脑死机,宕机了
数据存在在共享内存中,没有了
内存映射区的数据 ,由于磁盘文件中的数据还在,所以内存映射区的数据还存在。
- 进程突然退出
- 生命周期
- 内存映射区:进程退出,内存映射区销毁
- 共享内存:进程退出,共享内存还在,标记删除(所有的关联的进程数为0),或者关机
如果一个进程退出,会自动和共享内存进行取消关联。
26 介绍几个内存映射的函数mmap
-
两个函数mmap创建,munmap释放
-
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
- 功能:将一个文件或者设备的数据映射到内存中
- 参数:
- void *addr: NULL, 由内核指定
- length : 要映射的数据的长度,这个值不能为0。建议使用文件的长度。
- 获取文件的长度:stat lseek
- prot : 对申请的内存映射区的操作权限
- PROT_EXEC :可执行的权限
- PROT_READ :读权限
- PROT_WRITE :写权限
- PROT_NONE :没有权限
要操作映射内存,必须要有读的权限
PROT_READ、PROT_READ|PROT_WRITE
- flags :
- MAP_SHARED : 映射区的数据会自动和磁盘文件进行同步,进程间通信,必须要设置这个选项
- MAP_PRIVATE :不同步,内存映射区的数据改变了,对原来的文件不会修改,会重新创建一个新的文件。(copy on write)
- fd: 需要映射的那个文件的文件描述符
- 通过open得到,open的是一个磁盘文件
- 注意:文件的大小不能为0,open指定的权限不能和prot参数有冲突。
prot: PROT_READ open:只读/读写
prot: PROT_READ | PROT_WRITE open:读写
- offset:偏移量,一般不用。必须指定的是4k的整数倍,0表示不便宜。
- 返回值:返回创建的内存的首地址
失败返回MAP_FAILED,(void *) -1
-
int munmap(void *addr, size_t length);
- 功能:释放内存映射
- 参数:
- addr : 要释放的内存的首地址
- length : 要释放的内存的大小,要和mmap函数中的length参数的值一样。
-
-
使用内存映射实现进程间通信:
- 有关系的进程(父子进程)
- 还没有子进程的时候
- 通过唯一的父进程,先创建内存映射区
- 有了内存映射区以后,创建子进程
- 父子进程共享创建的内存映射区
- 还没有子进程的时候
- 没有关系的进程间通信
- 准备一个大小不是0的磁盘文件
- 进程1 通过磁盘文件创建内存映射区
- 得到一个操作这块内存的指针
- 进程2 通过磁盘文件创建内存映射区
- 得到一个操作这块内存的指针
- 使用内存映射区通信
注意:内存映射区通信,是非阻塞。
- 有关系的进程(父子进程)
27 你线程池处理完一个任务后的状态是怎么样的
这⾥要分两种情况考虑
(1)当处理完任务后如果请求队列为空时,则这个线程重新回到阻塞等待的状态
(2)当处理完任务后如果请求队列不为空时,那么这个线程将处于与其他线程竞争资源的状态,谁获得锁谁就获得了处理事件的资格。
28 实现一个简要的线程池(手撕)
29 如果同时1000个客户端进⾏访问请求,线程数不多,怎么能及时响应处理每⼀个呢?
⾸先这种问法就相当于问服务器如何处理⾼并发的问题。
⾸先我项⽬中使⽤了I/O多路复⽤技术,每个线程中管理⼀定数量的连接,只有线程池中的连接有请求,epoll就会返回请求的连接列表,管理该连接的线程获取活动列表,然后依次处理各个连接的请求。如果该线程没有任务,就会等待主reactor分配任务。这样就能达到服务器⾼并发的要求,同⼀时刻,每个线程都在处理⾃⼰所管理连接的请求。
30 如果⼀个客户请求需要占⽤线程很久的时间,会不会影响接下来的客户请求呢,有什么好的策略呢?
- 影响分析
会影响这个⼤请求的所在线程的所有请求,因为每个eventLoop都是依次处理它通过epoll获得的活动事件,也就是活动连接。如果该eventloop处理的连接占⽤时间过⻓的话,该线程后续的请求只能在请求队列中等待被处理,从⽽影响接下来的客户请求。 - 应对策略
- 主 reactor 的⻆度:可以记录⼀下每个从reactor的阻塞连接数,主reactor根据每个reactor的当前负载来分发请求,达到负载均衡的效果。
- 从 reactor 的⻆度:
- 超时时间:为每个连接分配⼀个时间⽚,类似于操作系统的进程调度,当当前连接的时间⽚⽤完以后, 将其重新加⼊请求队列,响应其他连接的请求,进⼀步来说,还可以为每个连接设置⼀个优先级,这样可以优先响应重要的连接,有点像 HTTP/2 的优先级。
- 关闭时间:为了避免部分连接⻓时间占⽤服务器资源,可以给每个连接设置⼀个最⼤响应时间,当⼀个连接的最⼤响应时间⽤完后,服务器可以主动将这个连接断开,让其重新连接。
31 说⼀下什么是ET,什么是LT,有什么区别?
- LT:⽔平触发模式,只要内核缓冲区有数据就⼀直通知,只要socket处于可读状态或可写状态,就会⼀直返回sockfd;是默认的⼯作模式,⽀持阻塞IO和⾮阻塞IO
- ET:边沿触发模式,只有状态发⽣变化才通知并且这个状态只会通知⼀次,只有当socket由不可写到可写或由不可读到可读,才会返回其sockfd;只⽀持⾮阻塞IO
32 LT什么时候会触发?ET呢?
- LT模式
- 对于读操作
- 只要内核读缓冲区不为空,LT模式返回读就绪。
- 对于写操作
- 只要内核写缓冲区还不满,LT模式会返回写就绪。
- 对于读操作
- ET模式
- 对于读操作
- 当缓冲区由不可读变为可读的时候,即缓冲区由空变为不空的时候。
- 当有新数据到达时,即缓冲区中的待读数据变多的时候。
- 当缓冲区有数据可读,且应⽤进程对相应的描述符进⾏EPOLL_CTL_MOD 修改EPOLLIN事件时。
- 对于写操作
- 当缓冲区由不可写变为可写时。
- 当有旧数据被发送⾛,即缓冲区中的内容变少的时候。
- 当缓冲区有空间可写,且应⽤进程对相应的描述符进⾏EPOLL_CTL_MOD 修改EPOLLOUT事件时。
- 对于读操作
33 为什么ET模式不可以⽂件描述符阻塞,⽽LT模式可以呢?
- 因为ET模式是当fd有可读事件时,epoll_wait()只会通知⼀次,如果没有⼀次把数据读完,那么要到下⼀次fd有可读事件epoll才会通知。⽽且在ET模式下,在触发可读事件后,需要循环读取信息,直到把数据读完。如果把这个fd设置成阻塞,数据读完以后read()就阻塞在那了。⽆法进⾏后续请求的处理。
- LT模式不需要每次读完数据,只要有数据可读,epoll_wait()就会⼀直通知。所以 LT模式下去读的话,内核缓冲区肯定是有数据可以读的,不会造成没有数据读⽽阻塞的情况。
34 抓过包吗,用过哪些抓包工具
-
抓包概念
抓包(packet capture)就是将网络传输发送与接收的数据包进行截获、重发、编辑、转存等操作,也用来检查网络安全。抓包也经常被用来进行数据截取等。 -
抓包的目的
抓包的目的就是为了获取到想要的原始数据,拿到数据以后,我们就可以做以下一些事情:- 分析数据传输协议。
- 定位网络协议的问题。
- 从数据包中获取想要的信息。
- 将截取到的数据包进行修改,伪造,重发。
-
抓包的用途:
- 从功能测试角度,通过抓包查看隐藏字段
Web 表单中会有很多隐藏的字段,这些隐藏字段一般都有一些特殊的用途,比如收集用户的数据,预防 CRSF 攻击,防网络爬虫,以及一些其他用途。这些隐藏字段在界面上都看不到,如果想检测这些字段,就必须要使用抓包工具。 - 通过抓包工具了解协议内容,方便开展接口和性能测试
性能测试方面,性能测试其实就是大量模拟用户的请求,所以我们必须要知道请求中的协议内容和特点,才能更好的模拟用户请求,分析协议就需要用到抓包工具;接口测试方面,在接口测试时,虽然我们尽量要求有完善的接口文档。但很多时候接口文档不可能覆盖所有的情况,或者因为文档滞后,在接口测试过程中,还时需要借助抓包工具来辅助我们进行接口测试。 - 需要通过抓包工具,检查数据加密
安全测试方面,我们需要检查敏感数据在传输过程中是否加密,也需要借助抓包工具才能检查。
- 抓包工具用过wireshark
项目中并没有抓过包,但是做了抓包的事情,就比如在程序编写中,我首先实现的是创建线程池对服务器的连接,连接后我获得了请求的报文信息,此时就相当于做了抓包的一个事情,接下来我就是对报文进行分析,编写的对应解析报文的http_conn类的编写。
35 在开发过程中有进⾏过测试吗?(做了单元测试、集成测试、功能测试、压⼒测试)压⼒测试是怎么做的?
单元测试,针对每一个类进行测试是否成功,比如创建了锁locker类就测试其中的上锁,解锁方法是否成功
集成测试,比如项目中的锁类、条件类、信号类和线程池类创建完毕之后,可以简单实现连接服务器的任务,此时并不能对请求做出解析和响应,但是我们就可以测试连接服务器的测试,此时可以输出日志为服务器请求来验证是否成功
功能测试,在后续的针对请求的解析和响应的类HTTP_CONN创建完毕,项目完成后,在浏览器输入公网IP,会得到设置回复的静态网页,测试功能成功
压力测试,通过开源软件webbench来进行测试,在5k左右客户端连接情况下,能够保证全部请求成功
36 HTTP一个典型流程/在浏览器地址栏键入URL,按下回车之后的流程
- 浏览器向 DNS 服务器请求解析该 URL 中的域名所对应的 IP 地址;
- 解析出 IP 地址后,根据该 IP 地址和默认端口 80(https默认443),和服务器建立 TCP 连接;
- 浏览器发出读取文件( URL 中域名后面部分对应的文件)的 HTTP 请求,该请求报文作为 TCP 三次握手的第三个报文的数据发送给服务器;
- 服务器对浏览器请求作出响应,并把对应的 HTML 文本发送给浏览器:
Web 服务器解析请求,定位请求资源。服务器将资源复本写到 TCP 套接字,由客户端读取。一个响应由状态行、响应头部、空行和响应数据 4 部分组成。 - 释放 TCP 连接:
若 connection 模式为 close,则服务器主动关闭 TCP连接,客户端被动关闭连接,释放 TCP 连接;若connection 模式为 keepalive,则该连接会保持一段时间,在该时间内可以继续接收请求; - 浏览器将该 HTML 文本并显示内容:
客户端浏览器首先解析状态行,查看表明请求是否成功的状态代码。然后解析每一个响应头,响应头告知以下为若干字节的 HTML 文档和文档的字符集。客户端浏览器读取响应数据 HTML,根据HTML 的语法对其进行格式化,并在浏览器窗口中显示。