欢迎光临
我们一直在努力

第1章:requests术语全景与HTTP协议原理

1. 项目背景

业务场景

某电商公司新入职了 3 名开发工程师,他们被分配到订单系统的对接组,需要调用支付网关、物流追踪、用户中心等十余个第三方 REST API。组长在第一天就抛出了任务:用 Python 写一个通用的 HTTP 客户端封装,对接各业务方的接口。 然而问题来了——当团队开始动手时,发现每个人对 HTTP 的理解程度参差不齐。有人用 os.system("curl …") 拼字符串,有人直接上 socket 编程手写 HTTP 报文,更有人不知道 HTTP 和 HTTPS 的区别,直接把 API Key 写在 URL 参数里明文传输。代码 review 时,资深工程师看得心惊肉跳。

痛点

问题一:术语混乱。 HTTP、HTTPS、URL、URI、Cookie、Session、代理——团队成员对同一概念的理解各不相同。有人说"发个 POST 请求"实际上发的是 GET 带 body;有人说"用 Session 保持登录"实际上理解成了服务端的 Session 机制而非客户端的 Cookie 持久化。

问题二:协议认知缺失。 大部分开发知道 HTTP 是"请求-响应"模型,但不清楚一次完整的 HTTPS 请求经历了 DNS 解析 → TCP 三次握手 → TLS 握手 → HTTP 请求 → HTTP 响应 → 四次挥手这一完整链路。当遇到线上"请求慢了 2 秒"时,无法定位是 DNS、TCP、TLS 还是服务端处理的问题。

问题三:requests 黑盒化使用。 很多开发者会 requests.get(url) 就算通关,但不知道底层经历了什么——Session 如何管理连接?Adapter 如何桥接 urllib3?urllib3 又如何调用标准库?这种黑盒用法导致在遇到超时、SSL 错误、连接池耗尽等生产问题时束手无策。

痛点可视化流程:

开发者视角:requests.get(url) → ❓魔法❓ → Response
实际情况:
DNS解析 → TCP握手 → TLS协商 → 发送HTTP报文 → 等待服务器处理 → 接收HTTP响应 → 解析响应体
↑ 可能慢 ↑ ↑ 可能失败 ↑ ↑ 证书过期 ↑ ↑超时↑ ↑连接断开↑


2. 项目设计

小胖(刚啃完一块披萨,满嘴油光): “大师,我就不明白了。不就是调个接口嘛,一个 requests.get() 不就搞定了?我上家公司的代码全是这么写的,从来没出过问题。为啥还要学什么 HTTP 协议、请求头、Cookie 这一大堆?这不就跟食堂打饭一样——上去说你要啥,大妈给你打,完事儿!”

大师(放下手中的咖啡,微微一笑): “小胖你这个比喻很形象。不过我问你一个问题——如果食堂有 10 个窗口,你怎么知道去哪个窗口?如果要刷卡付费,你的饭卡信息怎么传递给收银台?如果今天你感冒了,大妈怎么知道少给你打辣椒?”

小胖(挠了挠头): “呃……那我得先看窗口上写着卖什么。刷卡的话,把卡给大妈刷一下?”

大师: “你看,这不就对上了——窗口编号就是 URL 路径,刷卡就是认证(Authorization),告诉大妈不要辣椒就是请求头(Header)。HTTP 协议本质上就是客户端和服务器之间的一套约定——你告诉我你想要什么(Request),我告诉你我有没有(Response),中间用一套大家都懂的格式来传话。”

小白(从笔记本屏幕后探出头来,推了推眼镜): “那 requests.get() 到底帮我们做了什么?它不可能真的一行代码就搞定一切吧?我总觉得这背后有好多层抽象。”

大师: “好问题。让我们把 requests 的架构画出来——”

大师在白板上画出架构图:

