使用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()
服务器修正点说明
- 新增
recv_all函数:解决TCP粘包/recv不完整问题,确保收到指定长度的数据。 send_waiting_msg遍历列表副本:避免迭代原列表时remove元素导致的循环跳过。- 使用
sendall替代send:确保消息完整发送到客户端。 - 优化客户端断开处理:当recv返回空时,判定客户端断开并移除socket。
- 增加
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()
客户端修正点说明
- 调整Select检测列表:将
sys.stdin加入rlist,用来检测用户输入,而非把socket放进wlist(socket连接后通常一直可写,会导致无限触发输入逻辑)。 - 移除msvcrt依赖:改用标准输入+select实现跨平台的非阻塞输入,同时避免原代码中kbhit循环的阻塞问题。
- 新增
recv_all函数:和服务器端对应,处理TCP粘包问题。 - 优化交互提示:输入后重新打印提示,提升用户体验。
Select函数核心用法解析
Select的作用是监听多个文件描述符(包括socket)的可读/可写/异常状态,避免单个socket操作阻塞整个程序,核心参数:
rlist:需要检测是否可读的文件描述符列表(socket有数据待接收、标准输入有用户输入时触发)wlist:需要检测是否可写的文件描述符列表(socket可以发送数据时触发,通常用于处理大消息的异步发送)xlist:需要检测异常的文件描述符列表(一般留空)
返回值是三个列表:(可读列表, 可写列表, 异常列表),只有状态变化的描述符会被返回。
对于客户端,正确的逻辑应该是:
- 监听
客户端socket(检测服务器消息)和标准输入(检测用户输入) - 当socket在可读列表时,接收服务器消息
- 当标准输入在可读列表时,读取用户输入并发送
内容的提问来源于stack exchange,提问作者Orio20
相关产品推荐
相关产品推荐

