前言
上一篇文章我们学习了HTTP/1.1
的请求行(Request Line
),下面我们继续学习请求头的解析。
请求头格式
HTTP/1.1
的消息格式如下:
|
|
- start-line: 起始行
- *( header-field CRLF ): 头字段+分隔符(
\r\n
) - CRLF: 空行
- [ message-body ]: 可选的消息体
所有HTTP/1.1
消息均包含一个起始行,其后是一系列头字段,由分隔符(\r\n
)隔开,接着是一个空行(\r\n
),最后是可选的消息体。
上一篇文章学习的请求行就是起始行的一种,还有一种是状态行(Status Line),我们会在之后讲解。
我们可以发现,每一个头字段后面都需要跟随一个分隔符(\r\n
),当某一行只有分隔符(\r\n
)时,就意味着请求头数据已发送完毕。使用httpie就可以轻松查看一个真实的请求头数据:
|
|
解析请求头
我们再来看看fasthttp
是如何解析请求头的。当fasthttp
读取到完整的请求头数据后,开始解析请求头,并返回请求头的字节数:
|
|
我们看到了熟悉的parseFirstLine
,解析请求行,接着继续读取并保存请求头的原始数据h.rawHeaders, _, err = readRawHeaders(h.rawHeaders[:0], buf[m:])
。在readRawHeaders
中我们可以看到(完整源码):
|
|
目的就是找到单独的\r\n
或者\n
,一旦找到,就表示没有更多的请求头数据了。从源码中我们可以发现fasthttp
兼容了以\n
作为分隔符的数据格式。
最后是解析请求头信息n, err = h.parseHeaders(buf[m:])
,解析成功后,返回第一行数据和请求头的总字节数。parseHeaders
的代码比较长,就不再全部贴出来。
fasthttp
在解析请求头时,使用了headerScanner这个结构体,调用next方法一行一行解析。
针对指定的头字段:Host,UserAgent,Content-Type,Content-Length,Connection和Transfer-Encoding,fasthttp
会做一些特殊操作,我们会一一进行解析。其他头字段信息会存入h.h
(源码)中:h.h = appendArgBytes(h.h, s.key, s.value, argsHasValue)
。
我们先深入分析headerScanner.next()
。
头字段
首先我们看看头字段的结构:
|
|
每个头字段均由不区分大小写的字段名,后跟冒号(:
),可选的前导空白(OWS
),字段值和可选的尾随空白(OWS
)组成。
注:
OWS
指的是optional whitespace
,详情链接。
fasthttp
的headerScanner就是头部扫描器,负责扫描请求头数据,解析出头字段的字段名(key
)和字段值(value
),每次调用next()
都解析一行数据。
根据标准,头字段名称紧跟着一个:
,所以需要根据:
的位置确认头字段名称的值。这里fasthttp
加了一个处理::
之前不能有\n
,因为此时这个头字段肯定是非法的。
解析出头字段名称后,fasthttp
根据配置看需不需要标准化头名称,normalizeHeaderKey(s.key, s.disableNormalizing)
,这是为了统一客户端发送的请求头字段名称格式,防止出现类似cONTENT-lenGTH
的字段名而在使用时无法匹配的情况。
一般情况下,每个头字段的值在一行内,但是也有可能放在多行里(obs-fold),举个例子:
|
|
这两种格式的值是等价的,想深入了解的可以查看这里。fasthttp
也兼容了这种情况,然后对多行值进行格式化s.value, s.b, s.hLen = normalizeHeaderValue(s.value, oldB, s.hLen)
。
最终获取到s.key
和s.value
。
处理头字段键值对
获取到头字段键值对后,对于一般的头字段,fasthttp
将他们存储在h.h []argsKV
切片中(h.h = appendArgBytes(h.h, s.key, s.value, argsHasValue)
),argsKV
具体结构如下:
|
|
为什么不直接存在map
中呢?我们之后会讲解,主要是为了延迟解析以及内存复用。现在就想知道原因的可以查看youtu.be 13:42。
接下来我们讲讲几个特殊的头字段,以及fasthttp
对它们的处理。不过在此之前,我们需要看懂这行代码:
|
|
翻译一下就是将s.key[0]
,即头字段名称的首字母,转为小写。原理也很简单,用一段代码解释:
|
|
Host
Host
头字段(详情)提供了目标URI
的主机和端口信息,使服务器能够区分资源,同时为单个IP
地址上的多个主机名的请求提供服务。Host
具有指定的格式:
|
|
RFC7230
中指明Host
是必须字段。若Host
不合法(未提供,提供多个,格式非法),服务端必须响应400 Bad Request
。
通过caseInsensitiveCompare(s.key, strHost)
比较头字段名称和常量strHost
,确定是否将其存入请求头的host
字段:h.host = append(h.host[:0], s.value...)
。
注:
caseInsensitiveCompare
也用到了0x20
的技巧(源码)。
UserAgent
User-Agent
头字段(详情)包含有关发起请求的用户代理的信息,服务器通常使用该信息来帮助识别报告的互操作性问题的范围,解决或调整响应以避免特定的用户代理限制以及进行分析有关浏览器或操作系统的使用。用户代理应该在每个请求中发送一个User-Agent
字段。
它的结构如下:
|
|
该字段的值直接存入请求头的userAgent
字段中。
ContentType
Content-Type
头字段(详情)指定了请求的媒体类型。
常见的例子有:
|
|
该字段的值直接存入请求头的contentType
字段中。
ContentLength
当请求中没有Transfer-Encoding头字段时,可以设置Content-Length头字段,为潜在的消息体提供预期的大小(八位字节的十进制数);若包含Transfer-Encoding头字段,则无法设置Content-Length
。
fasthttp
使用parseContentLength
(源码)解析具体的数值,并处理了溢出的情况,避免了通过协议元素长度产生的攻击。
若解析成功,fasthttp
也会将原始的字节数据保存在h.contentLengthBytes = append(h.contentLengthBytes[:0], s.value...)
中。
Connection
Connection
头字段(详情)允许客户端控制当前连接。
Connection
值是不区分大小写的,所以fasthttp
使用bytes.Equal(s.value, strClose)
来比较连接值是否为常量strClose("close")
,并保存到connectionClose
字段中,同时将原始的字节数据保存在h.h = appendArgBytes(h.h, s.key, s.value, argsHasValue)
中。
TransferEncoding
Transfer-Encoding
头字段(详情)列出与已经(或将要)应用于有效载荷主体以形成消息主体的传输编码序列相对应的传输编码名称。传输编码在这里定义,有兴趣的读者可以自行了解。
我们看看fasthttp
的处理逻辑:
|
|
因为identity
已经被移除,所以fasthttp
忽略了identity
,且根据我们在Content-Length中学到的知识,此时应该忽略contentLength
,所以将其设为-1
,对应了Content-Length
头字段的if h.contentLength != -1 {...}
(源码)处理。最后把Transfer-Encoding
头字段的值设为chunked
。
收尾操作
解析完所有的头字段后,fasthttp
进行了一些收尾操作:
|
|
- 未设置
h.contentLength
时,清空h.contentLengthBytes
- 不是
HTTP/1.1
的情况下重新设置h.connectionClose
的值(排除Connection: keep-alive
的情况)
总结
请求头解析部分到这里就告一段落了,我们学习了不少请求头相关的知识,稍微总结:
- 每个请求头是根据分隔符(
\r\n
)分隔的,但可以包含多行值 - 空行代表请求头数据的终止
- 一些具体的请求头概念
- 使用
scanner
解析数据流 - 字母字符比较时使用
0x20
的小技巧
敬请期待之后的系列文章! 👋