webserver所需要的基础知识

编程语言

  1. 基本的C/C++语法
  2. C++11的特性(智能指针、function等),能够掌握C++14/17更好

操作系统

  1. 基本的linux指令
  2. 常见的系统调用

计算机网络

  1. TCP和UDP的连接机制及对应的函数
  2. 常见的服务器模式,单对单、多对单、多对多
  3. 抓包工具的简单使用,如tcpdump

数据库

  1. 常用的MySQL语句
  2. 数据库的安装

项目中的难点

  1. 一部分是去服务器网络框架、日志系统、存储引擎等一些基本系统的搭建,这部分的难点主要就是技术的理解和选型,以及一些开源的框架调整后应用到我的项目中去。
  2. 另一部分是为了提高服务器性能所做的一些优化,比如缓存机制、内存池等一些额外系统的搭建。这部分难点主要是找出服务器的性能瓶颈,然后结合自己的想法去突破这个瓶颈,提高服务器性能

针对项目做了哪些优化

程序本身

  1. 减少了程序等待IO的事件: 非阻塞IO + IO多路复用
  2. 设计高性能网络框架,同步IO(主从reactor + 线程池) 和异步IO(proactor)
  3. 【减少系统调用】 避免频繁申请/释放内存: 线程池、内存池和缓存机制
  4. 【减少系统调用】 对于文件发送、使用零拷贝函数sendFile() 老发送,避免拷贝数据到用户态
  5. 【减少系统调用】 尽量减少锁的使用,如果需要,尽量减小临界区(内置系统和线程池)

系统参数调优

  1. 最大文件描述符数(用户级和系统级)
  2. tcp连接的参数 (半连接/连接队列的长度、tcp syncookies)

C++面向对象特性在项目中的体现

C++面向对象特性有封装、继承、多态。

封装

首先是封装,我在项目中将各个模块使用类进行封装,比如连接httpconnection/ftpconnection类来封装,日志就用log类来封装,将类的属性私有化,比如请求的解析状态,并且对外的接口设置为公有,比如连接的重置,不对外暴露自己的私有方法,比如读写的回调函数等。还有一个就是,项目中的每个模块都使用了各自的命名空间进行封装,避免了命名冲突或名字污染。

继承

然后就是继承,项目中的继承用的比较少,主要是对工具类的继承,项目中多个地方使用到noncopyable和enable_shared_from_this,保证了代码的复用性。实际上对共有功能可以设计一个基类来继承,比如我项目中的connection目前有httpconnection和tcpconnection两种,可以通过继承connection积累来减少重复代码,因为我当时做的只考虑到http连接,ftp是后面加上去的,所以没用这样设计,后面可以进行优化

多态

最后是多态,我项目中的多态主要用了静态多态,动态多态没有涉及。静态多态在日志系统中对流输入运算符进行了重载,以及在日志系统和内存池中都有各种函数模板的泛型编程。实际上刚刚说的httpconnection和ftpconnection从connection派生出来后时可以使用动态多态的

项目细节

线程池

你的线程池工作线程处理完一个任务后的状态是什么?

  1. 当处理完任务后如果请求队列为空时,则这个线程重新回到阻塞等待的状态
  2. 当处理完任务后如果队列不为空时,那么这个线程将处于与其他线程竞争资源的状态,谁获得锁谁就获得了处理事件的资格

讲一下你项目中的线程池作用?具体是怎么实现的?有参考开源的线程池实现吗

分I/O线程池和计算线程池去讲。计算线程池参考了

并发性问题

如果同时1000个客户端进行访问请求,线程数不多,怎么能及时响应处理每一个呢?

首先这种问法就相当于问服务器如何处理高并发问题

首先我项目中使用了I/O多路复用技术,每个线程中管理一定数量的连接,只有线程池中的连接有请求,epoll就会返回请求的连接列表,管理该连接的线程获取活动列表,然后依次处理各个请求。如果该线程没有任务,就会等待主reactor分配任务,这样就能达到服务器高并发的要求,同一时刻,每个线程都在处理自己所管理连接的请求。

如果一个客户请求需要占用线程很久的时间,会不会影响接下来的客户请求呢,有什么好的策略呢?

影响分析

会影响这个大请求的所在线程的所有请求, 因为每个eventLoop都是依次处理它通过epoll获得的活动事件,也就是活动连接。如果该eventLoop都是依次处理的连接占用时长过长的话,该线程后续的请求只能在请求队列中等待处理,从而影响接下来的客户请求

应对策略

  1. 主reactor的角度: 可以记录一下每个从reactor的阻塞连接数,主reactor根据每个reactor的当前负载来分发请求,达到负载均衡的效果
  2. 从reactor的角度
    超发时间: 为每个连接分配一个时间片,类似于操作系统的进程调度,当当前连接的时间片用完以后,将其重新加入请求队列,响应其他连接的请求,进一步来说,还可以为每个连接设置一个优先级,这样可以优先响应重要的连接,有点像HTTP/2的优先级。
    关闭时间: 为了避免部分连接长时间占用服务器资源,可以给每个连接设置一个最大响应时间,当一个连接的最大响应时间用完后,服务器可以主动将这个连接断开,让其重新连接。

IO多路复用

说一下什么是ET,什么是LT,有什么区别?

  1. LT: 水平触发模式,只要内核缓冲区有数据就一直通知,只要socket处于可读状态或可写状态,就会一直返回sockfd;是默认的工作模式,支持阻塞IO和非阻塞IO
  2. ET: 边沿触发模式,只有状态发生变化才通知并且这个状态只会通知一次,只有当socket由不可写到可写或由不可读到可读,才会返回其sockfd;只支持非阻塞IO

