写点什么

搞定 HTTP 协议(三):如何严谨地描述一个 HTTP 报文?

用户头像
零和幺
关注
发布于: 2020 年 06 月 21 日
搞定 HTTP 协议(三):如何严谨地描述一个 HTTP 报文?

在 HTTP 协议中,最为核心的部分就是客户端和服务器之间通信时传输的报文了。HTTP 报文是由多行数据构成的字符串文本。通常情况下,一个 HTTP 报文由以下 4 部分构成:



  • 起始行(start line)



  • 头部字段(header fields)



  • 一个空行(CRLF)



  • 报文主体(message body)



在以上四部分中,起始行与头部字段,又经常被称作“请求头”或“响应头”,而报文主体经常被称为“实体(entity)”,两者由最初出现的空行(CRLF)来划分。而最后的报文主体是一个可选项,并不一定存在。



@ HTTP 报文结构



通常情况下,我们都采用上面这种口语化的形式来描述 HTTP 报文的格式,但是这种口语化的表达并不十分严谨。比如,我们通常会这样描述一个请求头:请求头中包含了请求方法、请求 URI、HTTP 版本号,但是它们之间是否要加空格呢?加两个空格可以么?再比如上图中的 Host 头部字段,冒号后面必须加一个空格么?加两个空格可以么?不加空格,而是用 Tab 制表符可以么?冒号前面可以加空格么?



如果用口语化的表达来描述 HTTP 报文,就很难说清楚上面这些问题。因此,RFC 7230 文档采用了 ABNF 范式来严谨的描述 HTTP 报文。

ABNF 范式



ABNF 范式大体上分为操作符核心规则两大方面,这里我们不做区分,统一介绍一下与 HTTP 协议相关的描述。



  • 选择:使用反斜杠"/",表示多个规则可选其一



规则1 / 规则2
start-line = request-line / status-line // 起始行可以是一个请求行,也可以是一个状态行



  • 可变重复



m * n
* 表示零个或多个元素:*(header field CRLF) // 可以有零或多个,以 CRLF 结尾的头部字段
1* // 表示一个或多个元素



  • 序列组合:使用小括号"()",将规则放在括号内组合起来,视作一个整体。



(规则1 规则2)
*(SP / HTAB) // 零个或多个(空格或者横向制表符)

  • 可选序列:用中括号表示"[]"



[]
[ message-body ]: 报文主体是一个可选参数



  • 空白符:使用"SP"来表示,用来分隔定义的元素



SP %x20 空格
request-line = method SP request-target SP HTTP-Version CRLF



  • 水平 tab:用"HTAB"来表示



HTAB %x09 水平tab
header-field = field-name ":" *(SP / HTAB) field-value *(SP / HTAB)
// 头部字段由字段名称和字段值组成,中间以冒号分隔,冒号后面可以有零个或多个空格或者横向制表符

  • CRLF:互联网标准换行,由 CR(回车)和 LF(换行)组成



CRLF

start-line
header-filed
CRLF
body-message
// 头部字段和报文主体之间必须有一个 CRLF



了解上面这些 ABNF 范式中的操作符和核心规则后,我们就可以用 ABNF 范式来严谨的定义 HTTP 报文了:



// HTTP 报文构成:一个起始行;零个或多个头部字段;一个空行;一个可选的报文主体

HTTP-message = start-line
*( header-field CRLF )
CRLF
[ message-body ]
// 起始行构成:请求行或者状态行
start-line = request-line / status-line
// 请求行构成:请求方法;空格;请求目标;空格;协议版本;换行
request-line = method SP request-target SP HTTP-version CRLF
// 状态行构成:协议版本;空格;状态码;空格;原因短语;换行
status-line = HTTP-version SP status-code SP reason-phrase CRLF
// 头部字段构成:
// 一个不区分大小写的字段名称;一个英文冒号;零个或多个空格或横向制表符;字段值;零个或多个空格或横向制表符
header-field = field-name ":" OWS field-value OWS
field-name = token
OWS = *(SP / HTAB)
field-value = *(field-content / obs-fold)
// 报文主体构成:用于携带请求或响应的有效载荷体
message-body = *OCTET



起始行



一个 HTTP 报文可以是从客户端到服务器的请求报文,也可以是从服务器到客户端的响应报文。通常情况下,对于请求报文来说,我们称它的起始行为请求行;而对于响应报文来说,我们称它的起始行为状态行



请求行



请求行描述了客户端想要如何操作服务器上的资源。它通常包括:



  • 请求方法(method):希望如何操作资源

  • 请求目标(request-target):通常是一个 URI,表示资源的位置

  • 协议版本(HTTP-version):使用的 HTTP 版本



@ 请求行结构



