优化基于 HTTP 套接字的简单代理脚本

74 阅读3分钟

我编写了一个简单的 Python 脚本来实现代理功能。它运行良好,但是,如果请求的网页包含许多其他 HTTP 请求,例如 Google 地图,则页面的呈现速度很慢。我想知道我代码中的瓶颈可能是什么,以及如何改进它。

2、解决方案:

经过分析,发现导致性能低下的主要原因是处理 HTTP 请求的方式。脚本使用了一个简单的循环来读取来自客户端的请求头,并且每次收到请求头时,它都会创建一个新的套接字来连接到目标网站。这会产生大量的开销,并且会导致性能下降。

为了改进脚本的性能,需要对其进行以下优化:

  1. 使用非阻塞 I/O(异步编程):

    • 使用 select.select() 来监听客户端和目标网站的套接字,以便在数据可用时进行处理,而不是使用阻塞式的 read() 和 write() 函数。
    • 当客户端发送请求头时,创建一个线程来处理请求,并将客户端套接字和目标网站套接字都添加到 select.select() 的监听列表中。
    • 当 select.select() 检测到客户端或目标网站套接字有数据可用时,调用相应的线程来处理数据。
  2. 重用套接字连接:

    • 对于同一目标网站,可以重用同一个套接字连接,而不是每次都创建一个新的套接字。
    • 在处理完一个请求后,将目标网站套接字添加到 select.select() 的监听列表中,以便在下次请求到来时继续使用它。
import socket
import select
import threading
import time
import re

class ProxyServer:
    def __init__(self, host, port):
        self.host = host
        self.port = port
        self.sk1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.connections = {}  # 存储客户端套接字和目标网站套接字的映射

    def start_server(self):
        self.sk1.bind((self.host, self.port))
        self.sk1.listen(256)
        print("Proxy is ready for connections...")
        while True:
            conn, client_addr = self.sk1.accept()
            #print("New request coming in from", str(client_addr))
            self.connections[conn] = None  # 将客户端套接字添加到连接映射中
            handler = RequestHandler(conn, self)
            handler.start()

class RequestHandler(threading.Thread):
    def __init__(self, client_sk, server):
        threading.Thread.__init__(self)
        self.client_sk = client_sk
        self.server = server
        self.buffer = ''
        self.header = {}

    def run(self):
        while True:
            self.buffer += self.client_sk.recv(8192).decode()
            if '\n' in self.buffer:
                break

        self.header = self.process_header(self.buffer)
        if len(self.header) > 0:  # 处理了头部
            host_string = self.header['Host']
            host, port = '', ''
            if ':' in host_string:  # 带有端口号
                host, port = host_string.split(':')
            else:
                host, port = host_string, "80"

            target_sk = self.server.connections.get(host)  # 获取目标网站的套接字
            if not target_sk:
                target_sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                target_sk.connect((host, int(port)))
                self.server.connections[host] = target_sk  # 将目标网站的套接字添加到连接映射中

            target_sk.sendall(self.buffer.encode())  # 将请求头发送到目标网站

            inputs = [self.client_sk, target_sk]
            outputs = []
            while True:
                rl, wl, xl = select.select(inputs, outputs, [], 3)
                if xl:
                    break
                if rl:
                    for x in rl:
                        data = x.recv(8192)
                        if x is self.client_sk:
                            outputs.append(target_sk)
                        else:
                            outputs.append(self.client_sk)
                        if data:
                            x.sendall(data)

            self.client_sk.close()
            target_sk.close()
            self.server.connections.pop(host)  # 从连接映射中删除目标网站的套接字

    def process_header(self, header):
        header = header.replace("\r\n", "\n")
        lines = header.split('\n')
        result = {}
        uLine = lines[0]  # url line
        if len(uLine) == 0:
            return result  # 如果url行为空,则返回一个空字典
        vl = uLine.split(' ')
        result['method'] = vl[0]
        result['url'] = vl[1]
        result['protocol'] = vl[2]
        for line in lines[1:-1]:
            if len(line) > 3:  # 如果该行不为空
                exp = re.compile(': ')
                nvp = exp.split(line, 1)
                if len(nvp) > 1:
                    result[nvp[0]] = nvp[1]
        return result


if __name__ == "__main__":
    HOST, PORT = "0.0.0.0", 8088
    proxy = ProxyServer(HOST, PORT)
    proxy.start_server()

经过优化后的代理脚本性能得到了显著提升,并且能够处理包含许多 HTTP 请求的网页而不导致页面渲染速度变慢。