Content-Encoding: gzip + Transfer-Encoding: chunked与gzip/zlib导致头部检查错误
如何处理使用gzip编码的分块数据?我有一个服务器,它以以下方式发送数据:
HTTP/1.1 200 OK\r\n
...
Transfer-Encoding: chunked\r\n
Content-Encoding: gzip\r\n
\r\n
1f50\r\n\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03\xec}\xebr\xdb\xb8\xd2\xe0\xef\xb8\xea\xbc\x03\xa2\xcc\x17\xd9\xc7\xba\xfa\x1e\xc9r*\x93\xcbL\xf6\xcc\x9c\xcc7\xf1\x9c\xf9\xb6r\xb2.H ... L\x9aFs\xe7d\xe3\xff\x01\x00\x00\xff\xff\x03\x00H\x9c\xf6\xe93\x00\x01\x00\r\n0\r\n\r\n
我尝试过几种不同的方法,但总觉得有些东西忘记了。
data = b''
depleted = False
while not depleted:
depleted = True
for fd, event in poller.poll(2.0):
depleted = False
if event == select.EPOLLIN:
tmp = sock.recv(8192)
data += zlib.decompress(tmp, 15 + 32)
尝试解码数据时(当然是从\r\n\r\n
之后开始):
zlib.error: Error -3 while decompressing data: incorrect header check
所以我想,数据应该在完整接收后再进行解压。
...
if event == select.EPOLLIN:
data += sock.recv(8192)
data = zlib.decompress(data.split(b'\r\n\r\n',1)[1], 15 + 32)
但还是出现同样的错误。我还尝试过解压data[:-7]
,因为数据最后有一个块ID,还有data[2:-7]
和其他各种组合,但结果还是一样的错误。
我也尝试过使用gzip
模块:
with gzip.GzipFile(fileobj=Bytes(data), 'rb') as fh:
fh.read()
但它告诉我“不是一个gzipped文件”。
即使我把服务器接收到的数据(包括头部和数据)记录到一个文件中,然后在80端口创建一个服务器套接字,把数据(保持原样)提供给浏览器,浏览器也能正常显示,所以数据是完整的。我把这些数据的头部去掉(只去掉头部),然后尝试在这个文件上使用gzip:

感谢@mark-adler,我写出了以下代码来处理分块数据:
unchunked = b''
pos = 0
while pos <= len(data):
chunkLen = int(binascii.hexlify(data[pos:pos+2]), 16)
unchunked += data[pos+2:pos+2+chunkLen]
pos += 2+len('\r\n')+chunkLen
with gzip.GzipFile(fileobj=BytesIO(data[:-7])) as fh:
data = fh.read()
这段代码产生了OSError: CRC check failed 0x70a18ee9 != 0x5666e236
,这算是更进一步。简单来说,我根据这四个部分来剪切数据:
<chunk length o' X bytes>
\r\n
<chunk>
\r\n
我可能快到了,但还不够近。
附注: 是的,套接字的实现远非最佳,但看起来是这样的,因为我觉得没有从套接字获取到所有数据,所以我设置了一个很长的超时,并尝试用depleted
来做一个安全措施 :)
2 个回答
@mark-adler 给了我一些关于 HTML 协议中分块 模式 的好建议,除此之外,我还尝试了不同的方法来 解压 数据。
- 你需要把这些小块拼接成一个大块
- 你应该使用
gzip
而不是zlib
- 你只能对拼接好的大块进行 解压,分开解压是行不通的
以上三个 问题 的解决方案如下:
unchunked = b''
pos = 0
while pos <= len(data):
chunkNumLen = data.find(b'\r\n', pos)-pos
# print('Chunk length found between:',(pos, pos+chunkNumLen))
chunkLen=int(data[pos:pos+chunkNumLen], 16)
# print('This is the chunk length:', chunkLen)
if chunkLen == 0:
# print('The length was 0, we have reached the end of all chunks')
break
chunk = data[pos+chunkNumLen+len('\r\n'):pos+chunkNumLen+len('\r\n')+chunkLen]
# print('This is the chunk (Skipping',pos+chunkNumLen+len('\r\n'),', grabing',len(chunk),'bytes):', [data[pos+chunkNumLen+len('\r\n'):pos+chunkNumLen+len('\r\n')+chunkLen]],'...',[data[pos+chunkNumLen+len('\r\n')+chunkLen:pos+chunkNumLen+len('\r\n')+chunkLen+4]])
unchunked += chunk
pos += chunkNumLen+len('\r\n')+chunkLen+len('\r\n')
with gzip.GzipFile(fileobj=BytesIO(unchunked)) as fh:
unzipped = fh.read()
return unzipped
我把调试输出留在里面,但注释掉是有原因的。
虽然看起来有点乱,但这对我来说非常有用,因为它能帮助我了解我到底在尝试解压哪些数据,哪些部分是从哪里获取的,以及每个计算带来了什么值。
这段代码会处理分块数据,格式如下:
<chunk length o' X bytes>
\r\n
<chunk>
\r\n
在提取 X bytes
时需要小心,因为它们是以 1f50
的形式出现的,我首先需要用 binascii.hexlify(data[0:4])
转换一下,然后再放入 int()
。不太明白为什么现在不需要这样做了,因为之前我需要这样才能得到大约 8000 的长度,但后来突然给了我一个非常大的数字,这不太合理,尽管我没有给它其他数据……无论如何。之后,只需确保数字正确,然后把所有小块合并成一大堆 gzip
数据,最后输入到 .GzipFile(...)
中。
三年后编辑:
我知道这最开始是一个客户端的问题,但这里有一个 服务器端 的函数,可以发送一个相对可用的测试:
def http_gzip(data):
compressed = gzip.compress(data)
# format(49, 'x') returns `31` which is `\x31` but without the `\x` notation.
# basically the same as `hex(49)` but ment for these kind of things.
return bytes(format(len(compressed), 'x')),'UTF-8') + b'\r\n' + compressed + b'\r\n0\r\n\r\n'
你不能直接用 \r\n
来分割数据,因为压缩后的数据可能会包含这个序列,而且如果数据足够长,肯定会有这个序列。你需要先进行“去块处理”,也就是根据提供的长度(比如第一个长度 1f50
)来处理数据,然后把得到的块传给解压缩程序。压缩数据的开头是 \x1f\x8b
。
这个“块”的格式是:十六进制数字,换行符,接着是指定字节数的块,换行符,再来一个十六进制数字,换行符,再来一个块,换行符,依此类推,最后一个块是零长度的,可能还有一些头信息,最后是换行符。