Aiohttp: Le client Websocket est bloqué / bloqué?

Créé le 26 sept. 2018  ·  12Commentaires  ·  Source: aio-libs/aiohttp

Longue histoire courte

ws.receive() semble se bloquer malgré les données provenant de websocket.

Comportement attendu

ws.receive devrait afficher et signaler quelque chose.

Comportement réel

il se bloque / bloque simplement sur le websocket.

Étapes à suivre pour reproduire

Le code ci-dessous est le code d'appel.

    async def _ws_subscribe(self,
                            session: ClientSession,
                            url: str
                            ) -> Dict:
        """ Fetch data from web socket asynchronously.

        This helper method fetches data from a web socket asynchronously. It is used to
        fetch subscribe to websockets of the SkyQ box.

        Args:
            session (aiohttp.ClientSession): Session to use when fetching the data.
            url (str): WebSocket URL to fetch.

        Returns:
            dict: The body of data returned.

        """
        LOGGER.debug(f"Inside _ws_subscribe()...")

        async with session.ws_connect(url, autoclose=False) as ws:
            while True:
                LOGGER.debug(f"Inside _ws_subscribe() infinite loop pre ws.receive.")
                payload = await ws.receive()
                LOGGER.debug(f"READ SOMETHING! _ws_subscribe() infinite loop post ws.receive")
                LOGGER.debug(f'type = {payload.type}')
                LOGGER.debug(f'payload data = {payload.data}')
                LOGGER.debug(f'payload exception = {ws.exception()}')
                if payload.type == aiohttp.WSMsgType.TEXT:
                    LOGGER.debug('Web-socket data received.')
                    asyncio.create_task(self._handle(payload))
                    # asyncio.ensure_future(self._handle(payload))
                elif payload.type == aiohttp.WSMsgType.BINARY:
                    LOGGER.debug('Web-socket binary data received.')
                elif payload.type == aiohttp.WSMsgType.PING:
                    LOGGER.debug('Web-socket ping received')
                    ws.pong()
                elif payload.type == aiohttp.WSMsgType.PONG:
                    LOGGER.debug('Web-socket pong received')
                else:
                    if payload.type == aiohttp.WSMsgType.CLOSE:
                        await ws.close()
                    elif payload.type == aiohttp.WSMsgType.ERROR:
                        LOGGER.info(f'Error during receive {ws.exception()}')
                    elif payload.type == aiohttp.WSMsgType.CLOSED:
                        pass

                    break

Journal résultant:

(pyskyq-4vSEKDfZ) ✔ [brad<strong i="6">@bradmac</strong>:~/Code/pyskyq] [16-create-status-end-point-property|✚ 5…2] $ pyskyq -vv green
[2018-09-26 09:12:14] DEBUG:pyskyq.cli:Starting SkyQ...
[2018-09-26 09:12:14] DEBUG:pyskyq.skyremote:Initialised SkyRemote object with host=skyq, port=49160
[2018-09-26 09:12:14] DEBUG:pyskyq.status:Initialised Status object object with host=skyq, port=9006
[2018-09-26 09:12:14] DEBUG:asyncio:Using selector: KqueueSelector
[2018-09-26 09:12:14] DEBUG:pyskyq.status:Asyncio event loop thread running...
[2018-09-26 09:12:14] DEBUG:pyskyq.skyq:Initialised SkyQ object with host=skyq.
[2018-09-26 09:12:14] INFO:pyskyq.cli:Script ends here
[2018-09-26 09:12:14] DEBUG:pyskyq.status:Setting up web socket listener on ws://skyq:9006/as/system/status.
[2018-09-26 09:12:14] DEBUG:pyskyq.status:Inside _ws_subscribe()...
[2018-09-26 09:12:14] DEBUG:asyncio:Get address info skyq:9006, type=<SocketKind.SOCK_STREAM: 1>
[2018-09-26 09:12:14] DEBUG:asyncio:Getting address info skyq:9006, type=<SocketKind.SOCK_STREAM: 1> took 15.352ms: [(<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_STREAM: 1>, 6, '', ('10.0.1.6', 9006))]
[2018-09-26 09:12:14] DEBUG:asyncio:poll 60478.847 ms took 14.299 ms: 1 events
[2018-09-26 09:12:14] DEBUG:asyncio:connect <socket.socket fd=8, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=6, laddr=('0.0.0.0', 0)> to ('10.0.1.6', 9006)
[2018-09-26 09:12:14] DEBUG:asyncio:poll 60461.657 ms took 21.045 ms: 1 events
[2018-09-26 09:12:14] DEBUG:asyncio:<socket.socket fd=8, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=6, laddr=('10.0.1.30', 50141), raddr=('10.0.1.6', 9006)> connected to 10.0.1.6:9006: (<_SelectorSocketTransport fd=8 read=polling write=<idle, bufsize=0>>, <aiohttp.client_proto.ResponseHandler object at 0x1033c8ac8>)
[2018-09-26 09:12:14] DEBUG:asyncio:poll 60429.970 ms took 7.192 ms: 1 events
[2018-09-26 09:12:14] DEBUG:pyskyq.status:Inside _ws_subscribe() infinite loop pre ws.receive.

