TinyWebserver学习笔记&常问问题整理

TinyWebserver学习笔记&常问问题整理_第1张图片

RAII:“Resource Acquisition is Initialization”资源获取即初始化

在构造函数中申请分配资源,在析构函数中释放资源

信号量: 它只能取自然数值并且只支持两种操作 :等待§和信号(V) ,假设有信号量SV

  • P,如果SV的值大于0,则将其减一;若SV的值为0,则挂起执行
  • V,如果有其他进程因为等待SV而挂起,则唤醒;若没有,则将SV值加一

互斥量

互斥锁,也成互斥量,可以保护关键代码段,以确保独占式访问.当进入关键代码段,获得互斥锁将其加锁;离开关键代码段,唤醒等待该互斥锁的线程.

条件变量

条件变量提供了一种线程间的通知机制,当某个共享数据达到某个值时,唤醒等待这个共享数据的线程.

好处:

**锁机制的功能:**实现多线程同步,通过锁机制,确保任一时刻只能有一个线程能进入关键代码段.

**封装的功能:**类中主要是Linux下三种锁进行封装,将锁的创建于销毁函数封装在类的构造与析构函数中,实现RAII机制

服务器编程基本框架:每个单元之间通过请求队列进行通信,从而协同完成任务。

I/O单元:用于处理客户端连接,读写网络数据;
逻辑单元:用于处理业务逻辑的线程;
网络存储单元:指本地数据库和文件等。

五种I/O模型

1)阻塞blocking:等待函数的返回,期间什么也不做
调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的去检查这个函数有没有返回,必须等这个函数返回才能进行下一步动作
2)非阻塞non-blocking:没有得到想要的函数返回值可以去做其他事情
每隔一段时间就去检测IO事件是否就绪。没有就绪就可以做其他事。非阻塞I/O执行系统调用总是立即返回,不管时间是否已经发生,若时间没有发生,则返回-1,此时可以根据errno区分这两种情况,对于accept,recv和send,事件未发生时,errno通常被设置成eagain
3)IO复用:一次检测多个客户端的事件
linux用select/poll函数实现IO复用模型,这两个函数也会使进程阻塞,和阻塞IO所不同的是这两个函数可以同时阻塞多个IO操作。同时对多个读操作、写操作的IO函数进行检测。知道有数据可读或可写时,才真正调用IO操作函数
4)信号驱动:消息通知机制,不需要用户轮询判断
装一个信号处理函数,进程继续运行并不阻塞,当IO时间就绪,进程收到SIGIO信号。然后处理IO事件。
5)异步:东西到达了,去通知
linux中,可以调用aio_read函数告诉内核描述字缓冲区指针和缓冲区的大小、文件偏移及通知的方式,然后立即返回,当内核将数据拷贝到缓冲区后,再通知应用程序。

注意:阻塞I/O,非阻塞I/O,信号驱动I/O和I/O复用都是同步I/O。同步I/O指内核向应用程序通知的是就绪事件,比如只通知有客户端连接,要求用户代码自行执行I/O操作,异步I/O是指内核向应用程序通知的是完成事件,比如读取客户端的数据后才通知应用程序,由内核完成I/O操作

事件处理模式

** reactor模式:主线程(I/O处理单元)只负责监听文件描述符上是否有事件发生,有的话立即通知工作线程(逻辑单元 ),读写数据、接受新连接及处理客户请求均在工作线程中完成。通常由同步I/O实现。
内核只通知用户有连接到了,用户自己I/O
** proactor模式:主线程和内核负责处理读写数据、接受新连接等I/O操作,工作线程仅负责业务逻辑,如处理客户请求。通常由异步I/O
实现。
内核完成I/O操作后通知用户

同步I/O模拟proactor模式: 由于异步I/O并不成熟,实际中使用较少

  • 主线程往epoll内核事件表注册socket上的读就绪事件。
  • 主线程调用epoll_wait等待socket上有数据可读
  • 当socket上有数据可读,epoll_wait通知主线程,主线程从socket循环读取数据,直到没有更多数据可读,然后将读取到的数据封装成一个请求对象并插入请求队列。
  • 睡眠在请求队列上某个工作线程被唤醒,它获得请求对象并处理客户请求,然后往epoll内核事件表中注册该socket上的写就绪事件
  • 主线程调用epoll_wait等待socket可写。
  • 当socket上有数据可写,epoll_wait通知主线程。主线程往socket上写入服务器处理客户请求的结果。

并发编程模式

并发编程方法的实现有多线程和多进程两种,这里涉及的并发模式指I/O处理单元与逻辑单元的协同完成任务的方法。

  • 半同步/半异步模式
  • 领导者/追随者模式

半同步/半反应堆

半同步/半反应堆并发模式是半同步/半异步的变体,将半异步具体化为某种事件处理模式.
并发模式中的同步和异步

  • 同步指的是程序完全按照代码序列的顺序执行
  • 异步指的是程序的执行需要由系统事件驱动

半同步/半异步模式工作流程

  • 同步线程用于处理客户逻辑
  • 异步线程用于处理I/O事件
  • 异步线程监听到客户请求后,就将其封装成请求对象并插入请求队列中
  • 请求队列将通知某个工作在同步模式的工作线程来读取并处理该请求对象

半同步/半反应堆工作流程(以Proactor模式为例)

  • 主线程充当异步线程,负责监听所有socket上的事件
  • 若有新请求到来,主线程接收之以得到新的连接socket,然后往epoll内核事件表中注册该socket上的读写事件
  • 如果连接socket上有读写事件发生,主线程从socket上接收数据,并将数据封装成请求对象插入到请求队列中
  • 所有工作线程睡眠在请求队列上,当有任务到来时,通过竞争(如互斥锁)获得任务的接管权

线程池

  • 空间换时间,浪费服务器的硬件资源,换取运行效率.
  • 池是一组资源的集合,这组资源在服务器启动之初就被完全创建好并初始化,这称为静态资源.
  • 当服务器进入正式运行阶段,开始处理客户请求的时候,如果它需要相关的资源,可以直接从池中获取,无需动态分配.
  • 当服务器处理完一个客户连接后,可以把相关的资源放回池中,无需执行系统调用释放资源.

静态成员变量

将类成员变量声明为static,则为静态成员变量,与一般的成员变量不同,无论建立多少对象,都只有一个静态成员变量的拷贝,静态成员变量属于一个类,所有对象共享
静态变量在编译阶段就分配了空间,对象还没创建时就已经分配了空间,放到全局静态区。

  • 静态成员变量
    • 最好是类内声明,类外初始化(以免类名访问静态成员访问不到)。
    • 无论公有,私有,静态成员都可以在类外定义,但私有成员仍有访问权限。
    • 非静态成员类外不能初始化。
    • 静态成员数据是共享的。

静态成员函数

将类成员函数声明为static,则为静态成员函数。

  • 静态成员函数
    • 静态成员函数可以直接访问静态成员变量,不能直接访问普通成员变量,但可以通过参数传递的方式访问。
    • 普通成员函数可以访问普通成员变量,也可以访问静态成员变量
    • 静态成员函数没有this指针。非静态数据成员为对象单独维护,但静态成员函数为共享函数,无法区分是哪个对象,因此不能直接访问普通变量成员,也没有this指针。

线程池分析

线程池的设计模式为半同步/半反应堆,其中反应堆具体为Proactor事件处理模式。
具体的**,主线程为异步线程,负责监听文件描述符,接收socket新连接,若当前监听的socket发生了读写事件,然后将任务插入到请求队列。工作线程从请求队列中取出任务,完成读写数据的处理**。

线程池创建与回收

构造函数中创建线程池,pthread_create函数中将类的对象作为参数传递给静态函数(worker),在静态函数中引用这个对象,并调用其动态方法(run)

向请求队列中添加任务

通过list容器创建请求队列,向队列中添加时,通过互斥锁保证线程安全,添加完成后通过信号量提醒有任务要处理,最后注意线程同步。

线程处理函数

内部访问私有成员函数run,完成线程处理要求。

run执行任务

工作线程从请求队列中取出某个任务进行处理,注意线程同步。

select/poll/epoll

  • 调用函数
    • select和poll都是一个函数,epoll是一组函数
  • 文件描述符数量
    • select通过线性表描述文件描述符集合,文件描述符有上限,一般是1024,但可以修改源码,重新编译内核,不推荐
    • poll是链表描述,突破了文件描述符上限,最大可以打开文件的数目
    • epoll通过红黑树描述,最大可以打开文件的数目,可以通过命令ulimit -n number修改,仅对当前终端有效
  • 将文件描述符从用户传给内核
    • select和poll通过将所有文件描述符拷贝到内核态,每次调用都需要拷贝
    • epoll通过epoll_create建立一棵红黑树,通过epoll_ctl将要监听的文件描述符注册到红黑树上
  • 内核判断就绪的文件描述符
    • select和poll通过遍历文件描述符集合,判断哪个文件描述符上有事件发生
    • epoll_create时,内核除了帮我们在epoll文件系统里建了个红黑树用于存储以后epoll_ctl传来的fd外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。
    • epoll是根据每个fd上面的回调函数(中断函数)判断,只有发生了事件的socket才会主动的去调用 callback函数,其他空闲状态socket则不会,若是就绪事件,插入list
  • 应用程序索引就绪文件描述符
    • select/poll只返回发生了事件的文件描述符的个数,若知道是哪个发生了事件,同样需要遍历
    • epoll返回的发生了事件的个数和结构体数组,结构体包含socket的信息,因此直接处理返回的数组即可
  • 工作模式
    • select和poll都只能工作在相对低效的LT模式下
    • epoll则可以工作在ET高效模式,并且epoll还支持EPOLLONESHOT事件,该事件能进一步减少可读、可写和异常事件被触发的次数。
  • 应用场景
    • 当所有的fd都是活跃连接,使用epoll,需要建立文件系统,红黑书和链表对于此来说,效率反而不高,不如selece和poll
    • 当监测的fd数目较小,且各个fd都比较活跃,建议使用select或者poll
    • 当监测的fd数目非常大,成千上万,且单位时间只有其中的一部分fd处于就绪状态,这个时候使用epoll能够明显提升性能
