Web-Server

阻塞和非阻塞、同步和异步(网络IO)

典型的一次IO 的两个阶段是: 数据就绪和数据读写

数据就绪:

  • 阻塞: 调用 IO 方法的线程会进入阻塞状态
  • 非阻塞: 不会改变线程的状态,通过返回值进行判断

数据读写:

  • 同步
  • 异步

在处理 IO 的时候,阻塞和非阻塞都是同步 IO, 只有使用论特殊的 API 才是异步 IO。

linux 中的异步 IO 接口:

aio_read();
aio_write();

一个典型的网络 IO 接口调用,分为两个阶段,分别是 ”数据就绪“ 和 ”数据读写“,数据就绪阶段分为阻塞和非阻塞,表现的结果就是, 阻塞当前线程或是直接返回。

同步表示 A 向 B 请求调用一个网络 IO 接口时(或者调用某个业务逻辑 API 接口时), 数据的读写都是由请求方 A 自己来完成的(不管阻塞还是非阻塞);

异步表示 A 向 B 请求调用一个网络 IO 接口时(或者调用某个业务逻辑 API 接口时),向 B 传入请求以及时间发生时通知的方式, A 就可以处理其他逻辑了, 当 B 监听到事件处理完成后,会用事先约定号的通知方式,通知 A 处理结果。

Unix,Linux上的五种IO模型

  • 1、阻塞 blocking

调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的去检查这个函数有没有返回,必须等这个函数返回才能进行下一步动作。

  • 2、非阻塞 non-blocking (NIO)

非阻塞等待,每隔一端时间就去检测 IO 事件是否就绪。没有就绪就可以做其他事情。 非阻塞 IO 执行系统调用总是立即返回,不管事件是否已经发生,若事件没有发生,则返回 -1,此时可以根据 errno 区分这两种情况, 对于 accept、recv、send, 事件未发生时, errno 通常被设置成 EAGAIN。

  • 3、IO复用 (IO multiplexing)

Linux 用 select、poll、epoll 函数实现 IO 复用模型,这些函数也会使进程阻塞,但是和阻塞 IO 所不同的是这些函数可以同时阻塞多个IO 操作。 而且可以同时对多个读操作,写操作的 IO 函数进行检测。知道有数据可读或可写时,才真正调用 IO 操作函数。

  • 4、信号驱动 (signal-driven)

Linux 用套接口进行信号驱动 IO, 安装一个信号处理函数,进程继续运行并不阻塞,当 IO 事件就绪,进程收到 SIGIO 信号,然后处理 IO 事件。

  • 5、异步 (asynchronous)

Linux 中,可以调用 aio_read 函数告诉内核文件文件描述符缓冲区指针和缓冲区的大小,文件偏移及通知的方式,然后立即方式,当内核将数据拷贝到缓冲区后,再通知程序。

Web服务器简介

一个 web server 就是一个服务器软件(程序),或者是运行这个服务器软件的硬件(计算机)。 其主要功能是通过 HTTP 协议与客户端(通常是浏览器 Browser)进行通信,来接收,存储,处理来自客户端的 HTTP 请求, 并对其请求作出 HTTP 响应,返回给客户端其请求的内容(文件、网页等)或返回一个 Error 信息。

通常用户使用 web 浏览器与响应服务器进行通信。在浏览器中键入 域名 或 IP 地址:端口号,浏览器先将域名解析成相应的 IP 地址或者 根据你的 IP 地址或者直接根据 IP 地址向对应的 web 服务器发送一个 HTTP 请求。 这一过程首先通过 TCP 协议的三次握手建立与目标 web 服务器的连接,然后 HTTP 协议生成针对目标 web 服务器的 HTTP 请求报文, 通过 TCP、IP 等协议发送到目标 web 服务器上。

HTTP协议(应用层协议)

超文本传输协议 (Hypertext Transfer Protocol, HTTP) 是一个简单的请求-响应协议,它通常运行在 TCP 之上。 它指定论客户端可能发送给服务器什么样的消息以及得到什么样的响应。 请求和响应消息的头以 ASCII 形式给出;而消息内容则具有一个类似 MIME 的格式。HTTP 是万维网的数据通信的基础。

请求报文: 请求行,请求头部,空行和请求数据 4 部分组成。

响应:状态行,响应头部,空行和响应数据 4 部分构成。

通信流程:

  1. 浏览器向 DNS 服务器请求解析该 URL 中的域名所对应的 IP 地址
  2. 解析出 IP 地址后,根据该 IP 地址和默认端口 80, 和服务器建立 TCP 连接。
  3. 浏览器发出读取文件(URL中域名后面部分对应的文件)的 HTTP 请求,该请求报文作为 TCP 三次握手的第三个报文的数据发送给服务器。
  4. 服务器对浏览器请求作出响应,并把对应的 HTML 文本发送给浏览器。
  5. 释放 TCP 连接
  6. 浏览器解析该 HTML 文本并显示内容。
  • HTTP 请求报文格式

  • HTTP 响应报文格式

  • HTTP 请求方法

