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

使用select实现Socket客户端接收服务器消息的问题求助

Socket + Select 编程问题修正与解析

核心问题梳理

你的代码主要问题集中在客户端的Select逻辑错误,以及服务器端的接收/发送逻辑不严谨,下面逐一修正并说明。


服务器端代码修正

主要修复recv不完整、消息发送时的列表迭代问题,以及异常断开处理:

import socket
import select

SERVER_IP = '127.0.0.1'
SERVER_PORT = 33333
SERVER_MSG_LENGTH = 6

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  # 避免端口占用问题
server_socket.bind((SERVER_IP, SERVER_PORT))
server_socket.listen(5)

open_client_socket = []
msg_to_send = []


def recv_all(sock, length):
    """确保接收指定长度的数据"""
    data = b''
    while len(data) < length:
        chunk = sock.recv(length - len(data))
        if not chunk:  # 客户端断开
            return None
        data += chunk
    return data


def send_waiting_msg(wlist, sender_socket):
    # 遍历副本避免迭代时修改原列表导致的跳过问题
    for message in list(msg_to_send):
        (_, data) = message
        length_str = str(len(data)).zfill(SERVER_MSG_LENGTH)
        msg = (length_str + data).encode()
        for client_socket in wlist:
            if client_socket != sender_socket:
                try:
                    client_socket.sendall(msg)  # sendall确保完整发送
                except Exception as e:
                    # 发送失败,移除异常客户端
                    open_client_socket.remove(client_socket)
                    print(f"Client disconnected unexpectedly: {e}")
        msg_to_send.remove(message)


def main():
    print("server is running and listening...")
    while True:
        rlist, wlist, xlist = select.select([server_socket] + open_client_socket, open_client_socket, [])
        
        for current_socket in rlist:
            if current_socket is server_socket:
                client_socket, address = server_socket.accept()
                open_client_socket.append(client_socket)
                print(f"connection with {address} has been made")
            else:
                # 先接收长度
                length_data = recv_all(current_socket, SERVER_MSG_LENGTH)
                if not length_data:
                    open_client_socket.remove(current_socket)
                    print("connection has been closed")
                    continue
                try:
                    msg_length = int(length_data.decode())
                    # 接收消息内容
                    msg_data = recv_all(current_socket, msg_length)
                    if not msg_data:
                        open_client_socket.remove(current_socket)
                        print("connection has been closed")
                        continue
                    data = msg_data.decode()
                    print(f"Received: {data}")
                    
                    if data == "quit":
                        open_client_socket.remove(current_socket)
                        print("connection has been closed")
                    else:
                        msg_to_send.append((current_socket, data))
                except Exception as err:
                    print(f"Error handling message: {err}")
                    open_client_socket.remove(current_socket)
        
        # 处理待发送消息
        if msg_to_send and wlist:
            send_waiting_msg(wlist, None)  # 这里sender_socket在遍历消息时已经区分
        
        # 没有客户端时退出
        if not open_client_socket:
            break
    server_socket.close()


if __name__ == '__main__':
    main()

服务器修正点说明

  1. 新增recv_all函数:解决TCP粘包/recv不完整问题,确保收到指定长度的数据。
  2. send_waiting_msg遍历列表副本:避免迭代原列表时remove元素导致的循环跳过。
  3. 使用sendall替代send:确保消息完整发送到客户端。
  4. 优化客户端断开处理:当recv返回空时,判定客户端断开并移除socket。
  5. 增加SO_REUSEADDR选项:避免重启服务器时的端口占用问题。

客户端代码修正

核心修复Select逻辑,将用户输入(标准输入)加入检测列表,避免无限触发输入逻辑:

import socket
import select
import sys

SERVER_HOST = '127.0.0.1'
SERVER_PORT = 33333
SERVER_MSG_LENGTH = 6

client_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_sock.connect((SERVER_HOST, SERVER_PORT))


def recv_all(sock, length):
    """确保接收指定长度的数据"""
    data = b''
    while len(data) < length:
        chunk = sock.recv(length - len(data))
        if not chunk:
            return None
        data += chunk
    return data


def send_msg(msg):
    if not msg:
        return
    length_str = str(len(msg)).zfill(SERVER_MSG_LENGTH)
    msg_send = (length_str + msg).encode()
    client_sock.sendall(msg_send)


def recv_msg():
    length_data = recv_all(client_sock, SERVER_MSG_LENGTH)
    if not length_data:
        print("Server disconnected")
        sys.exit(0)
    msg_length = int(length_data.decode())
    msg_data = recv_all(client_sock, msg_length)
    if not msg_data:
        print("Server disconnected")
        sys.exit(0)
    print(f"\nReceived from server: {msg_data.decode()}")
    print("Enter message (type 'quit' to exit): ", end='', flush=True)


def main():
    print("Connected to server. Enter message (type 'quit' to exit): ")
    while True:
        # Select检测:客户端socket(接收)和标准输入(用户输入)
        rlist, _, _ = select.select([client_sock, sys.stdin], [], [])
        
        for ready in rlist:
            if ready == client_sock:
                # 服务器有消息过来
                recv_msg()
            elif ready == sys.stdin:
                # 用户输入消息
                input_str = sys.stdin.readline().strip()
                if input_str == 'quit':
                    send_msg(input_str)
                    client_sock.close()
                    sys.exit(0)
                send_msg(input_str)
                print("Enter message (type 'quit' to exit): ", end='', flush=True)


if __name__ == "__main__":
    main()

客户端修正点说明

  1. 调整Select检测列表:将sys.stdin加入rlist,用来检测用户输入,而非把socket放进wlist(socket连接后通常一直可写,会导致无限触发输入逻辑)。
  2. 移除msvcrt依赖:改用标准输入+select实现跨平台的非阻塞输入,同时避免原代码中kbhit循环的阻塞问题。
  3. 新增recv_all函数:和服务器端对应,处理TCP粘包问题。
  4. 优化交互提示:输入后重新打印提示,提升用户体验。

Select函数核心用法解析

Select的作用是监听多个文件描述符(包括socket)的可读/可写/异常状态,避免单个socket操作阻塞整个程序,核心参数:

  • rlist:需要检测是否可读的文件描述符列表(socket有数据待接收、标准输入有用户输入时触发)
  • wlist:需要检测是否可写的文件描述符列表(socket可以发送数据时触发,通常用于处理大消息的异步发送)
  • xlist:需要检测异常的文件描述符列表(一般留空)

返回值是三个列表:(可读列表, 可写列表, 异常列表),只有状态变化的描述符会被返回。

对于客户端,正确的逻辑应该是:

  • 监听客户端socket(检测服务器消息)和标准输入(检测用户输入)
  • 当socket在可读列表时,接收服务器消息
  • 当标准输入在可读列表时,读取用户输入并发送

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 10:35:18