总结

select:线性表,文件描述符有上限1024
poll:链表,突破了上限
epoll:红黑树,更多

举例子送快递
select和poll会一直问快递员我的快递到没到:适合活跃连接多
epoll是让他先送到菜鸟驿站,然后通知我到了我去取:适合处理非活跃连接较多

ET、LT、EPOLLONESHOT

  • LT水平触发模式:不马上处理
    • epoll_wait检测到文件描述符有事件发生,则将其通知给应用程序,应用程序可以不立即处理该事件
    • 下一次调用epoll_wait时,epoll_wait还会再次向应用程序报告此事件,直至被处理
  • ET边缘触发模式:马上处理,一次性读完
    • epoll_wait检测到文件描述符有事件发生,则将其通知给应用程序,应用程序必须立即处理该事件
    • 必须要一次性将数据读取完,使用非阻塞I/O,读取到出现eagain
  • EPOLLONESHOT
    • 一个线程读取某个socket上的数据后开始处理数据,在处理过程中该socket上又有新数据可读,此时另一个线程被唤醒读取,此时出现两个线程处理同一个socket
    • 我们期望的是一个socket连接在任一时刻都只被一个线程处理,通过epoll_ctl对该文件描述符注册epolloneshot事件,一个线程处理socket时,其他线程将无法处理,当该线程处理完后,需要通过epoll_ctl重置epolloneshot事件

HTTP报文格式

HTTP报文分为请求报文和响应报文两种,每种报文必须按照特有格式生成,才能被浏览器端识别。
其中,浏览器端向服务器发送的为请求报文,服务器处理后返回给浏览器端的为响应报文。

请求报文

HTTP请求报文由请求行(request line)、请求头部(header)、空行和请求数据四个部分组成。
其中,请求分为两种,GET和POST
TinyWebserver学习笔记&常问问题整理_第2张图片

  • 请求行,用来说明请求类型,要访问的资源以及所使用的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响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。
TinyWebserver学习笔记&常问问题整理_第3张图片

  • 状态行,由HTTP协议版本号, 状态码, 状态消息 三部分组成。
    第一行为状态行,(HTTP/1.1)表明HTTP版本为1.1版本,状态码为200,状态消息为OK。
  • 消息报头,用来说明客户端要使用的一些附加信息。
    第二行和第三行为消息报头,Date:生成响应的日期和时间;Content-Type:指定了MIME类型的HTML(text/html),编码类型是UTF-8。
  • 空行,消息报头后面的空行是必须的。
  • 响应正文,服务器返回给客户端的文本信息。空行后面的html部分为响应正文。

HTTP状态码

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:服务器在执行请求时出现错误。

有限状态机

有限状态机,是一种抽象的理论模型,它能够把有限个变量描述的状态变化过程,以可构造可验证的方式呈现出来。比如,封闭的有向图。有限状态机可以通过if-else,switch-case和函数指针来实现,从软件工程的角度看,主要是为了封装逻辑。
有限状态机一种逻辑单元内部的一种高效编程方法,在服务器编程中,服务器可以根据不同状态或者消息类型进行相应的处理逻辑,使得程序逻辑清晰易懂。

http报文处理流程

  1. 浏览器端发出http连接请求,主线程创建http对象接收请求并将所有数据读入对应buffer,将该对象插入任务队列,工作线程从任务队列中取出一个任务进行处理。
  2. 工作线程取出任务后,调用process_read函数,通过主、从状态机对请求报文进行解析。
  3. 解析完之后,跳转do_request函数生成响应报文,通过process_write写入buffer,返回给浏览器端。

流程图与状态机

从状态机负责读取报文的一行,主状态机负责对该行数据进行解析,主状态机内部调用从状态机,从状态机驱动主状态机。
TinyWebserver学习笔记&常问问题整理_第4张图片

主状态机

三种状态,标识解析位置

  • CHECK_STATE_REQUESTLINE,解析请求行
  • CHECK_STATE_HEADER,解析请求头
  • CHECK_STATE_CONTENT,解析消息体,仅用于解析POST请求

从状态机

三种状态,标识解析一行的读取状态

  • LINE_OK,完整读取一行
  • LINE_BAD,报文语法有误
  • LINE_OPEN,读取的行不完整

HTTP_CODE含义

表示HTTP请求的处理结果,在头文件中初始化了八种情形,在报文解析与响应中只用到了七种。

  • NO_REQUEST
    • 请求不完整,需要继续读取请求报文数据
    • 跳转主线程继续监测读事件
  • GET_REQUEST
    • 获得了完整的HTTP请求
    • 调用do_request完成请求资源映射
  • NO_RESOURCE
    • 请求资源不存在
    • 跳转process_write完成响应报文
  • BAD_REQUEST
    • HTTP请求报文有语法错误或请求资源为目录
    • 跳转process_write完成响应报文
  • FORBIDDEN_REQUEST
    • 请求资源禁止访问,没有读取权限
    • 跳转process_write完成响应报文
  • FILE_REQUEST
    • 请求资源可以正常访问
    • 跳转process_write完成响应报文
  • INTERNAL_ERROR
    • 服务器内部错误,该结果在主状态机逻辑switch的default下,一般不会触发

解析报文整体流程

process_read通过while循环,将主从状态机进行封装,对报文的每一行进行循环处理。

  • 判断条件
    • 主状态机转移到CHECK_STATE_CONTENT,该条件涉及解析消息体
    • 从状态机转移到LINE_OK,该条件涉及解析请求行和请求头部
    • 两者为或关系,当条件为真则继续循环,否则退出
  • 循环体
    • 从状态机读取数据
    • 调用get_line函数,通过m_start_line将从状态机读取数据间接赋给text
    • 主状态机解析text

从状态机逻辑