┌─────────────────────────────────────────────────────┐
│ 【用户层】 │
│ requests.get() / post() / Session │
│ Request → PreparedRequest → Response │
├─────────────────────────────────────────────────────┤
│ 【适配层】 │
│ HTTPAdapter.send() │
│ 职责:代理解析、SSL配置、连接池选择、重试策略 │
├─────────────────────────────────────────────────────┤
│ 【传输层:urllib3】 │
│ PoolManager / HTTPConnectionPool │
│ 职责:连接复用、Keep-Alive、超时控制 │
├─────────────────────────────────────────────────────┤
│ 【标准库层】 │
│ http.client.HTTPConnection / ssl.SSLContext │
│ 职责:TCP连接、TLS握手、HTTP报文解析 │
├─────────────────────────────────────────────────────┤
│ 【操作系统层】 │
│ socket / TCP / DNS / TLS │
└─────────────────────────────────────────────────────┘

大师: “所以你看,requests.get(url) 这一行代码,至少跨越了四层抽象。每一层都有自己可能出问题的地方。”

小白(眼睛一亮): “那如果一个请求失败了,我怎么知道是请求构建的问题、连接层的问题,还是服务器的问题?如果每个层面都会抛出不同的异常,异常处理岂不是要写很多层?”

大师: "这就是为什么我们要理解 HTTP 协议的全链路。我给你一个故障定位的决策树:

  • 如果 DNS 解析失败 → 检查网线插没插、DNS 配置对不对
  • 如果 TCP 连接超时 → 检查防火墙规则、目标端口是否开放
  • 如果 TLS 握手失败 → 检查证书是否过期、CA 是否受信任
  • 如果 HTTP 请求发出但无响应 → 检查服务端是否 Hang、负载是否过高
  • 如果 HTTP 响应返回 4xx/5xx → 检查参数是否正确、服务端日志"
  • 小胖(若有所思): “等等,大师。你说 Session 可以保持 Cookie——那我是不是可以理解为,Session 就像我在食堂办了一张饭卡,上面有我的余额和消费记录。每次刷卡不用自报家门,刷卡机自己就知道我是谁了?”

    大师(赞许地点头): “正是!你这个比喻非常精准。”

    生活比喻技术映射
    食堂窗口编号 URL 路径(Path)
    饭卡余额与身份 Cookie / Session
    刷卡动作 每次请求自动携带 Cookie
    办卡充值的柜台 登录接口,返回 Set-Cookie
    这张卡只能在食堂用 Cookie 的 Domain 限制

    小白: “那最后一个问题。我听说 HTTP 是无状态的,但 Cookie 又让它看起来有状态?这不是矛盾吗?”

    大师: “这恰恰是 HTTP 协议设计的精髓——协议层面无状态,应用层面有状态。HTTP 每一次请求在协议层面都是独立的,服务器不记得上一秒谁来请求过。但通过在请求头里塞 Cookie/Token,我们在应用层实现了状态保持。就像你去菜市场买菜,卖菜大妈不认识你,但你掏出会员卡,系统就认出你和你上次的消费记录——菜市场本身是无状态的,会员系统是有状态的。”


    3. 项目实战

    环境准备

    依赖与版本:

    组件版本用途
    Python 3.9+ 运行环境
    requests 2.32.4 HTTP 客户端库
    urllib3 2.2.3 requests 底层传输依赖
    Wireshark 4.x 抓包分析 HTTP 报文

    安装命令:

    # 创建虚拟环境
    python -m venv venv
    # 激活虚拟环境 (Windows)
    venv\\Scripts\\activate
    # 激活虚拟环境 (Mac/Linux)
    source venv/bin/activate

    # 安装 requests
    pip install requests==2.32.4

    # 验证安装
    python -c "import requests; print(requests.__version__)"
    # 输出: 2.32.4

    分步实现

    步骤一:解剖一个最简单的 HTTP GET 请求

    目标:发起一个 GET 请求到 httpbin.org,理解 Response 对象的基本属性。

    import requests

    # 发送 GET 请求
    response = requests.get("https://httpbin.org/get")

    # Response 对象就是整个 HTTP 响应的 Python 表示
    print(f"状态码: {response.status_code}") # 200
    print(f"响应头: {dict(response.headers)}") # 所有响应头字典
    print(f"编码: {response.encoding}") # utf-8
    print(f"耗时: {response.elapsed}") # 0:00:00.523456
    print(f"请求URL: {response.url}") # https://httpbin.org/get
    print(f"响应体(前200字符): {response.text[:200]}")

    # 关键属性速查表
    # response.status_code → HTTP 状态码 (200, 404, 500…)
    # response.headers → 响应头 (CaseInsensitiveDict)
    # response.encoding → 自动检测的编码
    # response.text → 解码后的文本响应体
    # response.content → 原始二进制响应体
    # response.json() → JSON 解析后的 dict
    # response.url → 最终请求的 URL (可能经过了重定向)
    # response.elapsed → 从发送到接收的耗时 (timedelta)
    # response.request → 对应的 PreparedRequest 对象

    运行输出:

    状态码: 200
    响应头: {'Date': 'Sun, …', 'Content-Type': 'application/json', …}
    编码: utf-8
    耗时: 0:00:00.523456
    请求URL: https://httpbin.org/get
    响应体(前200字符): {
    "args": {},
    "headers": {
    "Accept": "*/*",
    "Accept-Encoding": "gzip, deflate",
    "Host": "httpbin.org",
    "User-Agent": "python-requests/2.32.4",

    }
    }

    步骤二:用 Wireshark 抓取一次完整的 HTTPS 请求

    目标:验证一次 requests.get() 背后的完整网络交互过程。

    抓包步骤:

  • 打开 Wireshark,选择网卡 → 过滤条件 ssl.handshake.extensions_server_name contains "httpbin.org" or http
  • 在 Python 中执行 requests.get("https://httpbin.org/get")
  • 停止抓包,分析捕获到的数据包
  • 抓包分析——看到的 TCP 交互序列:

    [1] 客户端 → 服务器: TCP SYN (三次握手开始)
    [2] 服务器 → 客户端: TCP SYN-ACK
    [3] 客户端 → 服务器: TCP ACK (TCP 连接建立)
    ↑ 以上:TCP 三次握手,耗时 ~50ms

    [4] 客户端 → 服务器: TLS ClientHello
    – 支持的 TLS 版本: 1.2, 1.3
    – 支持的加密套件: TLS_AES_256_GCM_SHA384…
    – SNI: httpbin.org
    [5] 服务器 → 客户端: TLS ServerHello, Certificate, ServerHelloDone
    – 选定 TLS 1.3
    – 服务器证书链
    [6] 客户端 → 服务器: TLS ClientKeyExchange, ChangeCipherSpec, Finished
    [7] 服务器 → 客户端: TLS ChangeCipherSpec, Finished
    ↑ 以上:TLS 握手,耗时 ~150ms

    [8] 客户端 → 服务器: HTTP GET /get HTTP/1.1
    Host: httpbin.org
    User-Agent: python-requests/2.32.4
    Accept: */*
    Accept-Encoding: gzip, deflate
    [9] 服务器 → 客户端: HTTP/1.1 200 OK
    Content-Type: application/json
    …(响应体)
    ↑ 以上:HTTP 请求-响应,耗时 ~300ms

    [10-12] TCP FIN/ACK (四次挥手)

    映射表——抓包结果 vs requests API:

    Wireshark 中看到的requests API 对应
    DNS A 记录查询 无需代码,操作系统自动完成
    TCP SYN → SYN-ACK → ACK 无需代码,urllib3 自动完成
    TLS ClientHello verify=True 触发证书验证
    TLS Certificate response.cert 获取服务器证书信息
    HTTP GET /get requests.get("https://httpbin.org/get")
    HTTP/1.1 200 OK response.status_code == 200
    Content-Type: application/json response.headers["Content-Type"]
    步骤三:打印完整的请求响应信息(含隐式头)

    目标:编写一个诊断函数,输出一次请求的完整信息,包括 requests 隐式添加的头。

    import requests

    def inspect_request(url: str, method: str = "GET", **kwargs):
    """打印请求和响应的完整诊断信息"""
    session = requests.Session()
    print("=" * 60)
    print(f"REQUEST: {method} {url}")
    print("=" * 60)

    # 构建 PreparedRequest 查看发送前的状态
    req = requests.Request(method, url, **kwargs)
    prepared = session.prepare_request(req)

    print(f"\\n[发送前的 PreparedRequest]")
    print(f" Method: {prepared.method}")
    print(f" URL: {prepared.url}")
    print(f" Headers:")
    for k, v in prepared.headers.items():
    print(f" {k}: {v}")
    print(f" Body: {prepared.body}")

    # 发送请求
    response = session.send(prepared)

    print(f"\\n[响应 Response]")
    print(f" Status: {response.status_code} {response.reason}")
    print(f" Elapsed: {response.elapsed}")
    print(f" Final URL: {response.url}")
    print(f" Redirects: {len(response.history)}")
    if response.history:
    for i, r in enumerate(response.history):
    print(f" [{i}] {r.status_code}{r.headers.get('Location', 'N/A')}")

    print(f" Response Headers:")
    for k, v in response.headers.items():
    print(f" {k}: {v}")

    print(f" Content ({len(response.content)} bytes): {response.text[:300]}…")
    print("=" * 60)

    return response

    # 运行诊断
    inspect_request("https://httpbin.org/get", params={"name": "小明", "page": 1})

    运行输出(关键部分):

    [发送前的 PreparedRequest]
    Method: GET
    URL: https://httpbin.org/get?name=%E5%B0%8F%E6%98%8E&page=1
    Headers:
    User-Agent: python-requests/2.32.4 ← 隐式添加
    Accept-Encoding: gzip, deflate ← 隐式添加
    Accept: */* ← 隐式添加
    Connection: keep-alive ← 隐式添加
    Host: httpbin.org ← 隐式添加
    Body: None

    [响应 Response]
    Status: 200 OK
    Elapsed: 0:00:00.623451
    Final URL: https://httpbin.org/get?name=%E5%B0%8F%E6%98%8E&page=1
    Redirects: 0

    可能遇到的坑及解决方法

    坑1:SSL 证书验证失败

    SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed

    原因:在 Windows 上 Python 可能找不到系统 CA 证书包。

    解决:

    # 方案1:安装 certifi 包并指定
    pip install certifi

    import certifi
    requests.get("https://example.com", verify=certifi.where())

    # 方案2:安装 python-certifi-win32 (Windows 专享)
    pip install pip-system-certs

    ⚠️ 警告:永远不要在生产代码中使用 verify=False,这会让你的应用暴露于中间人攻击。

    坑2:代理干扰

    ProxyError: Cannot connect to proxy.

    原因:系统环境变量中设置了 HTTP_PROXY,但代理不可用。

    解决:

    # 禁用系统代理
    requests.get("https://example.com", proxies={"http": None, "https": None})
    # 或配置正确的代理
    requests.get("https://example.com", proxies={"https": "http://127.0.0.1:7890"})

    测试验证

    编写单元测试验证对 requests 基本功能的理解:

    import pytest
    import requests

    class TestRequestsBasics:
    """验证第1章所学的 requests 基本概念"""

    def test_get_request_returns_200(self):
    """验证 GET 请求能正常返回 200"""
    resp = requests.get("https://httpbin.org/get")
    assert resp.status_code == 200

    def test_response_has_expected_attributes(self):
    """验证 Response 对象包含所有预期属性"""
    resp = requests.get("https://httpbin.org/get")
    assert hasattr(resp, "status_code")
    assert hasattr(resp, "headers")
    assert hasattr(resp, "text")
    assert hasattr(resp, "content")
    assert hasattr(resp, "request")
    assert hasattr(resp, "url")
    assert hasattr(resp, "elapsed")
    assert hasattr(resp, "encoding")

    def test_prepared_request_url_encoding(self):
    """验证 requests 能正确处理 URL 参数编码"""
    resp = requests.get("https://httpbin.org/get", params={"q": "小胖"})
    assert "小胖" not in resp.url
    assert "%E5%B0%8F%E8%83%96" in resp.url # UTF-8 percent-encoded

    def test_requests_adds_implicit_headers(self):
    """验证 requests 会自动添加 Host/User-Agent 等头"""
    session = requests.Session()
    req = requests.Request("GET", "https://httpbin.org/get")
    prepared = session.prepare_request(req)
    assert "Host" in prepared.headers
    assert "User-Agent" in prepared.headers

    运行测试:

    pytest test_chapter1.py -v


    4. 项目总结

    核心知识回顾

    层次关键技术requests 对应
    DNS 域名 → IP 操作系统自动完成
    TCP 三次握手/四次挥手 urllib3 连接管理
    TLS 加密协商/证书验证 verify/cert 参数
    HTTP 请求报文/响应报文 Request/Response 模型
    Session Cookie 自动管理 requests.Session
    Adapter 传输层抽象 HTTPAdapter

    优点 & 缺点

    对比维度requestsurllib(标准库)http.client(标准库)
    API 易用性 ★★★★★ ★★☆☆☆ ★☆☆☆☆
    Cookie 自动管理 ★★★★★ ☆☆☆☆☆ ☆☆☆☆☆
    Session 连接池 ★★★★☆ ☆☆☆☆☆ ☆☆☆☆☆
    SSL 证书处理 ★★★★☆ ★★☆☆☆ ★★☆☆☆
    异步支持 ☆☆☆☆☆ ★★★★☆ ☆☆☆☆☆
    学习曲线 ★★★★★ ★★★☆☆ ★★☆☆☆

    适用场景

    • API 数据采集:调用 RESTful 接口、Web Scraping
    • 微服务间通信:同步 HTTP 调用的首选方案
    • 自动化测试:接口测试、端到端测试
    • 运维监控:健康检查、指标拉取
    • 快速原型:验证第三方 API、PoC 开发

    不适用场景

    • 高并发异步场景:需要 asyncio 配合的推荐 aiohttp/httpx
    • WebSocket 长连接:requests 不支持 WebSocket 协议,需 websocket-client

    注意事项

    • 超时设置:requests 默认 timeout=None(永不超时),生产环境务必显式设置
    • Session 复用:不要每次请求都创建新 Session,否则连接池功能无效
    • verify=False 危险:仅在测试环境使用,生产环境绝对禁用
    • 版本兼容:requests 2.x 与 urllib3 1.x/2.x 的 API 有差异,注意版本锁定

    常见踩坑经验

    案例一:生产环境请求卡死。某次故障排查发现某个微服务在高峰期 CPU 飙升到 100%,最终定位到代码中 requests.get(url) 没有设置 timeout,后端 Hang 住后客户端连接永远不释放,导致线程池耗尽。根因:默认 timeout=None。修复:所有请求强制配置 timeout=(3.0, 30.0)。

    案例二:SSL 验证失败让 CI 全红。CI 流水线突然全部失败,报 SSL 证书错误。排查后发现 LetsEncrypt 根证书过期,CI 镜像未更新 CA 包。根因:镜像过旧。修复:Dockerfile 中主动 apt-get update && apt-get install ca-certificates。

    案例三:Cookie 丢失导致鉴权反复失败。开发反馈登录成功后的数据查询却报 401。排查发现每次请求用的是 requests.get() 而非 Session 对象,Cookie 无法跨请求保持。根因:滥用无状态 API。修复:登录 + 后续请求统一使用同一个 requests.Session 实例。

    思考题

  • 架构理解题:如果不依赖 requests,只使用 Python 标准库(socket + ssl),请写出一个最简单的 HTTP GET 请求客户端。你需要手动构造 HTTP 报文、解析响应。对比使用 requests 的代码量,理解 requests 帮你节省了多少工作。

  • 协议思考题:HTTP/1.1 中 Connection: keep-alive 的作用是什么?如果服务器返回 Connection: close,requests 如何处理?查阅 HTTPAdapter 源码,找出连接复用的判断条件(提示:src/requests/adapters.py 第 300 行附近)。

  • 延伸阅读与资源

    后端工程师的 AI 转型第一课:Ollama 与私有化大模型实战 10倍开发者的 Dify 魔法书:从零构建全栈 AI 应用 后端工程师转型AI第一课-Ollama 与私有化大模型实战

    大型语言模型(LLM) vLLM 高性能推理落地实战

    Agent开发之LlamaIndex 实战修炼与源码进阶

    大语言模型Transformers 实战修炼与源码剖析

    赞(0)
    未经允许不得转载:171主机测评 » 第1章:requests术语全景与HTTP协议原理
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址