LT什么时候会触发?ET呢

LT模式

  1. 对于读操作
    只要内核缓冲区不为空,LT模式返回读就绪。

  2. 对于写操作
    只要内核缓冲区还不满,LT模式会返回写就绪

ET模式

  1. 对于读操作
    当缓冲区由不可读变为可读的时候,即缓冲区由空变为不空的时候
    当有新数据到达时,即缓冲区中的待读数据变多的时候
    当缓冲区有数据可读,且应用进程对相应的描述符进行EPOLL_CTL_MOD修改EPOLLIN事件时。

  2. 对于写操作
    当缓冲区由不可写变为可写时
    当有旧数据被发送走,即缓冲区中的内容变少的时候
    当缓冲区有空间可写,且应用进程对相应的描述符进行EPOLL_CTL_MOD修改EPOLLOUT事件时。

为什么ET模式不可以文件描述符阻塞,而LT模式可以呢?

  1. 因为ET模式是当fd有可读事件时,epoll_wait()只会通知一次,如果没有一次把数据读完,那么要到下一次fd有可读事件epoll才会通知。而且在ET模式下,在触发可读事件后,需要循环读取信息,直到把数据读完。如果把这个fd设置成阻塞,数据读完以后read()就阻塞在那了。无法进行后续请求的处理。
  2. LT模式不需要每次读完数据,只要有数据可读,epoll_wait()就会一直通知。所以LT模式下去读的的话,内核缓冲区肯定是有数据可读的,不会造成没有数据读而阻塞的情况。

你用了epoll,说一下为什么用epoll,还有其他多路复用方式吗?区别是什么?

文件描述符集合的存储位置

对于select和poll来说,所有文件描述符都是在用户态被加入其文件描述符集合的,每次调用都需要将整个集合拷贝到内核态;epoll则将整个文件描述符集合维护在内核态,每次添加文件描述符的时候都需要执行一个系统调用。系统调用开销是很大的,而且在有很多短期活跃连接的情况下,由于这些大量的系统调用开销,epoll可能会慢于select和poll

文件描述符集合而表示方法

select使用线性表描述文件描述符集合,文件描述符有上限;poll用链表来表示;epoll底层通过红黑树来描述,并且维护一个就绪列表,将事件表中已经就绪的事件添加到这里,在使用epoll_wait调用时,仅观察这个list中有没有数据即可。

遍历方式

select和poll都只能工作在相对低效的LT模式下,而epoll同时支持LT和ET模式。

适用场景

当监测的fd数量较小,且各个fd都很活跃的情况下,建议使用select和poll;当监听的fd数量较多,且单位时间仅部分fd活跃的情况下,使用epoll会明显提升性能。

并发模型

reactor、proactor模型的区别

  1. Reactor是非阻塞同步网络模式,感知的是就绪可读事件。在每次感知到有事件发生(比如可读就绪事件)后,就需要应用进程主动调用read方法来完成数据的读取,也就是要应用进程主动将socket接收缓存中的数据读到应用程序内存中,这个过程是同步的,读取完数据后应用进程才能处理数据。
  2. Proatcor是异步网络模式,感知的是已完成的读写事件。在发生异步读写请求时,需要传入数据缓冲区的地址(用来存放结果数据)等信息,这样系统内核才可以自动帮我们把数据的读写工作完成,这里的读写工作全程由操作系用来做,并不需要像Reactor那样还需要应用进程主动发起read/wirte来读写数据,操作系用完成读写工作后,就会通知应用进程直接处理数据。

注: Proactor这么好用,那你为什么不用?
在linux下的异步I/O是不完善的,aio系列函数是有POSIX定义的异步操作接口,不是真正的操作系用级别支持的,也有考虑过使用模拟的proactor模式来开发,但这样需要浪费一个线程专门负责IO的处理
而Windows里实现了一套完整的支持socket的异步编程接口,这套接口就是IOCP,是由操作系统级别实现的异步I/O,真正意义上异步I/O,因此在操作系统级别实现的异步I/O,因此在Windows里实现高性能网络程序可以使用效率更高的Proactor方案

reactor模式中,各个模式的区别?

Reactor模型是一个针对同步I/O的网络模型,主要是使用一个reactor负责监听和分配事件,将I/O事件分派给对应的Handler。新的事件包含连接建立就绪、读就绪、写就绪等。reactor模型中又可以细分为单reactor线程、单reactor多线程、以及主从reactor模式。

  1. 单reactor单线程模型就是使用I/O多路复用技术,当其获取到活动的事件列表时,就在reactor中进行读取请求、业务处理、返回响应,这样的好处是整个模型都使用一个线程,不存在资源的争夺问题。但是如果一个事件的业务处理太过耗时,会导致后续所有的事件都得不到处理。
  2. 单reactor多线程就是用于解决这个问题,这个模型中reactor中只负责数据的接收和发送,reactor中只负责数据的接收和发送,reactor将业务处理分给线程池中的线程进行处理,完成后将数据返回给reactor进行发送,避免了reactor进行业务处理,但是IO操作都在reactor中进行,容易存在性能问题。而且因为是多线程,线程池中每个线程完成业务后都需要将结果传递给reactor进行发送,还会涉及到共享数据的互斥和保护机制。
  3. 主从reactor就是将reactor分为主reactor和从reactor,主reactor中只负责连接的建立和分配,读取请求、业务处理、返回响应等耗时的操作尽在从reactor中处理,能够有效的应该对高并发的场合

subreactor负责读写数据,由线程池进行业务处理