在HTTP报文中,每一行的数据由\r\n作为结束字符,空行则是仅仅是字符\r\n。因此,可以通过查找\r\n将报文拆解成单独的行进行解析,项目中便是利用了这一点。
从状态机负责读取buffer中的数据,将每行数据末尾的\r\n置为\0\0,并更新从状态机在buffer中读取的位置m_checked_idx,以此来驱动主状态机解析。

  • 从状态机从m_read_buf中逐字节读取,判断当前字节是否为\r
    • 接下来的字符是\n,将\r\n修改成\0\0,将m_checked_idx指向下一行的开头,则返回LINE_OK
    • 接下来达到了buffer末尾,表示buffer还需要继续接收,返回LINE_OPEN
    • 否则,表示语法错误,返回LINE_BAD
  • 当前字节不是\r,判断是否是\n(一般是上次读取到\r就到了buffer末尾,没有接收完整,再次接收时会出现这种情况
    • 如果前一个字符是\r,则将\r\n修改成\0\0,将m_checked_idx指向下一行的开头,则返回LINE_OK
  • 当前字节既不是\r,也不是\n
    • 表示接收不完整,需要继续接收,返回LINE_OPEN

主状态机逻辑

主状态机初始状态是CHECK_STATE_REQUESTLINE,通过调用从状态机来驱动主状态机,在主状态机进行解析前,从状态机已经将每一行的末尾\r\n符号改为\0\0,以便于主状态机直接取出对应字符串进行处理。

  • CHECK_STATE_REQUESTLINE
    • 主状态机的初始状态,调用parse_request_line函数解析请求行
    • 解析函数从m_read_buf中解析HTTP请求行,获得请求方法、目标URL及HTTP版本号
    • 解析完成后主状态机的状态变为CHECK_STATE_HEADER

总结
GET和POST请求报文的区别之一是有无消息体部分,GET请求没有消息体,当解析完空行之后,便完成了报文的解析。
后续的登录和注册功能,为了避免将用户名和密码直接暴露在URL中,我们在项目中改用了POST请求,将用户名和密码添加在报文中作为消息体进行了封装

流程图

浏览器端发出HTTP请求报文,服务器端接收该报文并调用process_read对其进行解析,根据解析结果HTTP_CODE,进入相应的逻辑和模块。
其中,服务器子线程完成报文的解析与响应;主线程监测读写事件,调用read_once和http_conn::write完成数据的读取与发送。
TinyWebserver学习笔记&常问问题整理_第5张图片

定时器基础知识

非活跃,是指客户端(这里是浏览器)与服务器端建立连接后,长时间不交换数据,一直占用服务器端的文件描述符,导致连接资源的浪费。
定时事件,是指固定一段时间之后触发某段代码,由该段代码处理一个事件,如从内核事件表删除事件,并关闭文件描述符,释放连接资源。
定时器,是指利用结构体或其他形式,将多种定时事件进行封装起来。具体的,这里只涉及一种定时事件,即定期检测非活跃连接,这里将该定时事件与连接资源封装为一个结构体定时器。
定时器容器,是指使用某种容器类数据结构,将上述多个定时器组合起来,便于对定时事件统一管理。具体的,项目中使用升序链表将所有定时器串联组织起来。

整体概述

服务器主循环为每一个连接创建一个定时器,并对每个连接进行定时。另外,利用升序时间链表容器将所有定时器串联起来,若主循环接收到定时通知,则在链表中依次执行定时任务。
Linux下提供了三种定时的方法:

  • socket选项SO_RECVTIMEO和SO_SNDTIMEO
  • SIGALRM信号
  • I/O复用系统调用的超时参数

三种方法没有一劳永逸的应用场景,也没有绝对的优劣。项目中使用的是SIGALRM信号,
具体的,利用alarm函数周期性地触发SIGALRM信号,信号处理函数利用管道通知主循环,主循环接收到该信号后对升序链表上所有定时器进行处理,若该段时间内没有交换数据,则将该连接关闭,释放所占用的资源。
定时器处理非活动连接模块,主要分为两部分,其一为定时方法与信号通知流程,其二为定时器及其容器设计与定时任务的处理。

信号通知流程

Linux下的信号采用的异步处理机制,信号处理函数和当前进程是两条不同的执行路线。具体的,当进程收到信号时,操作系统会中断进程当前的正常流程,转而进入信号处理函数执行操作,完成后再返回中断的地方继续执行。
为避免信号竞态现象发生,信号处理期间系统不会再次触发它。为确保该信号不被屏蔽太久,信号处理函数需要尽可能快地执行完毕。
一般的信号处理函数需要处理该信号对应的逻辑,当该逻辑比较复杂时,信号处理函数执行时间过长,会导致信号屏蔽太久。
这里的解决方案是,信号处理函数仅仅发送信号通知程序主循环,将信号对应的处理逻辑放在程序主循环中,由主循环执行信号对应的逻辑代码

**统一事件源:**是指将信号事件与其他事件一样被处理。

信号处理函数使用管道将信号传递给主循环,信号处理函数往管道的写端写入信号值,主循环则从管道的读端读出信号值,使用I/O复用系统调用来监听管道读端的可读事件,这样信号事件与其他文件描述符都可以通过epoll来监测,从而实现统一处理

信号处理机制

每个进程之中,都有存着一个表,里面存着每种信号所代表的含义,内核通过设置表项中每一个位来标识对应的信号类型。
TinyWebserver学习笔记&常问问题整理_第6张图片

  • 信号的接收
    • 接收信号的任务是由内核代理的,当内核接收到信号后,会将其放到对应进程的信号队列中,同时向进程发送一个中断,使其陷入内核态。注意,此时信号还只是在队列中,对进程来说暂时是不知道有信号到来的。
  • 信号的检测
    • 进程从内核态返回到用户态前进行信号检测
    • 进程在内核态中,从睡眠状态被唤醒的时候进行信号检测
    • 进程陷入内核态后,有两种场景会对信号进行检测:
    • 当发现有新信号时,便会进入下一步,信号的处理。
  • 信号的处理
    • ( 内核 )信号处理函数是运行在用户态的,调用处理函数前,内核会将当前内核栈的内容备份拷贝到用户栈上,并且修改指令寄存器(eip)将其指向信号处理函数。
    • ( 用户 )接下来进程返回到用户态中,执行相应的信号处理函数。
    • ( 内核 )信号处理函数执行完成后,还需要返回内核态,检查是否还有其它信号未处理。
    • ( 用户 )如果所有信号都处理完成,就会将内核栈恢复(从用户栈的备份拷贝回来),同时恢复指令寄存器(eip)将其指向中断前的运行位置,最后回到用户态继续执行进程。

定时器设计:将连接资源、定时事件和超时时间封装为定时器类

  • 连接资源包括客户端套接字地址、文件描述符和定时器
  • 定时事件为回调函数,将其封装起来由用户自定义,这里是删除非活动socket上的注册事件,并关闭
  • 定时器超时时间 = 浏览器和服务器连接时刻 + 固定时间(TIMESLOT),可以看出,定时器使用绝对时间作为超时值,这里alarm设置为5秒,连接超时为15秒。

**定时器容器设计:**将多个定时器串联组织起来统一处理,具体包括升序链表设计。
项目中的定时器容器为带头尾结点的升序双向链表,具体的为每个连接创建一个定时器,将其添加到链表中,并按照超时时间升序排列。执行定时任务时,将到期的定时器从链表中删除。
主要涉及双向链表的插入,删除操作,其中添加定时器的事件复杂度是O(n),删除定时器的事件复杂度是O(1)。
升序双向链表主要逻辑如下,具体的,

  • 创建头尾节点,其中头尾节点没有意义,仅仅统一方便调整
  • add_timer函数,将目标定时器添加到链表中,添加时按照升序添加
    • 若当前链表中只有头尾节点,直接插入
    • 否则,将定时器按升序插入
  • adjust_timer函数,当定时任务发生变化,调整对应定时器在链表中的位置
    • 客户端在设定时间内有数据收发,则当前时刻对该定时器重新设定时间,这里只是往后延长超时时间
    • 被调整的目标定时器在尾部,或定时器新的超时值仍然小于下一个定时器的超时,不用调整
    • 否则先将定时器从链表取出,重新插入链表
  • del_timer函数将超时的定时器从链表中删除
    • 常规双向链表删除结点

**定时任务处理函数:**该函数封装在容器类中,函数遍历升序链表容器,根据超时时间,处理对应的定时器。
使用统一事件源,SIGALRM信号每次被触发,主循环中调用一次定时任务处理函数,处理链表容器中到期的定时器。
具体的逻辑如下,

  • 遍历定时器升序链表容器,从头结点开始依次处理每个定时器,直到遇到尚未到期的定时器
  • 若当前时间小于定时器超时时间,跳出循环,即未找到到期的定时器
  • 若当前时间大于定时器超时时间,即找到了到期的定时器,执行回调函数,然后将它从链表中删除,然后继续遍历

代码分析-如何使用定时器

服务器首先创建定时器容器链表,然后用统一事件源将异常事件,读写事件和信号事件统一处理,根据不同事件的对应逻辑使用定时器。
具体的,

  • 浏览器与服务器连接时,创建该连接对应的定时器,并将该定时器添加到链表上
  • 处理异常事件时,执行定时事件,服务器关闭连接,从链表上移除对应定时器
  • 处理定时信号时,将定时标志设置为true
  • 处理读事件时,若某连接上发生读事件,将对应定时器向后移动,否则,执行定时事件
  • 处理写事件时,若服务器通过某连接给浏览器发送数据,将对应定时器向后移动,否则,执行定时事件

日志系统

基础知识

日志,由服务器自动创建,并记录运行状态,错误信息,访问数据的文件。
同步日志,日志写入函数与工作线程串行执行,由于涉及到I/O操作,当单条日志比较大的时候,同步模式会阻塞整个处理流程,服务器所能处理的并发能力将有所下降,尤其是在峰值的时候,写日志可能成为系统的瓶颈。
生产者-消费者模型,并发编程中的经典模型。以多线程为例,为了实现线程间数据同步,生产者线程与消费者线程共享一个缓冲区,其中生产者线程往缓冲区中push消息,消费者线程从缓冲区中pop消息
阻塞队列,将生产者-消费者模型进行封装,使用循环数组实现队列,作为两者共享的缓冲区。
异步日志,将所写的日志内容先存入阻塞队列,写线程从阻塞队列中取出内容,写入日志。
单例模式,最简单也是被问到最多的设计模式之一,保证一个类只创建一个实例,同时提供全局访问的方法。

整体概述

本项目中,使用单例模式创建日志系统,对服务器运行状态、错误信息和访问数据进行记录,该系统可以实现按天分类,超行分类功能,可以根据实际情况分别使用同步和异步写入两种方式。
其中异步写入方式,将生产者-消费者模型封装为阻塞队列,创建一个写线程,工作线程将要写的内容push进队列,写线程从队列中取出内容,写入日志文件。
日志系统大致可以分成两部分,其一是单例模式与阻塞队列的定义,其二是日志类的定义与使用。

单例模式

单例模式作为最常用的设计模式之一,保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享
实现思路:私有化它的构造函数,以防止外界创建单例类的对象;使用类的私有静态指针变量指向类的唯一实例,并用一个公有的静态方法获取该实例。
单例模式有两种实现方法,分别是懒汉和饿汉模式。

  1. 懒汉模式,即非常懒,不用的时候不去初始化,所以在第一次被使用时才进行初始化;
  2. 饿汉模式,即迫不及待,在程序运行时立即初始化。

经典的线程安全懒汉模式,使用双检测锁模式

TinyWebserver学习笔记&常问问题整理_第7张图片
为什么要用双检测,只检测一次不行吗?
如果只检测一次,在每次调用获取实例的方法时,都需要加锁,这将严重影响程序性能。双层检测可以有效避免这种情况,仅在第一次创建单例的时候加锁,其他时候都不再符合NULL == p的情况,直接返回已创建好的实例。

局部静态变量之线程安全懒汉模式

前面的双检测锁模式,写起来不太优雅,《Effective C++》(Item 04)中的提出另一种更优雅的单例模式实现,使用函数内的局部静态对象,这种方法不用加锁和解锁操作。
TinyWebserver学习笔记&常问问题整理_第8张图片
如果使用C++11之前的标准,还是需要加锁,这里同样给出加锁的版本。
TinyWebserver学习笔记&常问问题整理_第9张图片

饿汉模式

饿汉模式不需要用锁,就可以实现线程安全。原因在于,在程序运行时就定义了对象,并对其初始化。之后,不管哪个线程调用成员函数getinstance(),都只不过是返回一个对象的指针而已。所以是线程安全的,不需要在获取实例的成员函数中加锁。
TinyWebserver学习笔记&常问问题整理_第10张图片
饿汉模式虽好,但其存在隐藏的问题,在于非静态对象(函数外的static对象)在不同编译单元中的初始化顺序是未定义的。如果在初始化完成之前调用 getInstance() 方法会返回一个未定义的实例。

条件变量与生产者-消费者模型

条件变量API与陷阱

条件变量提供了一种线程间的通知机制,当某个共享数据达到某个值时,唤醒等待这个共享数据的线程。

基础API
  • pthread_cond_init函数,用于初始化条件变量
  • pthread_cond_destory函数,销毁条件变量
  • pthread_cond_broadcast函数,以广播的方式唤醒所有等待目标条件变量的线程
  • pthread_cond_wait函数,用于等待目标条件变量。该函数调用时需要传入 mutex参数(加锁的互斥锁) ,函数执行时,先把调用线程放入条件变量的请求队列,然后将互斥锁mutex解锁,当函数成功返回为0时,表示重新抢到了互斥锁,互斥锁会再次被锁上, 也就是说函数内部会有一次解锁和加锁操作.

使用pthread_cond_wait方式如下:
TinyWebserver学习笔记&常问问题整理_第11张图片pthread_cond_wait执行后的内部操作分为以下几步:

  • 将线程放在条件变量的请求队列后,内部解锁
  • 线程等待被pthread_cond_broadcast信号唤醒或者pthread_cond_signal信号唤醒,唤醒后去竞争锁
  • 若竞争到互斥锁,内部再次加锁
陷阱一

使用前要加锁,为什么要加锁?
多线程访问,为了避免资源竞争,所以要加锁,使得每个线程互斥的访问公有资源。
pthread_cond_wait内部为什么要解锁?
如果while或者if判断的时候,满足执行条件,线程便会调用pthread_cond_wait阻塞自己,此时它还在持有锁,如果他不解锁,那么其他线程将会无法访问公有资源。
具体到pthread_cond_wait的内部实现,当pthread_cond_wait被调用线程阻塞的时候,pthread_cond_wait会自动释放互斥锁。
为什么要把调用线程放入条件变量的请求队列后再解锁?
线程是并发执行的,如果在把调用线程A放在等待队列之前,就释放了互斥锁,这就意味着其他线程比如线程B可以获得互斥锁去访问公有资源,这时候线程A所等待的条件改变了,但是它没有被放在等待队列上,导致A忽略了等待条件被满足的信号。
倘若在线程A调用pthread_cond_wait开始,到把A放在等待队列的过程中,都持有互斥锁,其他线程无法得到互斥锁,就不能改变公有资源。
为什么最后还要加锁?
将线程放在条件变量的请求队列后,将其解锁,此时等待被唤醒,若成功竞争到互斥锁,再次加锁。

陷阱二

为什么判断线程执行的条件用while而不是if?
一般来说,在多线程资源竞争的时候,在一个使用资源的线程里面(消费者)判断资源是否可用,不可用,便调用pthread_cond_wait,在另一个线程里面(生产者)如果判断资源可用的话,则调用pthread_cond_signal发送一个资源可用信号。
在wait成功之后,资源就一定可以被使用么?答案是否定的,如果同时有两个或者两个以上的线程正在等待此资源,wait返回后,资源可能已经被使用了。
再具体点,有可能多个线程都在等待这个资源可用的信号,信号发出后只有一个资源可用,但是有A,B两个线程都在等待,B比较速度快,获得互斥锁,然后加锁,消耗资源,然后解锁,之后A获得互斥锁,但A回去发现资源已经被使用了,它便有两个选择,一个是去访问不存在的资源,另一个就是继续等待,那么继续等待下去的条件就是使用while,要不然使用if的话pthread_cond_wait返回后,就会顺序执行下去。
所以,在这种情况下,应该使用while而不是if:

while(resource == FALSE)
    pthread_cond_wait(&cond, &mutex);

如果只有一个消费者,那么使用if是可以的。

生产者-消费者模型

process_msg相当于消费者,enqueue_msg相当于生产者,struct msg* workq作为缓冲队列。
生产者和消费者是互斥关系,两者对缓冲区访问互斥,同时生产者和消费者又是一个相互协作与同步的关系,只有生产者生产之后,消费者才能消费。
TinyWebserver学习笔记&常问问题整理_第12张图片

阻塞队列代码分析

阻塞队列类中封装了生产者-消费者模型,其中push成员是生产者,pop成员是消费者。
阻塞队列中,使用了循环数组实现了队列,作为两者共享缓冲区,当然了,队列也可以使用STL中的queue。

自定义队列

当队列为空时,从队列中获取元素的线程将会被挂起;当队列是满时,往队列里添加元素的线程将会挂起。
阻塞队列类中,有些代码比较简单,这里仅对push和pop成员进行详解。

 1class block_queue
 2{
 3public:
 4
 5    //初始化私有成员
 6    block_queue(int max_size = 1000)
 7    {
 8        if (max_size <= 0)
 9        {
 10            exit(-1);
 11        }
 12
 13        //构造函数创建循环数组
 14        m_max_size = max_size;
 15        m_array = new T[max_size];
 16        m_size = 0;
 17        m_front = -1;
 18        m_back = -1;
 19
 20        //创建互斥锁和条件变量
 21        m_mutex = new pthread_mutex_t;
 22        m_cond = new pthread_cond_t;
 23        pthread_mutex_init(m_mutex, NULL);
 24        pthread_cond_init(m_cond, NULL);
 25    }
 26
 27    //往队列添加元素,需要将所有使用队列的线程先唤醒
 28    //当有元素push进队列,相当于生产者生产了一个元素
 29    //若当前没有线程等待条件变量,则唤醒无意义
 30    bool push(const T &item)
 31    {
 32        pthread_mutex_lock(m_mutex);
 33        if (m_size >= m_max_size)
 34        {
 35            pthread_cond_broadcast(m_cond);
 36            pthread_mutex_unlock(m_mutex);
 37            return false;
 38        }
 39
 40        //将新增数据放在循环数组的对应位置
 41        m_back = (m_back + 1) % m_max_size;
 42        m_array[m_back] = item;
 43        m_size++;
 44
 45        pthread_cond_broadcast(m_cond);
 46        pthread_mutex_unlock(m_mutex);
 47
 48        return true;
 49    }
 50
 51    //pop时,如果当前队列没有元素,将会等待条件变量
 52    bool pop(T &item)
 53    {
 54        pthread_mutex_lock(m_mutex);
 55
 56        //多个消费者的时候,这里要是用while而不是if
 57        while (m_size <= 0)
 58        {
 59            //当重新抢到互斥锁,pthread_cond_wait返回为0
 60            if (0 != pthread_cond_wait(m_cond, m_mutex))
 61            {
 62                pthread_mutex_unlock(m_mutex);
 63                return false;
 64            }
 65        }
 66
 67        //取出队列首的元素,这里需要理解一下,使用循环数组模拟的队列 
 68        m_front = (m_front + 1) % m_max_size;
 69        item = m_array[m_front];
 70        m_size--;
 71        pthread_mutex_unlock(m_mutex);
 72        return true;
 73    }
 74
 75    //增加了超时处理,在项目中没有使用到
 76    //在pthread_cond_wait基础上增加了等待的时间,只指定时间内能抢到互斥锁即可
 77    //其他逻辑不变
 78    bool pop(T &item, int ms_timeout)
 79    {
 80        struct timespec t = {0, 0};
 81        struct timeval now = {0, 0};
 82        gettimeofday(&now, NULL);
 83        pthread_mutex_lock(m_mutex);
 84        if (m_size <= 0)
 85        {
 86            t.tv_sec = now.tv_sec + ms_timeout / 1000;
 87            t.tv_nsec = (ms_timeout % 1000) * 1000;
 88            if (0 != pthread_cond_timedwait(m_cond, m_mutex, &t))
 89            {
 90                pthread_mutex_unlock(m_mutex);
 91                return false;
 92            }
 93        }
 94
 95        if (m_size <= 0)
 96        {
 97            pthread_mutex_unlock(m_mutex);
 98            return false;
 99        }
100
101        m_front = (m_front + 1) % m_max_size;
102        item = m_array[m_front];
103        m_size--;
104        pthread_mutex_unlock(m_mutex);
105        return true;
106    }
107};
108
109#endif

fflush

#include 
int fflush(FILE *stream);

fflush()会强迫将缓冲区内的数据写回参数stream 指定的文件中,如果参数stream 为NULL,fflush()会将所有打开的文件数据更新。
在使用多个输出函数连续进行多次输出到控制台时,有可能下一个数据再上一个数据还没输出完毕,还在输出缓冲区中时,下一个printf就把另一个数据加入输出缓冲区,结果冲掉了原来的数据,出现输出错误。
在prinf()后加上fflush(stdout); 强制马上输出到控制台,可以避免出现上述错误。

流程图

  • 日志文件
    • 局部变量的懒汉模式获取实例
    • 生成日志文件,并判断同步和异步写入方式
  • 同步
    • 判断是否分文件
    • 直接格式化输出内容,将信息写入日志文件
  • 异步
    • 判断是否分文件
    • 格式化输出内容,将内容写入阻塞队列,创建一个写线程,从阻塞队列取出内容写入日志文件

TinyWebserver学习笔记&常问问题整理_第13张图片

数据库连接池

什么是数据库连接池?

池是一组资源的集合,这组资源在服务器启动之初就被完全创建好并初始化。通俗来说,池是资源的容器,本质上是对资源的复用
顾名思义,连接池中的资源为一组数据库连接,由程序动态地对池中的连接进行使用,释放。
当系统开始处理客户请求的时候,如果它需要相关的资源,可以直接从池中获取,无需动态分配;当服务器处理完一个客户连接后,可以把相关的资源放回池中,无需执行系统调用释放资源。
数据库访问的一般流程是什么?
当系统需要访问数据库时,先系统创建数据库连接,完成数据库操作,然后系统断开数据库连接。
为什么要创建连接池?
从一般流程中可以看出,若系统需要频繁访问数据库,则需要频繁创建和断开数据库连接,而创建数据库连接是一个很耗时的操作,也容易对数据库造成安全隐患。
在程序初始化的时候,集中创建多个数据库连接,并把他们集中管理,供程序使用,可以保证较快的数据库读写速度,更加安全可靠。

整体概述

池可以看做资源的容器,所以多种实现方法,比如数组、链表、队列等。这里,使用单例模式和链表创建数据库连接池,实现对数据库连接资源的复用。
项目中的数据库模块分为两部分,其一是数据库连接池的定义,其二是利用连接池完成登录和注册的校验功能。具体的,工作线程从数据库连接池取得一个连接,访问数据库中的数据,访问完毕后将连接交还连接池

单例模式创建

使用局部静态变量懒汉模式创建连接池。
TinyWebserver学习笔记&常问问题整理_第14张图片

连接池代码实现

连接池的定义中注释比较详细,这里仅对其实现进行解析。
连接池的功能主要有:初始化,获取连接、释放连接,销毁连接池。

初始化

值得注意的是,销毁连接池没有直接被外部调用,而是通过RAII机制来完成自动释放;使用信号量实现多线程争夺连接的同步机制,这里将信号量初始化为数据库的连接总数。

获取、释放连接

当线程数量大于数据库连接数量时,使用信号量进行同步,每次取出连接,信号量原子减1,释放连接原子加1,若连接池内没有连接了,则阻塞等待。
另外,由于多线程操作连接池,会造成竞争,这里使用互斥锁完成同步,具体的同步机制均使用lock.h中封装好的类。

销毁连接池

通过迭代器遍历连接池链表,关闭对应数据库连接,清空链表并重置空闲连接和现有连接数量。

RAII机制释放数据库连接

将数据库连接的获取与释放通过RAII机制封装,避免手动释放。

定义

这里需要注意的是,在获取连接时,通过有参构造对传入的参数进行修改。其中数据库连接本身是指针类型,所以参数需要通过双指针才能对其进行修改。

实现

不直接调用获取和释放连接的接口,将其封装起来,通过RAII机制进行获取和释放

注册登陆

整体概述

本项目中,使用数据库连接池实现服务器访问数据库的功能,使用POST请求完成注册和登录的校验工作

本文内容

本篇将介绍同步实现注册登录功能,具体的涉及到流程图,载入数据库表,提取用户名和密码,注册登录流程与页面跳转的的代码实现。
流程图,描述服务器从报文中提取出用户名密码,并完成注册和登录校验后,实现页面跳转的逻辑。
载入数据库表,结合代码将数据库中的数据载入到服务器中。
提取用户名和密码,结合代码对报文进行解析,提取用户名和密码。
注册登录流程,结合代码对描述服务器进行注册和登录校验的流程。
页面跳转,结合代码对页面跳转机制进行详解。
TinyWebserver学习笔记&常问问题整理_第15张图片

载入数据库表

将数据库中的用户名和密码载入到服务器的map中来,map中的key为用户名,value为密码。

提取用户名和密码

服务器端解析浏览器的请求报文,当解析为POST请求时,cgi标志位设置为1,并将请求报文的消息体赋值给m_string,进而提取出用户名和密码。

同步线程登录注册

通过m_url定位/所在位置,根据/后的第一个字符判断是登录还是注册校验。

  • 2
    • 登录校验
  • 3
    • 注册校验

根据校验结果,跳转对应页面。另外,对数据库进行操作时,需要通过锁来同步。

页面跳转

通过m_url定位/所在位置,根据/后的第一个字符,使用分支语句实现页面跳转。具体的,

  • 0
    • 跳转注册页面,GET
  • 1
    • 跳转登录页面,GET
  • 5
    • 显示图片页面,POST
  • 6
    • 显示视频页面,POST
  • 7
    • 显示关注页面,POST

面试题

大文件传输

:::info

  • 由于报文消息报头较小,第一次传输后,需要更新m_iv[1].iov_base和iov_len,m_iv[0].iov_len置成0,只传输文件,不用传输响应消息头
  • 每次传输后都要更新下次传输的文件起始位置和长度
    :::
项目介绍,线程池相关,并发模型相关,HTTP报文解析相关,定时器相关,日志相关,压测相关,综合能力等。

项目介绍

  • 为什么要做这样一个项目?
  1. 网络通信和协议:Web 服务器涉及到与客户端之间的网络通信,你将学习和理解 HTTP 协议的工作原理、请求和响应的结构以及常见的状态码和头部字段。还可以学习其他相关的网络协议,如 TCP/IP、UDP 等。
  2. 服务器端编程:实现 Web 服务器需要具备服务器端编程的技能。学习C++怎么处理 HTTP 请求到相应的处理函数、生成合适的 HTTP 响应以及与数据库和其他服务进行交互等。
  3. 并发处理和多线程编程:Web 服务器需要能够处理多个并发请求,这就需要学习并发处理的技术。你将学习如何设计和实现并发处理机制,如多线程编程、线程池、异步处理等,以提高服务器的性能和吞吐量。
  4. 文件和资源管理:Web 服务器通常需要提供静态文件(如 HTML、CSS、JavaScript、图像等)。你将学习如何处理文件读取、文件路径解析、资源缓存等,以便有效地提供静态内容。
  5. 动态内容生成:除了静态文件,Web 服务器还需要能够生成动态内容,例如通过服务器端脚本(如 PHP、Python、Ruby 等)生成 HTML 页面或通过 API 返回 JSON 数据。你将学习如何处理动态内容的生成和响应。
  6. 安全性和身份验证:Web 服务器需要具备一定的安全性和身份验证机制,以确保对敏感资源的合适访问。你将学习如何实施用户身份验证、使用 SSL/TLS 加密连接、处理跨站脚本攻击(XSS)和跨站请求伪造(CSRF)等安全问题。
  7. 错误处理和日志记录:在 Web 服务器项目中,你将学习如何处理错误情况和异常,并进行适当的错误处理和日志记录,以便于故障排除和系统监控。
  8. 性能优化和扩展性:Web 服务器需要具备良好的性能和可扩展性,以处理大量的并发请求和高负载情况。你将学习如何进行性能优化,如缓存、请求分流、负载均衡等,以及如何设计可扩展的架构。
  • 介绍下你的项目

此项目是在Linux环境下使用C/C++语言开发的轻量级多线程Web服务器,所搭建的服务器支持一定数量的客户端连接以及响应,并且支持客户端访问服务器的图片、视频等资源。

线程池相关

手写线程池
#include 
#include 
#include 
#include 

template <typename T>
class threadpool{
public:
    /*thread_number是线程池中线程的数量,max_requests是请求队列中最多允许的、等待处理的请求的数量*/
    threadpool(connection_pool *connPool, int thread_number = 8, int max_request = 10000);
    ~threadpool();
    bool append(T *request);

private:
    /*工作线程运行的函数,它不断从工作队列中取出任务并执行之*/
    static void *worker(void *arg);
    void run();

private:
    int m_thread_number;        //线程池中的线程数
    int m_max_requests;         //请求队列中允许的最大请求数
    pthread_t *m_threads;       //描述线程池的数组,其大小为m_thread_number
    std::list<T *> m_workqueue; //请求队列
    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();
    m_threads = new pthread_t[m_thread_number];
    if (!m_threads)
        throw std::exception();
    for (int i = 0; i < thread_number; ++i){
        //printf("create the %dth thread\n",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>
threadpool<T>::~threadpool(){
    delete[] m_threads;
    m_stop = true;
}

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;
}
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;

        connectionRAII mysqlcon(&request->mysql, m_connPool);
        
        request->process();
    }
}
线程的同步机制有哪些?

