作为高性能Web服务器,Nginx能轻松应对百万级并发,核心秘密藏在它的「请求处理流程」里。很多初学者只会配置Nginx,却不懂背后的逻辑——比如为什么Worker进程数要设为CPU核心数?为什么location规则写不对会导致404?为什么长连接能提升性能?
一、TCP建立连接的三次握手与Worker进程的「抢单」逻辑
在浏览器输入网址按下回车的瞬间,第一步是建立TCP连接——这是所有HTTP请求的基础。Nginx通过「Master-Worker多进程模型」高效处理连接,我们先从这个模型说起。
1.1 先搞懂:Nginx的「经理-员工」模型
Nginx启动后会生成两类进程:
- Master进程(经理):不处理具体请求,只负责「管理团队」——加载配置、启动Worker进程、监控Worker状态(Worker挂了就重启)。
- Worker进程(员工):真正处理用户请求的「执行者」,数量通常设为CPU核心数(比如4核CPU设4个Worker),避免进程切换开销。
举个例子:你开了一家餐厅,Master是店长,负责招聘服务员(Worker)、制定规则(加载配置);Worker是服务员,专门接待顾客(处理请求)——店长不直接服务顾客,这样效率最高。
1.2 TCP三次握手:Nginx如何「接电话」?
TCP三次握手是「客户端与服务器建立连接」的标准流程,类比成「打电话」更易理解:
- 客户端(你):“喂,Nginx服务器在吗?”(发送SYN报文);
- Nginx(服务员):“在呢,我能听到你说话!”(回复SYN+ACK报文);
- 客户端(你):“好的,那我开始说需求了!”(发送ACK报文)。
三次握手成功后,TCP连接建立,接下来就能传输HTTP请求了。
关键细节:Nginx如何「监听」连接?
Nginx启动时,Master进程会让所有Worker进程「监听同一个端口」(比如80端口)——这和普通程序不同(普通程序一个端口只能被一个进程监听),Nginx通过「共享内存」实现端口共享。
当TCP连接建立后,会触发「惊群效应」吗?
早期版本会:多个Worker同时抢一个连接,像一群服务员抢一个顾客,效率低。
现在Nginx优化了:Worker进程通过「互斥锁」竞争,只有一个Worker能抢到连接,避免资源浪费。
1.3 案例:1000个客户端同时连接,Nginx怎么处理?
假设Nginx配置了4个Worker进程,1000个客户端同时发起连接:
- Master进程早已让4个Worker监听80端口;
- 1000个客户端完成TCP三次握手,生成1000个TCP连接;
- 4个Worker通过互斥锁「公平抢单」,每个Worker处理约250个连接(Nginx会尽量让负载均衡);
- 每个Worker通过「事件驱动模型」(epoll),在一个进程内高效处理250个连接(无需为每个连接开线程)。
这就是Nginx高并发的核心:多进程+事件驱动,既利用多核CPU,又避免线程切换开销。
二、请求处理:Nginx的「业务处理流水线」
TCP连接建立后,客户端会发送HTTP请求(比如GET /index.html HTTP/1.1
),Nginx会按「解析→匹配→处理→响应」四个步骤处理,像一条「流水线」。
2.1 步骤1:解析HTTP请求行与请求头——「读懂客户需求」
客户端发送的HTTP请求是「文本格式」,Nginx首先要「读懂」这些内容,核心是解析「请求行」和「请求头」。
真实请求示例(浏览器发送的HTTP请求)
我们用curl -v http://localhost/index.html
命令,就能看到客户端发送的原始请求:
GET /index.html HTTP/1.1 # 请求行
Host: localhost # 请求头1:目标主机(对应Nginx的server_name)
User-Agent: curl/7.68.0 # 请求头2:客户端类型(curl还是Chrome)
Accept: */* # 请求头3:接受的响应格式
Nginx解析逻辑(分2步)
-
解析请求行:提取「请求方法、URI、HTTP协议版本」
- 请求方法:GET(获取资源)、POST(提交数据)、PUT(修改数据)等;
- URI:
/index.html
(客户端要访问的资源路径); - 协议版本:HTTP/1.1(常见版本,支持长连接)。
→ 若请求行格式错误(比如GET /index.html HTTP/2.0
但Nginx没开HTTP/2),直接返回400 Bad Request。
-
解析请求头:提取关键字段,为后续处理做准备
Host: localhost
:告诉Nginx“我要访问哪个网站”(对应虚拟主机的server_name
);User-Agent
:用于识别客户端(比如限制爬虫请求);Cookie
:客户端携带的Cookie信息(比如用户登录态);Connection: keep-alive
:客户端希望建立长连接(后续会讲)。
案例:解析失败的场景
如果客户端发送的请求头格式错误(比如少了Host
字段),Nginx会返回400错误,错误日志(/var/log/nginx/error.log
)会记录:
2024/10/20 15:30:00 [error] 1234#0: *5432 no host in request header, client: 192.168.1.100, server: 0.0.0.0:80
2.2 步骤2:匹配虚拟主机(server)与location规则——「找到对应业务窗口」
解析完请求后,Nginx要确定两件事:① 该请求对应哪个虚拟主机(多个网站共用80端口);② 该请求对应哪个处理规则(location)。
这一步是初学者最容易踩坑的地方——比如配置了多个server
,但请求总是匹配到默认网站;或者location规则写反,导致静态资源404。
2.2.1 匹配虚拟主机(server_name):「找到正确的网站」
Nginx支持「一个端口对应多个网站」(比如80端口同时服务www.aaa.com
和www.bbb.com
),核心靠server_name
匹配。
真实配置案例(2个虚拟主机)
# 虚拟主机1:服务www.aaa.com(静态网站)
server {
listen 80; # 监听80端口
server_name www.aaa.com; # 网站域名
root /usr/share/nginx/aaa; # 网站根目录
index index.html;
}
# 虚拟主机2:服务www.bbb.com(反向代理到后端Tomcat)
server {
listen 80;
server_name www.bbb.com;
location / {
proxy_pass http://localhost:8080; # 转发到Tomcat
}
}
匹配逻辑:按「Host请求头」找对应的server
当客户端发送Host: www.aaa.com
时:
- Nginx遍历所有
listen 80
的server块; - 找到
server_name = www.aaa.com
的server块; - 后续请求就交给这个server块处理。
匹配优先级(避免踩坑)
如果多个server的server_name
都能匹配(比如通配符*.aaa.com
),Nginx按以下优先级选择:
- 精确匹配(
www.aaa.com
>*.aaa.com
); - 前缀通配符(
*.aaa.com
>aaa.com
); - 后缀通配符(
www.aaa.*
,很少用); - 默认server(第一个
listen 80
的server,或显式指定default_server
)。
案例:请求为什么总是匹配到默认网站?
如果客户端访问www.ccc.com
,但没有对应的server块,Nginx会把请求交给「默认server」——这就是为什么有时配置了新网站,却打开了旧网站的原因。
解决方法:在需要作为默认的server块中添加default_server
:
server {
listen 80 default_server; # 显式指定为默认server
server_name _; # 匹配所有未命中的Host
return 403; # 拒绝访问(避免泄露默认网站)
}
2.2.2 匹配location规则:
虚拟主机匹配完成后,Nginx要根据「URI」匹配对应的location
块——location
是Nginx的「业务处理单元」,负责定义“不同URI该怎么处理”(比如静态文件、反向代理)。
真实配置案例(多个location)
server {
listen 80;
server_name www.aaa.com;
root /usr/share/nginx/aaa;
# location1:精确匹配 /favicon.ico(网站图标)
location = /favicon.ico {
expires 30d; # 缓存30天
}
# location2:前缀匹配 /static/(CSS/JS/图片)
location /static/ {
expires 7d; # 缓存7天
}
# location3:正则匹配 .php$(PHP动态请求)
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000; # 转发到PHP-FPM
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
# location4:默认匹配(所有未命中的URI)
location / {
index index.html; # 访问根目录时返回index.html
}
}
匹配逻辑:按URI找对应的location
假设客户端请求http://www.aaa.com/static/css/style.css
:
- URI是
/static/css/style.css
; - Nginx遍历所有location规则,先检查「精确匹配」(没有匹配
= /static/...
); - 再检查「前缀匹配」,命中
/static/
; - 最终由
location /static/
处理请求(返回缓存头,加快加载)。
匹配优先级(关键!避免404)
location匹配有严格的优先级,写反顺序会导致规则失效,记住以下顺序:
- 精确匹配(
location = /uri
):优先级最高,比如= /favicon.ico
; - 前缀匹配(
location ^~ /uri
):带^~
表示“优先匹配”,比如^~ /admin/
; - 正则匹配(
location ~ /uri
或~* /uri
,区分大小写,*不区分):比如~ \.php$
; - 普通前缀匹配(
location /uri
):比如/static/
; - 默认匹配(
location /
):匹配所有未命中的URI。
案例:为什么PHP请求总是返回404?
如果把正则匹配的location ~ \.php$
写在普通前缀匹配location /
后面:
# 错误顺序!
location / {
index index.html;
}
location ~ \.php$ { # 永远不会被匹配到!
fastcgi_pass 127.0.0.1:9000;
}
原因:/
会匹配所有URI(包括/test.php
),正则匹配永远没机会生效。
解决方法:把正则匹配的location放在前面,或用^~
提升优先级。
2.3 步骤3:处理请求——「按规则干活」
匹配到对应的location后,Nginx会按location的配置处理请求,常见场景有3种:静态文件、反向代理、FastCGI(动态请求)。
场景1:处理静态文件(最常见,比如HTML/CSS/图片)
静态文件处理是Nginx的强项——无需后端服务,直接从磁盘读取文件返回给客户端,性能极高。
真实配置案例
server {
listen 80;
server_name www.aaa.com;
root /usr/share/nginx/aaa; # 静态文件根目录
location / {
index index.html; # 访问根目录时,默认返回root下的index.html
try_files $uri $uri/ /index.html; # 单页应用路由适配(比如Vue/React)
}
}
处理逻辑(请求/static/img/logo.png
)
- Nginx拼接「根目录+URI」得到文件路径:
/usr/share/nginx/aaa/static/img/logo.png
; - 检查文件是否存在:
- 存在:读取文件,设置正确的
Content-Type
(比如image/png
),返回给客户端; - 不存在:返回404错误,错误日志记录“open() “/usr/share/nginx/aaa/static/img/logo.png” failed (2: No such file or directory)”。
- 存在:读取文件,设置正确的
关键优化:sendfile加速静态文件
Nginx默认启用sendfile on
,这是个「性能黑科技」:
- 普通文件读取:磁盘→内核缓冲区→用户缓冲区(Nginx)→内核缓冲区→网卡;
- sendfile读取:磁盘→内核缓冲区→网卡(跳过用户缓冲区),减少数据拷贝,提升20%+性能。
场景2:反向代理(转发请求到后端服务,比如Tomcat/Node.js)
当请求需要后端服务处理(比如Java接口、Node.js API),Nginx会作为「代理」,把请求转发到后端服务器,再把后端的响应返回给客户端。
真实配置案例(转发到Tomcat)
server {
listen 80;
server_name api.bbb.com;
location / {
# 核心:转发到后端Tomcat(localhost:8080)
proxy_pass http://localhost:8080;
# 重要:转发请求头,让后端知道真实客户端信息
proxy_set_header Host $host; # 传递原始Host
proxy_set_header X-Real-IP $remote_addr; # 传递真实客户端IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 传递代理链IP
}
}
处理逻辑(请求/api/user
)
- 客户端发送
GET /api/user HTTP/1.1
到Nginx; - Nginx按
proxy_pass
配置,把请求转发到http://localhost:8080/api/user
; - Tomcat处理请求,返回
{"code":200,"data":{"name":"张三"}}
; - Nginx接收Tomcat的响应,再转发给客户端。
避坑点:proxy_pass结尾是否加「/」?
这是初学者最容易混淆的点,直接看案例:
- 案例1:
proxy_pass http://localhost:8080
(不加/)
请求/api/user
→ 转发到http://localhost:8080/api/user
(正确); - 案例2:
proxy_pass http://localhost:8080/
(加/)
请求/api/user
→ 转发到http://localhost:8080/user
(丢失/api前缀,错误)。
结论:如果后端服务需要URI前缀(比如/api
),proxy_pass
结尾不加/;反之加/。
场景3:FastCGI转发(处理动态请求,比如PHP)
PHP等动态语言需要通过FastCGI协议与Nginx交互——Nginx不处理PHP代码,只负责把PHP请求转发给「PHP-FPM」(PHP的FastCGI进程管理器),再把处理结果返回给客户端。
真实配置案例(Nginx+PHP-FPM)
server {
listen 80;
server_name www.ccc.com;
root /usr/share/nginx/ccc;
location ~ \.php$ {
# 转发到PHP-FPM(默认监听9000端口)
fastcgi_pass 127.0.0.1:9000;
# 告诉PHP-FPM文件路径(核心参数)
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
# 引入FastCGI默认参数(避免手动配置所有参数)
include fastcgi_params;
}
}
处理逻辑(请求/test.php?id=1
)
- 客户端发送
GET /test.php?id=1 HTTP/1.1
到Nginx; - Nginx拼接文件路径:
/usr/share/nginx/ccc/test.php
; - Nginx通过FastCGI协议,把请求转发给PHP-FPM(127.0.0.1:9000),并传递
SCRIPT_FILENAME
等参数; - PHP-FPM执行
test.php
代码,处理id=1
参数,返回HTML结果; - Nginx接收结果,返回给客户端。
避坑点:SCRIPT_FILENAME配置错误
如果fastcgi_param SCRIPT_FILENAME
配置错误(比如少了$document_root
):
# 错误配置!
fastcgi_param SCRIPT_FILENAME $fastcgi_script_name;
PHP-FPM会找不到test.php
文件,返回“No input file specified.”错误,解决方法是加上$document_root
(即网站根目录)。
2.4 步骤4:构建HTTP响应并返回——「给客户回传结果」
处理完请求后,Nginx要构建HTTP响应,返回给客户端。响应和请求结构类似,包含「响应行、响应头、响应体」三部分。
真实响应示例(静态文件200成功)
客户端请求/static/img/logo.png
,Nginx返回的响应:
HTTP/1.1 200 OK # 响应行:协议版本+状态码+状态描述
Server: nginx/1.24.0 # 响应头1:服务器版本
Date: Sun, 20 Oct 2024 10:00:00 GMT # 响应头2:时间
Content-Type: image/png # 响应头3:文件类型(告诉浏览器怎么解析)
Content-Length: 12345 # 响应头4:响应体大小(字节)
Expires: Sat, 19 Nov 2024 10:00:00 GMT # 响应头5:缓存过期时间
Cache-Control: max-age=2592000 # 响应头6:缓存控制(30天)
[图片二进制数据] # 响应体:真实的图片数据
响应构建逻辑
-
响应行:根据处理结果设置状态码
- 200 OK:请求成功(静态文件找到、后端返回成功);
- 404 Not Found:文件不存在;
- 502 Bad Gateway:后端服务挂了(比如Tomcat没启动);
- 301 Moved Permanently:永久重定向(比如HTTP跳HTTPS)。
-
响应头:设置缓存、编码、跨域等规则
Content-Type
:告诉客户端响应体的类型(比如text/html
、application/json
);Cache-Control
/Expires
:控制客户端缓存(减少重复请求);Access-Control-Allow-Origin
:解决跨域问题(比如允许https://aaa.com
访问)。
-
响应体:真实的返回数据
- 静态文件:二进制数据(图片、HTML);
- 反向代理/FastCGI:后端服务返回的文本(JSON、HTML)。
案例:为什么图片在浏览器中显示为乱码?
如果Nginx返回图片时,Content-Type
被错误设置为text/plain
(文本类型),浏览器会把图片二进制数据当作文本显示,导致乱码。
解决方法:确保Nginx加载了MIME类型配置(默认已加载):
http {
include /etc/nginx/mime.types; # 加载MIME类型映射(如.png→image/png)
default_type application/octet-stream; # 未知类型默认值
}
三、连接关闭:短连接与长连接的「选择艺术」
响应返回后,TCP连接不会立刻关闭——Nginx会根据「连接模式」决定是否保留连接,这直接影响性能。
3.1 短连接(默认模式):「一次请求,一次连接」
短连接的逻辑:每次请求完成后,Nginx主动关闭TCP连接。
类比成「打电话问一个问题,挂掉电话」。
短连接的流程
- 客户端与Nginx建立TCP连接;
- 客户端发送1个HTTP请求,Nginx返回响应;
- Nginx发送
Connection: close
响应头,告诉客户端“处理完了,关闭连接吧”; - 双方完成TCP四次挥手,连接关闭。
短连接的优缺点
- 优点:无需保留连接,节省服务器资源(适合并发低、请求少的场景);
- 缺点:每次请求都要重新建立TCP连接(三次握手),增加延迟(适合动态请求多的场景)。
3.2 长连接(keepalive配置):「一次连接,多次请求」
长连接的逻辑:请求完成后,Nginx保留TCP连接,客户端后续请求可以复用这个连接,不用重新三次握手。
类比成「打电话问多个问题,问完再挂掉」。
真实配置案例(启用长连接)
http {
# 启用长连接
keepalive_timeout 65; # 连接空闲65秒后关闭(核心参数)
keepalive_requests 100; # 一个连接最多处理100个请求(避免连接占用过久)
}
长连接的流程
- 客户端发送
Connection: keep-alive
请求头,希望建立长连接; - Nginx返回
Connection: keep-alive
响应头,同意保留连接; - 客户端通过同一个TCP连接,连续发送多个请求(比如加载HTML、CSS、JS);
- 如果65秒内没有新请求,Nginx关闭连接;或处理100个请求后,主动关闭连接。
长连接的优缺点
- 优点:减少TCP三次握手开销,提升页面加载速度(适合静态资源多的场景,比如电商首页);
- 缺点:连接会占用服务器资源(文件描述符、内存),需合理设置
keepalive_timeout
(不宜过长,避免资源浪费)。
案例:为什么电商网站要启用长连接?
电商首页有大量静态资源(1个HTML+20个CSS/JS+50张图片):
- 短连接:需要建立71个TCP连接(每个资源一个连接),三次握手开销大,页面加载慢;
- 长连接:只需建立1-2个TCP连接,复用连接加载所有资源,加载速度提升50%+。
3.3 如何选择:短连接还是长连接?
场景 | 推荐连接模式 | 配置建议 |
---|---|---|
静态资源多(首页) | 长连接 | keepalive_timeout 65; keepalive_requests 100; |
动态请求多(API) | 短连接 | 不配置keepalive_timeout (默认关闭),或设为10秒 |
高并发场景(秒杀) | 长连接 | keepalive_timeout 30; keepalive_requests 50; (缩短超时和请求数) |
四、全流程总结:从输入网址到看到页面的10个步骤
我们用「访问http://www.aaa.com/static/img/logo.png
」为例,总结完整流程:
- 客户端(浏览器)解析域名
www.aaa.com
,得到服务器IP(比如192.168.1.100); - 客户端与Nginx(192.168.1.100:80)完成TCP三次握手,建立连接;
- Worker进程通过互斥锁抢到该连接,开始处理;
- 客户端发送HTTP请求(
GET /static/img/logo.png HTTP/1.1
,Host: www.aaa.com
); - Nginx解析请求行和请求头,确认请求合法;
- Nginx匹配虚拟主机(
server_name www.aaa.com
); - Nginx匹配location(
/static/
); - Nginx拼接文件路径(
/usr/share/nginx/aaa/static/img/logo.png
),读取文件; - Nginx构建HTTP响应(200 OK,
Content-Type: image/png
),返回给客户端; - 因启用长连接,连接保留65秒;客户端后续请求可复用该连接,否则65秒后关闭。
五、初学者必记的3个核心知识点
- Worker进程数:设为CPU核心数(比如4核设4个),避免进程切换开销;
- location优先级:精确匹配(=)> 前缀优先(^~)> 正则匹配(~)> 普通前缀(/)> 默认(/);
- 长连接配置:静态资源多的场景一定要启用
keepalive_timeout
,设为30-60秒。
通过本文,你应该能理解Nginx请求处理的每个环节——从连接建立到响应返回,每个步骤的设计都为了「高性能」和「灵活性」。后续学习反向代理、负载均衡、HTTPS时,这些基础流程会帮你更快理解进阶配置的本质。