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=

关键要点:

  1. HTTP 代理通过修改请求行(添加完整 URL)来告知代理目标地址
  2. CONNECT 方法用于建立隧道,代理不再解析后续数据
  3. 代理认证通过 Proxy-Authorization 头实现
  4. 理解代理工作原理有助于排查网络问题和优化爬虫策略

Fluxisp
3 声望166 粉丝

Zooproxy海外代理IP,提供静态、动态、移动、数据中心IP,涵盖200+国家/城市,1000万IP池资源,新用户注册免费体验,7*24小时技术支持,为您的IP保驾护航!