HTTP Keep-Alive
我们需要理解什么是 HTTP 长连接,在 HTTP/1.0 中,默认情况下,每次客户端和服务器之间的请求/响应完成后,TCP 连接就会被关闭,如果客户端需要再次请求资源,就必须重新建立一个新的 TCP 连接,这个过程包括“三次握手”,会带来额外的延迟和开销。

为了解决这个问题,HTTP/1.1 引入了 Keep-Alive 机制,它允许在同一个 TCP 连接上发送多个 HTTP 请求和响应,而不必为每个请求都重新建立连接,这大大减少了网络延迟,提高了性能,尤其是在需要请求同一服务器上多个资源的场景下(一个网页加载 HTML、CSS、JS、图片等)。
requests 库默认就支持并开启了 Keep-Alive。
requests 如何默认使用长连接
当你使用 requests 发起请求时,它会自动在请求头中添加 Connection: keep-alive,并尝试复用底层的 TCP 连接。
示例:

import requests
# 第一次请求
response1 = requests.get('https://httpbin.org/get')
print(f"第一次请求状态码: {response1.status_code}")
# 第二次请求到同一个域名
response2 = requests.get('https://httpbin.org/get')
print(f"第二次请求状态码: {response2.status_code})
# 第三次请求到同一个域名的不同路径
response3 = requests.get('https://httpbin.org/headers')
print(f"第三次请求状态码: {response3.status_code}")
在上面的例子中,requests 会尽可能地在同一个 TCP 连接上完成这三个请求,而不是为每个请求都新建一个连接。
如何手动控制长连接行为
虽然 requests 默认开启长连接,但你也可以手动进行控制。
A. 禁用长连接
如果你想在特定请求中禁用长连接,可以手动设置 Connection: close 头。
import requests
headers = {'Connection': 'close'}
response = requests.get('https://httpbin.org/get', headers=headers)
print(f"禁用长连接后的状态码: {response.status_code}")
print(f"服务器返回的响应头: {response.headers}")
在这个例子中,requests 会发送 Connection: close 头,告诉服务器这个请求完成后就关闭连接,服务器在响应中通常也会包含 Connection: close。

B. 强制使用新连接
有时,你可能想强制 requests 不使用已经存在的连接,而是创建一个新的,这在调试或者需要确保请求完全独立的情况下非常有用。
你可以通过 Session 对象的 close() 方法来关闭底层连接,或者使用 requests.request() 而不是 Session 的方法(但这不是最优雅的方式)。
更推荐的方式是,当你想强制使用新连接时,创建一个新的 Session 对象。
import requests
# 创建一个 Session 对象
session = requests.Session()
# 第一次请求,会建立一个连接
response1 = session.get('https://httpbin.org/get')
print(f"第一次请求状态码: {response1.status_code}")
# 关闭当前 Session 的所有连接
session.close()
# 再次请求时,requests 会发现连接已关闭,从而建立新连接
response2 = session.get('https://httpbin.org/get')
print(f"关闭连接后再次请求的状态码: {response2.status_code}")
# 完全关闭 Session
session.close()
另一种更直接的方式是,在每次需要新连接时,都创建一个新的 Session。
import requests
# 第一次请求
session1 = requests.Session()
response1 = session1.get('https://httpbin.org/get')
session1.close()
# 第二次请求,使用全新的 Session
session2 = requests.Session()
response2 = session2.get('https://httpbin.org/get')
session2.close()
print("两次请求使用了不同的 Session 对象,因此必然是不同的连接。")
连接池(requests.Session)
requests.Session 是实现高效复用连接的关键,当你创建一个 Session 对象时,它会创建一个连接池(Connection Pool)。
- 工作原理:当
Session发起请求时,它会从连接池中查找一个可用的、空闲的连接到目标服务器,如果找到了,就直接使用;如果没有,或者连接池已满,它就会创建一个新的连接。 - 优点:对于向同一个服务器发起多个请求的场景(爬虫与 API 交互),使用
Session可以极大地减少连接建立的开销,显著提升性能。
最佳实践: 在一个应用程序的生命周期内(一个爬虫任务),尽量复用一个 Session 对象。
import requests
# 推荐:在整个应用生命周期内复用 Session
session = requests.Session()
try:
# 发送多个请求
response1 = session.get('https://api.example.com/users/1')
print(f"请求1: {response1.json()}")
response2 = session.get('https://api.example.com/users/2')
print(f"请求2: {response2.json()}")
response3 = session.post('https://api.example.com/users', json={'name': 'John Doe'})
print(f"请求3: {response3.json()}")
finally:
# 确保在最后关闭 Session,释放连接池
session.close()
超时设置
长连接虽然高效,但如果服务器长时间没有响应,可能会导致连接一直占用,设置合理的超时非常重要。
timeout参数可以防止程序无限期等待。- 它可以是一个单一的浮点数(
timeout=5.0),这表示连接和读取的总超时时间。 - 也可以是一个元组
(connect_timeout, read_timeout),分别指定连接超时和读取超时。
import requests
# 设置总超时为 5 秒
try:
response = requests.get('https://httpbin.org/delay/10', timeout=5)
except requests.exceptions.Timeout:
print("请求超时了!")
# 分别设置连接超时和读取超时
try:
# 连接超时 2 秒,读取超时 5 秒
response = requests.get('https://httpbin.org/delay/10', timeout=(2, 5))
except requests.exceptions.ConnectTimeout:
print("连接超时了!")
except requests.exceptions.ReadTimeout:
print("读取超时了!")
总结与最佳实践
| 场景 | 推荐做法 | 原因 |
|---|---|---|
| 单个、独立的请求 | 直接使用 requests.get() 等 |
简单直接,requests 会自动处理。 |
| 向同一服务器发起多个请求 | 使用 requests.Session() |
这是关键! Session 的连接池会复用 TCP 连接,性能提升巨大。 |
| 需要强制使用新连接 | 创建一个新的 Session 对象,或调用 session.close() |
确保请求的独立性,避免复用可能带来问题的旧连接。 |
| 需要禁用长连接 | 在请求头中添加 {'Connection': 'close'} |
特殊场景下的需求,如调试或兼容旧服务器。 |
| 所有网络请求 | 务必设置 timeout |
防止因网络问题或服务器无响应而导致程序卡死。 |
| 代码健壮性 | 使用 try...except 处理异常 |
网络请求不可靠,必须处理可能发生的 ConnectionError, Timeout, HTTPError 等。 |
最终示例:一个健壮的、使用长连接的爬虫/客户端
import requests
from requests.exceptions import RequestException
def fetch_data(url, session, timeout=10):
"""
使用给定的 Session 从 URL 获取数据。
"""
try:
response = session.get(url, timeout=timeout)
# 检查 HTTP 状态码
response.raise_for_status() # 如果状态码不是 2xx,则抛出 HTTPError
return response.json()
except RequestException as e:
print(f"请求 {url} 时发生错误: {e}")
return None
def main():
# 目标 API 的基础 URL
base_url = "https://jsonplaceholder.typicode.com"
# 创建一个 Session 对象来复用连接
with requests.Session() as session:
# 1. 获取所有用户
users_url = f"{base_url}/users"
users = fetch_data(users_url, session)
if users:
print(f"成功获取到 {len(users)} 个用户。")
# 2. 获取第一个用户的帖子
first_user_id = users[0]['id']
posts_url = f"{base_url}/posts?userId={first_user_id}"
posts = fetch_data(posts_url, session)
if posts:
print(f"成功获取到用户 {first_user_id} 的 {len(posts)} 篇帖子。")
if __name__ == "__main__":
main()
在这个例子中,我们:
- 使用
with requests.Session() as session:来管理Session,确保它在代码块执行完毕后自动关闭。 - 将网络请求逻辑封装在
fetch_data函数中,并处理了常见的异常。 - 通过
session.get()复用连接,提高了与jsonplaceholder.typicode.com服务器交互的效率。
