4. HTTP客户端

任务说明

本案例使用Python原生socket实现一个HTTP/1.1客户端,向指定服务器发送GET请求,并解析响应中的状态行、响应头与正文内容。通过该案例理解HTTP协议在TCP层之上的报文格式与解析方法。

核心实现

import socket

def http_get(host, path="/", port=80, timeout=5):
    """发送HTTP GET请求并解析响应,返回(状态行, 响应头dict, 正文bytes)"""
    # 1. 建立TCP连接
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.settimeout(timeout)
    sock.connect((host, port))

    # 2. 构造并发送HTTP GET请求
    request = (
        f"GET {path} HTTP/1.1\r\n"
        f"Host: {host}\r\n"
        "Connection: close\r\n"      # 响应结束后关闭连接
        "User-Agent: socket-demo/1.0\r\n"
        "\r\n"
    )
    sock.sendall(request.encode("utf-8"))

    # 3. 接收全部响应数据
    response = b""
    while True:
        chunk = sock.recv(4096)
        if not chunk:
            break
        response += chunk
    sock.close()

    # 4. 解析响应:分割头部与正文
    header_end = response.find(b"\r\n\r\n")
    if header_end == -1:
        raise ValueError("无效的HTTP响应:未找到头部结束标记")

    header_data = response[:header_end].decode("utf-8", errors="replace")
    body = response[header_end + 4:]  # 跳过\r\n\r\n

    # 5. 解析状态行与响应头
    lines = header_data.split("\r\n")
    status_line = lines[0]
    headers = {}
    for line in lines[1:]:
        if ":" in line:
            key, value = line.split(":", 1)
            headers[key.strip().lower()] = value.strip()

    return status_line, headers, body

if __name__ == "__main__":
    # 示例:请求httpbin.org的/get接口(返回请求方IP等JSON信息)
    status, headers, body = http_get("httpbin.org", "/get")
    print("状态行:", status)
    print("Content-Type:", headers.get("content-type"))
    print("正文前200字节:", body[:200].decode("utf-8", errors="replace"))

运行与验证

运行方式:直接执行脚本(需联网):

python http_client.py

验证方法:程序输出应包含:

  • 状态行以 HTTP/1.1 200 OK 开头;
  • Content-Type 为 application/json;
  • 正文为JSON格式,包含 "url": "http://httpbin.org/get" 字段。

若网络不可用,可改为请求本地服务验证:在另一终端运行 python -m http.server 8000,然后将调用改为 http_get("127.0.0.1", "/", 8000),预期状态行为 HTTP/1.0 200 OK,正文为目录文件列表HTML。

关键说明

  • 使用 Connection: close 简化接收逻辑——服务器发送完响应后主动断开,客户端据此判断接收完毕;
  • 响应头解析统一转为小写键名,便于无差别访问;
  • 正文按原始字节返回,由调用方根据 Content-Type 决定解码方式,避免二进制内容被错误解码。