You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

构建持久化HTTP Web服务器:复用连接处理多请求问题

问题描述

我写的Web服务器代码里,内部while循环包含msg = client_socket.recv(1024).decode(),但如果不把client_socket, addr = server_socket.accept()放进这个内部循环,就没法处理多个客户端请求。我搞不懂怎么复用同一个连接来处理所有请求,当前代码如下:

from socket import *
import sys

def run_server(port):
    # 创建服务器套接字
    server_socket = socket(AF_INET, SOCK_STREAM)
    # 将套接字绑定到指定地址
    server_socket.bind(('localhost', port))

    # 参数1表示最大排队连接数
    server_socket.listen(1)
    print(f"Serving on port {port}")

    while 1: 
        # 接受客户端的传入连接
        client_socket, addr = server_socket.accept()
        print(f"Connection established from address {addr}")

        while 1:    
            # 接收客户端数据
            msg = client_socket.recv(1024).decode()
            # 提取文件名
            file_name = msg.split('\n')[0].split()[1].replace('/', '')
        
            # 处理客户端请求
            try:
                with open(file_name, 'rb') as file:
                    status_line = "HTTP/1.1 200 OK\r\n"

                    file_extension = file_name.split('.')[-1].lower()

                    if file_extension == 'html':
                        content_type_header = "Content-Type: text/html\r\n\r\n"
                    elif file_extension == 'png':
                        content_type_header = "Content-Type: image/png\r\n\r\n"

                    content = file.read()

                    # 组合状态行、头部和内容
                    http_response = (status_line + "Connection: keep-alive\r\n" + content_type_header).encode() + content
            except OSError as _:
                # 文件未找到,生成404响应
                status_line = "HTTP/1.1 404 Not Found\r\n"
                content_type_header = "Content-Type: text/html\r\n\r\n"
                content = "<h1>404 Not Found</h1><p>The requested file was not found on this server.</p>"
                
                http_response = (status_line + content_type_header + content).encode()

            # 将HTTP响应发送回客户端
            client_socket.sendall(http_response)
            # 处理多个请求后关闭连接
        client_socket.close()
        print("Connection closed")

    server_socket.close()

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("Usage: python WebServer.py <port>")
        sys.exit(1)

    # 从命令行参数获取端口号
    port = int(sys.argv[1])
    run_server(port)
解决方案

你的代码存在两个核心问题:一是单线程模型下同一时间只能处理一个客户端,二是Keep-Alive连接的处理逻辑缺失,导致内部死循环无法正常退出,也没法正确复用连接处理多个请求。以下是具体修复方案:

1. 核心逻辑梳理

  • server_socket.accept()必须放在外层循环,用来持续接受新的客户端连接,一个客户端连接处理完毕后,主线程才能继续等待下一个客户端。
  • 复用同一连接处理多个请求的关键:在单个客户端连接的生命周期内,持续接收请求并响应,直到客户端主动断开,或者请求明确要求关闭连接。

2. 多线程实现(最易上手的多客户端支持)

单线程无法同时处理多个客户端,我们可以用多线程方案:每接受一个客户端连接就启动一个新线程处理该客户端的所有请求,主线程继续等待新连接,这样就能同时服务多个客户端,且每个客户端的连接可以复用处理多次请求。

修改后的完整代码

from socket import *
import sys
import threading

