2. HTTP报文格式解析与构造

任务目标

编写一个HTTP报文解析与构造工具,能够:

  1. 解析完整的HTTP/1.1请求报文,提取方法、URL、头部字段和消息主体;
  2. 根据给定参数构造合法的HTTP请求报文。

实现方案

使用Python标准库实现,不依赖第三方包。核心包含两个函数:parse_http_request() 负责解析,build_http_request() 负责构造。解析采用逐行扫描方式,正确处理请求行、头部区与主体之间的空行分隔。

完整代码

def parse_http_request(raw: str) -> dict:
    """解析HTTP/1.1请求报文,返回结构化字典"""
    # 分割请求行与后续部分
    lines = raw.split("\r\n")
    if len(lines) < 1 or not lines[0]:
        raise ValueError("空报文")

    # 解析请求行: METHOD SP URL SP VERSION
    request_line = lines[0].split(" ")
    if len(request_line) != 3:
        raise ValueError(f"非法请求行: {lines[0]}")
    method, url, version = request_line

    # 解析头部与主体
    headers = {}
    body = ""
    body_started = False

    idx = 1
    while idx < len(lines):
        line = lines[idx]
        if body_started:
            # 主体部分(可能包含空行,直接拼接)
            body += line + ("\r\n" if idx < len(lines) - 1 else "")
        elif line == "":
            # 空行:头部结束,后续为主体
            body_started = True
        else:
            # 头部字段: Name: Value
            if ":" not in line:
                raise ValueError(f"非法头部行: {line}")
            name, value = line.split(":", 1)
            headers[name.strip()] = value.strip()
        idx += 1

    return {
        "method": method,
        "url": url,
        "version": version,
        "headers": headers,
        "body": body
    }

def build_http_request(method: str, url: str,
                       headers: dict = None,
                       body: str = "") -> str:
    """构造HTTP/1.1请求报文"""
    if not method or not url:
        raise ValueError("method和url不能为空")

    # 请求行
    lines = [f"{method} {url} HTTP/1.1"]

    # 头部
    headers = headers or {}
    # 自动补充必要头部
    if "Host" not in headers:
        headers["Host"] = "localhost"
    if body and "Content-Length" not in headers:
        headers["Content-Length"] = str(len(body.encode("utf-8")))

    for name, value in headers.items():
        lines.append(f"{name}: {value}")

    # 空行分隔头部与主体
    lines.append("")

    # 主体
    if body:
        lines.append(body)

    return "\r\n".join(lines)

# ---------- 验证示例 ----------
if __name__ == "__main__":
    # 构造一个POST请求
    raw_request = build_http_request(
        method="POST",
        url="/api/login",
        headers={
            "Host": "example.com",
            "Content-Type": "application/json",
            "User-Agent": "test-agent"
        },
        body='{"username":"admin","password":"123456"}'
    )

    print("=== 构造的报文 ===")
    print(raw_request)
    print()

    # 解析该报文
    parsed = parse_http_request(raw_request)

    print("=== 解析结果 ===")
    print(f"方法: {parsed['method']}")
    print(f"URL: {parsed['url']}")
    print(f"版本: {parsed['version']}")
    print(f"头部数量: {len(parsed['headers'])}")
    for k, v in parsed['headers'].items():
        print(f"  {k}: {v}")
    print(f"主体: {parsed['body']}")

    # 断言验证
    assert parsed["method"] == "POST"
    assert parsed["url"] == "/api/login"
    assert parsed["headers"]["Content-Type"] == "application/json"
    assert parsed["body"] == '{"username":"admin","password":"123456"}'
    print("\n✅ 所有断言通过:解析结果与构造参数一致")

运行与验证

  1. 将代码保存为 http_message.py;
  2. 在终端执行 python3 http_message.py;
  3. 观察输出:程序先打印构造的完整报文,再打印解析出的各字段;
  4. 通过条件:终端显示“✅ 所有断言通过”,且解析出的方法、URL、头部值与主体内容与构造时传入的参数完全一致。

关键说明

  • 解析器严格按 \r\n 作为行分隔符,符合HTTP/1.1规范;
  • 头部名称统一去除首尾空格后作为字典键,值同样去除首尾空格;
  • 构造时若未提供 Host 头则自动补 localhost,若存在主体且未指定 Content-Length 则按UTF-8字节数自动计算;
  • 本实现聚焦请求报文,响应报文格式类似(状态行+头部+主体),可复用头部与主体处理逻辑。