先说结论
不是 Session 有魔法,是它复用了 TCP 连接。
| 方式 | 100次请求耗时 | 原理 |
|---|---|---|
requests.get() |
~8.5s | 每次都新建 TCP 连接 |
requests.Session() |
~0.8s | 复用 TCP 连接(Keep-Alive) |
快了 10 倍,不是夸张,是实测。
一、先看现象:一段代码说明一切
python
import requests
import time
url = "https://httpbin.org/get"
# 方式一:直接请求(不用 Session)
start = time.time()
for _ in range(100):
requests.get(url)
print(f"直接请求: {time.time() - start:.2f}s") # ~8.5s
# 方式二:使用 Session
start = time.time()
with requests.Session() as s:
for _ in range(100):
s.get(url)
print(f"Session 请求: {time.time() - start:.2f}s") # ~0.8s
同一个 URL,同样 100 次,Session 快了一个数量级。
为什么?
二、根本原因:TCP 连接的代价
HTTP 请求的底层是 TCP。每次 requests.get() 默认都在做这些事:
直接请求(100次)的流程:
第1次: DNS解析 → TCP三次握手 → SSL握手 → 发送请求 → 收响应 → 断开连接
第2次: DNS解析 → TCP三次握手 → SSL握手 → 发送请求 → 收响应 → 断开连接
第3次: DNS解析 → TCP三次握手 → SSL握手 → 发送请求 → 收响应 → 断开连接
...
第100次: 同上
Session 请求(100次)的流程:
第1次: DNS解析 → TCP三次握手 → SSL握手 → 发送请求 → 收响应 → 保持连接 ✅
第2次: (复用连接)→ 发送请求 → 收响应 → 保持连接 ✅
第3次: (复用连接)→ 发送请求 → 收响应 → 保持连接 ✅
...
第100次: (复用连接)→ 发送请求 → 收响应 → 保持连接 ✅
每次新建连接的开销:
| 步骤 | 耗时 | 说明 |
|---|---|---|
| DNS 解析 | 10~100ms | 域名 → IP |
| TCP 三次握手 | 10~50ms | SYN → SYN+ACK → ACK |
| SSL/TLS 握手 | 50~200ms | HTTPS 必须,比 HTTP 多这一步 |
| 发送 + 接收 | 5~50ms | 真正的业务请求 |
| 合计 | 100400ms | 每次都要付一次 |
Session 复用连接后:
| 步骤 | 耗时 | 说明 |
|---|---|---|
| DNS 解析 | 0ms | 第1次已缓存 |
| TCP 握手 | 0ms | 连接已建立 |
| SSL 握手 | 0ms | 连接已加密 |
| 发送 + 接收 | 5~50ms | 只有这一步在跑 |
| 合计 | 550ms | 快了 10 倍 |
三、三个关键技术点
1. HTTP Keep-Alive(连接保持)
HTTP/1.1 默认支持 Connection: keep-alive,意思是:
"这次请求完了,TCP 连接别关,下次还用。"
requests.Session() 默认开启了这个。
而 requests.get() 每次用完就关连接(Connection: close)。
python
# 直接请求的响应头
Connection: close ← 每次都关
# Session 的响应头
Connection: keep-alive ← 保持住
2. Connection Pooling(连接池)
Session 内部维护了一个连接池(用 urllib3 实现):
python
# Session 默认配置
pool_connections=10 # 每个域名最多保持 10 个连接
pool_maxsize=10 # 总共最多 10 个连接
也就是说,如果你并发请求 10 个不同的 URL,Session 会同时保持 10 条 TCP 连接,而不是串行重建。
3. SSL Session Resumption(SSL 会话复用)
HTTPS 的 SSL 握手最贵(50~200ms)。
Session 复用 TCP 连接后,SSL 会话也复用了,不需要重新握手。
第1次请求: 完整 SSL 握手(贵)
第2~100次: SSL Session Resumption(几乎免费,<1ms)
四、实测对比:三种方式
python
import requests
import time
url = "https://httpbin.org/get"
n = 100
# 1. 直接 requests.get(无连接复用)
def test_direct():
start = time.time()
for _ in range(n):
requests.get(url)
return time.time() - start
# 2. requests.Session(有连接复用)
def test_session():
start = time.time()
with requests.Session() as s:
for _ in range(n):
s.get(url)
return time.time() - start
# 3. requests.Session + 流式(减少内存,速度基本一样)
def test_session_stream():
start = time.time()
with requests.Session() as s:
for _ in range(n):
s.get(url, stream=True)
return time.time() - start
print(f"直接请求: {test_direct():.2f}s") # ~8.5s
print(f"Session: {test_session():.2f}s") # ~0.8s
print(f"Session流: {test_session_stream():.2f}s") # ~0.7s
结果:Session 快 10 倍,stream 版本再省一点内存,速度差不多。
五、什么时候该用 Session?
| 场景 | 推荐 | 原因 |
|---|---|---|
| 循环请求同一个接口 | ✅ 必须用 Session | 复用连接,快 10 倍 |
| 爬虫批量请求 | ✅ 必须用 Session | 否则被封 IP + 慢 |
| 单次请求 | 🤷 无所谓 | 一次请求,建连开销占比小 |
| 并发请求不同域名 | ✅ 用 Session | 自动维护连接池 |
| 需要保持 Cookie | ✅ 必须用 Session | 自动管理 Cookie |
六、进阶:aiohttp 更快(异步)
如果你追求极致性能,aiohttp + Session 更猛:
python
import asyncio
import aiohttp
async def fetch(url, session):
async with session.get(url) as resp:
return await resp.text()
async def main():
url = "https://httpbin.org/get"
async with aiohttp.ClientSession() as session: # 异步 Session
tasks = [fetch(url, session) for _ in range(100)]
await asyncio.gather(*tasks)
asyncio.run(main())
# 100次请求约 0.3s(比 requests.Session 还快 2~3 倍)
原因:异步 IO + 连接复用,一个线程能处理几百个并发。
七、一句话总结
requests.get()每次都在"打车",requests.Session()是"自己有车"。
TCP 连接的建立和销毁才是真正的性能杀手,Session 只是把车留在了门口。
附:常见误区
| 误区 | 真相 |
|---|---|
| "Session 只是个语法糖" | 不是,它背后是连接池 + Keep-Alive + Cookie 管理 |
| "用了 Session 就一定快" | 单次请求差异不大,批量请求才有数量级差距 |
| "HTTP/2 就不需要 Session 了" | HTTP/2 多路复用更强,但 Session 依然有用(Cookie 管理) |
| "timeout 设置不影响 Session" | 会影响,连接池中的连接超时后会被回收,需配置 pool_maxsize |