Aiohttp: Websocket Client hängen / blockieren?

Erstellt am 26. Sept. 2018  ·  12Kommentare  ·  Quelle: aio-libs/aiohttp

Um es kurz zu machen

ws.receive() scheint zu hängen, obwohl Daten aus dem Websocket fließen.

Erwartetes Verhalten

ws.receive sollte etwas ausgeben und melden.

Tatsächliches Verhalten

es hängt / blockiert nur am Websocket.

Schritte zum Reproduzieren

Der folgende Code ist der aufrufende Code.

    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

Ergebnisprotokoll:

(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.

Der Socket ist jedoch offen und liefert Daten, wie von netcat gesehen werden kann.

(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] $

Ihre Umgebung

(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] $

aktualisieren...

Die Python-Version ist 3.7.0

bug

Alle 12 Kommentare

GitMate.io glaubt, dass möglicherweise verwandte Probleme https://github.com/aio-libs/aiohttp/issues/2200 (Hintergrund-Client-Websockets hängen an SIGINT), https://github.com/aio-libs/aiohttp/issues/ sind. 1002 (Websocket-Antwort .close () kann unbegrenzt hängen bleiben), https://github.com/aio-libs/aiohttp/issues/376 (ProactorEventLoop hängt), https://github.com/aio-libs/aiohttp/issues / 3027 (ws_connect hang) und https://github.com/aio-libs/aiohttp/issues/265 (Implementieren des clientseitigen Websockets).

es sieht so aus, als würde client_ws.py # L204 nicht zurückkehren ... irgendwelche Ideen?

Der Server ist eine SkyQ-Set-Top-Box, daher ist WS möglicherweise nicht vollständig RFC-konform. Zum Beispiel werden Ping / Poings nicht berücksichtigt ... Und da ist das seltsame �~� in der Nutzlast ... Ich bin mir nicht sicher, ob das ein Opcode oder eine andere Eigenart ist ...

Websocket ist ein Binärprotokoll. �~� codiert einen Nachrichtentyp und eine Länge.
Entschuldigung, ich kann Ihnen nicht helfen, ohne dass Sie daran beteiligt sind, es zu dekodieren und mit der tatsächlichen Nutzlastgröße zu vergleichen.

�~� ist 001111110111111000111111 in binärer Form, dh: 3F7E3F in hex.

Diese Bytes sehen wie diese Ende-zu-Ende aus ...
00111111
01111110
00111111

Ich bin kein Experte für RFC, aber was sagt das in Bezug auf Typ und Länge aus?

Glaubst du, das Problem liegt darin? Dh es stimmt nicht mit der tatsächlichen Nutzlastlänge überein und das Ding wartet zum Beispiel auf einen zusätzlichen Frame?

Interessanterweise verarbeitet dieser in go geschriebene

`` `
ws ws: // skyq: 9006 / as / system / status
<{
"camessage": {
"Grund": "keine Nachricht",
"state": "nicht verfügbar"
},
"drmstatus": {
"state": "available"
},
"Ansprüche": [
"ANALYTICS",
"BIGBASIC",
"ETHAN_APP_1",
"HD",
"PDL",
"SKY_DRM_CE",
"SKY_DRM_MR",
"SKY_IPPV",
"ULTRA +",
"SKY +",
"GATEWAYENABLER",
"SIDELOAD"
],
"epginfobits": {
"epginfobits": "0xFDE5FFC0",
"Maske": "0x5FFA003F",
"state": "available"
},
"gatewayservices": {
"state": "available"
},
"hdmi": {
"2160p10bitCapable": false,
"authenticatedHDCP": "NONE",
"Grund": "HDMI-Ausgangsanschluss ist deaktiviert",
"sinkHDCP": "NONE",
"sinkHLG": falsch,
"sinkUHD": false,
"state": "nicht verfügbar",
"uhdConfigured": false
},
"Netzwerk": {
"state": "available"
},
"nssplayback": {
"state": "available"
},
"pvr": {
"state": "available"
},
"Zeitplan" : {
"lastdate": "20181003",
"state": "available"
},
"Serviceliste": {
"state": "available"
},
"Chipkarte" : {
"aktiv": wahr,
"Bouquet": "4101",
"countryCode": "GBR",
"Währung": "GBP",
"cwe": wahr,
"Haushalts-ID": "10947783",
"gepaart": wahr,
"state": "available",
"Subbouquet": "1",
"Transaktionslimit": 65535,
"ViewingCardNumber": "725 325 260"
},
"swupdate": {
"Grund": "LEERLAUF",
"state": "nicht verfügbar"
},
"Systemaktualisierung" : {
"Ansprüche": 2,
"install": 1,
"servicegenres": 1,
"Smartcard": 1
},
"updatetask": {
"Grund": "kein Update",
"state": "nicht verfügbar"
}}
}}

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

Das Protokoll besagt: Das erste 3F Byte in der WS-Nachricht ist verboten (für zukünftige Verwendung reserviert).
Bist du sicher, dass du etwas nicht verpasst hast?

Ich gehe davon aus, dass Sie sich auf das 2., 3. und 4. Bit im Basis-Framing-Protokoll beziehen. In diesem Fall ja, vielleicht habe ich es falsch verstanden. Es könnte sich um ein Unicode / ASCII-Problem handeln. Ich werde noch einen Blick darauf werfen.

Ich habe ein paar andere Online-Konverter ausprobiert ... Ich bekomme das jetzt:
hex = ef bf bd 7e ef bf bd
binär = 11101111 10111111 10111101 01111110 11101111 10111111 10111101

Wenn nach 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_.

Dann sieht es so aus, als würden RSV1 und RSV2 auf 1 gesetzt, wie Sie vorschlagen, es sei denn, es wird über eine gewisse Spannung verhandelt? Ich habe keine Ahnung.

Die Anzahl der Zeichen im JSON-Dokument beträgt 1703

Der RFC sagt dies über die Nutzlastlänge:

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".

Also, angesichts all dessen, wohin gehen wir als nächstes? Wenn der Server hirntot und nicht RFC-kompatibel ist, muss ich ihn umgehen, wie es andere Clients eindeutig können ... Können Sie Vorschläge machen?

Sieht immer noch verdächtig aus.
Die einzige bekannte Erweiterung ist die WebSocket-Komprimierung, bei der RSV1 verwendet wird. Ich weiß nicht, welche Erweiterung RSV2 verwendet.
Darüber hinaus ist auch der Opcode FF reserviert und sollte nicht verwendet werden.

Wenn Sie nicht wissen, was der Server sendet, können Sie einen Client nicht "reparieren".

@asvetlov Ich habe die websockets -Bibliothek ausprobiert und sie funktioniert ... Also werde ich damit weitermachen, da ich nicht mehr Zeit damit verbringen kann, diese Bibliothek mit Ihnen zu debuggen ...

aoihttp - funktioniert nicht

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 - funktioniert

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())

Sie können dieses Ticket gerne schließen, wenn Sie möchten, aber Sie möchten es möglicherweise offen halten, da es sich anscheinend um einen Fehler handelt ... oder zumindest um eine Situation, die nicht dem Gesetz von Postel entspricht.

Ich schätze Ihre Hilfe bei dem Versuch, dies anzugehen.

Sieht immer noch verdächtig aus.
Die einzige bekannte Erweiterung ist die WebSocket-Komprimierung, bei der RSV1 verwendet wird. Ich weiß nicht, welche Erweiterung RSV2 verwendet.
Darüber hinaus ist auch der Opcode FF reserviert und sollte nicht verwendet werden.

Hallo @asvetlov

Wenn Sie nicht wissen, was der Server sendet, können Sie einen Client nicht "reparieren".

Ich denke, Sie meinten: "Wenn Sie nicht wissen, was ein Server sendet, können Sie keinen Client reparieren."

In diesem Fall stimme ich diesem Punkt überhaupt nicht zu.

Sie kennen das Robustheitsprinzip ?

"Seien Sie konservativ in dem, was Sie senden, und liberal in dem, was Sie empfangen."

Unabhängig von MUSTs und SHOULDs und MUST NOTs, im RFC, denke ich, dass Sie Entitäten interpretieren und erwarten, dass sie den RFC vollständig einhalten schreckliche bastardisierte Halbimplementierungen von Protokollen.

Eine gute Bibliotheks- / Protokollimplementierung muss diese so weit wie möglich verzeihen. Siehe RFC1122 . Es ist durchaus möglich, einen Protokollclient zu schreiben, um (einige) Protokollverletzungen zu verzeihen, ohne Zugriff auf den anderen Teilnehmer an der Kommunikation zu haben.

Ich bin der Meinung, dass dies hier nicht der Fall ist. Sie scheinen die harte Ansicht zu vertreten, dass wir nicht mit ihm sprechen werden, wenn ein Host nicht 100% konform ist. Das ist schade. Es ist wie zu sagen: "Ich spreche nur das Englisch der Königin, und wenn Sie mit mir in einem seltsamen Dialekt des Englischen sprechen, werde ich nicht mit Ihnen sprechen."

Dies ist eindeutig kein guter Ansatz, wenn der Sinn der Bibliothek darin besteht, die Kommunikation zu erleichtern.

Mein Vorschlag ist daher, zu überlegen, wo Sie die strengen Anforderungen des Protokolls lockern können (möglicherweise über Methodenkwargs, die die "entspannte" Handhabung aktivieren) und zu prüfen, ob Sie die Dinge zum Sprechen bringen können, auch wenn sich die andere Seite nicht richtig verhält .

Abgesehen von dieser Kritik möchte ich Ihnen und den Mitwirkenden von aio-libs für all Ihre Arbeit für die Community danken. 👍 🥇

@asvetlov Nur ein Update dazu ... Ich habe herausgefunden, dass mein (nicht kompatibler) Server einen Großbuchstaben Upgrade: bevor er funktioniert. Ich dachte nur, ich würde es erwähnen, falls das Problem der Titelfall-Header von euch in Betracht gezogen wird ... Dies ist ein realer Fall einer Protokollverletzung von einem schlecht implementierten Server, die dazu führt, dass der Websocket-Client nicht funktioniert.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen