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

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

  • 起始行(start line)git

  • 头部字段(header fields)github

  • 一个空行(CRLF)安全

  • 报文主体(message body)bash

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

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

若是咱们用口语化的表达来描述 HTTP 报文,就很难说清楚上面这些问题。所以,RFC 7230 文档采用了 ABNF 范式来严谨的描述 HTTP 报文。code

ABNF 范式

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

  • 选择:使用反斜杠"/",表示多个规则可选其一
规则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 报文了:htm

// 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. 报文主体能够不存在

最后的话

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

参考资料

  1. RFC 7230 文档
  2. 极客时间 - 《透视 HTTP 协议》
  3. 极客时间 - 《Web 协议详解与抓包实战》
  4. 《图解 HTTP》