Nginx 作为高性能的 HTTP 服务器和反向代理服务器,在处理 HTTP 请求时,对 HTTP 头部的处理是至关重要的一环。
接收请求事件模块
Nginx 使用了一个事件驱动的架构,这使得它能够高效地处理大量的并发连接。下面是 Nginx 处理 HTTP 请求的详细流程:
1. 建立连接
三次握手:客户端通过发送 SYN 包开始与 Nginx 建立 TCP 连接。Nginx 响应 SYN ACK 包,客户端再次发送 ACK 包完成握手。 负载均衡:如果有多个 worker 进程在监听相同的端口,操作系统的负载均衡机制会选择一个 worker 进程来处理新的连接。 读事件:一旦连接建立,Nginx 的事件模块会监听来自客户端的数据。当有数据到达时,操作系统会通知 Nginx。
Nginx Worker 负载均衡选择
Nginx 使用事件驱动和异步 I/O 来处理请求,它的工作进程(worker processes)可以并行处理多个客户端连接。当多个工作进程监听相同的端口时,操作系统的负载均衡机制会介入:
- 操作系统负载均衡:现代操作系统(如 Linux)通常具备内核级别的负载均衡功能。当多个进程监听同一个端口时,操作系统内核会使用特定的算法(如 round-robin)来分配新的连接请求到不同的工作进程。这种机制在多个Nginx工作进程监听同一个端口时非常有效。
- Nginx 工作进程模型:Nginx 的配置文件中可以设置
worker_processes
指令,这决定了 Nginx 将启动多少个工作进程。每个工作进程都是独立的,并且能够处理自己的连接和请求。
读事件监听与处理
一旦 TCP 连接建立,Nginx 需要准备接收客户端发送的数据。这是通过 Nginx 的事件模块来实现的:
- I/O 多路复用:Nginx 使用
epoll
(或其他类似的 I/O 多路复用技术)来同时监控多个网络套接字上的事件。epoll
允许 Nginx 以非阻塞的方式检测哪些套接字上有数据可读。 - 事件通知:当操作系统检测到某个网络套接字上有数据到达时,
epoll
会通知 Nginx。Nginx 的事件模块会捕获这个事件,并将事件加入到事件队列中。 - 事件处理:Nginx 会从事件队列中取出事件,并调用相应的处理函数来读取数据。在 Nginx 的源码中,这个处理函数通常是
ngx_event_accept
,它会处理新的连接请求。
2. 接收请求
epoll_wait:Nginx 使用
epoll_wait
系统调用来等待 I/O 事件的发生,如客户端发送的数据到达。 读取请求:当epoll_wait
检测到读事件时,Nginx 会调用ngx_http_wait_request_handler
来读取客户端发送的 HTTP 请求数据。
epoll_wait 系统调用
epoll_wait
是 Linux 系统中用于等待 I/O 事件的系统调用,它是 epoll
I/O 多路复用机制的一部分。Nginx 使用 epoll
来监控大量的网络套接字,以检测哪些套接字上有数据可读或可写。
- 事件循环:Nginx 通过
epoll_wait
进入一个事件循环,在这个循环中,Nginx 会阻塞等待事件发生。当epoll_wait
返回时,它提供了一组就绪的文件描述符(即套接字),这些套接字上的数据已经准备好读取或写入。 - 非阻塞 I/O:由于
epoll
是非阻塞的,Nginx 可以在等待事件发生时执行其他任务,例如处理其他连接或执行定时任务。
读取请求数据
一旦 epoll_wait
检测到读事件,Nginx 将调用相应的处理函数来读取客户端发送的数据。这个过程在 Nginx 源码中是由 ngx_http_wait_request_handler
函数负责的。
- 请求处理链:
ngx_http_wait_request_handler
函数是请求处理链的一部分,它负责从客户端读取请求行和请求头。 - 缓冲区管理:读取到的数据会被存储在 Nginx 配置的缓冲区中,这个缓冲区由
client_header_buffer_size
和large_client_header_buffers
指令控制其大小。 - 状态机解析:Nginx 使用内部的状态机来解析请求行和请求头。状态机根据 HTTP 协议的规范逐步解析请求数据,并将其存储在
ngx_http_request_t
结构体中。 - 请求上下文:解析过程中,Nginx 会为每个请求创建一个请求上下文,其中包含了请求的所有信息,如方法、URI、头部字段等。这个上下文会在请求的整个生命周期中被使用。
3. 分配内存资源
分配连接内存池:Nginx 会为每个新的连接分配一个连接内存池,其大小由
connection_pool_size
配置指令指定,默认为 512 字节。 设置回调方法:Nginx 会设置回调方法ngx_http_init_connection
来处理新的连接,并将其读事件加入到epoll
监控中。 添加超时定时器:为了防止客户端长时间不发送请求,Nginx 会添加一个超时定时器client_header_timeout
,默认为 60 秒。
分配连接内存池
Nginx 使用内存池来管理连接相关的数据,这样可以提高内存使用的效率并减少内存分配和释放的开销。
- connection_pool_size:这个配置指令定义了每个连接的连接内存池的大小。默认情况下,这个大小设置为 512 字节,足以存储大多数连接状态信息。
- 连接内存池的结构:在 Nginx 源码中,
ngx_connection_t
结构体代表了单个连接,它包含了连接的状态、套接字文件描述符、地址信息等。ngx_connection_t
结构体中的某些字段会使用连接内存池来存储数据。
设置回调方法
Nginx 通过设置回调方法来处理新的连接,这是事件驱动编程的一个重要部分。
- ngx_http_init_connection:这个回调方法在新的连接建立时被调用,它负责初始化连接状态、设置读取事件的处理函数,并准备接收客户端的 HTTP 请求。
- epoll 监控:
ngx_http_init_connection
会将新连接的读取事件注册到epoll
系统中,这样当有数据可读时,epoll
能够通知 Nginx。
添加超时定时器
为了防止客户端长时间不发送请求,Nginx 会为每个连接设置一个超时定时器。
- client_header_timeout:这个配置指令定义了客户端发送请求头的超时时间,默认为 60 秒。如果在这个时间内客户端没有发送任何数据,Nginx 会认为连接已经超时,并关闭连接。
- 超时处理:在源码中,超时处理通常由
ngx_http_request_handler
或类似的处理函数来管理。当超时发生时,Nginx 会停止等待客户端的数据,并关闭连接。
4. 用户正式请求
读取数据:Nginx 读取客户端发送的数据,并将其存储在读缓冲区中。 分配读缓冲区:读缓冲区的大小由
client_header_buffer_size
配置指令指定,默认为 1KB。(这个值也不是越大越好,因为当用户有一个请求进来的时候,nignx 就会分配1kb 内存出来,)
在 Nginx 处理用户正式请求的过程中,读取数据和分配读缓冲区是两个基础而关键的步骤。这两个步骤确保了 Nginx 能够接收和存储客户端发送的 HTTP 请求数据。以下是结合 Nginx 底层原理与源码对这两个步骤的详细展开:
读取数据
Nginx 通过读取客户端发送的数据来开始处理 HTTP 请求。这个过程是在 I/O 事件触发时进行的,通常是在 epoll
事件循环中,当检测到读事件(即客户端发送数据)时,Nginx 会执行以下操作:
- 读取数据到缓冲区:Nginx 使用
read
系统调用来从网络套接字读取数据。读取的数据被存储在一个特定的缓冲区中,这个缓冲区称为读缓冲区。 - 处理分片数据:由于网络传输的特性,客户端发送的数据可能会被分片(即分成多个数据包)。Nginx 需要处理这些分片,以便正确地重组 HTTP 请求。
分配读缓冲区
读缓冲区是用来临时存储从客户端接收到的数据的内存区域。Nginx 为每个连接分配一个读缓冲区,以便存储请求头和请求体。
- client_header_buffer_size:这个配置指令定义了读缓冲区的初始大小,默认为 1KB。这个大小适用于大多数标准的 HTTP 请求头。
- 动态扩展:虽然默认大小为 1KB,但 Nginx 的读缓冲区是可以根据需要动态扩展的。如果接收到的请求头超过了 1KB,Nginx 会根据需要分配更多的内存。这种动态分配机制确保了 Nginx 能够有效地处理各种大小的请求,同时避免了不必要的内存浪费。
- 内存管理:在 Nginx 源码中,内存管理是通过
ngx_pool_t
结构体来实现的,它提供了内存分配和释放的功能。ngx_palloc
函数用于从内存池中分配内存,而ngx_pfree
用于释放内存。
上面是nginx 处理连接,下面我们来看下nginx 处理请求的过程,处理请求的过程跟处理连接是不一样的,因为系统需要进行大量的上下文分析,分析http 协议跟http的header 信息。
接收请求HTTP模块
1. 解析请求
状态机解析请求行:Nginx 使用状态机来解析客户端发送的 HTTP 请求行,这包括请求方法、URI 和 HTTP 版本。 接收 URI 和 Header:Nginx 继续读取请求的 URI 和 Header 信息。
在 Nginx 的工作流程中,解析请求是一个至关重要的步骤,它涉及到从客户端接收的原始 HTTP 请求中提取出有用的信息,如请求方法、URI 和 HTTP 版本等。这一过程是通过状态机来实现的,状态机是一种编程模式,用于按顺序处理输入数据,这里是指 HTTP 请求的不同部分。以下是结合 Nginx 底层原理与源码对解析请求过程的详细展开:
状态机解析请求行
Nginx 使用内部的状态机来逐行解析客户端发送的 HTTP 请求。状态机的主要任务是识别请求行中的各个组成部分,并将其存储在相应的数据结构中。
- 请求行的结构:HTTP 请求行通常包含三个部分:请求方法(如 GET、POST)、请求的资源路径(URI)和 HTTP 版本(如 HTTP/1.1)。
- 状态机的工作方式:状态机根据当前读取的字符和预定义的规则(如空格分隔方法和 URI)来确定请求的各个部分。状态机在 Nginx 源码中通常由一系列函数和跳转表组成,这些函数会根据解析的进度调用彼此。
- 错误处理:如果在解析过程中遇到不符合 HTTP 协议规范的数据,状态机会触发错误处理机制,这可能导致请求被拒绝或产生 400 错误响应。
接收 URI 和 Header
在请求行被成功解析之后,Nginx 会继续读取请求的 URI 和 Header 信息。
- 读取数据:Nginx 会从客户端读取更多的数据,直到遇到请求头的结束标志(即两个连续的换行符)。
- 解析 Header:每个 HTTP 头部由一个字段名、一个冒号和一个字段值组成。Nginx 会逐个解析这些头部字段,并将它们存储在请求的上下文中,以便后续的处理阶段可以使用。
- 内存管理:在解析过程中,如果遇到大的请求头或 URI,Nginx 会动态地分配更多的内存来存储这些数据。这是通过
large_client_header_buffers
指令来控制的,它定义了可以分配的最大内存块的数量和大小。
2. 分配内存资源
分配请求内存池:为了存储请求数据,Nginx 会分配一个请求内存池,其大小由
request_pool_size
指令指定,默认为 4KB。 分配大内存:如果请求行或 Header 超过了基础的内存池大小,Nginx 会根据large_client_header_buffers
指令分配更大的内存块,该指令默认设置为 4 个 8KB 的内存块。
分配请求内存池
当一个 HTTP 请求到达 Nginx 时,Nginx 需要一块内存区域来存储请求的各个部分,包括请求行(包含方法、URI 和 HTTP 版本)、请求头(包含各种头部字段)以及可能的请求体(例如 POST 请求中的数据)。为了高效地管理这些数据,Nginx 使用了一个称为“内存池”的机制。
- request_pool_size:这个指令定义了每个请求的内存池的大小,默认为 4KB。这个内存池足够存储大多数请求的头部和部分请求体数据。
分配大内存
在某些情况下,请求的头部或请求行可能会非常大,超出了默认的 4KB 内存池的限制。例如,如果客户端发送了一个包含大量头部字段的请求,或者 URI 非常长,那么就需要更多的内存来存储这些数据。
- large_client_header_buffers:这个指令允许 Nginx 分配更大的内存块来存储请求头部。默认情况下,它设置为 4 个 8KB 的内存块。这意味着 Nginx 可以处理最大 32KB 的请求头部。
- large_client_header_buffers 的工作机制:当状态机解析请求头时,如果发现当前的内存池不足以存储更多的数据,Nginx 会动态地分配一个 8KB 的大内存块,然后把1KB里面的内容拷贝到这个8KB里面。这些大内存块是按需分配的,只有当实际需要时才会分配更多的内存。
状态机解析请求行
Nginx 使用内部的状态机来解析客户端发送的 HTTP 请求行和请求头。状态机是一种编程模型,它根据输入数据(在这种情况下是 HTTP 请求的各个部分)和当前的状态来决定下一步的操作。
- 解析请求行:状态机首先解析请求行,这包括识别 HTTP 方法(如 GET、POST 等)、URI 和 HTTP 版本。这一步骤需要从接收到的数据中提取这些关键信息,并为后续的处理做准备。
- 解析请求头:请求行之后,状态机开始解析请求头。它会逐行读取头部数据,并根据头部字段的名称和值执行相应的操作。例如,如果遇到
Content-Length
字段,状态机需要记录下请求体的大小,以便后续能够正确地读取请求体。 - 动态内存分配:在解析过程中,如果状态机发现需要更多的内存来存储请求头或请求行,它会触发内存分配机制,如上文所述的
large_client_header_buffers
。
通过这种机制,Nginx 能够灵活地处理各种大小的 HTTP 请求,同时保持内存使用的高效性。状态机的解析过程是 Nginx 请求处理的核心部分,它确保了请求数据的正确解析和后续处理的顺利进行。
3.标识 URI、状态机解析 Header 和分配大内存
在 Nginx 处理 HTTP 请求的过程中,标识 URI、状态机解析 Header 和分配大内存是关键步骤,这些步骤确保了 Nginx 能够正确解析客户端请求并为后续处理做好准备。以下是结合 Nginx 底层原理与源码对这些步骤的详细展开:
标识 URI
- 解析请求行:Nginx 首先使用状态机解析请求行,这包括识别请求方法(如 GET 或 POST)、URI 和 HTTP 版本。
- URI 处理:解析出的 URI 会被进一步处理,Nginx 会根据配置的路由规则和重写规则来确定最终的请求目标。
- 位置匹配:Nginx 会查找与请求的 URI 匹配的
location
块,这决定了请求将如何被处理,例如转发到代理服务器或直接提供静态文件。
状态机解析 Header
- 读取请求头:在请求行被解析之后,Nginx 继续读取请求头。请求头包含了客户端传递的元数据,如
Host
、User-Agent
、Content-Type
等。 - 状态机:Nginx 使用一个内部状态机来逐行解析请求头。状态机根据 HTTP 协议规范和请求头的格式来逐个处理头部字段。
- 存储头部信息:解析出的头部信息被存储在
ngx_http_request_t
结构体中,以便在后续的请求处理阶段中使用。
分配大内存
- 默认内存池:每个请求都会分配一个默认大小的内存池,由
request_pool_size
指令指定,默认为 4KB。 - 大内存需求:如果请求头的大小超过了默认内存池的容量,Nginx 需要分配额外的内存来存储这些数据。
- 动态内存分配:Nginx 根据
large_client_header_buffers
指令动态分配额外的内存。这个指令定义了可以分配的最大内存块的数量和大小,通常设置为 4 个 8KB 的内存块。
标识 Header
- 处理头部字段:一旦请求头被读取和解析,Nginx 会根据头部字段的内容执行特定的操作。例如,如果存在
Content-Length
字段,Nginx 会知道请求体的大小,并准备读取相应的数据。 - 变量赋值:Nginx 会将请求头中的某些值赋给内部变量,这些变量可以在配置文件中引用,用于重写规则、日志记录等。
- 模块处理:不同的 Nginx 模块可能会对请求头进行特定的处理。例如,安全模块可能会检查
Sec-WebSocket-Key
字段以支持 WebSocket 连接。
4. 处理请求
移除超时定时器:在请求行和 Header 被成功解析之后,Nginx 会移除之前设置的
client_header_timeout
超时定时器,该定时器默认设置为 60 秒,用于检测客户端是否在超时时间内发送完整的请求头。 开始 11 个阶段的 HTTP 请求处理:Nginx 将请求处理分为 11 个阶段,每个阶段可以包含多个模块的处理函数。这些阶段包括重写、权限检查、内容生成、日志记录等。
处理请求是 Nginx 接收到客户端 HTTP 请求后的核心环节,涉及到多个阶段的执行和多个模块的参与。以下是结合 Nginx 底层原理与源码对处理请求过程的详细展开:
移除超时定时器
在 Nginx 配置中,client_header_timeout
指令用于设置读取客户端请求头的超时时间。如果在指定时间内,Nginx 未能接收到完整的请求头,那么连接将被关闭以避免资源占用。
- 超时机制:Nginx 使用定时器来实现超时机制。当请求头和请求行被成功解析后,Nginx 会检查是否有设置超时定时器,并将其移除,因为此时请求已经被认为是有效的。
- 事件通知:如果超时发生,Nginx 会收到一个事件通知,这将触发一个内部函数来关闭连接并释放资源。
11 个阶段的 HTTP 请求处理
Nginx 将每个请求的处理分为 11 个阶段,每个阶段负责处理特定的任务。这种模块化的设计使得 Nginx 能够灵活地配置和扩展其功能。
- 服务器重写阶段 (
NGX_HTTP_SERVER_REWRITE_PHASE
):在这个阶段,Nginx 可以重写请求的 URI 和请求方法。 - 查找配置阶段 (
NGX_HTTP_FIND_CONFIG_PHASE
):Nginx 根据请求的 URI 查找最合适的服务器配置。 - 重写阶段 (
NGX_HTTP_REWRITE_PHASE
):在这个阶段,Nginx 可以根据 location 块进一步重写 URI。 - 权限检查阶段 (
NGX_HTTP_ACCESS_PHASE
):Nginx 检查客户端是否有权限访问请求的资源。 - 内容生成阶段 (
NGX_HTTP_CONTENT_PHASE
):在这个阶段,Nginx 调用内容生成模块来生成响应内容。 - 日志记录阶段 (
NGX_HTTP_LOG_PHASE
):Nginx 记录请求的日志信息。 - 其他阶段:还包括尝试文件阶段、文件查找阶段、错误处理阶段等。
上面的所有步骤都是nginx 的框架执行的,后面的11 个阶段的 HTTP 请求处理 是nginx http 模块的执行流程
5. 详细处理流程
代码语言:javascript复制NGX_HTTP_SERVER_REWRITE_PHASE:服务器重写阶段,用于修改请求的 URI。
NGX_HTTP_FIND_CONFIG_PHASE:查找配置阶段,用于确定请求应该由哪个 server 块处理。
NGX_HTTP_REWRITE_PHASE:重写阶段,用于修改请求的 URI 和头部。
NGX_HTTP_POST_REWRITE_PHASE:重写后阶段,用于处理重写后的结果。
NGX_HTTP_PREACCESS_PHASE:访问前阶段,用于进行权限检查。
NGX_HTTP_ACCESS_PHASE:访问阶段,用于执行访问控制。
NGX_HTTP_POST_ACCESS_PHASE:访问后阶段,用于执行访问控制后的清理工作。
NGX_HTTP_TRY_FILES_PHASE:尝试文件阶段,用于尝试查找请求的文件。
NGX_HTTP_CONTENT_PHASE:内容生成阶段,用于生成响应的内容。
NGX_HTTP_LOG_PHASE:日志记录阶段,用于记录请求的日志。
NGX_HTTP_CLOSE_REQUEST_PHASE:关闭请求阶段,用于清理请求资源。
6. 结束处理
- 发送响应:Nginx 根据处理结果构建 HTTP 响应,并将其发送给客户端。
- 清理资源:请求结束后,Nginx 释放分配的内存资源,并关闭连接或保持连接以待后续请求。
Nginx 处理 HTTP 头部的过程是高效且灵活的,它通过精细的内存管理和状态机解析,确保了在各种情况下都能快速准确地处理客户端请求。我们在下一篇详细的学习Nginx 处理 HTTP 请求的 11 个阶段的过程与原理。