UDP客户端需定时发送Ping并监听消息,求异步实现方案
解决UDP客户端同时发送与监听的异步方案
下午好!刚接触Socket和异步编程不用着急,你遇到的是同步IO阻塞的典型问题——当前代码里listen函数的recvfrom是阻塞式的,没收到服务器消息就会一直卡着,自然没法按时继续发送Ping了。异步确实是解决这个问题的核心思路,我给你几个实用的实现方案,从易到难:
方案1:多线程(最直观,改动最小)
把发送Ping和监听消息放到两个独立的线程里,这样监听的阻塞不会影响发送的定时任务。
修改后的代码如下:
import socket from time import sleep from contextlib import contextmanager import threading UDP_IP_ADDRESS = "127.0.0.1" UDP_PORT_NO = 14004 Message = b"Hello World, From Client B" @contextmanager def socket_ctx(): my_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) my_socket.bind(('127.0.0.1', 0)) try: yield my_socket finally: my_socket.close() def send_loop(client_sock): while True: client_sock.sendto(Message, (UDP_IP_ADDRESS, UDP_PORT_NO)) print("Ping sent") sleep(2) def listen_loop(client_sock): while True: data, addr = client_sock.recvfrom(100) print(f"Received from {addr}: {data}") with socket_ctx() as sock: # 启动监听线程,设置daemon=True让线程随主线程退出 listen_thread = threading.Thread(target=listen_loop, args=(sock,), daemon=True) listen_thread.start() # 主线程负责定时发送Ping send_loop(sock)
- 关键点:UDP Socket是线程安全的,所以可以直接在两个线程共享;守护线程的设置能避免程序退出时残留线程。
- 优点:理解简单,改动原有代码少;缺点:线程切换有少量开销,对于你的游戏客户端场景完全够用。
方案2:asyncio异步IO(更现代的异步方式)
用Python原生的asyncio库实现协程异步,不需要线程,单线程内处理并发任务,适合高并发场景。
代码示例:
import asyncio import socket UDP_IP_ADDRESS = "127.0.0.1" UDP_PORT_NO = 14004 Message = b"Hello World, From Client B" async def send_ping(sock): while True: sock.sendto(Message, (UDP_IP_ADDRESS, UDP_PORT_NO)) print("Ping sent") await asyncio.sleep(2) async def listen_messages(sock): while True: # 用asyncio提供的异步socket方法,不会阻塞事件循环 data, addr = await asyncio.get_event_loop().sock_recvfrom(sock, 100) print(f"Received from {addr}: {data}") async def main(): sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind(('127.0.0.1', 0)) try: # 同时运行发送和监听两个协程 await asyncio.gather( send_ping(sock), listen_messages(sock) ) finally: sock.close() if __name__ == "__main__": asyncio.run(main())
- 关键点:
asyncio.gather能让多个协程并行执行;sock_recvfrom是异步IO方法,不会卡主整个程序的事件循环。 - 优点:无线程切换开销,适合大量客户端的高并发场景;缺点:需要理解协程和异步IO的基础概念,学习成本稍高。
方案3:selectors多路复用(底层高效方案)
用selectors模块实现IO多路复用,单线程内同时监听Socket的可读事件,同时定时触发发送操作,适合需要精细控制IO的场景。
代码示例:
import socket import selectors import time UDP_IP_ADDRESS = "127.0.0.1" UDP_PORT_NO = 14004 Message = b"Hello World, From Client B" sel = selectors.DefaultSelector() def send_ping(sock): sock.sendto(Message, (UDP_IP_ADDRESS, UDP_PORT_NO)) print("Ping sent") # 记录下次发送时间 global next_send_time next_send_time = time.time() + 2 def handle_read(sock, mask): data, addr = sock.recvfrom(100) print(f"Received from {addr}: {data}") sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind(('127.0.0.1', 0)) # 注册Socket的可读事件 sel.register(sock, selectors.EVENT_READ, handle_read) next_send_time = time.time() send_ping(sock) try: while True: # 计算下次发送前的等待时间,避免无限阻塞 timeout = max(0, next_send_time - time.time()) events = sel.select(timeout=timeout) for key, mask in events: callback = key.data callback(key.fileobj) # 检查是否到了发送Ping的时间 if time.time() >= next_send_time: send_ping(sock) finally: sel.unregister(sock) sock.close() sel.close()
- 关键点:通过
select监听Socket的可读事件,同时定时检查发送时间,实现非阻塞的并发处理。 - 优点:底层高效,完全可控;缺点:代码相对复杂,适合有一定Socket编程基础的场景。
你可以根据自己的需求选择方案,刚入门的话推荐先试多线程方案,理解起来最快,能快速解决你的问题;如果想深入异步编程,再尝试asyncio的方案。
内容的提问来源于stack exchange,提问作者Will Parzybok
相关产品推荐
相关产品推荐

