SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor

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: gzip
  • GET 是方法,用于说明要执行的操作。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,并通过长度为零的分块标记结束。
  • Cache-Control 告诉浏览器和中间缓存可以将此响应视为新鲜内容多长时间。超过 max-age 秒后,响应即变为过期内容,但这不表示已删除:缓存可以保留过期副本,并向服务器重新验证,而不必再次下载完整请求体。
  • 在请求和响应中,头部之后的空行都用于将请求头与请求体分隔开。

请求头名称不区分大小写。每一行都以回车后跟换行结束,而不是仅使用换行。您不会手动输入这些内容,但会在数据包捕获中看到它们。

要观察实际的一对请求和响应,请针对您拥有的网站运行以下命令:

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(传输控制协议)连接的地址。如果请求经过代理,这里记录的是代理地址,而不是访问者地址;除非您已配置下文所述的 realip 模块来替换该值。
  • 第一个 - 是固定占位符。第二个是 $remote_user,仅在使用 HTTP 基本身份验证时才会填充。
  • "GET /pricing HTTP/1.1" 是 $request,即完整的原始请求行。使用 nginx 的默认日志转义规则时,双引号、反斜杠以及任何小于 32 或大于 126 的字节都会写成 \xXX,因此该行内容完整,但不一定逐字节一致。
  • 200 是服务器返回的状态码,不是访问者实际看到的状态码。
  • 5310 是 $body_bytes_sent,即仅包含响应正文。响应标头不计入其中,因此该数字始终小于实际发送的字节数。
  • 最后两个带引号的字段是 Referer 和 User-Agent。两者都来自客户端,因此可以包含任意内容。

垃圾数据也会以转义形式记录到日志中。客户端如果在明文端口 80 上向您的服务器发送 TLS(传输层安全)流量,会生成一条 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 用于正常读取请求。创建资源的 201 Created 请求成功后返回 POST。204 No Content 表示请求成功但无需返回内容,通常用于响应 DELETE。

3xx 表示需要访问其他位置。 301 表示永久重定向。浏览器会强力缓存它,有时只有用户清除配置文件后才会失效。因此,指向错误主机名的 301 很难撤销。仍在测试重定向时,应使用 302。304 Not Modified 表示成功,不是错误:客户端发送了条件请求,或者通过 If-None-Match 携带了服务器仍能识别的 ETag(实体标签),或者通过 If-Modified-Since 携带了资源自该日期后未发生变化的日期。因此,服务器仅返回响应头,不返回响应体。日志中大量出现 304,说明缓存正在生效。

4xx 表示请求有误。 400 Bad Request 表示输入格式错误。401 Unauthorized 实际表示未通过身份验证,并且必须携带说明认证方案的 WWW-Authenticate 请求头。403 Forbidden 表示服务器理解了请求,但仍拒绝执行。404 Not Found 表示路径不存在,或服务器不愿承认该路径存在,因为某些应用会返回 404 而不是 403,以便向无权访问的客户端隐藏资源。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 表示 nginx 等待上游响应超时。最常见的情况是,上游接受连接后,在 proxy_read_timeout 内没有发送任何内容;该值默认为 60 seconds,日志会记录为 upstream timed out (110: Connection timed out) while reading response header from upstream。如果连接尝试在 proxy_connect_timeout 内未完成,同样默认为 60 seconds,最终也会返回 504,并且日志行会以 while connecting to 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;

这两个指令属于 nginx 的 realip 模块。该模块会将 $remote_addr 替换为转发的地址,并将连接自身的地址保存在 $realip_remote_addr 中。如果自行编译 nginx,请添加 --with-http_realip_module,因为该模块默认不会构建。

只列出由您控制的地址范围。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 握手中,而不是叠加在其上,因此新连接需要更少的往返次数。由此带来两个实际影响:服务器用于 HTTP/3 的 UDP 端口必须在路径上的每个防火墙中放行;任何限制或阻断 UDP 的网络都会使客户端回退到 HTTP/2。该端口通常是 443,但协议允许服务器使用任意 UDP 端口,因为将浏览器指向 HTTP/3 的通告始终会明确指定端口。

具体来说,您会遇到以下变化。浏览器通常不会从 HTTP/3 开始连接。它们会先通过 HTTP/2 或 HTTP/1.1 连接,看到响应中的 Alt-Svc: h3=":443"; ma=86400 响应头,然后在后续连接到该主机时使用 HTTP/3。例外是 DNS 中的 HTTPS 记录,其 alpn 值列出了 h3;这样,Firefox 等浏览器就能在首次连接时打开 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;。HTTP/3 模块默认不会构建(--with-http_v3_module),nginx 文档仍将其支持称为实验性功能。

不同代理对此功能的成熟度不同,应结合您实际运行的版本进行确认。截至 2026 年 11 月,Caddy 默认提供 HTTP/3,无需配置。nginx 需要显式配置 quic 监听器,以及上文所述的 Alt-Svc 响应头。Traefik 则通过入口点中的显式 http3 选项启用该功能。如果您在多个 Docker 应用前使用 Traefik 作为反向代理终止 TLS,访问者使用的协议版本由 Traefik 决定;无论浏览器协商使用哪种版本,代理到容器之间通常仍是普通的 HTTP/1.1。

请通过验证确认,而不要凭假设判断。只有当 curl -V 的功能列表中包含 HTTP3 时,curl --http3 -sS -o /dev/null -D - https://example.com/ 才能正常工作,而大多数发行版构建版本并未包含它。可靠的检查方法是查看您自己的日志:将 $server_protocol 添加到日志格式中,然后读取真实浏览器协商出的协议。执行上述操作前,先确认 quic 监听器使用的 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。方法和状态码完全相同。区别在于,从客户端到 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 对比;该错误表示 nginx 等待上游响应超时,可能是在 proxy_connect_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 请求头,或看到列出 h3 的 HTTPS DNS 记录后,才会切换到 HTTP/3。因此,如果没有这两者之一,无论 listen 行如何配置,都不会发生变化。在日志格式中添加 $server_protocol,先测量访问者实际协商使用的协议,再决定是否投入时间启用它。

文件存在时,403 Forbidden 表示什么?

对于静态网站,403 通常表示文件系统权限问题,而不是 HTTP 规则问题。open() ... failed (13: Permission denied) 出现在 /var/log/nginx/error.log 中时,表示 nginx worker 用户无法读取该文件。最常见的原因是父目录没有为其他用户设置执行权限,而不是文件自身的权限模式错误。directory index of ... is forbidden 表示请求解析到了一个没有索引文件的目录,同时 autoindex 处于关闭状态。匹配的 location 块中如果有明确的 deny 规则,也会返回 403。因此,当错误日志没有提供信息时,请检查该配置块。

这篇文章对您有帮助吗?

#http#https#web-server#headers#http3