HTTP是什么?服务器管理员的协议指南
面向服务器管理员解释 HTTP:请求方法、状态码、关键标头、nginx 访问日志,以及 TLS 与 HTTP/3 的关系,并说明无状态和缓存如何影响配置。
什么是 HTTP?
HTTP(超文本传输协议)是一组规则,客户端和 Web 服务器通过这些规则请求资源并返回响应。客户端发送请求,其中包括 GET 等方法、/pricing 等路径、协议版本、标头列表,有时还包括请求正文。服务器返回响应,其中包括 200 等状态码,随后是服务器自己的标头,通常还包括响应正文。服务器上的每次页面访问和每次 API(应用程序编程接口)调用,都是这一交换过程的重复执行。
HTTP 本身不保存状态。服务器不会记住您一秒钟前请求过什么,因此任何表现为记忆的信息,例如登录会话,都会通过标头随每个请求发送。缓存完全由标头控制,负载均衡器也可以将您的下一个请求发送到不同的后端,而不会导致请求失败。这一特性解释了后文的大部分内容。
下面将从服务器端说明这一模型的表现形式,包括访问日志和 nginx 配置。
原始请求和响应(附注释)
下面是一个完整的 HTTP/1.1 请求。空行表示请求头结束,该行之后的内容都是请求体。GET 通常没有请求体。
GET /pricing HTTP/1.1
Host: example.com
User-Agent: curl/8.5.0
Accept: */*
Accept-Encoding: gzipGET是方法,用于说明要执行的操作。GET用于读取,POST用于发送数据,PUT用于替换,DELETE用于删除,HEAD用于请求GET的请求头而不获取请求体。/pricing是路径。主机名不属于请求行,因此请求中还需要下一个请求头。HTTP/1.1是客户端使用的协议版本。Host: example.com指定客户端要访问的站点。HTTP/1.1 要求必须提供该请求头,因此 nginx 会对缺少该请求头的请求返回400 Bad Request。- 其余内容表示偏好。
Accept-Encoding: gzip表示客户端可以解压缩,因此服务器可以压缩请求体。
响应的结构相同,只是在顶部包含状态行。
HTTP/1.1 200 OK
Date: Thu, 06 Aug 2026 09:12:44 GMT
Server: nginx
Content-Type: text/html; charset=utf-8
Content-Length: 5310
Cache-Control: public, max-age=300
<!doctype html>...200 OK是状态码及其原因短语。状态码才是有效信息。原因短语只是说明文本,客户端会忽略它。Content-Type告诉客户端如何处理后续字节。Content-Length是请求体的字节数,客户端据此确定请求体的结束位置。如果事先不知道大小,服务器会改为发送Transfer-Encoding: chunked,并使用长度为 0 的分块标记结束。Cache-Control告诉浏览器和中间的缓存可以保留此响应多长时间。- 在请求和响应中,头部之后的空行都用于将头部与请求体或响应体分隔开。
请求头名称不区分大小写。每一行都以回车后跟换行结束,而不是只使用换行。您不会手动输入这些内容,但会在数据包捕获中看到它们。
要观察一组真实的请求和响应,请对您拥有的网站运行以下命令:
curl -sS -o /dev/null -D - https://example.com/-D - 将响应头写入终端,-o /dev/null 丢弃响应体。建议使用它,而不是 curl -I,因为 -I 会发送 HEAD 请求。如果应用服务器对 HEAD 和 GET 的处理方式不同(很多服务器确实如此),您看到的请求头就会是浏览器永远不会接收到的请求头。curl -v 会同时打印请求和响应,并分别使用 > 标记请求行、使用 < 标记响应行。
nginx 访问日志中的请求行格式
nginx 自带一个 combined 日志格式,其定义如下:
log_format combined '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';它生成的一行日志如下:
203.0.113.45 - - [06/Aug/2026:09:12:44 +0000] "GET /pricing HTTP/1.1" 200 5310 "https://example.com/" "Mozilla/5.0 (X11; Linux x86_64) Chrome/127.0.0.0 Safari/537.36"203.0.113.45是$remote_addr,即建立 TCP(传输控制协议)连接的地址。在代理后面时,这里记录的是代理地址,而不是访问者地址。- 第一个
-是固定占位符。第二个是$remote_user,仅在使用 HTTP basic authentication 时填充。 "GET /pricing HTTP/1.1"是$request,即按原样复制收到的请求行。200是服务器返回的状态码,不是访问者实际看到的状态。5310是$body_bytes_sent,仅统计响应正文。不统计响应头,因此该数字始终小于实际发送的字节数。- 最后两个带引号的字段是
Referer和User-Agent。两者都来自客户端,因此都可能包含任意内容。
由于 $request 会逐字复制,垃圾内容也会原样出现在日志中。客户端如果通过 TLS(传输层安全)连接明文端口 80,日志中会出现一行 400,其请求字段以类似 "\x16\x03\x01\x02\x00\x01" 的转义字节开头。\x16 是 TLS 握手记录类型,因此这些字节实际上是 ClientHello 的开头,根本不是请求行。服务器行为正常。说明有内容将 HTTPS 请求发送到了 HTTP 端口。
同时将 $server_protocol 添加到日志格式中。它会输出 HTTP/1.1、HTTP/2.0 或 HTTP/3.0,这是证明协议变更确实已生效的最快方法。
自有网站返回这些常见状态码时,它们表示什么
第一位数字表示类别,应先查看类别。
2xx 表示请求已成功。 200 OK 用于普通读取请求。创建资源的 POST 请求之后返回 201 Created。204 No Content 表示请求成功且没有内容需要返回,通常用于响应 DELETE。
3xx 表示需要前往其他位置。 301 表示永久重定向。浏览器会强烈缓存它,有时只有清除用户配置文件后才会更新,因此指向错误主机名的 301 很难撤销。在仍处于测试阶段时,应使用 302。304 Not Modified 表示成功,而不是错误:客户端发送了携带 ETag(实体标签)的 If-None-Match,且该标签仍被服务器识别,因此服务器只返回响应头,不返回响应体。日志中大量出现 304,表示缓存正在正常工作。
4xx 表示请求有误。 400 Bad Request 表示输入格式错误。401 Unauthorized 实际表示未通过身份验证,并且必须携带用于指定认证方案的 WWW-Authenticate 响应头。403 Forbidden 表示服务器理解了请求,但仍拒绝执行。404 Not Found 表示请求的路径不存在。405 Method Not Allowed 表示路径正确,但请求方法不正确;对静态文件位置执行 POST 时,就会返回此状态码。413 表示请求体超过 nginx 的 client_max_body_size。该值默认为 1 megabyte,错误日志会通过 client intended to send too large body 确认这一点。
静态文件返回 403 时,问题几乎总在文件系统,而不是 HTTP 规则。修改配置前先查看 /var/log/nginx/error.log。open() "/srv/site/index.html" failed (13: Permission denied) 表示 nginx 工作进程用户无法读取文件,最常见的原因是某个父目录没有为其他用户设置执行权限。directory index of "/srv/site/" is forbidden 表示路径解析到了一个目录,但该目录没有索引文件,同时 autoindex 处于关闭状态。
5xx 表示服务器端发生故障。 500 表示应用程序中出现未处理的错误。502 Bad Gateway 表示 nginx 无法从上游获得可用响应。错误日志会说明原因:connect() failed (111: Connection refused) while connecting to upstream 表示 proxy_pass 中的地址没有进程监听。504 Gateway Timeout 表示上游接受了连接,但在 proxy_read_timeout 内没有返回任何内容;该值默认为 60 秒,日志会记录为 upstream timed out (110: Connection timed out) while reading response header from upstream。503 Service Unavailable 表示有意拒绝请求。请注意,nginx 自带的速率限制器会返回 503,因为 limit_req_status 默认为 503。如果你在日志中查找 429 Too Many Requests,却找到 503,原因就在这里。设置 limit_req_status 429; 才能获得准确的状态码。
运行服务器时需要关注的请求头
Host用于选择站点。一个 IP 地址可以提供数百个主机名,nginx 会将 Host 与 server_name 匹配,以决定由哪个 server 块响应。如果没有匹配项,nginx 会使用默认服务器。默认服务器通常是监听该地址和端口的第一个块,除非其他块标记为 default_server。新建虚拟主机后返回了错误站点,几乎总是因为名称不匹配,请求回退到了默认服务器。无需修改 DNS 即可测试:
curl -sS -o /dev/null -D - -H 'Host: app.example.com' http://127.0.0.1/User-Agent是客户端提供的自我描述信息,内容为自由文本。读取日志时可以将它作为参考。不要将它用作控制条件,因为想伪造该信息的客户端可以直接伪造。因此,按 User-Agent 屏蔽爬虫只会过滤掉遵守规则的爬虫。
Content-Type决定如何解释字节:API 请求使用 application/json,页面使用 text/html; charset=utf-8。nginx 通过 /etc/nginx/mime.types 将文件扩展名映射到类型;软件包提供的 nginx.conf 会设置 default_type application/octet-stream;。因此,扩展名不在 nginx 识别范围内的文件会作为下载文件提供,而不会被渲染。常见现象是页面可以加载,但没有样式,同时浏览器控制台显示 Refused to apply style from ... because its MIME type ('text/plain') is not a supported stylesheet MIME type。这里的 MIME 是 multipurpose internet mail extensions,即这些类型字符串所采用的命名方案。
Cache-Control用于控制服务器与读者之间的所有缓存。对于文件名包含内容哈希的资源,public, max-age=31536000, immutable 很合适,因为内容变化时文件名也会变化。任何包含用户特定内容的资源都应使用 no-store,因为共享缓存如果保存了登录后的页面,就会将该页面提供给下一个请求同一 URL 的用户。private 是中间设置:浏览器可以缓存,共享缓存不可以缓存。
X-Forwarded-For用于解决代理隐藏访问者地址的问题。请求经过反向代理后,$remote_addr 是代理的地址,因此日志、地理定位和速率限制都会将所有请求视为来自同一个客户端。代理必须传递原始地址:
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;接收请求的服务器还必须配置为信任该地址,并且必须明确指定信任哪些代理:
set_real_ip_from 10.0.0.0/8;
real_ip_header X-Forwarded-For;只列出由您控制的网段。X-Forwarded-For 是任何客户端都可以发送的纯文本,因此 set_real_ip_from 0.0.0.0/0; 会允许访问者指定日志记录的地址,以及速率限制统计的地址。
X-Forwarded-Proto可以避免一种非常常见的故障。代理终止 TLS,然后通过普通 HTTP 将请求转发给应用。应用看到的是普通 HTTP 请求,于是判断访问者应使用 HTTPS,并返回 301 https://example.com/。浏览器跟随重定向,代理再次终止 TLS,然后再次通过普通 HTTP 转发请求,循环会不断重复,直到浏览器因 ERR_TOO_MANY_REDIRECTS 而放弃。发送 X-Forwarded-Proto: https 可以告知应用访问者已经使用 HTTPS,从而停止重定向。
HTTP/1.1、HTTP/2 和 HTTP/3:它们对您有哪些变化
HTTP/1.1 使用文本格式,并且每个连接一次只处理一个请求。Connection: keep-alive 允许后续请求复用同一个 TCP 连接,从而节省建立连接的开销,但响应仍会按照请求发出的顺序返回。一个缓慢的响应会阻塞其后排队的所有内容。这称为队头阻塞,浏览器通过同时向同一主机名建立多个连接来规避它。
HTTP/2 保留相同的方法和状态码,但将分帧方式改为二进制。多个请求可以作为相互独立的流共享一个连接,并且会压缩重复的请求头文本;现代请求通常包含大量请求头,因此这一点很重要。连接仍使用 TCP,所以一个丢失的数据包会阻塞该连接上的所有流,直到重传数据包到达。队头阻塞并没有消失,而是从 HTTP 层下移到了传输层。服务器推送曾是 HTTP/2 的一部分,但实际上已经不再使用,因为 Chrome 在 2022 年移除了对它的支持。
HTTP/3 再次保留相同的语义,并用 QUIC 取代 TCP。QUIC 是一种构建在 UDP(用户数据报协议)之上的传输协议。QUIC 流在整个协议栈中都彼此独立,因此丢失的数据包只会阻塞它所属的流。TLS 1.3 内置于 QUIC 握手中,而不是作为上层协议叠加,因此建立新连接所需的往返次数更少。由此带来两个实际影响:路径上的每个防火墙都必须开放 UDP 端口 443;任何限制或阻断 UDP 的网络都会使客户端退回到 HTTP/2。
具体来说,您需要注意以下变化。浏览器不会直接从 HTTP/3 开始连接。它们会先通过 HTTP/2 或 HTTP/1.1 连接,看到响应中的 Alt-Svc: h3=":443"; ma=86400 响应头,然后在以后连接该主机时使用 HTTP/3。因此,该响应头不是可有可无的装饰,而是服务发现机制。在 nginx 中,HTTP/2 在 1.25.1 版本中成为独立指令(在 server 块中使用 http2 on;,取代旧的 listen ... http2 参数);QUIC 则在主线版本 1.25.0 中加入。此时,HTTP/3 站点除了普通的 listen 443 ssl; 之外,还需要配置 listen 443 quic reuseport;。
不同代理在这方面的成熟度不同,您应根据实际运行的版本进行检查。截至 2026 年 8 月,Caddy 默认提供 HTTP/3,无需配置。nginx 需要显式配置 quic 监听器,以及上文所述的 Alt-Svc 响应头。Traefik 则通过显式的 http3 选项,为每个 entry point 单独启用 HTTP/3。如果您在多个 Docker 应用前使用 Traefik 作为反向代理终止 TLS,访问者使用的协议版本由 Traefik 决定;无论浏览器协商出的版本是什么,从代理到容器的这一跳通常仍是普通 HTTP/1.1。
请进行验证,不要只依赖配置推断。curl --http3 -sS -o /dev/null -D - https://example.com/ 只有在 curl -V 的功能列表中包含 HTTP3 时才有效,而大多数发行版构建不会包含该功能。可靠的检查方法是查看您自己的日志:将 $server_protocol 加入日志格式,然后读取实际浏览器协商出的协议。开始这些检查前,先确认 UDP 443 确实已开放,因为只允许 TCP 443 的防火墙会让 HTTP/3 静默失败,而网站仍会通过 HTTP/2 正常工作。首先应确认 Linux 服务器上哪些端口已开放并处于监听状态。
HTTPS:HTTP 是协议,TLS 是封装层
HTTPS 不是独立的协议。它是在 TLS 会话中传输的相同请求和相同状态码。端口 80 以明文传输,端口 443 以加密方式传输。TLS 握手先完成,然后 HTTP 请求通过加密通道传输。正因如此,证书问题不会带有状态码:失败发生在发送任何 HTTP 字节之前,因此不存在可编号的响应。
在一台服务器托管多个站点时,有一个顺序细节很重要。服务器使用 SNI(服务器名称指示)选择证书。SNI 是 TLS 握手中的一个字段,会在任何 HTTP 标头存在之前以明文携带主机名。因此,服务器先根据 SNI 选择证书,再根据 Host 标头选择虚拟主机。这是两个彼此独立的查找过程,通常会得到一致的结果。如果不一致,浏览器会显示类似 NET::ERR_CERT_COMMON_NAME_INVALID 的名称不匹配错误,并且完全不会发送请求,因为服务器提供的默认证书对应的名称不包含该主机名。
对于公网站点,应获取有效证书,并让证书自动续期。在 nginx 上使用 Let's Encrypt 的 Certbot 会将证书路径写入服务器块,并为您安装续期计时器。对于任何公共机构都无法验证的主机名,例如内部名称或您自己网络中的裸 IP 地址,在 Ubuntu 上使用自签名证书是合适的选择,但您必须接受每个客户端都需要配置为信任该证书。
TLS 正常工作后,将端口 80 上的所有请求重定向到端口 443:
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}只有在确认无误后才添加 Strict-Transport-Security。add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; 标头会告诉浏览器,在两年内拒绝通过明文 HTTP 访问该主机名。浏览器会将此规则保存在自己的缓存中,因此之后删除该标头也无法撤销规则。先将 max-age 设置为几小时,确认每个子域名确实都使用 HTTPS,然后再延长有效期。
FAQ
HTTP 和 HTTPS 有什么区别?
HTTPS 是在 TLS(传输层安全)会话中传输的 HTTP。HTTP 方法和状态码完全相同。变化在于,客户端与 TLS 终止端之间的字节会被加密,默认端口也从 80 改为 443。由于 TLS 握手会在发送第一个 HTTP 字节之前完成,证书失败不会产生状态码。因此,浏览器的证书警告会显示类似 NET::ERR_CERT_COMMON_NAME_INVALID 的错误名称,而不是 403 这样的数字。
为什么我的站点返回 502 Bad Gateway?
nginx 返回 502,表示 nginx 无法从它代理的上游获取可用响应。因此,访问者的请求没有问题,nginx 后方的某个组件出现了问题。查看 /var/log/nginx/error.log。connect() failed (111: Connection refused) while connecting to upstream 表示 proxy_pass 中的地址和端口没有进程监听,因此请检查应用是否正在运行,并确认它绑定到了预期的地址。no live upstreams while connecting to upstream 表示上游块中的所有服务器在反复失败后都已被标记为不可用。将其与 504 Gateway Timeout 区分开来;后者表示上游已接受连接,但未能在 proxy_read_timeout 内响应。
为什么我的访问日志中每个访问者都显示相同的 IP 地址?
因为 $remote_addr 记录的是建立 TCP 连接的地址,而在反向代理或内容分发网络后方,该地址就是代理的地址。访问者的地址会出现在 X-Forwarded-For 标头中。先在代理上设置 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;,然后在接收请求的 nginx 上将 set_real_ip_from 设置为代理的地址范围,并配置 real_ip_header X-Forwarded-For;。只列出由您控制的地址范围,因为该标头是客户端可以发送的文本。如果信任来自整个互联网的该标头,访问者就可以选择您记录的地址以及用于限流的地址。
是否需要启用 HTTP/2 或 HTTP/3?
值得启用 HTTP/2,因为对于已经配置 TLS 的站点,只需添加一条指令,而且它可以消除每个连接的请求限制。该限制会导致包含大量小文件的页面加载缓慢。HTTP/3 的收益较小,也不确定,同时需要开放 UDP 443 端口,并使用支持 QUIC 的代理构建版本。请注意,浏览器只有在较早的响应中看到 Alt-Svc 标头后,才会切换到 HTTP/3。因此,如果没有该标头,无论 listen 行如何配置,都不会发生变化。在日志格式中添加 $server_protocol,先测量访问者实际协商使用的协议,再决定是否投入时间配置 HTTP/3。
文件确实存在时,403 Forbidden 表示什么?
对于静态站点,403 通常表示文件系统权限问题,而不是 HTTP 规则问题。/var/log/nginx/error.log 中的 open() ... failed (13: Permission denied) 表示 nginx worker 用户无法读取该文件。最常见的原因是父目录没有为其他用户设置执行权限,而不是文件自身的权限模式错误。directory index of ... is forbidden 表示请求解析到了一个没有索引文件的目录,同时 autoindex 处于关闭状态。如果匹配的 location 块中存在明确的 deny 规则,也会返回 403。因此,当错误日志没有提供信息时,请检查该块。