Python:同步程序中的Websocket

时间:2018-07-05 17:34:46

标签: python websocket python-asyncio

我有一个沼泽标准的同步python程序,该程序需要能够从websocket读取数据并使用该数据更新GUI。但是,异步蠕动不断使我绊倒。

我如何制作一个模块:

  1. 接受对多个来源的多次订阅
  2. 有数据时将更新发送给请求者
  3. 每个URL完全打开一个websocket连接
  4. 如果关闭,则重置网络套接字

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

  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)

2 个答案:

答案 0 :(得分:1)

  

我有一个沼泽标准的同步python程序,该程序需要能够从websocket读取数据并使用该数据更新GUI。但是,异步蠕动不断使我绊倒。

正如您提到的GUI一样,它可能不是“沼泽标准同步python程序”。通常,GUI程序具有非阻塞事件驱动主线程,该线程允许并发用户行为和回调。这与asyncio非常相似,并且通常是asyncio与GUI一起使用的一种常见方法,以使用特定于GUI的事件循环替换asyncio中的默认事件循环,以便您的asyncio协程仅在GUI事件循环中运行,您可以避免调用run_forever()阻止所有内容。

另一种方法是在单独的线程中运行asyncio事件循环,以便您的程序可以同时等待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)

答案 1 :(得分:-3)

您可以尝试使用龙卷风和高速公路将其用于网络套接字。