线程的同步机制用于协调多个线程的执行顺序,确保它们按照一定的顺序和规则访问共享资源。

  1. 互斥锁(Mutex):互斥锁是一种最基本的同步机制。一次只允许一个线程持有该锁,其他线程需要等待锁释放后才能继续执行。它可以确保在任意时刻只有一个线程访问被保护的共享资源。
  2. 信号量(Semaphore):信号量是一种计数器,用于控制对共享资源的访问。它可以限制同时访问某个资源的线程数量。信号量可以分为二进制信号量(只有0和1两个值)和计数信号量(可以有多个值)。
  3. 条件变量(Condition):条件变量用于在多个线程之间进行通信和协调。它允许线程等待某个特定条件的发生,一旦条件满足,线程可以被唤醒继续执行。
  4. 读写锁(ReadWrite Lock):读写锁允许多个线程同时读取共享资源,但只有一个线程可以写入共享资源。读写锁可以提高并发读取的效率,适用于读操作远远超过写操作的场景。
  5. 屏障(Barrier):屏障用于控制多个线程在某个点上进行同步,要求所有线程到达屏障前必须等待,直到最后一个线程到达时,屏障才会开放,允许所有线程继续执行。
  6. 事件(Event):事件是一种线程间的通信机制,允许一个或多个线程等待某个事件的发生。当事件触发时,等待的线程将被唤醒继续执行。
