HTTP 代理是互联网中最基础的代理协议之一,广泛应用于爬虫开发、API 调用、网络调试等场景。理解 HTTP 代理的工作原理,有助于开发者更好地配置和使用代理,也能帮助排查代理相关的网络问题。
本文将从技术角度详细介绍 HTTP 代理的工作机制,包括普通请求转发和 CONNECT 隧道两种模式。
一、HTTP 代理的基本概念
1.1 什么是 HTTP 代理
HTTP 代理是一个中间服务器,位于客户端和目标服务器之间,专门处理 HTTP 协议的请求。
客户端 → HTTP代理 → 目标服务器
↓
解析HTTP请求
转发/修改/过滤1.2 HTTP 代理的两种工作模式
二、普通代理模式(HTTP 请求转发)
2.1 工作原理
在普通代理模式下,客户端发送的请求格式与直连略有不同:
直连请求格式:
GET /index.html HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0通过代理的请求格式:
GET http://www.example.com/index.html HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0区别在于:代理模式下,请求行中的 URI 包含完整的 URL(包含协议和域名),而不仅仅是路径。
2.2 完整交互流程
sequenceDiagram
participant Client as 客户端
participant Proxy as HTTP代理
participant Server as 目标服务器
Client->>Proxy: GET http://example.com/ HTTP/1.1
Proxy->>Server: 解析Host头,建立TCP连接
Proxy->>Server: GET / HTTP/1.1
Server-->>Proxy: HTTP/1.1 200 OK
Proxy-->>Client: 转发响应
Proxy->>Server: 关闭连接(或保持)2.3 代码示例:实现一个简单的 HTTP 代理
import socket
import threading
class SimpleHTTPProxy:
"""简单的 HTTP 代理服务器"""
def __init__(self, host='127.0.0.1', port=8888):
self.host = host
self.port = port
self.server_socket = None
def start(self):
"""启动代理服务器"""
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.server_socket.bind((self.host, self.port))
self.server_socket.listen(5)
print(f"HTTP 代理运行在 {self.host}:{self.port}")
while True:
client_socket, addr = self.server_socket.accept()
print(f"收到连接: {addr}")
thread = threading.Thread(target=self.handle_client, args=(client_socket,))
thread.start()
def parse_request(self, data: bytes):
"""解析请求,提取目标主机和端口"""
first_line = data.split(b'\n')[0].decode('utf-8')
parts = first_line.split(' ')
if len(parts) < 3:
return None, None
method = parts[0]
url = parts[1]
# 解析完整URL(代理模式)
if url.startswith('http://'):
# 格式: http://example.com/path
host_part = url.split('/')[2]
host = host_part.split(':')[0]
port = 80
# 重写请求行,去掉协议和域名
path = '/' + '/'.join(url.split('/')[3:])
if path == '':
path = '/'
new_first_line = f"{method} {path} HTTP/1.1"
data = data.replace(first_line.encode(), new_first_line.encode())
else:
# 非代理模式,从 Host 头提取
host = None
port = 80
for line in data.split(b'\r\n'):
if line.lower().startswith(b'host:'):
host = line.decode().split(':')[1].strip()
break
return (host, port), data
def handle_client(self, client_socket):
"""处理单个客户端请求"""
try:
# 接收请求
request_data = client_socket.recv(8192)
if not request_data:
return
# 解析目标地址
target, modified_request = self.parse_request(request_data)
if not target or not target[0]:
client_socket.close()
return
host, port = target
# 连接到目标服务器
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.connect((host, port))
# 转发请求
server_socket.send(modified_request)
# 接收响应并转发回客户端
while True:
response_data = server_socket.recv(4096)
if not response_data:
break
client_socket.send(response_data)
server_socket.close()
except Exception as e:
print(f"处理请求时出错: {e}")
finally:
client_socket.close()
# 启动代理服务器
if __name__ == '__main__':
proxy = SimpleHTTPProxy('127.0.0.1', 8888)
proxy.start()2.4 使用示例
# 使用上面搭建的代理发送请求
import requests
proxies = {
'http': 'http://127.0.0.1:8888',
'https': 'http://127.0.0.1:8888' # 注意:HTTPS 走 CONNECT 隧道
}
response = requests.get('http://httpbin.org/ip', proxies=proxies)
print(response.json())三、CONNECT 隧道模式(HTTPS 代理)
3.1 为什么需要 CONNECT
HTTPS 流量是加密的,代理无法解析请求内容。如果代理像处理 HTTP 那样修改请求行,会破坏加密数据。因此需要 CONNECT 方法建立透明隧道。
3.2 CONNECT 工作原理
3.3 CONNECT 请求示例
客户端发送:
CONNECT www.example.com:443 HTTP/1.1
Host: www.example.com:443
User-Agent: Mozilla/5.0
Proxy-Connection: Keep-Alive代理响应:
HTTP/1.1 200 Connection Established
Proxy-Agent: Proxy/1.0之后,客户端与目标服务器之间的所有数据(已加密)都被代理透明转发,代理不再解析内容。
3.4 支持 CONNECT 的代理实现
import socket
import threading
import re
class HTTPProxyWithConnect:
"""支持 CONNECT 隧道的 HTTP 代理"""
def __init__(self, host='127.0.0.1', port=8888):
self.host = host
self.port = port
def start(self):
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((self.host, self.port))
server.listen(5)
print(f"支持 CONNECT 的代理运行在 {self.host}:{self.port}")
while True:
client, addr = server.accept()
threading.Thread(target=self.handle, args=(client,)).start()
def handle(self, client):
"""处理客户端请求"""
try:
data = client.recv(8192)
if not data:
return
# 检查是否为 CONNECT 请求
first_line = data.split(b'\n')[0].decode()
if first_line.startswith('CONNECT'):
self.handle_connect(client, first_line, data)
else:
self.handle_http(client, data)
except Exception as e:
print(f"错误: {e}")
finally:
client.close()
def handle_connect(self, client, first_line, initial_data):
"""处理 CONNECT 隧道"""
# 解析目标地址: CONNECT example.com:443
match = re.search(r'CONNECT ([^:]+):(\d+)', first_line)
if not match:
client.send(b'HTTP/1.1 400 Bad Request\r\n\r\n')
return
host = match.group(1)
port = int(match.group(2))
try:
# 连接到目标服务器
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.connect((host, port))
# 响应 200 Connection Established
client.send(b'HTTP/1.1 200 Connection Established\r\n\r\n')
# 开始双向转发(隧道模式)
self.forward_bidirectional(client, server)
except Exception as e:
client.send(b'HTTP/1.1 502 Bad Gateway\r\n\r\n')
def handle_http(self, client, data):
"""处理普通 HTTP 请求"""
# 解析目标主机
lines = data.split(b'\n')
first_line = lines[0].decode()
# 代理模式下的完整URL格式
if 'http://' in first_line:
parts = first_line.split(' ')
url = parts[1]
host = url.split('/')[2]
# 重写请求行
path = '/' + '/'.join(url.split('/')[3:])
new_first_line = f"{parts[0]} {path} HTTP/1.1\r\n"
data = new_first_line.encode() + b'\r\n'.join(lines[1:])
else:
# 从 Host 头提取
for line in lines:
if line.lower().startswith(b'host:'):
host = line.decode().split(':')[1].strip()
break
# 连接到目标服务器
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.connect((host, 80))
server.send(data)
# 转发响应
while True:
resp = server.recv(8192)
if not resp:
break
client.send(resp)
server.close()
def forward_bidirectional(self, client, server):
"""双向转发数据(隧道模式)"""
def forward(src, dst):
try:
while True:
data = src.recv(8192)
if not data:
break
dst.send(data)
except:
pass
finally:
src.close()
dst.close()
# 两个方向同时转发
threading.Thread(target=forward, args=(client, server)).start()
threading.Thread(target=forward, args=(server, client)).start()
# 启动代理
proxy = HTTPProxyWithConnect('127.0.0.1', 8888)
proxy.start()四、HTTP 代理的请求头处理
4.1 代理专用请求头
4.2 代理认证
import requests
from requests.auth import HTTPProxyAuth
# 需要认证的代理
proxies = {
'http': 'http://proxy.example.com:8080',
'https': 'http://proxy.example.com:8080'
}
auth = HTTPProxyAuth('username', 'password')
response = requests.get('http://httpbin.org/ip', proxies=proxies, auth=auth)代理认证请求头:
GET http://httpbin.org/ip HTTP/1.1
Host: httpbin.org
Proxy-Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=关键要点:
- HTTP 代理通过修改请求行(添加完整 URL)来告知代理目标地址
- CONNECT 方法用于建立隧道,代理不再解析后续数据
- 代理认证通过 Proxy-Authorization 头实现
- 理解代理工作原理有助于排查网络问题和优化爬虫策略
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用。你还可以使用@来通知其他用户。