简体   繁体   中英

Python websockets lib client persistent connection (with class implementation)

I'm trying to implement a websocket client in python using websockets and the apparently mandatory asyncio which I never used before (and I have a hard time to understand...).

I've read a lot on the subject and saw (too) many examples here and everywhere, but I can't find a way to properly make a websocket client with a persistent connection.

  • I need to have a persistent connection because the commands need to be requested on the same connection, the first one being an authentication command.
  • The remote server is a 3rd party API I don't have any control over.
  • I suppose I could run an authentication request along with each command my program sends but that does not feel right to open > auth > request > close for each command instead of keeping one connection alive during the whole program's life
  • My implementation is a library using many classes and I need to wrap the websocket connector/handler in one of them

Here's what I have right now, based on examples I found here and there (with some obfuscated data) :

import json
import asyncio
from websockets import connect


URL = 'wss://server.com/endpoint'


class Websocket:
    async def __aenter__(self):
        self._conn = connect(URL)
        self.websocket = await self._conn.__aenter__()
        return self

    async def __aexit__(self, *args, **kwargs):
        await self._conn.__aexit__(*args, **kwargs)

    async def send(self, message):
        await self.websocket.send(message)

    async def receive(self):
        return await self.websocket.recv()


class Handler:
    def __init__(self):
        self.wws = Websocket()
        self.loop = asyncio.get_event_loop()

    def command(self, cmd):
        return self.loop.run_until_complete(self.__async__command(cmd))

    async def __async__command(self, cmd):
        async with self.wws as echo:
            await echo.send(json.dumps(cmd))
            return await echo.receive()


def main():
    handler = Handler()

    foo = handler.command('authentication command')
    print('auth: ', foo)

    bar = handler.command('another command to run depending on the first authentication')
    print('command: ', bar)


if __name__ == '__main__':
    main()

Basically right now I get these answers (simplified and obfuscated) :

auth: Ok, authenticated
command: Command refused, not authenticated

I suppose my problem is that the block async with self.wws as echo: kind of create the connection, runs its code then drop it instead of keeping the connection alive. Since we are not using a usual __init__ here but some asyncio voodoo I don't understand, I'm kind of stuck.

I think your diagnosis is correct, the problem is that the async context manager it creating and closing a connection for each call of Handler.command ... really not want you want.

Instead you could just synchronously establish the websocket connection during the init of Handler and then store the connection websocket (instance of type WebSocketClientProtocol ) as a class member for later use, as in this sample code:

import json
import asyncio
from websockets import connect

URL = 'ws://localhost:8000'

class Handler:

    def __init__(self):
        self.ws = None
        self.loop = asyncio.get_event_loop()
        # perform a synchronous connect
        self.loop.run_until_complete(self.__async__connect())

    async def __async__connect(self):
        print("attempting connection to {}".format(URL))
        # perform async connect, and store the connected WebSocketClientProtocol
        # object, for later reuse for send & recv
        self.ws = await connect(URL)
        print("connected")

    def command(self, cmd):
        return self.loop.run_until_complete(self.__async__command(cmd))

    async def __async__command(self, cmd):
        await self.ws.send(json.dumps(cmd))
        return await self.ws.recv()


def main():
    handler = Handler()

    foo = handler.command('authentication command')
    print('auth: ', foo)

    bar = handler.command('another command to run depending on the first authentication')
    print('command: ', bar)


if __name__ == '__main__':
    main()

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM