Python实现ChatServer/ChatClient时客户端连接8800端口失败排查
问题原因
代码里存在3个直接导致连接失败、程序崩溃的核心bug,按影响优先级排序:
- 服务端监听socket被意外提前关闭
ChatServer.run()方法末尾的self.server.close()缩进错误,被写在了while running循环内部,服务端第一次处理完可读事件(比如收到客户端连接请求)后,就会直接关闭监听socket,直接导致服务端无法响应后续请求、甚至进程异常退出。 receive()工具函数逻辑完全错误
接收数据时使用buf = channel.recv(...)直接覆盖缓冲区,而非增量拼接已接收的数据,会导致永远无法拿到完整的序列化消息,触发反序列化异常,直接打断服务端和客户端的连接流程。- 服务端消息处理分支缩进完全错乱
处理客户端消息的代码块缩进错误,if data:判断下只有注释,消息转发、客户端断开处理逻辑的缩进完全不匹配,会直接触发运行时错误,导致服务端崩溃退出。
另外还有几个隐性问题会影响稳定性:
send()函数直接用send()发送数据,TCP流传输可能出现部分发送的情况,会导致对端解析消息失败- 结构体打包用
"L"格式是平台相关长度,跨Windows/Linux运行会出现长度解析错误 - 异常捕获范围太窄,反序列化、索引错误等非socket异常会直接让程序崩溃退出
修复步骤
- 调整
self.server.close()的缩进,把它移到while running循环外部,仅在服务端退出循环后才关闭监听socket - 重写
receive()函数的缓冲区逻辑,改用增量拼接方式接收完整数据,统一用字节类型处理socket收发内容 - 修正服务端处理客户端消息分支的缩进,把消息转发逻辑放到
if data:块内,客户端断开逻辑放到对应else块 - 把
send()里的send()替换为sendall(),保证数据完整发送 - 调整结构体打包格式为定长网络字节序,避免跨平台兼容问题
- 补全必要的异常捕获,避免单个客户端错误导致整个服务端崩溃
修复后的可运行完整代码
import select import socket import sys import signal import pickle as cPickle import struct import argparse SERVER_HOST = 'localhost' CHAT_SERVER_NAME = 'server' # 定长包头用网络字节序4字节无符号整数,跨平台兼容 PACKET_SIZE_FMT = "!I" PACKET_HEADER_SIZE = struct.calcsize(PACKET_SIZE_FMT) # 数据收发工具 def send(channel, *args): buffer = cPickle.dumps(args) pkt_size = len(buffer) size_header = struct.pack(PACKET_SIZE_FMT, pkt_size) # 用sendall保证数据完整发送 channel.sendall(size_header) channel.sendall(buffer) def receive(channel): # 先读固定长度包头 header_data = b"" while len(header_data) < PACKET_HEADER_SIZE: chunk = channel.recv(PACKET_HEADER_SIZE - len(header_data)) if not chunk: return '' header_data += chunk try: pkt_size = struct.unpack(PACKET_SIZE_FMT, header_data)[0] except struct.error: return '' # 增量拼接读取完整包体 buf = b"" while len(buf) < pkt_size: chunk = channel.recv(pkt_size - len(buf)) if not chunk: return '' buf += chunk return cPickle.loads(buf)[0] class ChatServer(object): def __init__(self, port, backlog=5): self.clients = 0 self.clientmap = {} self.outputs = [] self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 允许端口复用 self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.server.bind((SERVER_HOST, port)) print('Server listening to port: %s...' % port) self.server.listen(backlog) # 捕获Ctrl+C中断信号 signal.signal(signal.SIGINT, self.sighandler) def sighandler(self, signum, frame): print("\nShutting down server...") # 关闭所有客户端连接 for output in self.outputs: output.close() self.server.close() sys.exit(0) def get_client_name(self,client): info = self.clientmap[client] host, name = info[0][0], info[1] return '@'.join((name, host)) def run(self): inputs = [self.server, sys.stdin] self.outputs = [] running = True while running: try: readable, writeable, exceptional = select.select(inputs, self.outputs, []) except select.error: break for sock in readable: if sock == self.server: # 处理新连接 client, address = self.server.accept() print("Chat Server: got connection %d from %s" % (client.fileno(), address)) # 读取客户端登录名 try: cname = receive(client).split('NAME: ')[1] except Exception: client.close() continue self.clients += 1 send(client, 'CLIENT: ' + str(address[0])) inputs.append(client) self.clientmap[client] = (address, cname) # 广播新客户端上线通知 msg = "\n(Connected: New client (%d) from %s)" % (self.clients, self.get_client_name(client)) for output in self.outputs: try: send(output, msg) except Exception: pass self.outputs.append(client) elif sock == sys.stdin: # 处理服务端控制台输入,输入任意内容回车即可关闭服务端 junk = sys.stdin.readline() running = False else: # 处理客户端消息 try: data = receive(sock) if data: # 转发消息给其他所有客户端 msg = '\n[' + self.get_client_name(sock) + ']>>' + data for output in self.outputs: if output != sock: try: send(output, msg) except Exception: pass else: # 客户端断开连接 print("Chat server: %d hung up" % sock.fileno()) self.clients -= 1 sock.close() inputs.remove(sock) self.outputs.remove(sock) # 广播客户端离线通知 msg = "\n(Now hung up: Client from %s)" % self.get_client_name(sock) for output in self.outputs: try: send(output, msg) except Exception: pass except socket.error: # 清理异常断开的客户端 if sock in inputs: inputs.remove(sock) if sock in self.outputs: self.outputs.remove(sock) sock.close() # 循环结束后才关闭服务端监听socket self.server.close() class ChatClient(object): def __init__(self, name, port, host=SERVER_HOST): self.name = name self.connected = False self.host = host self.port = port # 初始化命令行提示符 self.prompt = '[' + '@'.join((name, socket.gethostname().split('.')[0])) + ']> ' # 连接服务端 try: self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.connect((host, self.port)) print("Now connected to chat server@ port %d" % self.port) self.connected = True # 发送登录用户名 send(self.sock, 'NAME: ' + self.name) data = receive(self.sock) # 解析服务端返回的客户端地址,更新提示符 addr = data.split('CLIENT: ')[1] self.prompt = '[' + '@'.join((self.name, addr)) + ']> ' except Exception: print("Failed to connect to chat server @ port %d" % self.port) sys.exit(1) def run(self): while self.connected: try: sys.stdout.write(self.prompt) sys.stdout.flush() # 同时监听控制台输入和服务端消息 readable, writable, exceptional = select.select([0, self.sock], [], []) for sock in readable: if sock == 0: # 读取控制台输入发送给服务端 data = sys.stdin.readline().strip() if data: send(self.sock, data) elif sock == self.sock: # 读取服务端转发的消息 data = receive(self.sock) if not data: print('Client shutting down') self.connected = False break else: sys.stdout.write(data + '\n') sys.stdout.flush() except KeyboardInterrupt: print("\nClient interrupted") self.sock.close() break if __name__ == "__main__": parser = argparse.ArgumentParser(description='Socket Chat Room with Select') parser.add_argument('--name', action="store", dest="name", required=True) parser.add_argument('--port', action="store", dest="port", type=int, required=True) given_args = parser.parse_args() port = given_args.port name = given_args.name if name == CHAT_SERVER_NAME: server = ChatServer(port) server.run() else: client = ChatClient(name=name, port=port) client.run()
运行验证
- 开第一个终端启动服务端:
python chat.py --name=server --port=8800,正常输出Server listening to port: 8800...即启动成功 - 开第二个终端启动客户端:
python chat.py --name=client1 --port=8800,正常输出Now connected to chat server@ port 8800、出现聊天提示符即连接成功,可以正常发送消息 - 支持开启多个客户端用不同name参数连接,消息会自动广播给所有在线客户端
内容的提问来源于stack exchange,提问作者Eric Livshiz
相关产品推荐
相关产品推荐