Cependant, le socket est ouvert et sert des données comme cela peut être vu par netcat.

(pyskyq-4vSEKDfZ) ✔ [brad<strong i="6">@bradmac</strong>:~/Code/pyskyq] [16-create-status-end-point-property|✚ 5…2] $ nc skyq 9006
GET /as/system/status HTTP/1.1
Host: skyq:9006
Accept: */*
Connection: Upgrade
Upgrade: websocket

�~�{
   "camessage" : {
      "reason" : "no message",
      "state" : "unavailable"
   },
   "drmstatus" : {
      "state" : "available"
   },
   "entitlements" : [
      "ANALYTICS",
      "BIGBASIC",
      "ETHAN_APP_1",
      "HD",
      "PDL",
      "SKY_DRM_CE",
      "SKY_DRM_MR",
      "SKY_IPPV",
      "ULTRA+",
      "SKY+",
      "GATEWAYENABLER",
      "SIDELOAD"
   ],
   "epginfobits" : {
      "epginfobits" : "0xFDE5FFC0",
      "mask" : "0x5FFA003F",
      "state" : "available"
   },
   "gatewayservices" : {
      "state" : "available"
   },
   "hdmi" : {
      "2160p10bitCapable" : false,
      "authenticatedHDCP" : "1.x",
      "sinkHDCP" : "1.x",
      "sinkHLG" : false,
      "sinkUHD" : false,
      "state" : "available",
      "uhdConfigured" : false
   },
   "network" : {
      "state" : "available"
   },
   "nssplayback" : {
      "state" : "available"
   },
   "pvr" : {
      "state" : "available"
   },
   "schedule" : {
      "lastdate" : "20181003",
      "state" : "available"
   },
   "servicelist" : {
      "state" : "available"
   },
   "smartcard" : {
      "active" : true,
      "bouquet" : "4101",
      "countryCode" : "GBR",
      "currency" : "GBP",
      "cwe" : true,
      "householdid" : "10947783",
      "paired" : true,
      "state" : "available",
      "subbouquet" : "1",
      "transactionlimit" : 65535,
      "viewingCardNumber" : "725 325 260"
   },
   "swupdate" : {
      "reason" : "IDLE",
      "state" : "unavailable"
   },
   "systemupdates" : {
      "entitlements" : 2,
      "install" : 1,
      "servicegenres" : 1,
      "smartcard" : 1
   },
   "updatetask" : {
      "reason" : "no update",
      "state" : "unavailable"
   }
}
^C(pyskyq-4vSEKDfZ) ✔ [brad<strong i="7">@bradmac</strong>:~/Code/pyskyq] [16-create-status-end-point-property|✚ 5…2] $

Votre environnement

(pyskyq-4vSEKDfZ) ✔ [brad<strong i="6">@bradmac</strong>:~/Code/pyskyq] [16-create-status-end-point-property|✚ 5…2] $ uname -a
Darwin bradmac 17.7.0 Darwin Kernel Version 17.7.0: Thu Jun 21 22:53:14 PDT 2018; root:xnu-4570.71.2~1/RELEASE_X86_64 x86_64
(pyskyq-4vSEKDfZ) ✔ [brad<strong i="7">@bradmac</strong>:~/Code/pyskyq] [16-create-status-end-point-property|✚ 5…2] $ pip freeze
aiohttp==3.4.4
appnope==0.1.0
arrow==0.12.1
async-timeout==3.0.0
async-upnp-client==0.12.4
attrs==18.2.0
backcall==0.1.0
binaryornot==0.4.4
bleach==2.1.3
certifi==2018.4.16
chardet==3.0.4
click==6.7
cookiecutter==1.6.0
cycler==0.10.0
decorator==4.3.0
Django==2.1.1
entrypoints==0.2.3
ez-setup==0.9
future==0.16.0
html5lib==1.0.1
idna==2.7
idna-ssl==1.1.0
ipykernel==4.8.2
ipython==6.4.0
ipython-genutils==0.2.0
ipywidgets==7.2.1
jedi==0.12.0
Jinja2==2.10
jinja2-time==0.2.0
jsonschema==2.6.0
jupyter==1.0.0
jupyter-client==5.2.3
jupyter-console==5.2.0
jupyter-core==4.4.0
kaggle==1.4.2
kiwisolver==1.0.1
MarkupSafe==1.0
matplotlib==2.2.2
mistune==0.8.3
multidict==4.4.2
nbconvert==5.3.1
nbformat==4.4.0
notebook==5.5.0
numpy==1.14.4
pandocfilters==1.4.2
parso==0.2.1
pexpect==4.6.0
pickleshare==0.7.4
pipenv==2018.7.1
poyo==0.4.1
prompt-toolkit==1.0.15
ptyprocess==0.6.0
Pygments==2.2.0
pyparsing==2.2.0
PyScaffold==3.0.3
python-dateutil==2.7.3
python-didl-lite==1.1.0
pytz==2018.4
pyzmq==17.0.0
qtconsole==4.3.1
requests==2.19.1
Send2Trash==1.5.0
simplegeneric==0.8.1
six==1.11.0
terminado==0.8.1
testpath==0.3.1
tornado==5.0.2
tqdm==4.24.0
traitlets==4.3.2
urllib3==1.22
virtualenv==16.0.0
virtualenv-clone==0.3.0
voluptuous==0.11.5
wcwidth==0.1.7
webencodings==0.5.1
whichcraft==0.4.1
widgetsnbextension==3.2.1
yarl==1.2.6
(pyskyq-4vSEKDfZ) ✔ [brad<strong i="8">@bradmac</strong>:~/Code/pyskyq] [16-create-status-end-point-property|✚ 5…2] $

mettre à jour...

La version Python est 3.7.0

bug

Tous les 12 commentaires

GitMate.io pense que les problèmes éventuellement liés sont https://github.com/aio-libs/aiohttp/issues/2200 (les websockets client en arrière-plan se bloquent sur SIGINT), https://github.com/aio-libs/aiohttp/issues/ 1002 (la réponse Websocket .close () peut se bloquer indéfiniment), https://github.com/aio-libs/aiohttp/issues/376 (ProactorEventLoop se bloque), https://github.com/aio-libs/aiohttp/issues / 3027 (ws_connect se bloque) et https://github.com/aio-libs/aiohttp/issues/265 (implémentation du websocket côté client).

il semble que client_ws.py # L204 ne renvoie pas ... des idées?

Le serveur est un décodeur SkyQ, il se peut donc qu'il ne s'agisse pas d'une implémentation entièrement conforme à la RFC de WS. Par exemple, il n'honore pas ping / poings ... Et il y a cet étrange �~� dans la charge utile ... Je ne sais pas si c'est un opcode ou une autre bizarrerie ...

Websocket est un protocole binaire �~� code un type et une longueur de message.
Désolé, je ne peux pas vous aider sans votre participation au décodage et à la comparaison avec la taille réelle de la charge utile.

�~� est 001111110111111000111111 en binaire, soit: 3F7E3F en hexadécimal.

ces octets ressemblent à ça de bout en bout ...
00111111
01111110
00111111

Je ne suis pas un expert de la RFC, mais que dit-il en termes de type et de longueur?

Pensez-vous que le problème vient de cela? Ie, il n'accepte pas la longueur de la charge utile réelle, et la chose qui attend une trame supplémentaire, par exemple?

Fait intéressant, ce client cli de socket Web , écrit en go, gère correctement la sortie, y compris les éléments binaires en haut:

''
ws ws: // skyq: 9006 / as / system / status
<{
"camessage": {
"raison": "pas de message",
"état": "indisponible"
},
"drmstatus": {
"état": "disponible"
},
"droits": [
"ANALYTIQUE",
"BIGBASIC",
"ETHAN_APP_1",
"HD",
"PDL",
"SKY_DRM_CE",
"SKY_DRM_MR",
"SKY_IPPV",
"ULTRA +",
"SKY +",
"GATEWAYENABLER",
"SIDELOAD"
],
"epginfobits": {
"epginfobits": "0xFDE5FFC0",
"masque": "0x5FFA003F",
"état": "disponible"
},
"gatewayservices": {
"état": "disponible"
},
"hdmi": {
"2160p10bitCapable": faux,
"authenticatedHDCP": "AUCUN",
"raison": "Le port de sortie HDMI est désactivé",
"sinkHDCP": "AUCUN",
"sinkHLG": faux,
"sinkUHD": faux,
"état": "indisponible",
"uhdConfigured": false
},
"réseau" : {
"état": "disponible"
},
"nssplayback": {
"état": "disponible"
},
"pvr": {
"état": "disponible"
},
"programme" : {
"lastdate": "20181003",
"état": "disponible"
},
"servicelist": {
"état": "disponible"
},
"carte à puce" : {
"active": vrai,
"bouquet": "4101",
"countryCode": "GBR",
"devise": "GBP",
"cwe": vrai,
"Householdid": "10947783",
"apparié": vrai,
"état": "disponible",
"subbouquet": "1",
"limite de transaction": 65535,
"visualisationCardNumber": "725 325 260"
},
"swupdate": {
"raison": "IDLE",
"état": "indisponible"
},
"mises à jour système" : {
"droits": 2,
"installer": 1,
"servicegenres": 1,
"carte à puce": 1
},
"updatetask": {
"raison": "pas de mise à jour",
"état": "indisponible"
}
}

^ C
Interrompre
✔ [ brad @ bradmac : ~] $ `` `

Le protocole dit: le premier octet 3F dans le message WS est interdit (réservé pour une utilisation future).
Etes-vous sûr de ne rien manquer?

Je suppose que vous faites référence aux 2d, 3ème et 4ème bits du protocole de trame de base. Dans ce cas oui, je me suis peut-être trompé. Il peut s'agir d'un problème Unicode / ASCII. Je vais jeter un autre regard.

J'ai essayé quelques autres convertisseurs en ligne ... J'obtiens ceci maintenant:
hex = ef bf bd 7e ef bf bd
binaire = 11101111 10111111 10111101 01111110 11101111 10111111 10111101

Si selon la RFC 6455

5.2.  Base Framing Protocol

   This wire format for the data transfer part is described by the ABNF
   [RFC5234] given in detail in this section.  (Note that, unlike in
   other sections of this document, the ABNF in this section is
   operating on groups of bits.  The length of each group of bits is
   indicated in a comment.  When encoded on the wire, the most
   significant bit is the leftmost in the ABNF).  A high-level overview
   of the framing is given in the following figure.  In a case of
   conflict between the figure below and the ABNF specified later in
   this section, the figure is authoritative.

      0                   1                   2                   3
      0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
     +-+-+-+-+-------+-+-------------+-------------------------------+
     |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
     |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
     |N|V|V|V|       |S|             |   (if payload len==126/127)   |
     | |1|2|3|       |K|             |                               |
     +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
     |     Extended payload length continued, if payload len == 127  |
     + - - - - - - - - - - - - - - - +-------------------------------+
     |                               |Masking-key, if MASK set to 1  |
     +-------------------------------+-------------------------------+
     | Masking-key (continued)       |          Payload Data         |
     +-------------------------------- - - - - - - - - - - - - - - - +
     :                     Payload Data continued ...                :
     + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
     |                     Payload Data continued ...                |
     +---------------------------------------------------------------+

   FIN:  1 bit

      Indicates that this is the final fragment in a message.  The first
      fragment MAY also be the final fragment.

   RSV1, RSV2, RSV3:  1 bit each

      MUST be 0 unless an extension is negotiated that defines meanings
      for non-zero values.  If a nonzero value is received and none of
      the negotiated extensions defines the meaning of such a nonzero
      value, the receiving endpoint MUST _Fail the WebSocket
      Connection_.

Ensuite, il semble que RSV1 et RSV2 sont mis à 1, comme vous le suggérez, à moins qu'une etxension ne soit en cours de négociation? Je n'ai aucune idée.

Le nombre de caractères dans le document JSON est 1703

La RFC dit ceci à propos de la longueur de la charge utile:

Payload length:  7 bits, 7+16 bits, or 7+64 bits

      The length of the "Payload data", in bytes: if 0-125, that is the
      payload length.  If 126, the following 2 bytes interpreted as a
      16-bit unsigned integer are the payload length.  If 127, the
      following 8 bytes interpreted as a 64-bit unsigned integer (the
      most significant bit MUST be 0) are the payload length.  Multibyte
      length quantities are expressed in network byte order.  Note that
      in all cases, the minimal number of bytes MUST be used to encode
      the length, for example, the length of a 124-byte-long string
      can't be encoded as the sequence 126, 0, 124.  The payload length
      is the length of the "Extension data" + the length of the
      "Application data".  The length of the "Extension data" may be
      zero, in which case the payload length is the length of the
      "Application data".

Alors, étant donné tout cela, où allons-nous ensuite? Si le serveur est en état de mort cérébrale et non conforme à la RFC, je dois le contourner comme les autres clients peuvent clairement le faire ... Pouvez-vous proposer des suggestions?

Ça a toujours l'air suspect.
La seule extension connue est la compression WebSocket qui utilise RSV1. Je ne sais pas quelle extension utilise RSV2.
De plus, l'opcode FF est également réservé et ne doit pas être utilisé.

À moins que vous ne sachiez pas ce que le serveur envoie, il n'y a aucun moyen de «réparer» un client.

@asvetlov J'ai essayé la bibliothèque websockets et cela fonctionne ... Je vais donc y aller, car je ne peux pas passer plus de temps à déboguer cette bibliothèque avec vous ...

aoihttp - ne fonctionne pas

import asyncio
import logging
import sys

import aiohttp

LOGGER = logging.getLogger(__name__)
logformat = "[%(asctime)s] %(levelname)s:%(name)s:%(message)s"
logging.basicConfig(level=logging.DEBUG, stream=sys.stdout,
                    format=logformat, datefmt="%Y-%m-%d %H:%M:%S")


async def main():
    async with aiohttp.ClientSession() as session:
        async with session.ws_connect('http://skyq:9006/as/system/status') as ws:
            payload = await ws.receive()
            print(payload.text)

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

websockets - fonctionne

import asyncio
import websockets
import logging
import sys

LOGGER = logging.getLogger(__name__)
logformat = "[%(asctime)s] %(levelname)s:%(name)s:%(message)s"
logging.basicConfig(level=logging.DEBUG, stream=sys.stdout,
                    format=logformat, datefmt="%Y-%m-%d %H:%M:%S")


async def hello():
    async with websockets.connect(
            'ws://skyq:9006/as/system/status') as websocket:

        payload = await websocket.recv()
        print(f"{payload}")

asyncio.get_event_loop().run_until_complete(hello())

La fermeture de ce ticket est gratuite si vous le souhaitez, mais vous voudrez peut-être le garder ouvert car cela semble être un bug ... Ou du moins une situation qui n'obéit pas à la loi de Postel.

J'apprécie votre aide pour essayer de résoudre ce problème.

Ça a toujours l'air suspect.
La seule extension connue est la compression WebSocket qui utilise RSV1. Je ne sais pas quelle extension utilise RSV2.
De plus, l'opcode FF est également réservé et ne doit pas être utilisé.

Salut @asvetlov

À moins que vous ne sachiez pas ce que le serveur envoie, il n'y a aucun moyen de «réparer» un client.

Je pense que vous vouliez dire "à moins que vous ne sachiez ce qu'un serveur envoie, vous ne pouvez pas réparer un client".

Dans ce cas, je ne suis pas du tout d'accord avec ce point.

Vous connaissez le principe de robustesse ?

"Soyez conservateur dans ce que vous envoyez et libéral dans ce que vous recevez."

Indépendamment des MUST et DEVRAIT et NE DOIT PAS, dans la RFC, je pense que vous interprétez et attendez des entités qu'elles se conforment à la RFC _completely _... horribles demi-implémentations bâtardées de protocoles.

Une bonne implémentation de bibliothèque / protocole doit les tolérer dans la mesure du possible. Voir RFC1122 . Il est tout à fait possible d'écrire un client de protocole pour pardonner (certaines) violations de protocole sans avoir accès à l'autre participant à la communication.

Je pense que ce n'est pas le cas ici, vous semblez adopter le point de vue catégorique que si un hôte n'est pas conforme à 100%, tant pis pour lui, nous ne lui parlerons pas. C'est dommage. C'est comme dire: "Je ne parle que l'anglais de la reine, et si vous venez me parler dans un étrange dialecte anglais, je ne vous parlerai pas."

De toute évidence, ce n'est pas une bonne approche si le but de la bibliothèque est de faciliter la communication.

Ma suggestion, par conséquent, est de considérer où vous pouvez assouplir les exigences strictes du protocole (peut-être via la méthode kwargs, qui active la gestion "détendue"), et voir si vous pouvez faire parler les choses même lorsque l'autre côté ne se comporte pas correctement .

En plus de cette critique, je tiens à vous remercier ainsi que les contributeurs de aio-libs pour tout votre travail pour la communauté. 👍 🥇

@asvetlov Juste une mise à jour à ce sujet ... J'ai compris que mon serveur (non conforme) nécessite un en-tête Upgrade: majuscule avant de fonctionner. Je pensais juste que je le mentionnerais au cas où le problème des en-têtes Title Case serait considéré par vous ... Il s'agit d'un cas réel de violation de protocole d'un serveur mal implémenté qui empêche le client websocket de fonctionner.

Cette page vous a été utile?
0 / 5 - 0 notes

Questions connexes

kbaston picture kbaston  ·  3Commentaires

JulienPalard picture JulienPalard  ·  3Commentaires

amsb picture amsb  ·  3Commentaires

Codeberg-AsGithubAlternative-buhtz picture Codeberg-AsGithubAlternative-buhtz  ·  3Commentaires

asvetlov picture asvetlov  ·  4Commentaires