线程池中的工作线程是一直等待吗?

在Web服务器的线程池中,工作线程通常是在任务队列中等待任务的到来,而不是一直等待。当有新的请求到达服务器时,服务器将任务封装为一个工作单元,并将其放入任务队列中。然后,空闲的工作线程会从任务队列中获取任务并执行。
一旦工作线程完成了当前任务,它将再次回到线程池中等待下一个任务的到来。这种机制允许服务器同时处理多个请求,而不需要为每个请求都创建一个新的线程,从而提高服务器的性能和资源利用率。
通过使用线程池,可以避免频繁创建和销毁线程的开销,以及线程数量过多导致系统资源耗尽的问题。线程池中的工作线程在等待任务时,会保持活跃状态以接收新的任务,但并不会一直忙碌运行,而是在任务到来时才会被唤醒并执行相应的操作

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

空闲状态:工作线程完成当前任务后,如果没有新的任务可执行,它会进入空闲状态。在空闲状态下,线程会等待新的任务到来,从任务队列中获取下一个任务并开始执行

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

当面对大量并发请求时,如果线程数有限,可以采取以下几种策略来提高系统的响应能力和吞吐量:

  1. 使用线程池:使用线程池可以避免频繁地创建和销毁线程的开销。通过合理配置线程池的大小,可以控制并发执行的线程数量,使得系统能够处理更多的请求。线程池会复用线程,降低线程创建和上下文切换的开销,提高系统的性能。
  2. 使用非阻塞 I/O:传统的阻塞 I/O 在每个请求到来时都会阻塞等待数据的读取或写入完成,导致线程被长时间占用。而非阻塞 I/O(如使用 NIO)可以利用较少的线程同时处理多个请求,当一个请求需要等待 I/O 操作时,线程可以切换到处理其他请求,提高系统的并发处理能力。
    3.** 使用异步编程模型**:使用异步编程模型可以将 I/O 操作与业务逻辑分离,使得线程在等待 I/O 完成时可以执行其他任务,从而提高线程的利用率。常见的异步编程模型包括回调函数等。
    4.** 优化算法和数据结构**:通过优化算法和数据结构,可以减少处理请求的时间复杂度,提高系统的响应速度。合理选择和设计高效的数据结构和算法,可以在有限的线程资源下更快地处理请求。
  3. 使用缓存:合理使用缓存可以减轻服务器的负载,提高响应速度。将频繁访问的数据缓存起来,减少对后端资源的访问,从而更快地响应客户端请求。
  4. 水平扩展:如果单台服务器无法满足高并发需求,可以考虑采用水平扩展的方式,即增加服务器的数量来处理更多的请求。通过负载均衡技术将请求分发到多台服务器上,提高系统的处理能力和可用性。
