http 协议的结束符
突然想起很久之前一次面试,面试官问我,当请求头没有 content-length
时,怎么知道请求体结束了?
http 的 header
和 body
之间空行分割的,又因为每个头部项是以 \r\n
作为结束符,所以,数据流中是以 \r\n\r\n
来分割解析请求头(响应头)与请求体(响应体)的。如下图所示:
那么怎么知道(请求体)响应体结束了呢? http 协议规定,响应头的字段 content-length
用来表示响应体长度大小,但是,有可能发送请求头时,并不能知道完整的响应体长度(比如当响应数据太大,服务端流式处理的情况),这时需要设置请求头Transfer-Encoding: chunked
,使用数据块的方式传输,数据块格式如下图所示:
每个数据块分为两个部分:数据长度和数据内容,以 \r\n
分割,最后长度为 0 的数据块,内容为空行(\r\n
),表示没有数据再传输了,响应结束。需要注意的是,此时, content-length
不应该被设置,就算设置了,也会被忽略掉。
回到最开始的那个问题,我当时对 http 协议不太清楚,回答不上来,那位面试官就告诉我,可以使用 \r\n\r\n
来判断,现在看来,他说的并不严谨。首先,http 协议并没有规定请求体(响应体)要以 \r\n\r\n
作为结束符,其次,很重要的一点是,响应体(请求体)的内容是多种多样的,你没法做限制,当数据内容包含\r\n\r\n
时,显然解析出来的响应体就是不全的。
当然,如果是自己实现 http 服务端的话,怎么兼容这种情况呢?
如果是短连接的话,比较简单,连接关闭就表示数据传输完成了。如果是长连接的话,一种不太优雅的方式就是使用超时机制,当读取超过一定时间,就认为数据已经传输完成。
总之,判断数据(块)结束最严谨的方式是计算长度,而不是使用结束符,但是,一般可控的场景下(双方约定),还是可以选择使用结束符来判断的,这样实现起来会更简洁。此时,为了防止内容中包含约定的结束符,导致数据内容被提前截断,客户端可以在发送数据时先对内容中的约定结束符进行编码。
扩展
因为服务端在解析请求头和请求体时,都需要依据以上协议,来读取完整数据。Slow Headers
与 Slow POST
两类 DDoS 慢速攻击正是利用了这个原理。
正常的 HTTP 报文中请求头部的后面会有结束符 0x0d0a(\r\n 的十六进制表示方式),
而攻击报文中不包含结束符,并且攻击者会持续发送不包含结束符的 HTTP 头部报文,
维持连接状态,消耗目标服务器的资源。