知行札记
专题技术实践通信协议实践HTTP 网络通信

HTTP 角色、消息与无状态

理解客户端、服务端、中间节点和 HTTP/1.1 报文的边界。

本页沿 HTTP 的具体接口和通信行为展开。沿用归档中的消息与代码示例,归档所称的实测仅作为原作者历史记录;2026-10-05 整理未向外部站点发送实验请求,也未操作浏览器。命令输出随服务、网络、版本和代理条件变化。

在地址栏敲下网址、按下回车,页面随即出现——这背后是一场看不见的对话:你的电脑向另一台电脑发了一条消息,对方回了一条。这场对话遵循的规则,就是 HTTP。

先从名字说起:「超文本传输协议」

HTTP 全称 Hypertext Transfer Protocol,中文译作「超文本传输协议」。

为什么叫「超文本」?万维网最初的目的,是让文档里的某个词可以「链接」到另一份文档——这种带链接的文本就是超文本,最典型的格式是 HTML 网页。HTTP 当初正为传输这类文档而生:1991 年的 HTTP/0.9 简陋到只能传输 HTML 文件;今天它运的早已不止网页,但名字留了下来。「传输协议」则是一套双方事先约定的通信规则:先说什么、后说什么、每句什么格式,双方都遵守才能对上话。

HTTP 处于哪一层?它是应用层(application layer)的协议。打个电话的比方:TCP 负责把电话接通、保证每个字都送达,HTTP 是你们在电话里说的「语言」。HTTP/1.1 和 HTTP/2 都跑在 TCP 之上;TLS(Transport Layer Security,安全传输层)则是叠在 TCP 之上的一层加密——它在 TCP 的字节流之上提供加密与认证,叠上 TLS 的 HTTP 就是 HTTPS。最新的 HTTP/3 才真正换了线路,改走基于 UDP 的 QUIC 协议。

一问一答:谁先开口很重要

HTTP 采用客户端-服务器(client-server)的请求-响应(request-response)模型,像在餐厅点餐:你(客户端)先开口点单,服务员(服务器)听完上菜。MDN 说得很明确:浏览器总是首先发起请求的那个实体,永远不会是服务端——服务器的每条响应,都是对某条请求的应答。

三个常见误解,逐一拆掉:

  • 「服务器主动发送消息」——经典模型里不行,永远客户端先开口。后来加入的机制能「模拟」出服务端主动发消息,但来路不同:Server-Sent Events(SSE)仍基于 HTTP——由客户端先发一条请求,服务器让这条响应持续开着、不断往外写数据;WebSocket 则是先借一次 HTTP 请求完成「升级」握手,随后变成另一个独立协议。它们都是对经典模型的事后补强,不是请求-响应模型本身。
  • 「HTTP 客户端就是浏览器」——任何发起 HTTP 请求的程序都是客户端,RFC(Request for Comments,互联网技术标准的正式文档)统称它们为用户代理(user agent):浏览器只是典型代表,curl、爬虫、手机 App、家电固件脚本都算,背后未必有真人操作。
  • 「客户端和服务器是两种固定的机器」——RFC 9110 明确:client/server 只是程序在某条特定连接上扮演的角色。同一个程序可以既当客户端又当服务器,代理就是例子。

一次完整交互的全过程

一次完整的 HTTP 对话分四步:

  1. 建立连接:浏览器先把网址里的域名通过 DNS 解析成 IP 地址(好比先从通讯录查到对方号码),再与服务器建立 TCP 连接。连接需指定端口——IP 地址像大楼地址,端口像房间号。URL 不写端口时有默认值:http:// 走 TCP 80,https:// 走 TCP 443,这两个数字最容易记反。
  2. 客户端发请求:一条请求报文发过去。
  3. 服务器回响应:状态码(status code)、响应头、可选的正文。
  4. 复用或关闭连接:「一问一答后连接立刻断开」是 HTTP/1.0 的旧行为(每对请求/响应新开一个 TCP 连接,用完就关)。HTTP/1.1 默认持久连接(persistent connection),一个连接可连续跑多对请求和响应。至于何时关闭:任一方都可以随时直接关掉连接(连接空闲太久被服务器关掉就很常见);若想把「这次之后就关」明确告知对方,才带上 Connection: close 头。本章后面的归档实验记录里,服务器回的是 Connection: keep-alive,对话结束后连接由 curl 直接关闭——自始至终没有任何一方发过 Connection: close。

报文长什么样:一封人类能读懂的信

很多人以为网络上跑的都是看不懂的二进制。对 HTTP/1.1 恰恰相反:它的报文是明文、语义可读的文本,每行你都读得懂——既是「设计得简单易读」的优点,也埋下安全隐患(后面「明文的代价」一节细说)。HTTP/2 起报文才被封装进二进制结构「帧(frame)」。

RFC 9112 给出的报文结构文法:

HTTP-message = start-line CRLF *( field-line CRLF ) CRLF [ message-body ]

这是 RFC 描述格式用的 ABNF 记法:*( ... ) 读作「重复零次或多次」,[ ] 表示「可有可无」。翻译成人话:一条报文 = 起始行 + 若干行头部字段 + 一个空行 + 可选正文,每行以 CRLF(回车加换行)结尾;空行是「头部结束、正文开始」的分界线。

一个最小的请求示例:

GET / HTTP/1.1
Host: developer.mozilla.org
Accept-Language: zh