如果一个客户请求需要占用线程很久的时间,会不会影响接下来的客户请求呢,有什么好的策略呢?
  1. 异步处理:将长时间运行的请求设计为异步任务,即将任务提交给线程池或任务队列,在后台进行处理,而不是直接占用工作线程。工作线程可以立即释放,可以接受和处理其他请求,而不会被长时间运行的请求阻塞。
  2. 使用多线程:可以将长时间运行的请求分配给专门的线程池或线程组进行处理,而不是使用主线程或默认线程池。这样可以避免长时间运行的请求占用过多的工作线程,从而保持其他请求的正常处理。
  3. 超时机制:为每个客户请求设置一个合理的超时时间。如果请求在超时时间内未完成,可以中断或取消该请求,并向客户端返回适当的错误或超时信息。这样可以避免长时间运行的请求持续占用资源,并确保系统能够及时响应其他请求。
  4. 任务分片:如果长时间运行的请求可以被分解为多个子任务,可以将这些子任务分片执行,每个子任务占用的时间较短。这样可以允许工作线程在处理长时间运行的请求期间,周期性地释放资源并响应其他请求。
  5. 资源限制和控制:为了避免某个请求过度消耗系统资源,可以设置资源限制和控制,如限制同时进行的长时间运行的请求数量,限制占用的内存或CPU时间等。这样可以平衡资源的分配,防止某个请求对整个系统产生过大的影响。

并发模型相关

简单说一下服务器使用的并发模型?
  1. 多进程模型:每个客户请求都由一个独立的进程来处理。当有新的请求到达时,服务器会创建一个新的进程来处理该请求。多进程模型可以实现较好的隔离性,每个进程有独立的内存空间,但进程间的切换和资源开销较大。
  2. 多线程模型:每个客户请求都由一个独立的线程来处理。与多进程模型类似,当有新的请求到达时,服务器会创建一个新的线程来处理该请求。多线程模型相比多进程模型具有更小的开销,线程间共享内存,但需要注意线程安全和同步问题。
  3. 事件驱动模型:使用单线程或少量线程处理多个请求。服务器通过事件循环机制监听并处理请求事件,当有请求到达时,将事件加入到事件队列中,并通过事件驱动的方式处理请求。事件驱动模型通常使用非阻塞 I/O,以避免线程阻塞,提高并发能力。
  4. 线程池模型:服务器预先创建一组工作线程,并将请求分发给空闲的工作线程处理。线程池模型通过复用线程、减少线程创建和销毁的开销,提高系统的并发处理能力和资源利用率。
  5. 协程模型:协程是一种轻量级的线程,可以在代码级别上实现并发。服务器通过协程调度器(如事件循环)在单个线程内切换执行不同的协程,以实现并发处理。协程模型可以避免线程切换的开销,提高系统的并发性能。
reactor、proactor、主从reactor模型的区别?
  1. Reactor模型:Reactor模型基于事件驱动的编程,主要由一个事件循环、事件分发器和多个事件处理器组成。事件循环负责监听事件,事件分发器负责将事件分发给对应的事件处理器进行处理。在Reactor模型中,事件处理器通常是同步执行的,即一个事件处理器处理完一个事件后再处理下一个事件。
  2. Proactor模型:Proactor模型也是基于事件驱动的编程,与Reactor模型不同的是,Proactor模型将事件处理的工作交给操作系统或框架来完成。在Proactor模型中,事件的等待和处理都由操作系统或框架来处理,而应用程序只需关注事件的回调函数,当事件完成时,操作系统或框架会调用相应的回调函数进行处理。这种模型可以充分利用操作系统的异步I/O功能,提高并发性能。
  3. 主从Reactor模型:主从Reactor模型是一种结合了Reactor和Proactor模型的混合模型。它基于一个主Reactor和多个从Reactor组成。主Reactor负责监听事件的到达,并将事件分发给相应的从Reactor从Reactor负责具体的事件处理。主从Reactor模型中,从Reactor可以采用Reactor模式或Proactor模式来处理事件。
    总结:Reactor模型和主从Reactor模型都是在应用程序中同步处理事件,而Proactor模型则是通过异步I/O让操作系统或框架来处理事件。Reactor模型适合于处理少量并发连接的情况,Proactor模型适合于处理大量并发连接的情况,而主从Reactor模型则可以结合两者的优点,适用于中等规模的并发处理。选择合适的模型取决于应用程序的需求和性能要求。
你用了epoll,说一下为什么用epoll,还有其他复用方式吗?区别是什么?

使用epoll是为了高效处理大量并发连接的网络编程场景。epoll是Linux操作系统提供的一种事件通知机制,通过将文件描述符(socket)注册到epoll实例中,可以监听和处理多个连接的事件。与传统的select和poll方式相比,epoll具有以下几个优点:

  1. **高效的事件通知:**epoll使用事件驱动的方式,当有事件发生时,操作系统会立即通知应用程序,而不需要应用程序主动轮询查询。这样可以避免了频繁的系统调用,减少了系统资源的占用。
  2. 高扩展性:epoll使用了内核的事件触发机制,支持同时监听大量的文件描述符。在大规模并发连接的场景下,epoll能够更好地应对高并发请求,提高系统的吞吐量。
  3. 高效的数据结构:epoll使用红黑树和双向链表等数据结构来管理和维护待处理的事件集合,可以快速地插入、删除和查找事件。这样可以提高事件的管理和处理效率。
    除了epoll之外,常见的IO复用方式还包括:
  4. select:select是一种较为传统的IO复用方式,可以同时监视多个文件描述符的可读、可写和异常事件。它使用了线性扫描的方式,需要遍历整个文件描述符集合来查找就绪的事件。当文件描述符数量较大时,效率会受到一定影响。
  5. poll:poll是select的改进版本,也可以同时监视多个文件描述符的可读、可写和异常事件。与select不同的是,poll使用了链表来保存文件描述符,避免了文件描述符数量限制的问题,但仍然需要遍历整个链表来查找就绪的事件。
  6. kqueue:kqueue是在BSD系统中提供的一种事件通知机制。它与epoll类似,也使用了事件驱动的方式,并能够高效地处理大量并发连接。kqueue在BSD系统上提供了更多的功能和灵活性。
    总体而言,epoll相比于select和poll,在处理大规模并发连接时具有更好的性能和扩展性。它利用操作系统提供的事件驱动机制,能够高效地监听和处理大量的文件描述符事件。其他的复用方式如select、poll和kqueue在一定程度上也能实现IO复用,但在处理大规模并发连接时可能效率不及epoll。选择合适的IO复用方式取决于操作系统的支持和特定场景的需求。

HTTP报文解析相关

用了状态机啊,为什么要用状态机?

在 Web 服务器中使用状态机是为了处理请求和响应的状态转换和逻辑。状态机是一种数学模型,可以描述系统在不同状态之间的转换和行为。
使用状态机的主要优点是它提供了清晰的状态转换逻辑和可扩展性。在处理 Web 服务器请求时,可以根据当前的状态和输入来确定下一个状态以及相应的操作。这种逻辑可以用状态机的形式表示,并且易于理解和维护。
以下是一些使用状态机的原因:

  1. 清晰的状态转换:状态机提供了明确的状态转换图,可以清楚地描述服务器在不同请求和响应状态之间的转换。这使得代码更易于阅读、调试和维护。
  2. 灵活性和可扩展性:通过使用状态机,可以轻松地添加新的状态和相应的转换规则,以适应服务器的需求变化。这种可扩展性对于处理复杂的业务逻辑和状态管理非常有用。
  3. 错误处理和容错性:状态机可以定义错误状态和相应的错误处理机制。这使得服务器能够更好地处理异常情况和错误,并采取适当的措施来恢复或回滚到先前的状态。
  4. 可测试性:由于状态机的状态转换逻辑清晰可见,因此可以更轻松地编写测试用例来验证服务器的不同状态和行为。
    总的来说,使用状态机可以使服务器的逻辑更加模块化、可读性更高,并且更容易扩展和维护。它提供了一种结构化的方法来处理复杂的请求和响应逻辑,并使得代码更易于理解和维护。
状态机的转移图画一下

TinyWebserver学习笔记&常问问题整理_第16张图片

https协议为什么安全?

