这是一份关于「一条 HTTP 请求从敲回车到看到网页,中间发生了什么」的知识体系。
读完脑子有一张地图,不只是知道8个知识点。
目录
- 00 一条请求的完整旅程
- 01 请求进门:Nginx 怎么知道该找哪个网站
- 02 静态还是动态:Nginx 读文件还是转给后端
- 03 容器里 Nginx:配置怎么不丢
- 04 后端一直在跑:PM2 干的是什么
- 05 HTTPS 怎么工作的:证书链是什么
- 06 免费证书怎么申请:DNS-01 绕过拦截
- 07 部署自动化:怎么把代码部署到服务器
- 08 完整旅程:把地图从头走一遍
00 一条请求的完整旅程
你今天看了多少次网页?
你有没有想过,从你敲下回车,到页面出现在屏幕上,这中间到底发生了什么?
我不是在说"打开浏览器"这种废话。我说的是——这条请求,它走了哪些站,每站干什么,它怎么从你这里出去,又怎么回来。
这条路线搞清楚,你就知道网站是怎么跑起来的,不是背下来的。
从敲回车开始
你在浏览器地址栏输入 https://pbti.duckdns.org/,回车。
你看到的是网页。
但这中间发生的事,我跟你说,我一开始以为就是"发了个请求,服务器返回了网页"。就这么简单。结果我发现不是。
我们来一层层拆。
第一站:DNS——域名在哪
你输入的是域名,服务器只认 IP。
DNS 干的就是这件事:把域名翻译成 IP 地址。
pbti.duckdns.org → 49.234.178.53
就像你打电话,查号台告诉你号码在哪。但查号台不管接通之后说什么。
DNS 完成了它的任务:把域名翻译成 IP 地址。
请求现在带着 IP 地址出发了。
第二站:到达服务器
请求到了服务器,IP 你是知道的,端口是 80(HTTP)或 443(HTTPS)。
但服务器上可能跑着十个网站。
服务器怎么知道这个请求应该交给哪个网站处理?
这就到了 Nginx 登场的时候了。
第三站:Nginx——请求进门
Nginx 是服务器上的守门人。请求进门,先经过它。
Nginx 看请求头里的一个字段:Host。
当你访问 https://pbti.duckdns.org/ 时,浏览器发出的请求里有一行:
Host: pbti.duckdns.org
就这一行。浏览器在告诉你:"我要找的是 pbti.duckdns.org。"
Nginx 根据这个字段,把请求交给对应的配置块处理。
一个 IP 能跑多个网站,靠的就是这个字段。
第四站:Nginx 的第二个决策
Nginx 知道该找哪个网站之后,还要决定一件事:
这个请求是直接读文件,还是转给后端程序处理?
两种情况:
情况一:静态网站。
PBTI 是 Next.js 编译出来的静态文件,Nginx 直接读文件,返回 index.html。就跟你打开本地文件夹一样,读文件返回内容。
情况二:动态网站。 Observatory 有 Node 进程在跑,请求要转给这个程序处理。程序处理完了,把结果返回给 Nginx,Nginx 再返回给你。
这个决策由 location 匹配规则决定。location /observatory/ 匹配到了,就把请求 proxy_pass 转给后端程序。
第五站:后端程序
对于动态网站,请求已经到了后端程序手里。
Node 进程跑在服务器上,但不是你打开终端的那个会话——是 PM2 这个工具在管着它。
PM2 让程序脱离终端一直跑。关掉电脑网络还在,后端还在。
这跟直接 node app.js 跑不一样——那样关掉终端进程就没了。
第六站:SSL 加密(HTTPS)
如果你是通过 HTTPS 访问,在 Nginx 返回之前,还经过 SSL 加密这一站。
Nginx 拿证书证明"我是 pbti.duckdns.org"。浏览器用证书链验证信任。
证书哪来的?Let's Encrypt 签的。Let's Encrypt 怎么验证域名是你的?DNS-01 验证——在 DNS 记录里加一条 TXT 记录来证明。
这个流程走通了,浏览器才显示小锁。
第七站:返回
结果从后端程序流出来:
- 后端程序 → Nginx → 你的电脑 → 浏览器渲染 → 页面出现
Nginx 收到后端程序的响应,转给你。如果用了反向代理,它还会在返回路径上做一些处理——比如把一些内部的头信息去掉。
把整条路走一遍
现在你脑子里的图是这样的:
你敲回车
↓
DNS:域名 → IP
↓
服务器:请求到达
↓
Nginx(第一决策):根据 Host 头选哪个网站
↓
Nginx(第二决策):读文件还是转给后端
↓
后端程序(PM2 管着,一直在跑)
↓
SSL 加密(返回之前)
↓
回到你的屏幕
这是一条完整的路。每篇讲一个站。
全部读完,你脑子里就有了这张图——不是背的,是理解的。
01 请求进门:Nginx 怎么知道该找哪个网站
我那天卡住的问题
我以为一台服务器只能跑一个网站。
我拿到一个 IP 地址,想跑两个网站——一个 PBTI,一个 Observatory。我当时是真的想过买两台服务器。
后来发现不是这么回事。
HTTP 请求头里有一个字段叫 Host,浏览器在请求里写着"我要找哪个网站"。Nginx 就是看这个字段决定把请求交给哪个配置块处理。
DNS 告诉你域名在哪。Host 头告诉你"我找谁"。Nginx 把你转到该去的地方。
就这么回事。
一条 HTTP 请求长什么样
你敲回车,浏览器发出去的请求大概长这样:
GET / HTTP/1.1
Host: pbti.duckdns.org
User-Agent: Mozilla/5.0...
就三行。中间那行,Host 字段。
服务器收到请求,Nginx 看到 Host: pbti.duckdns.org,就去找负责这个域名的配置块。
配置块怎么写:
server {
listen 443 ssl;
server_name pbti.duckdns.org;
...
}
server_name pbti.duckdns.org 意思就是:这个配置块负责处理 pbti.duckdns.org 的请求。
再看第二个网站:
server {
listen 443 ssl;
server_name observatory-jie.duckdns.org;
...
}
两个配置块,共用同一个 443 端口,但 server_name 不同。Nginx 自动分流,不用任何特殊配置。
我踩过的坑:proxy_pass 后面那个 /
我配好之后,Observatory 一直收不到正确的路径。浏览器访问 /observatory/admin,Node 那边收到的是 /admin,源码里明明写的是 /observatory/admin。
排查了一下午。
规则是这样:location 匹配了什么路径,就从转发路径里去掉什么路径。
location /observatory/ {
proxy_pass http://172.17.0.1:8080; # 后面没 /
}
location /observatory/ 匹配了 /observatory/ 这四个字符,Nginx 转发时把它从 URI 里剥掉了。所以浏览器请求 /observatory/admin,Nginx 转发出去的是 /admin。
如果 proxy_pass 后面加个 /,那就是完整路径转发:/observatory/admin。
我一开始以为 Nginx 有 bug,搞清楚规则之后就明白了——不是 bug,是我不知道这个规则。
你第一次配可能会卡的地方
Let's Encrypt 续期的问题。 很多教程让你把 HTTP 全部跳转到 HTTPS,一行 return 301 https://$host$request_uri。但 Let's Encrypt 续期时要访问 /.well-known/acme-challenge/ 路径做验证,全站重定向就把这个路径也跳走了,续期失败。
记得单独放行这个路径:
location /.well-known/acme-challenge/ {
root /var/www/acme-challenge;
}
这一站结束了: 请求到了 Nginx,Nginx 看 Host 头把它送到该去的地方——这是第一个岔路口。
02 静态还是动态:Nginx 读文件还是转给后端
两种网站,两种跑法
我一开始以为网站就是"把代码扔到服务器上,别人就能访问"。后来发现不是这么回事。
有两种完全不同的跑法。
第一种:静态网站。 代码提前编译好了,就是一堆 HTML、CSS、JS 文件。Nginx 做的事跟打开文件夹一样——浏览器要哪个文件,Nginx 读出来返回。文件在硬盘上,不在任何程序里。
PBTI 就是这样。Next.js 编译出来 out/ 目录,往服务器上一扔,Nginx 直接读。
第二种:动态网站。 代码里有程序在跑。用户发请求,程序处理完返回结果。程序处理的东西每次都不一样——根据用户是谁、数据在哪、处理逻辑是什么。
Observatory 就是这样。Express 写的后端程序跑着,每次返回的文章列表、弹幕内容,都是程序实时生成的。
这两种网站,Nginx 的处理方式完全不一样。
静态:Nginx 读文件
对于 PBTI,Nginx 配置是这样的:
server {
server_name pbti.duckdns.org;
root /var/www/html/; # 文件在这个目录里
location / {
index index.html; # 找不到文件时返回 index.html
}
}
root /var/www/html/ 告诉 Nginx:文件在这个目录里。浏览器要哪个文件,Nginx 去这个目录里找,返回。
就这么简单。文件在硬盘上,不在任何程序里。
动态:Nginx 转给后端
对于 Observatory,Nginx 配置是这样的:
server {
server_name observatory-jie.duckdns.org;
location /observatory/ {
proxy_pass http://172.17.0.1:8080;
}
}
location /observatory/ 匹配到路径,就把请求转发给 172.17.0.1:8080——那是 Express 程序跑的端口。
请求在 Express 程序里走一圈,处理完了,返回给 Nginx,Nginx 再返回给浏览器。
关键区别:文件不在硬盘上,在程序里。
坑:localhost 在容器里是谁
我 Nginx 跑在 Docker 容器里,Express 跑在宿主机上。
一开始 proxy_pass 配的是 http://localhost:8080,一直 502。
容器里的 localhost 指的是容器自己,不是服务器。容器要访问宿主机,得用 172.17.0.1——Docker bridge 网络给宿主机分配的地址。
改了就通了。
坑:子路径部署的路径问题
如果你把网站部署在子路径下,比如 /observatory/,但源码里所有静态资源的路径写的是 /style.css,部署上去浏览器请求的是 /observatory/style.css,Nginx 去找,找不到,因为文件其实在 /observatory/style.css。
构建的时候就要处理路径前缀这件事。
这一站结束了: Nginx 看完 Host 头,决定——这个请求是直接读文件,还是转给后端程序。
03 容器里 Nginx:配置怎么不丢
为什么用 Docker 跑 Nginx
我一开始 Nginx 直接装在系统里。后来换成 Docker。
原因很简单:服务器重装或者迁移的时候,Docker 一把拉起来,所有环境不用重新配。
但有个坑——我第一次用的时候,没把配置目录挂载出来。
坑:容器重建后配置没了
有一天容器崩了,我 docker rm web 删除重建。
结果 Nginx 配置全没了。从头配了一遍。
因为配置文件在容器内部。删容器 = 删配置。
后来的做法是把配置目录从宿主机挂载出来:
docker run -d --name web \
-p 80:80 -p 443:443 \
-v /var/www/html:/usr/share/nginx/html:ro \
-v /etc/nginx/conf.d:/etc/nginx/conf.d:ro \
-v /etc/nginx/certs:/etc/nginx/certs:ro \
nginx:alpine
conf.d(配置)、certs(证书)、auth(认证)、html(静态文件)——四个目录全部从宿主机挂出来,用 :ro 只读挂载。
容器删了重建,配置和证书都还在,不会丢。
:ro 只读的意思是:配置文件虽然从宿主机挂出来了,但容器只能读,不能改。这样容器内部不能破坏配置,更安全。
这一站结束了: Nginx 部署方式决定了配置会不会丢——容器化要持久化,挂载是必须做的事。
04 后端一直在跑:PM2 干的是什么
坑:半夜爬起来手动开服务
有一天服务器断电重启。
第二天我打开网站,打不开。SSH 连上去一看,进程没了。
我用的是 node app.js 直接跑。终端关掉,进程就没了。
后来才知道原因——终端退出会发送 SIGHUP 信号,这个信号会终止进程。
进程与会话的关系
你在终端里跑 node app.js,这个进程属于这个终端会话。终端关掉,会话结束,SIGHUP 信号发给进程,进程终止。
程序跟着终端一起死了。
但网站不能这样——你睡觉的时候也得有人访问,断电重启了也得自动跑起来。
PM2 怎么解决
PM2 接管 Node 进程,让它脱离终端会话。
pm2 start app.js --name observatory
pm2 save # 保存进程列表
pm2 startup # 生成开机自启脚本
pm2 start 跑起来的进程,不属于任何终端会话。关掉终端,进程还活着。
pm2 save 把当前进程列表保存下来,pm2 startup 生成开机自启脚本,服务器重启之后自动把所有进程拉起来。
服务器断电重启,PM2 自动把 Observatory 拉起来,不用手动开。
这一站结束了: 后端程序跑在服务器上,PM2 让它脱离终端一直跑——这是让网站24小时在线的关键。
05 HTTPS 怎么工作的:证书链是什么
坑:配了证书浏览器还是报错
我配好 Nginx SSL 证书,浏览器一直报 NET::ERR_CERT_AUTHORITY_INVALID。
我反复检查配置,没问题啊。证书签下来了,怎么浏览器还不认?
后来才发现,我只配了"叶子证书",没有配"中间证书"。
证书链是什么
浏览器信任一个网站,不是信任那个网站本身,而是信任签发证书的那个机构(CA)。
但 CA 不是直接签叶子证书——它通过中间证书来签。
链条是这样的:
浏览器内置的根证书(信任)
↓ 签
Let's Encrypt 的中间证书
↓ 签
给我域名签的叶子证书
浏览器验证的时候,从叶子证书出发,一路追溯到根证书。全链路可信,浏览器才显示小锁。
acme.sh 签出来的文件有两个:
pbti.crt(1277字节)——只有叶子证书pbti.fullchain.crt(3645字节)——叶子 + 中间证书,完整的信任链
我只配了叶子证书,浏览器找不到中间证书,无法建立信任链,所以报错。
修复:
ssl_certificate /etc/nginx/certs/pbti.fullchain.crt; # 指向 fullchain,不是叶子
ssl_certificate_key /etc/nginx/certs/pbti.key;
然后 docker exec web nginx -s reload。
所以 HTTPS 是这样的
HTTPS 返回之前,Nginx 拿证书证明"我是 pbti.duckdns.org"。浏览器用证书链验证信任——从叶子证书追到根证书,全链路可信才显示小锁。
就这么回事。
这一站结束了: HTTPS 返回之前,Nginx 拿证书证明"我是这个网站"——证书链是信任的根基。fullchain.crt 包含完整的信任链。
06 免费证书怎么申请:DNS-01 绕过拦截
坑:从本地测明明是通的
Let's Encrypt 需要验证域名是你的。HTTP-01 验证是:你服务器上放一个 Let's Encrypt 指定的文件,Let's Encrypt 来读,读到了证明域名是你的。
从我自己电脑上测,curl http://pbti.duckdns.org/.well-known/acme-challenge/test 返回 200 OK,没问题。
但 Let's Encrypt 那边报 Connection reset。
腾讯云的安全组件把 Let's Encrypt 验证服务器的 IP 拦了。
从外面测是通的,但 Let's Encrypt 那边被拦了,根本访问不到。
解法:DNS-01 验证
HTTP-01 的问题是 Let's Encrypt 要连你的服务器,云厂商能拦。
DNS-01 不连你的服务器,而是在 DNS 记录里加一条 TXT 记录来证明域名是你的。Let's Encrypt 去 DNS 服务商那里查这条记录,查到了就证明域名是你的。
云厂商能拦 HTTP 请求,但管不了 DNS 记录。
做法
注册 DuckDNS(免费子域名),安装 acme.sh,用 DNS-01 验证:
export DuckDNS_Token="你的token"
~/.acme.sh/acme.sh --issue --dns dns_duckdns \
-d pbti.duckdns.org
acme.sh 会自动在 DuckDNS 上加一条 TXT 记录,Let's Encrypt 去查,查到了就签发证书。然后 acme.sh 设置 cron 任务,到期前自动续签,完全不用管。
这一站结束了: 证书申请这一步,用 DNS-01 验证绕过云厂商拦截——这是免费证书能成功签出来的关键。
07 部署自动化:怎么把代码部署到服务器
坑:Windows ssh 怎么敲都没反应
我一开始用 Windows 自带的 ssh 连服务器,经常卡死。敲命令没反应,有时候超时断开。
scp 传大文件也不稳定,有几次传到一半断了。
后来我用 Python paramiko 写了一个统一入口,不再用原生 ssh。
方案
打包 → 上传 → 服务器构建 → PM2 重启 → 验证
# 打包
tar --exclude=node_modules --exclude=.git --exclude=_site \
-czf /tmp/deploy.tar.gz .
# 上传
python ssh_connect.py upload /tmp/deploy.tar.gz /tmp/deploy.tar.gz
# 服务器构建
python ssh_connect.py "
cd /var/www/observatory
tar xzf /tmp/deploy.tar.gz -C .
npm install
npm run build
pm2 restart observatory
"
关键是用 tar 打包成一个文件传,比 scp -r 稳定很多。
这一站结束了: 代码写完了,要部署到服务器上——这一步决定了开发效率。tar + paramiko 替代 ssh/scp,是 Windows 环境的最佳实践。
08 完整旅程:把地图从头走一遍
现在我们把整条路走一遍。
你在浏览器地址栏输入 https://pbti.duckdns.org/,回车。
第一站:DNS
pbti.duckdns.org → 49.234.178.53。查号台告诉你号码在哪。
第二站:到达服务器 请求带着 IP 地址到达服务器的 443 端口,进入 Nginx。
第三站:Nginx 第一决策(Host 头)
Nginx 看到请求头的 Host: pbti.duckdns.org,找到负责这个域名的 server block。
第四站:Nginx 第二决策(location 匹配)
location / 匹配根路径,不需要 proxy_pass,直接读文件。
第五站:读文件
从 /var/www/html/ 目录读 index.html,加上 SSL 加密(证书链验证)。
第六站:返回 Nginx 把文件返回给你的浏览器。浏览器渲染,页面出现。
再看 Observatory 这条路:
第一站:DNS
observatory-jie.duckdns.org → 49.234.178.53。
第二站:到达服务器 请求到达 443 端口,进入 Nginx。
第三站:Nginx 第一决策(Host 头) Nginx 找到 Observatory 的 server block。
第四站:Nginx 第二决策(location 匹配)
location /observatory/ 匹配到了,proxy_pass 转发给 172.17.0.1:8080——Express 程序。
第五站:后端程序 PM2 管着的 Node 进程处理请求,生成内容。
第六站:返回 Express 把内容返回给 Nginx,Nginx 加 SSL 加密,返回给你。
两张图,现在你脑子里都有了。
不是背的,是理解的。
完