def handle_client(client_socket, addr):
    print(f"Connection established from address {addr}")
    keep_alive = True
    while keep_alive:
        # 接收客户端数据,客户端断开时recv返回空字符串
        msg = client_socket.recv(1024).decode()
        if not msg:
            break
        
        # 解析请求行和请求头,处理空请求情况
        request_lines = msg.split('\r\n')
        if not request_lines or not request_lines[0].strip():
            break
        
        request_line = request_lines[0].strip()
        # 提取文件名,处理无效请求
        try:
            file_name = request_line.split()[1].replace('/', '')
        except IndexError:
            status_line = "HTTP/1.1 400 Bad Request\r\n"
            content_type = "Content-Type: text/html\r\n\r\n"
            content = "<h1>400 Bad Request</h1>"
            client_socket.sendall((status_line + content_type + content).encode())
            continue
        
        # 处理文件请求
        try:
            with open(file_name, 'rb') as file:
                status_line = "HTTP/1.1 200 OK\r\n"
                # 处理不同文件类型的Content-Type
                file_ext = file_name.split('.')[-1].lower() if '.' in file_name else ''
                content_type = ""
                if file_ext == 'html':
                    content_type = "Content-Type: text/html\r\n"
                elif file_ext == 'png':
                    content_type = "Content-Type: image/png\r\n"
                else:
                    content_type = "Content-Type: application/octet-stream\r\n"
                
                content = file.read()
                # 判断是否保持连接
                connection_header = "close"
                for line in request_lines[1:]:
                    if line.lower().startswith('connection:'):
                        connection_header = line.split(':')[1].strip().lower()
                        break
                # HTTP/1.1默认Keep-Alive,1.0默认关闭
                http_version = request_line.split()[0].split('/')[1]
                if http_version == "1.1" and connection_header != "close":
                    keep_alive = True
                    conn_line = "Connection: keep-alive\r\n"
                else:
                    keep_alive = False
                    conn_line = "Connection: close\r\n"
                
                # 组装响应
                response = (status_line + conn_line + content_type + "\r\n").encode() + content
        except OSError:
            # 文件不存在返回404
            status_line = "HTTP/1.1 404 Not Found\r\n"
            content_type = "Content-Type: text/html\r\n"
            content = "<h1>404 Not Found</h1><p>The requested file was not found on this server.</p>"
            # 同样判断连接是否保持
            connection_header = "close"
            for line in request_lines[1:]:
                if line.lower().startswith('connection:'):
                    connection_header = line.split(':')[1].strip().lower()
                    break
            http_version = request_line.split()[0].split('/')[1]
            if http_version == "1.1" and connection_header != "close":
                keep_alive = True
                conn_line = "Connection: keep-alive\r\n"
            else:
                keep_alive = False
                conn_line = "Connection: close\r\n"
            
            response = (status_line + conn_line + content_type + "\r\n" + content).encode()
        
        client_socket.sendall(response)
    
    client_socket.close()
    print(f"Connection closed with {addr}")

def run_server(port):
    server_socket = socket(AF_INET, SOCK_STREAM)
    # 设置端口复用,避免服务器重启时端口被占用
    server_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    server_socket.bind(('localhost', port))
    # 增大排队连接数,应对多客户端同时连接
    server_socket.listen(5)
    print(f"Serving on port {port}")

    while True:
        client_socket, addr = server_socket.accept()
        # 启动新线程处理客户端请求
        thread = threading.Thread(target=handle_client, args=(client_socket, addr))
        thread.daemon = True  # 设置为守护线程,服务器退出时自动销毁
        thread.start()
    
    server_socket.close()

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("Usage: python WebServer.py <port>")
        sys.exit(1)
    port = int(sys.argv[1])
    run_server(port)

关键修改说明

  • 拆分出handle_client函数,专门处理单个客户端的所有请求,每个客户端对应独立线程,主线程专注于接受新连接。
  • 修复内部循环退出条件:当recv()返回空字符串时,说明客户端已断开,退出循环并关闭连接。
  • 正确解析HTTP请求的Connection头和版本,决定是否保持连接,实现同一连接下处理多个请求。
  • 增加无效请求处理逻辑,避免索引错误导致程序崩溃。
  • 添加SO_REUSEADDR选项,解决服务器重启时的端口占用问题。
  • 提高listen的排队数,应对多客户端同时发起连接的场景。

内容的提问来源于stack exchange,提问作者Ling Foong

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 21:41:04