HTTPS(Hypertext Transfer Protocol Secure)是一种安全的通信协议,它在传输过程中使用了加密来确保数据的机密性和完整性。下面是HTTPS协议为什么安全的几个关键原因:
1.** 数据加密**:HTTPS使用加密算法对传输的数据进行加密。这样,当数据在客户端和服务器之间传输时,即使被第三方截获,也很难理解其中的内容。加密使用的是公钥加密和私钥解密的方式,确保只有服务器能够解密和读取数据。
2. 身份验证:HTTPS使用SSL(Secure Sockets Layer)或其继任者TLS(Transport Layer Security)协议来验证服务器的身份。服务器使用数字证书来证明其身份的合法性。数字证书由可信任的第三方机构颁发,可以确保访问的网站是合法的,防止中间人攻击和欺骗。
3. 数据完整性:HTTPS使用消息认证码(MAC)来验证传输的数据在传输过程中是否被篡改。接收方可以验证接收到的数据是否与发送方发送的数据完全一致,以确保数据没有被修改或篡改。
4. 安全协商:HTTPS协议支持安全协商机制,客户端和服务器可以协商选择合适的加密算法和密钥长度。这确保了通信双方使用最强大的加密方法来保护数据的安全性。
5. 防止窃听和篡改:由于HTTPS加密了数据传输过程,窃听者无法直接获取和理解传输的数据。同时,由于数据的完整性验证,即使攻击者试图篡改数据,接收方也可以检测到篡改并拒绝接受被篡改的数据。
综上所述,HTTPS通过加密、身份验证、数据完整性和安全协商等机制,提供了更安全的数据传输方式。它能够防止数据被窃听、篡改和伪造,为用户和网站提供了更高的安全性和保护。

https的ssl连接过程

HTTPS的SSL连接过程如下:

  1. 客户端发送连接请求:客户端通过向服务器发送连接请求开始SSL连接过程。连接请求通常使用HTTPS的默认端口443。
  2. 服务器证书:服务器接收到客户端的连接请求后,会将其数字证书发送给客户端。证书包含了服务器的公钥、证书颁发机构的签名以及其他相关信息。
  3. 客户端验证证书:客户端接收到服务器的证书后,会进行验证。验证包括检查证书的有效性、颁发机构的可信性以及证书中的域名与访问的域名是否匹配。如果验证成功,客户端将继续进行下一步;否则,会发出警告或终止连接。
    4.** 生成随机数**:客户端会生成一个随机数,用于后续的密钥协商过程。
  4. 共享密钥协商:客户端使用服务器的公钥加密生成的随机数,并将其发送给服务器。服务器收到加密的随机数后,使用自己的私钥进行解密,得到随机数。此时,客户端和服务器都有了相同的随机数,用于生成对称加密的会话密钥。
    6.** 对称加密通信**:客户端和服务器使用协商好的会话密钥来进行对称加密通信。这意味着之后的数据传输将使用更高效的对称加密算法进行,以确保数据的机密性和完整性。
  5. 数据传输:在SSL连接建立后,客户端和服务器之间进行安全的数据传输。所有的数据都会经过对称加密和完整性验证,以确保安全性。
