Python Socket聊天程序随机停止运行并抛出socket.timeout错误如何解决
问题背景
我此前基于Socket编写了一款聊天程序原型,功能逻辑为客户端连接服务端后,发送的消息会被广播给所有已连接的客户端。另有一个client2.py文件,与给出的客户端代码逻辑完全一致,仅发送的内容为MESSAGE2。
该程序此前可以正常运行,后续开发完优化版本后再运行该原型代码时已经无法正常工作:程序可以成功建立连接,也能向客户端回传一条消息被客户端正常接收,之后服务端报超时错误,客户端侧仅不断执行错误代码分支,不会崩溃。
服务端代码
import socket as sk import selectors as sl import types import time def accept(key, mask): sock = key.fileobj conn, addr = sock.accept() print("accepted", conn, "from", addr) conn.setblocking(False) conn.settimeout(0.5) #this is really quite sloppy but basically this timeout #is required since the newly accepted #socket has not yet been registered conn.send("nick".encode("utf-8")) nick = conn.recv(1024) print(nick) print("registering", nick) sel.register(conn, sl.EVENT_READ|sl.EVENT_WRITE,data=(handle,nick)) clientlist.append(conn) def handle(key, mask): conn = key.fileobj data = key.data message = conn.recv(1024) broadcast(message) def broadcast(message): #for key, mask in clients: # print("sending",message,"to",key.data[1]) # key.fileobj.send(message) for client in clientlist: client.send(message) host = "127.0.0.1" port = 65432 sel = sl.DefaultSelector() server=sk.socket(sk.AF_INET,sk.SOCK_STREAM) #this server socket will listen for clients #and register them for communication server.bind((host,port)) server.listen() server.setblocking(False) sel.register(server, sl.EVENT_READ, data=(accept,None)) #our listening socket is only interested in reading #getting data from the clients to accept or decline it clientlist=[] print("initialisation complete") while True: clients=sel.select() for key, mask in clients: callback = key.data[0] callback(key, mask)
客户端代码
import socket as sk import threading as thr import time class Client: def __init__(self,hospor): self.nick = input("enter your nick: ") self.nickGiven=False self.hospor = hospor self.soc = sk.socket(sk.AF_INET,sk.SOCK_STREAM) self.soc.connect(hospor) msgThread = thr.Thread(target = self.msgLoop) receiveThread = thr.Thread(target = self.receiveLoop) receiveThread.start() msgThread.start() def msgLoop(self): while True: if self.nickGiven: msg = "MESSAGE" print("sending message") self.soc.send(msg.encode("utf-8")) time.sleep(10) def receiveLoop(self): while True: message = self.soc.recv(1024) if message == b"nick": print("received") self.soc.send(self.nick.encode("utf-8")) self.nickGiven=True else: print(message) client=Client(("127.0.0.1",65432))
服务端报错信息
initialisation complete accepted <socket.socket fd=268, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 65432), raddr=('127.0.0.1', 65011)> from ('127.0.0.1', 65011) b'nick' registering b'nick' Traceback (most recent call last): File "C:\Users\Ottie_boi\Documents\00000 actual documents\0 School\0 Computer Science\0 NEA\repo\Actual-NEA\Testing\chatapp proto\v1\server.py", line 66, in <module> callback(key, mask) File "C:\Users\Ottie_boi\Documents\00000 actual documents\0 School\0 Computer Science\0 NEA\repo\Actual-NEA\Testing\chatapp proto\v1\server.py", line 26, in handle message = conn.recv(1024) socket.timeout: timed out Process finished with exit code 1
错误原因
- socket模式冲突:你在
accept函数中先给新连接设置了setblocking(False),随后调用settimeout(0.5)覆盖了之前的非阻塞配置,将socket改为了带0.5s超时的阻塞模式,不符合selector管理非阻塞socket的要求。 - 多余的写事件监听:注册客户端socket时同时监听了
EVENT_READ和EVENT_WRITE,而TCP socket绝大多数时间都是可写状态,会导致selector频繁触发handle回调。 - 无判断直接调用recv:
handle函数没有判断当前触发的是读事件还是写事件,只要触发回调就直接调用recv,此时如果没有客户端发来的数据,就会触发socket的超时异常。 - 客户端发消息间隔长:客户端设置了每10秒才发一次消息,远长于服务端socket的0.5s超时时间,进一步放大了无数据可读的问题。
修复方案
服务端修改点
- 拿到客户端昵称后,将socket改回非阻塞模式,移除超时配置
- 注册客户端socket时仅监听
EVENT_READ事件,不需要监听写事件 handle函数中先判断当前是否为读事件再调用recv,同时添加异常捕获处理连接断开、无数据的场景- 广播消息时添加异常捕获,处理客户端断开的情况
修复后服务端代码
import socket as sk import selectors as sl import time def accept(key, mask): sock = key.fileobj conn, addr = sock.accept() print("accepted", conn, "from", addr) # 临时设置超时用于同步获取昵称 conn.settimeout(0.5) conn.send("nick".encode("utf-8")) nick = conn.recv(1024) print(nick) print("registering", nick) # 获取昵称后改回非阻塞模式,移除超时 conn.setblocking(False) # 仅监听读事件即可 sel.register(conn, sl.EVENT_READ, data=(handle,nick)) clientlist.append(conn) def handle(key, mask): conn = key.fileobj # 仅处理读事件 if mask & sl.EVENT_READ: try: message = conn.recv(1024) # 客户端断开连接时recv会返回空字节 if not message: print(f"客户端 {key.data[1]} 断开连接") sel.unregister(conn) clientlist.remove(conn) conn.close() return broadcast(message) except (BlockingIOError, ConnectionResetError): print(f"客户端 {key.data[1]} 连接异常") sel.unregister(conn) clientlist.remove(conn) conn.close() return def broadcast(message): # 遍历发送时添加异常捕获 invalid_clients = [] for client in clientlist: try: client.send(message) except: invalid_clients.append(client) # 清理无效连接 for c in invalid_clients: clientlist.remove(c) try: sel.unregister(c) c.close() except: pass host = "127.0.0.1" port = 65432 sel = sl.DefaultSelector() server=sk.socket(sk.AF_INET,sk.SOCK_STREAM) server.bind((host,port)) server.listen() server.setblocking(False) sel.register(server, sl.EVENT_READ, data=(accept,None)) clientlist=[] print("initialisation complete") while True: clients=sel.select() for key, mask in clients: callback = key.data[0] callback(key, mask)
客户端可选优化
可以给客户端的recv也添加异常捕获,处理服务端断开的场景,避免客户端无限阻塞。
内容的提问来源于stack exchange,提问作者Ogglie Ostrich
相关产品推荐
相关产品推荐