以实际的例子来说:



GET /index.html HTTP/1.1



“GET” 是请求方法, “/index.html” 是请求目标,“HTTP/1.1”是协议版本。利用这一行请求行,就可以明确的告诉服务器:我想获取根目录下的 index.html 文件,我的 HTTP 版本号是 1.1。



状态行



状态行描述了服务器的响应状态。它通常包括:



  • 协议版本(HTTP-version):使用 HTTP 的版本



  • 状态码(status-code):状态码其实也有对应的 ABNF 描述 3DIGIT, 表示一个三位整数,比如常见的 200



  • 描述状态码的原因短语(reason-phrase):用来解释状态码的具体原因



@ 状态行结构



还是以实际的状态行来说:



HTTP/1.1 200 ok



“HTTP/1.1” 是协议版本,“200” 是状态码,“ok” 是原因短语。意思就是告诉客户端:找到了相应资源,我已经处理好了你的请求。



头部字段



@ 头部字段结构



从上面的图中可以知道:每个头部字段是一个典型的 key-value 格式,最后以 CRLF 表示结束,并且在整个头部字段的最后,必须由一个 CRLF 表示头部字段的结束。



Host: 127.0.0.1:9090
Content-Type: text/html
...



对于头部字段来说,有一些特点需要我们注意:



  • 头部字段是完全可扩展的,使用新字段名称是没有限制的

  • 字段名称大小写均可,但通常情况下首字母需要大写

  • 不同字段名称的字段顺序不重要,但是先发送包含控制数据的头部字段是一个良好的实践。例如请求中的 Host 和响应中的 Date,这样当实现不处理一个报文的时候,可以尽可能早的做出判断

  • 字段名称和冒号之间不允许出现空白,因为可能会导致安全漏洞。冒号后可以有一个或多个空格(也可以是横向制表符)。字段值后也可以跟一个或多个空格(也可以是横向制表符),但最后要有一个 CRLF。通常情况,在冒号后面加一个空格是良好的习惯



头部字段通常分为以下四种:



  • 通用头部:既可以出现在请求头中,也可以出现在响应头中,如 Date 字段;

  • 请求头部:只能出现在请求头中,用于解释说明请求信息,如 Host 字段;

  • 响应头部:只能出现在响应头中,用于解释说明响应信息,如 Server 字段;

  • 实体头部:用于表述报文主体,如 Content-Length 字段,表示报文主体的长度。



HTTP 报文是 HTTP 协议的核心,而头部字段就是 HTTP 报文的核心。充分理解了常见的头部字段,HTTP 协议就不在话下了,后面的文章会重点介绍常见的重要头部字段。



报文主体



HTTP 协议中不要求报文主体必须存在,如果存在的话,报文主体用于携带请求或响应的有效载荷体。



通常情况下,首部字段中的 Content-LengthTransfer-Encoding 是请求中报文主体存在的信号。而响应中报文主体的存在取决于响应的请求方法状态码。如 HEAD 请求方法的响应从不包括报文主体,而所有的 1xx,204 以及 304 的响应也不包含报文主体。



小结



本文详细介绍了 HTTP 的报文结构,除了常见的口语化表达外,还引入了 RFC 7230 文档中用于描述 HTTP 报文的 ABNF 范式,进行严谨的描述。



  1. HTTP 报文主要由起始行、零个或多个头部字段、CRLF 以及可选的报文主体构成

  2. 起始行与头部字段经常被称为请求头或响应头

  3. 请求中的起始行叫做请求行,由请求方法、请求目标、协议版本组成

  4. 响应中的起始行叫做状态行,由协议版本、状态码、原因短语组成

  5. 头部字段的字段名一般大写第一个字母,后面紧跟冒号,不允许有空格;冒号与字段值直接可以有零个或多个空格或横向制表符

  6. 头部字段通常分为四种:通用头部、请求头部、响应头部以及实体头部

  7. 头部字段与报文主体之间,必须以一个 CRLF 区分

  8. 报文主体可以不存在



@ HTTP 报文结构脑图总结



最后的话



你的点赞会给我一天好心情,如果能顺手 来个 star,再顺便关注下公众号(零幺小馆)就更完美了。





参考资料



  1. RFC 7230 文档

  2. 极客时间 - 《透视 HTTP 协议》

  3. 极客时间 - 《Web 协议详解与抓包实战》

  4. 《图解 HTTP》



发布于: 2020 年 06 月 21 日阅读数: 82
用户头像

零和幺

关注

API 调用师,CV 工程师。 公粽号:零幺小馆 2017.10.17 加入

还未添加个人简介

评论

发布
暂无评论
搞定 HTTP 协议(三):如何严谨地描述一个 HTTP 报文?