【问题标题】:How to handle chunked encoding in Python BaseHTTPRequestHandler?如何在 Python BaseHTTPRequestHandler 中处理分块编码?
【发布时间】:2020-07-08 17:31:09
【问题描述】:

我有以下简单的网络服务器,利用 Python 的 http 模块:

import http.server
import hashlib


class RequestHandler(http.server.BaseHTTPRequestHandler):
    protocol_version = "HTTP/1.1"

    def do_PUT(self):
        md5 = hashlib.md5()

        remaining = int(self.headers['Content-Length'])
        while True:
            data = self.rfile.read(min(remaining, 16384))
            remaining -= len(data)
            if not data or not remaining:
                break
            md5.update(data)
        print(md5.hexdigest())

        self.send_response(204)
        self.send_header('Connection', 'keep-alive')
        self.end_headers()


server = http.server.HTTPServer(('', 8000), RequestHandler)
server.serve_forever()

当我使用 curl 上传文件时,效果很好:

curl -vT /tmp/test http://localhost:8000/test

因为文件大小是预先知道的,curl 将发送一个Content-Length: 5 标头,所以我可以知道我应该从套接字读取多少。

但是如果文件大小未知,或者客户端决定使用chunkedTransfer-Encoding,这种方式就失败了。

可以通过以下命令进行模拟:

curl -vT /tmp/test -H "Transfer-Encoding: chunked" http://localhost:8000/test

如果我从块的self.rfile 过去读取,它将永远等待并挂起客户端,直到它中断 TCP 连接,其中self.rfile.read 将返回一个空数据,然后它会跳出循环。

需要什么来扩展上述示例以支持chunked Transfer-Encoding?

【问题讨论】:

    标签: python http chunked


    【解决方案1】:

    正如您在Transfer-Encoding 的描述中看到的那样,分块传输将具有以下形状:

    chunk1_length\r\n
    chunk1 (binary data)
    \r\n
    chunk2_length\r\n
    chunk2 (binary data)
    \r\n
    0\r\n
    \r\n
    

    您只需要读取一行,获取下一个块的大小,并同时使用二进制块后续换行符。

    此示例将能够处理带有Content-LengthTransfer-Encoding: chunked 标头的请求。

    from http.server import HTTPServer, SimpleHTTPRequestHandler
    
    PORT = 8080
    
    class TestHTTPRequestHandler(SimpleHTTPRequestHandler):
        def do_PUT(self):
            self.send_response(200)
            self.end_headers()
    
            path = self.translate_path(self.path)
    
            if "Content-Length" in self.headers:
                content_length = int(self.headers["Content-Length"])
                body = self.rfile.read(content_length)
                with open(path, "wb") as out_file:
                    out_file.write(body)
            elif "chunked" in self.headers.get("Transfer-Encoding", ""):
                with open(path, "wb") as out_file:
                    while True:
                        line = self.rfile.readline().strip()
                        chunk_length = int(line, 16)
    
                        if chunk_length != 0:
                            chunk = self.rfile.read(chunk_length)
                            out_file.write(chunk)
    
                        # Each chunk is followed by an additional empty newline
                        # that we have to consume.
                        self.rfile.readline()
    
                        # Finally, a chunk size of 0 is an end indication
                        if chunk_length == 0:
                            break
    
    httpd = HTTPServer(("", PORT), TestHTTPRequestHandler)
    
    print("Serving at port:", httpd.server_port)
    httpd.serve_forever()
    

    注意我选择从 SimpleHTTPRequestHandler 继承而不是 BaseHTTPRequestHandler,因为这样可以使用方法 SimpleHTTPRequestHandler.translate_path() 允许客户端选择目标路径(这可能很有用与否,取决于用例;我的示例已经编写为使用它)。

    您可以使用 curl 命令测试这两种操作模式,正如您所提到的:

    # PUT with "Content-Length":
    curl --upload-file "file.txt" \
      "http://127.0.0.1:8080/uploaded.txt"
    
    # PUT with "Transfer-Encoding: chunked":
    curl --upload-file "file.txt" -H "Transfer-Encoding: chunked" \
      "http://127.0.0.1:8080/uploaded.txt"
    

    【讨论】:

    • 您的块处理中有一个小错误。请在chunk_length == 0 检查(循环中断之前的那个)之后添加self.rfile.readline(),因为在线路上仍有'\r\n' 字节来结束块流。如果有人(比如我)想要持久连接,下次框架调用handle_one_request 时,它会读取线路上剩余的两个字节,认为有问题,然后关闭连接。不过感谢您的代码,它让我朝着正确的方向前进。
    • 不错的收获!我已经修改了代码,无论大小如何,始终使用尾随换行符。我认为这种方式更清楚 0 的大小实际上是“传输结束”指示,而读取器的逻辑对于所有块仍然保持相同。
    猜你喜欢
    • 2011-03-18
    • 1970-01-01
    • 1970-01-01
    • 2012-08-18
    • 2011-05-13
    • 1970-01-01
    • 2018-07-14
    相关资源
    最近更新 更多