HTTP 1.1 协议中共定义了八种方法(也叫动作)来以不同方式操作指定的资源: GET、HEAD、POST、PUT、DELETE、TRACE、OPTIONS、CONNECT。

  • 状态码
类别 原因短语
1xx informational 信息性状态码 接收的请求正在处理
2xx Success 成功状态码 请求正常处理完毕
3xx Redirection 重定向状态码 需要进行附加操作以完成请求
4xx Client Error 客户端错误状态码 服务器无法处理请求
5xx Server Error 服务器错误状态码 服务器处理请求出错

服务器编程基本框架

模块 功能
IO 处理单元 处理客户连接,读写网络数据
逻辑单元 业务进程或线程
网络存储单元 数据库、文件或缓存
请求队列 各单元之间的通信方式

IO 处理单元是服务器管理客户端连接的模块。它通常要完成以下工作:等待并接收新的客户端连接,接收客户数据, 将服务器响应数据返回给客户端。但是数据收发不一定在 IO 处理单元中执行,也可能在逻辑单元中执行,具体在何处执行取决于事件处理模式。

一个逻辑单元通常是一个进程或线程。它分析并处理客户数据,然后将结果传送给 IO 单元或者直接发送给客户端(具体使用那种方式取决于事件处理模式)。 服务器通常拥有多个逻辑单元,以实现对多个客户任务的并发处理。

网络存储单元可以是数据库、缓存和文件、但不是必须的。

请求队列是各个单元之间的通信方式的抽象。 IO 处理反员单元接收到客户请求时,需要以某种方式通知一个逻辑单元来处理该请求。 同样,多个逻辑单元同时访问一个存储单元时,也需要采用某种机制来协调处理竟态条件。请求队列通常被实现为池的一部分。

两种高效的事件处理模式

服务器通常需要处理三类时间: IO事件、信号及定时事件。有两种高效的事件处理模式: Reactor 和 Proactor, 在同步 IO 模型通常实现 Reactor 模式, 异步 IO 通常用于实现 Proactor 模式。

Reactor模式

要求主线程(IO 处理单元)只负责监听文件描述符上是否有事件发生,有的话立即将该事件通知工作线程(逻辑单元), 将 socket 可读可写事件放入请求队列,交给工作线程处理。 除此之外,主线程不做任何其他实质性的工作。读写数据,接受新的连接,以及处理客户请求均在工作线程中完成。

使用同步 IO( 以 poll_wait 为例)实现 Reactor 模式的工作流程是:

  1. 主线程往 epoll 内核事件表中注册 socket 上的读就绪事件。
  2. 主线程调用 epoll_wait 等待 socket 上有数据可读。
  3. 当 socket 上有数据可读时,epoll_wait 通知主线程。主线程则将 socket 可读事件放入请求队列。
  4. 睡眠在请求队列上的某个工作线程被唤醒,它从 socket 读取数据,并处理客户请求,然后往 epoll 内核事件表中注册该 socket 上写就绪事件。
  5. 当主线程调用 epoll_wait 等待 socket 可写。
  6. 当 socket 可写时, epoll_wait 通知主线程。主线程将 socket 可写事件放入请求队列。
  7. 睡眠在请求队列上的某个工作线程被唤醒,它往 socket 上写入服务器处理客户请求的结果。

Reactor 模式的工作流程:

Proactor模式