第一行是请求行,语法为 method SP request-target SP HTTP-version(SP 即单个空格):GET 是方法(「获取」资源),/ 是目标(网站根路径),HTTP/1.1 是版本。之后每行是一个头部字段(「名字: 值」)。整封信的意思:把首页给我,找的是 developer.mozilla.org 这个网站,内容优先给中文。

其中 Host 头是必需的:RFC 9112 规定所有 HTTP/1.1 请求必须携带 Host,缺失时服务器必须返回 400 Bad Request。原因:如今一个 IP 上往往托管许多网站,像一栋楼里住着许多户——不写清找哪一家(域名),服务器无从处理。删掉 Host 头原调研记录,example.com 果然回 400(见最后一节)。

服务器的回信结构类似:状态行、响应头、空行、可选正文。实测 example.com 的响应开头:

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked

200 是状态码,三位数字、首位表大类,共五类:1xx 信息、2xx 成功、3xx 重定向、4xx 客户端错误(如 400)、5xx 服务器错误。Content-Type 则说明正文是 HTML 文本。那行 Transfer-Encoding: chunked 意思是「分块传输」:正文被切成一块块发送,每块自带长度标记,最后以一个零长度的块收尾——因此这条响应没有整体的 Content-Length 长度声明。

「无状态」:一场没有记忆的对话

RFC 9110 给 HTTP 下的定义里有个关键词:无状态(stateless)——每个请求的语义都可以孤立地理解,服务器处理这次请求时不依赖「上一个请求」的记忆;RFC 甚至规定:不得假定同一条连接上的两个请求来自同一个用户代理——因为连接可能被中间的代理复用,前后两个请求的主人未必是同一位;例外是确知这条连接安全地专属于单一客户端的场景。这像去银行办业务:柜员不记得你上次办过什么,你每次都得重新出示证件——每次请求都需要独立携带所需上下文。

两个误解要拆:

  • 「无状态 = 服务器不存任何数据」——不对。无状态只描述协议层面的对话规则;服务器照样可以写访问日志、存数据库,那是应用层的事。
  • 「无状态 = 每个请求新建连接、完事就断」——混了两个维度:「是否无状态」是语义层面(请求间无记忆),「连接是否持久」是传输层面(TCP 复用)。HTTP/1.1 一个连接可跑多个请求,但每个请求在语义上依然彼此独立。

那登录态、购物车怎么实现?HTTP 无状态,但会话可以建在 Cookie 之上:你第一次访问时,服务器先通过 Set-Cookie 响应头发给浏览器一张小「凭证」,浏览器存下来,之后每次请求都自动带上;服务器凭这张凭证认出「还是刚才那个用户」,会话状态就这样在应用层补了回来。登录态、Session、Token 方案都建在这个思路上。

明文的代价:谁都能看

报文人类可读,意味着线路上的任何人都读得懂:访问 http:// 网站时,请求和内容在途中是明文。办法就是前面提过的:在 TCP 之上叠一层 TLS 加密,即 HTTPS——默认端口 443(HTTP 是 80)。顺带分清:HTTP/2 的「二进制帧」只是格式变化,不等于加密;加密始终是 TLS 的事。

三十年进化:从一行文本到 QUIC

版本演进的主线一句话:文本 → 二进制帧 → 换传输层。

  • HTTP/0.9(1991):单行协议,只有 GET,无头部、无状态码,只能传 HTML。
  • HTTP/1.0(1996,RFC 1945):引入版本号、状态码、Content-Type 等头部;每对请求/响应新开一个 TCP 连接。
  • HTTP/1.1(1997,RFC 2068):引入持久连接与 Host 头,流传最广;现行规范为 2022 年的 RFC 9110/9112。
  • HTTP/2(2015,RFC 7540):报文封装为二进制帧,支持多路复用(multiplexing)——多个请求在同一连接上并行交错传输。
  • HTTP/3(RFC 9114):不再基于 TCP,改走 QUIC over UDP。

本章以 HTTP/1.1 讲报文格式,正因为它明文可读,是理解 HTTP 的最佳入口。

动手验证:亲眼看一场 HTTP 对话

下面两条命令归档作者在 macOS 上运行并记录过,你可以照做。

看一场完整对话:

curl -sv --http1.1 http://example.com/ -o /dev/null --max-time 15

命令里 -s 是安静模式(不显示进度条),-o /dev/null 把下载的网页丢掉不保存(Windows 上改写成 -o NUL),--max-time 15 限制最多等 15 秒、防止卡住。关键是 -v:把双方说的每个字打印出来——> 开头是你发出的请求行和请求头,< 开头是服务器返回的状态行和响应头。实际输出(节选):

> GET / HTTP/1.1
> Host: example.com
> User-Agent: curl/8.7.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/html; charset=utf-8
< Transfer-Encoding: chunked
< Connection: keep-alive
< Server: cloudflare

请求行、Host、状态行、响应头,与前几节全部对上——这本身就是「HTTP/1.1 报文是可读文本」最直观的演示。

再验证「删掉 Host 头会被拒收」:

curl -sv --http1.1 -H "Host:" http://example.com/ -o /dev/null --max-time 15

-H "Host:" 会把 Host 头删掉,服务器随即回答:

> GET / HTTP/1.1
> User-Agent: curl/8.7.1
> Accept: */*
>
< HTTP/1.1 400 Bad Request

前面讲的规则,眼见为实。没有命令行也不妨事:浏览器按 F12 打开开发者工具的 Network(网络)面板,刷新页面,点开任意一条请求,能看到同样的请求头、响应头和状态码。

最后更新于

本页目录