Skip to main content
 首页 » 编程设计

Python:同步程序中的Websockets

2025年05月04日33www_RR

我有一个标准的同步 python 程序,需要能够从 websockets 读取数据并使用数据更新 GUI。然而,asyncio creep 总是让我绊倒。

我如何制作一个模块:

  1. 接受对多个来源的多个订阅
  2. 只要有数据就向请求者发送更新
  3. 为每个 URL 打开一个 websocket 连接
  4. 如果关闭则重置 websocket

这是我已经拥有的,但它在很多地方都失败了:

  1. run_forever() 表示循环在订阅完成之前卡住,然后 handle() 卡在错误的 while 循环
  2. 它似乎不想在套接字关闭时重新启动套接字,因为 websockets 对象没有 connected 属性(没有 s 的 websocket 有,但我不清楚它们之间的区别也无法在网上找到信息)
  3. 我完全不确定我的方法是否正确。

与此抗争了数周。希望得到一些指点。

class WSClient(): 
    subscriptions = set() 
    connections = {} 
    started = False 
 
    def __init__(self): 
        self.loop = asyncio.get_event_loop() 
 
    def start(self): 
        self.started = True 
        self.loop.run_until_complete(self.handle()) 
        self.loop.run_until_forever()  # problematic, because it does not allow new subscribe() events 
 
    async def handle(self): 
        while len(self.connections) > 0: 
            # listen to every websocket 
            futures = [self.listen(self.connections[url]) for url in self.connections] 
            done, pending = await asyncio.wait(futures) 
 
            # the following is apparently necessary to avoid warnings 
            # about non-retrieved exceptions etc 
            try: 
                data, ws = done.pop().result() 
            except Exception as e: 
                print("OTHER EXCEPTION", e) 
 
            for task in pending: 
                task.cancel() 
 
    async def listen(self, ws): 
        try: 
            async for data in ws: 
                data = json.loads(data) 
                # call the subscriber (listener) back when there's data 
                [s.listener._handle_result(data) for s in self.subscriptions if s.ws == ws] 
        except Exception as e: 
            print('ERROR LISTENING; RESTARTING SOCKET', e) 
            await asyncio.sleep(2) 
            self.restart_socket(ws) 
 
    def subscribe(self, subscription): 
        task = self.loop.create_task(self._subscribe(subscription)) 
        asyncio.gather(task) 
 
        if not self.started: 
            self.start() 
 
    async def _subscribe(self, subscription): 
        try: 
            ws = self.connections.get(subscription.url, await websockets.connect(subscription.url)) 
            await ws.send(json.dumps(subscription.sub_msg)) 
 
            subscription.ws = ws 
            self.connections[subscription.url] = ws 
            self.subscriptions.add(subscription) 
        except Exception as e: 
            print("ERROR SUBSCRIBING; RETRYING", e) 
            await asyncio.sleep(2) 
            self.subscribe(subscription) 
 
    def restart_socket(self, ws): 
        for s in self.subscriptions: 
            if s.ws == ws and not s.ws.connected: 
                print(s) 
                del self.connections[s.url] 
                self.subscribe(s) 

请您参考如下方法:

I have a bog-standard synchronous python program that needs to be able to read data from websockets and update the GUI with the data. However, asyncio creep is constantly tripping me up.

正如您提到的 GUI,那么它可能不是“沼泽标准同步 Python 程序”。通常一个 GUI 程序有一个非阻塞的事件驱动的主线程,它允许并发的用户行为和回调。这与 asyncio 非常相似,它通常是 asyncio 与 GUI 一起工作的常用方式,使用特定于 GUI 的事件循环来替换 asyncio 中的默认事件循环,这样您的 asyncio 协程就可以在 GUI 事件循环中运行,您可以避免调用 run_forever() 阻塞一切。

另一种方法是在单独的线程中运行异步事件循环,这样您的程序就可以同时等待 websocket 数据和用户点击。我已经按如下方式重写了您的代码:

import asyncio 
import threading 
import websockets 
import json 
 
 
class WSClient(threading.Thread): 
 
    def __init__(self): 
        super().__init__() 
        self._loop = None 
        self._tasks = {} 
        self._stop_event = None 
 
    def run(self): 
        self._loop = asyncio.new_event_loop() 
        self._stop_event = asyncio.Event(loop=self._loop) 
        try: 
            self._loop.run_until_complete(self._stop_event.wait()) 
            self._loop.run_until_complete(self._clean()) 
        finally: 
            self._loop.close() 
 
    def stop(self): 
        self._loop.call_soon_threadsafe(self._stop_event.set) 
 
    def subscribe(self, url, sub_msg, callback): 
        def _subscribe(): 
            if url not in self._tasks: 
                task = self._loop.create_task( 
                    self._listen(url, sub_msg, callback)) 
                self._tasks[url] = task 
 
        self._loop.call_soon_threadsafe(_subscribe) 
 
    def unsubscribe(self, url): 
        def _unsubscribe(): 
            task = self._tasks.pop(url, None) 
            if task is not None: 
                task.cancel() 
 
        self._loop.call_soon_threadsafe(_unsubscribe) 
 
    async def _listen(self, url, sub_msg, callback): 
        try: 
            while not self._stop_event.is_set(): 
                try: 
                    ws = await websockets.connect(url, loop=self._loop) 
                    await ws.send(json.dumps(sub_msg)) 
                    async for data in ws: 
                        data = json.loads(data) 
 
                        # NOTE: please make sure that `callback` won't block, 
                        # and it is allowed to update GUI from threads. 
                        # If not, you'll need to find a way to call it from 
                        # main/GUI thread (similar to `call_soon_threadsafe`) 
                        callback(data) 
                except Exception as e: 
                    print('ERROR; RESTARTING SOCKET IN 2 SECONDS', e) 
                    await asyncio.sleep(2, loop=self._loop) 
        finally: 
            self._tasks.pop(url, None) 
 
    async def _clean(self): 
        for task in self._tasks.values(): 
            task.cancel() 
        await asyncio.gather(*self._tasks.values(), loop=self._loop)