HTTP 怎么结束
HTTP 双换行结尾、Transfer-Encoding: chunked 的包格式,以及它们和文件系统 EOF 的类比。
~/posts/http-end-of-story $ cat post.md
EOF 与可靠的文件系统
很多文件格式以 EOF 标识收尾——表示文件结束。如果这个文件被某个文件流读取,意味着这个流也可以关掉了。对于可靠的文件系统来说这是一个很合理的实践。
文件系统也是分层的。以 Linux 为例(只考虑硬盘上的文件):
- 应用层 之下是 Kernel 层。
- System Call Interface (SCI) 层向应用提供可靠的系统调用接口。这些系统调用经常被各种语言封装成脚本语言的函数——比如 Node 的
fs就是封装好的 SCI 调用。 - SCI 之下是 Virtual File System (VFS) 层,统一暴露”读取一个文件”这样的语义,不管文件实际来自硬盘还是其它设备。硬盘分区也在这里处理。
- VFS 之下是 General Block Device 层,隐藏具体硬件的实现细节,给上层提供一个虚拟化的块设备访问接口。
- 再往下就是硬件,本文不展开。
这和网络协议的分层是同源同根的——每一层只关心自己范围内的事情。最终给应用看的 API 大多数情况下不需要操心”响应不及时要重试”这类底层问题。
铺垫这么多想说的其实就是:EOF 作为应用层的”文件结束”标识,是合理的。
HTTP 和应用层结束
很多人会问:HTTP 是应用层协议,那它为什么不能直接用 EOF 这样的标识收尾呢?
答案是”先问是不是,再问为什么”——HTTP 协议本来就有自己的结束标识:\n\n(更准确是 \r\n\r\n)。
举例:一个 GET 请求的 header 区结束时,一个双换行就告诉对端”这个请求结束了,你可以回复了”。POST 请求里,第一个双换行表示 header 区结束、接下来是 body;第二个双换行表示 body 也结束了,可以处理。
但当我们在同一条 TCP 连接里要传输多个相关的包,且数量不定的时候,双换行就不够用了。
和双换行也没差多远
HTTP 头 Transfer-Encoding 用来描述分块传输的细节。Transfer-Encoding: chunked 表示包数量未定——客户端要收完所有 chunk 再拼起来才能得到完整结果。
每个 chunk 的格式:
- 一个 16 进制的数字,表示这个 chunk 的字节数。
- 紧跟一个换行(按规范是
\r\n,但有些实现比如 Chrome 在大小和\r\n之间会塞一个空格0x20)。 - 数据本身。
- 一个换行结尾。
所有 chunk 的数据拼接起来就是完整内容。最后一个 chunk 是一个”空 chunk”——以一个空格开头,可能带些额外的尾部 header(trailing headers),以一个换行结尾,标记整个传输结束。
这个模式和 EOF 或者双换行差别不大,只是 EOF 本身就是一个文件流的概念。
归根结底还是应用层
总的看,应用层的实现把”重试”、“内容校验”等等都省了——靠的是站在 TCP 之上。比如包的顺序问题 HTTP 完全不需要操心,TCP 已经处理过了。