GET和POST的区别:
  1. 数据传输位置
    • GET:数据通过 URL 的查询字符串(Query String)传输,附加在 URL 后面,以 ? 开始,多个参数使用 & 分隔。例如:[https://example.com/path?param1=value1¶m2=value2](https://example.com/path?param1=value1¶m2=value2`)
    • POST:数据通过请求的消息体(Request Body)传输,作为请求的一部分发送给服务器。
  2. 数据长度限制:
    • GET:由于数据附加在 URL 上,所以有长度限制。不同浏览器和服务器对 URL 长度有不同的限制,但通常在几千个字符左右。
    • POST:理论上没有长度限制,但实际上会受限于服务器和网络的配置。
  3. 数据安全性:
    • GET:数据通过 URL 传输,会被保存在浏览器的历史记录、服务器的访问日志等中,因此对于敏感信息不宜使用 GET 请求。
    • POST:数据通过消息体传输,不会保存在浏览器的历史记录中,相对于 GET 请求更安全,适合传输敏感信息。
  4. 数据缓存
    • GET:由于数据附加在 URL 上,GET 请求会被浏览器缓存。当再次请求相同的 URL 时,浏览器可能直接返回缓存的结果,不会再次向服务器发送请求。
    • POST:POST 请求默认不被缓存,每次都会向服务器发送请求。
  5. 幂等性
    • GET:GET 请求是幂等的,即对于同一个 URL 的多次请求,服务器的响应应该是相同的,不会改变服务器的状态。
    • POST:POST 请求不是幂等的,每次请求可能会改变服务器的状态,例如创建新资源、提交表单等。
      总的来说,GET 方法适合用于获取资源,参数少且不敏感;而 POST 方法适合用于发送、提交数据,参数多且可能包含敏感信息。

数据库登录注册相关

登录说一下?
  • 用户输入凭据:用户提供登录所需的凭据,通常是用户名和密码。
  • 验证凭据:将用户提供的凭据与数据库中存储的凭据进行比对,验证用户身份。
你这个保存状态了吗?如果要保存,你会怎么做?
  • 登录状态管理:如果凭据验证成功,将在服务器端建立用户的登录状态,可以使用会话(Session)或令牌(cookie)等机制。
登录中的用户名和密码你是load到本地,然后使用map匹配的,如果有10亿数据,即使load到本地后hash,也是很耗时的,你要怎么优化?
  1. 数据库索引:将用户名作为唯一索引或主键,在数据库层面上进行优化。这样可以利用数据库的查询优化机制,加快匹配速度。使用索引可以在常数时间内快速检索用户记录,而不需要遍历整个数据集。
  2. 哈希算法和散列存储:使用哈希算法将用户名进行哈希处理,并将其存储在散列结构中,例如哈希表(Hash Table)。这样可以快速计算哈希值,并通过哈希表快速查找匹配。
  3. 分布式存储和并行处理:如果数据量巨大,可以考虑分布式存储和并行处理。将数据分散存储在多个节点上,每个节点负责一部分数据。在进行用户名和密码匹配时,可以将查询任务分发给多个节点并行处理,提高整体的匹配效率。
  4. 内存缓存:将部分热门用户数据加载到内存缓存中,例如使用内存数据库(如Redis)或缓存框架(如Memcached)。这样可以减少对磁盘读取的次数,提高访问速度。
  5. 分片和分区:将数据分片或分区存储,使得数据集在物理上分布在多个节点上。根据用户名的特征将数据进行分片,可以使得每个节点只处理部分数据,减少加载和匹配的负担。
  6. 预处理和预计算:在数据加载过程中,进行一些预处理和预计算,以减少后续匹配过程中的计算开销。例如,可以提前计算用户名的哈希值或其他特征,以便在匹配时直接使用。
  7. 分级存储:对于非常大的数据集,可以采用分级存储策略。将数据划分为多个层级,根据访问频率将热门数据存储在高速存储介质(如SSD),将冷数据存储在低速存储介质(如磁盘),以达到平衡性能和存储成本的目的。

定时器相关

为什么要用定时器?
  1. **超时处理:**当客户端与服务器建立连接后,服务器可能需要监控连接的活动状态,并在一定时间内检测是否存在超时情况。通过使用定时器,服务器可以设置超时时间,并在超过指定时间后中断连接或执行特定的超时处理逻辑。这有助于释放资源并防止长时间的闲置连接占用服务器资源。
  2. 会话管理:Web应用程序通常使用会话(Session)来跟踪用户状态和数据。服务器可以使用定时器来管理会话的有效期。当会话超过一定时间没有活动时,服务器可以将其标记为无效或删除,以释放相关资源并维护系统的安全性。
  3. 缓存刷新:Web服务器经常使用缓存来提高性能,但某些情况下需要定期刷新缓存以确保数据的及时性和一致性。通过使用定时器,服务器可以定期触发缓存刷新操作,从而更新缓存中的数据。
  4. 日志切割:Web服务器通常会生成日志文件,用于记录访问日志、错误日志等。为了避免日志文件过大,服务器可以使用定时器来触发日志切割操作,将当前日志文件归档并创建新的日志文件。
  5. 定时任务:Web服务器可能需要执行一些定时任务,例如定时备份数据、定时生成报表、定时清理临时文件等。通过使用定时器,服务器可以按照预定的时间间隔触发这些任务的执行。
说一下定时器的工作原理

利用alarm函数周期性地触发SIGALRM信号,信号处理函数利用管道通知主循环,主循环接收到该信号后对升序链表上所有定时器进行处理,若该段时间内没有交换数据,则将该连接关闭,释放所占用的资源。

双向链表啊,删除和添加的时间复杂度说一下?还可以优化吗?

在升序链表中进行删除和插入操作的时间复杂度取决于具体的实现方式。下面是常见的实现方式及其时间复杂度:

  1. 单向链表:
    • 插入操作:
      • 在链表头部插入节点:O(1)时间复杂度。
      • 在链表尾部插入节点:需要遍历整个链表,时间复杂度为O(n),其中n是链表的长度。
      • 在链表中间插入节点:需要找到正确的位置插入节点,需要遍历链表,平均时间复杂度为O(n/2),最坏情况下为O(n)。
    • 删除操作:
      • 删除链表头部节点:O(1)时间复杂度。
      • 删除链表尾部节点:需要遍历整个链表,时间复杂度为O(n)。
      • 删除链表中间节点:需要找到要删除的节点位置,需要遍历链表,平均时间复杂度为O(n/2),最坏情况下为O(n)。
  2. 双向链表:
    • 插入操作:
      • 在链表头部插入节点:O(1)时间复杂度。
      • 在链表尾部插入节点:O(1)时间复杂度。
      • 在链表中间插入节点:需要找到正确的位置插入节点,需要遍历链表,平均时间复杂度为O(n/2),最坏情况下为O(n)。
    • 删除操作:
      • 删除链表头部节点:O(1)时间复杂度。
      • 删除链表尾部节点:O(1)时间复杂度。
      • 删除链表中间节点:需要找到要删除的节点位置,需要遍历链表,平均时间复杂度为O(n/2),最坏情况下为O(n)。
        需要注意的是,以上时间复杂度是在最坏情况下给出的。在一些特殊情况下,例如在已知位置插入或删除节点时,时间复杂度可能会更低。另外,如果升序链表中的节点按照特定规则进行组织(例如平衡二叉搜索树),可以进一步优化插入和删除操作的时间复杂度。
        综上所述,升序链表的插入和删除操作的时间复杂度通常为O(n)或O(n/2),其中n是链表的长度。双向链表相对于单向链表具有更好的插入和删除性能。
最小堆优化?说一下时间复杂度和工作原理
  • 最小堆优化双向链表的一个优势是在维护链表顺序的同时,可以快速访问链表中的最小元素。这对于需要频繁查找和操作最小值的场景非常有用。
  • 该数据结构在一些问题中具有应用,例如优先级队列(Priority Queue),其中需要快速获取和删除最小优先级的元素。
  • 最小堆优化双向链表的时间复杂度相对较低,但由于需要同时维护链表和最小堆的一致性,会增加一些额外的开销和复杂性。因此,选择是否使用最小堆优化双向链表需要根据具体的问题需求和性能要求进行评估。

日志相关

说下你的日志系统的运行机制?

使用单例模式创建日志系统,对服务器运行状态、错误信息和访问数据进行记录,该系统可以实现按天分类,超行分类功能,可以根据实际情况分别使用同步和异步写入两种方式。
其中异步写入方式,将生产者-消费者模型封装为阻塞队列,创建一个写线程,工作线程将要写的内容push进队列,写线程从队列中取出内容,写入日志文件。
日志系统大致可以分成两部分,其一是单例模式与阻塞队列的定义,其二是日志类的定义与使用。

为什么要异步?和同步的区别是什么?

同步日志,日志写入函数与工作线程串行执行,由于涉及到I/O操作,当单条日志比较大的时候,同步模式会阻塞整个处理流程,服务器所能处理的并发能力将有所下降,尤其是在峰值的时候,写日志可能成为系统的瓶颈。
异步日志,将所写的日志内容先存入阻塞队列,写线程从阻塞队列中取出内容,写入日志。

现在你要监控一台服务器的状态,输出监控日志,请问如何将该日志分发到不同的机器上?
  1. 安装和配置消息队列:选择适合你需求的消息队列系统,例如 RabbitMQ、Kafka 或者 ActiveMQ,并按照它们的官方文档进行安装和配置。
  2. 定义消息格式:确定你的监控日志的消息格式,包括需要传递的数据和字段。这可以是一个 JSON、XML 或其他自定义的格式。
  3. 服务器端发送日志消息:
    • 在服务器端的监控程序中,将产生的日志封装成消息,按照消息格式发送到消息队列。
    • 消息队列可以提供相应的客户端库或API,用于连接到消息队列,并发送消息到指定的队列。
  4. 消费者端接收日志消息:
    • 在每个接收日志消息的机器上,配置一个消息队列的消费者。
    • 消费者从消息队列中订阅(或消费)特定的队列或主题,并接收到达的日志消息。
    • 消费者可以根据需要对日志消息进行处理,例如将其存储到文件、数据库或其他存储系统中,或者进行进一步的处理和分析。
      通过使用消息队列,你可以实现日志消息的分发和解耦。监控程序只需要将日志消息发送到消息队列,而消费者可以根据自己的需求和处理能力来接收和处理日志消息。这样的架构可以提供灵活性、可伸缩性和高可用性,同时减少了监控程序与消费者之间的直接依赖。

压测相关

服务器并发量测试过吗?怎么测试的?
  1. 压力测试工具:使用专门的压力测试工具来模拟大量并发用户并发送请求给服务器。一些常见的压力测试工具包括 Apache JMeter、LoadRunner、Gatling 等。这些工具可以模拟并发用户的行为,向服务器发送请求并测量响应时间和吞吐量等指标。
  2. 设计测试场景:根据你的需求和应用场景,设计合适的测试场景。这包括确定并发用户数量、请求类型(如GET、POST等)、请求频率和持续时间等参数。你可以模拟不同的使用情况和负载模式,以更全面地评估服务器的性能。
  3. 配置测试环境:设置测试环境,包括服务器、网络和客户端机器。确保服务器和网络环境与实际生产环境尽可能接近,以获得更准确的测试结果。同时,为了避免影响其他系统和网络资源,可以将测试环境与生产环境隔离开来。
  4. 运行测试:使用压力测试工具配置测试场景并启动测试。工具将模拟并发用户的行为,向服务器发送请求。测试期间,收集和监控服务器的响应时间、吞吐量和错误率等指标。
  5. 分析结果:根据测试运行完成后的结果数据,分析服务器的性能和瓶颈。关注响应时间、吞吐量和错误率等指标,识别性能瓶颈和资源消耗情况。这有助于确定服务器的性能极限、优化需求和容量规划等。
    在进行服务器并发量测试时,还需要注意以下事项:
  • 确保测试环境与实际生产环境相似,包括硬件配置、网络带宽、服务器配置等。
  • 根据具体情况选择适当的压力测试工具和测试方法。
  • 监控服务器的资源利用率,例如 CPU 使用率、内存占用和网络带宽等。
  • 进行多次测试,并记录测试结果,以获取更准确和可靠的数据。
  • 注意测试对服务器造成的负载,确保测试过程不会导致服务器性能下降或故障。
webbench是什么?介绍一下原理

Webbench是一款简单而轻量级的开源压力测试工具,用于评估Web服务器的性能和并发处理能力。它由Lionbridge Technologies开发,最初用于测试Web服务器的稳定性和性能。Webbench的原理如下:

  1. 单机压力测试:Webbench以单机为基础,模拟多个并发用户向目标Web服务器发送请求。它通过创建多个客户端进程(或线程)来模拟并发用户的行为。
  2. HTTP请求模拟:Webbench发送简单的HTTP GET请求到目标Web服务器。它不支持其他HTTP请求方法,如POST或PUT。这使得Webbench适用于对Web服务器的基本性能评估。
  3. 固定URL:Webbench使用一个固定的URL来测试,它将该URL发送给目标服务器,并等待响应。这意味着Webbench无法模拟复杂的请求场景,如动态URL、会话状态等。
    4.** 多并发用户**:Webbench可以配置并发用户的数量,通过创建并发的客户端进程或线程来模拟并发请求。每个客户端向目标服务器发送请求并等待响应。
  4. 统计和报告:Webbench记录每个客户端的响应时间、成功请求数和错误数等指标。在测试结束后,它会生成一个简单的报告,显示总体吞吐量、平均响应时间和错误率等数据。
    Webbench的优点是简单易用,具有轻量级的特性,适用于快速评估Web服务器的基本性能。然而,由于其简化的设计,它不适用于复杂的压力测试场景和更高级的性能分析需求。在进行真实的生产环境压力测试时,更加复杂和全面的工具和方法可能更为适合,如Apache JMeter、LoadRunner、Gatling等。

综合能力

说一下前端发送请求后,服务器处理的过程,中间涉及哪些协议?
  1. 用户发送请求:前端通过浏览器或应用程序发送HTTP请求到服务器。请求可以是GET、POST、PUT、DELETE等HTTP方法之一。
  2. HTTP协议:HTTP(Hypertext Transfer Protocol)是一种用于在客户端和服务器之间传输数据的协议。请求中包含了请求头和请求体,请求头包括请求方法、URL、请求头字段等信息。
  3. 网络传输:请求通过网络传输到服务器。通常使用TCP/IP协议栈进行数据传输。TCP(Transmission Control Protocol)提供可靠的、面向连接的数据传输,而IP(Internet Protocol)则负责网络寻址和路由。
  4. Web服务器:服务器接收到请求后,通过Web服务器软件(如Apache HTTP Server、Nginx等)进行处理。Web服务器负责解析请求、路由请求到正确的处理程序,并生成响应。
  5. 应用服务器和应用程序:Web服务器通常会将请求转发给应用服务器或应用程序来处理。应用服务器可以是Web框架(如Django、Ruby on Rails)或应用程序服务器(如Tomcat、Node.js)。应用服务器执行相应的业务逻辑,访问数据库或其他资源,并生成响应数据。
    6.** 数据库和SQL协议**:如果应用程序需要从数据库中检索或更新数据,应用程序将使用适当的数据库客户端与数据库服务器通信。常见的数据库包括MySQL、PostgreSQL、Oracle等。数据库通常使用SQL(Structured Query Language)协议进行通信。
  6. 响应生成和传输:应用程序处理请求后,生成相应的响应数据。响应数据包括响应头和响应体,响应头包含响应状态码、内容类型、响应头字段等信息。响应通过网络传输回前端。
  7. 网络传输和协议:响应通过网络传输回前端。同样,TCP/IP协议栈被用于可靠的数据传输。
  8. 前端渲染:前端接收到响应后,根据响应数据进行渲染和展示。这可能涉及HTML、CSS、JavaScript等技术,用于呈现响应内容给用户。
    总结起来,前端发送请求后,服务器处理的过程中涉及到HTTP协议、TCP/IP协议、数据库通信协议(如SQL),以及Web服务器、应用服务器和数据库服务器等软件和组件。这些协议和组件相互配合,实现了请求的处理、数据的传输和响应的生成,最终将结果呈现给前端用户。

你可能感兴趣的:(学习,笔记,c++,http,websocket)