Proactor 模式将所有 I/O 操作都交给主线程和内核来处理(进行读、写),工作线程仅仅负责业务逻辑。 使用异步 I/O 模型(以 aio_read() 和 aio_write() 为例实现的 Proactor 模式的工作流程是:

  1. 主线程调用 aio_read 函数向内核注册 socket 上的读完成事件,并告诉内核用户读缓冲区的位置, 以及读操作完成时如何通知应用程序(这里以信号为例)。
  2. 主线程继续处理其他逻辑。
  3. 当 socket 上的数据被读入用户缓冲区后,内核将向应用程序发送一个信号,以通知应用程序数据 已经可用。
  4. 应用程序预先定义好的信号处理函数选择一个工作线程来处理客户请求。工作线程处理完客户请求 后,调用 aio_write 函数向内核注册 socket 上的写完成事件,并告诉内核用户写缓冲区的位置,以 及写操作完成时如何通知应用程序。
  5. 主线程继续处理其他逻辑。
  6. 当用户缓冲区的数据被写入 socket 之后,内核将向应用程序发送一个信号,以通知应用程序数据 已经发送完毕。
  7. 应用程序预先定义好的信号处理函数选择一个工作线程来做善后处理,比如决定是否关闭 socket。

Proactor 模式的工作流程:

模拟Procator模式

使用同步 I/O 方式模拟出 Proactor 模式。原理是:主线程执行数据读写操作,读写完成之后,主线程向 工作线程通知这一”完成事件“。那么从工作线程的角度来看,它们就直接获得了数据读写的结果,接下 来要做的只是对读写的结果进行逻辑处理。 使用同步 I/O 模型(以 epoll_wait为例)模拟出的 Proactor 模式的工作流程如下:

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

同步 IO 模拟 Proactor 模式的工作流程:

线程同步机制类封装及线程池实现

线程池

线程池是由服务器预先创建的一组子线程,线程池中的线程数量应该和 CPU 数量差不多。 线程池中的所有子线程都运行着相同的代码。 当有新的任务到来时,主线程将通过某种方式选择线程池中的某一个子线程来为之服务。 相比与动态的创建子线程,选择一个已经存在的子线程的代价显然要小的多。 至于主线程选择哪个子线程来为新任务服务,有多种方式:

  • 主线程使用某种算法来主动选择子线程。最简单的算法是随机算法和 Round Robin (轮流选取)算法, 但更优秀、更智能的算法将使任务在各个工作线程中更均匀地分配,从而减轻服务器的整体压力。
  • 主线程和所有子线程通过一个共享的工作队列来同步,子线程都睡眠在工作队列上。当有新的任务到来时, 主线程将任务添加到工作队列中。这将唤醒正在等待任务的子线程,不过只有一个子线程将获得任务的“接管权”, 它可以从工作队列中去除任务并执行之,而其他子线程将继续睡眠在工作队列上。

线程池的一般模型:

线程池中的线程数量最直接的限制因素是中央处理器 CPU 的处理器 (processors/cores)的数量N: 如果 4-cores 的 CPU, 对于 CPU 密集型的任务(如视频剪辑等消耗 CPU 计算资源的任务)来说, 那线程池的线程数量最好也设置为4,(或者+1防止其他因素造成的线程阻塞); 对于 IO 密集型的任务,一般要多于 CPU 的核数,因为线程间竞争的不是 CPU 的计算资源而是 IO, IO 处理一般较慢,多于 cores 数的线程将为 CPU 争取更多的任务,不至于在线程处理 IO 的过程造成CPU 空闲导致资源浪费。

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

EPOLLONESHOT事件

即使可以使用 ET 模式,一个 socket 上的某个事件还是可能被触发多次。 这在并发程序中就会引发一个问题。比如一个线程在读取完某个 socket 上的数据后开始处理这些数据, 而在数据的处理过程中该 socket 上又有新数据可以读(EPOLLIN 再次被触发),此时另外一个线程被唤醒来读取这些新的数据。 于是出现了两个线程同时操作一个 socket 的局面。一个socket连接在任意时刻都只被一个线程处理,可以使用 epoll 的 EPOLLONESHOT 事件实现。

对于注册了 EPOLLONESHOT 事件的文件描述符,操作系统最多触发其上注册的一个可读、可写或者异常事件,且只触发一次, 除非我们使用 epoll_ctl 函数重置该文件描述符上注册的 EPOLLONESHOT 事件。这样,当一个线程在处理某个 socket 时, 其他线程是不可能有机会操作该 socket 的。反过来思考,注册了 EPOLLONESHOT 事件的 socket 一旦被某个线程处理完毕,该线程就应该 立即重置这个 socket 上的 EPOLLONESHOT 事件, 以确保这个 socket 下一次可读时, 其 EPOLLIN 事件能被触发,进而让其他工作线程有机会处理这个 socket。

有限状态机

逻辑单元内部的一种高效的编程方法: 有限状态机 (finite state machine); STATE_MACHINE ()

  • 项目整体流程
  • 解析HTTP请求报文
  • 解析请求完成及生成响应信息
  • 定时检测非活跃连接

服务器压力测试

Webbench 是 Linux 上一款知名的 web 性能压力测试工具。它是由 Lionbridge 公司开发。

测试处在相同硬件上,不同服务的请能以及不同硬件上同一个服务的运行状况。 展示服务器的两项内容:每秒钟响应请求数和每秒钟传输数据量。

基本原理: Webbench 首先 fork 出多个子进程,每个子进程都循环做 web 访问测试。 子进程把访问结果通过 pipe 告诉父进程,父进程做最终的统计结果。

webbench -c 1000 -t 30 http://172.0.0.1:10000/index.html
# -c 表示客户端数
# -t 时间