Aiohttp: ¿Cliente de Websocket colgando / bloqueando?

Creado en 26 sept. 2018  ·  12Comentarios  ·  Fuente: aio-libs/aiohttp

Larga historia corta

ws.receive() parece estar colgando a pesar de que los datos fluyen desde websocket.

Comportamiento esperado

ws.receive debería generar e informar algo.

Comportamiento real

simplemente se cuelga / bloquea en el websocket.

pasos para reproducir

El siguiente código es el código de llamada.

    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

Registro resultante:

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

Sin embargo, el socket está abierto y sirve datos, como puede ver 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] $

Tu entorno

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

actualizar...

La versión de Python es 3.7.0

bug

Todos 12 comentarios

GitMate.io cree que los posibles problemas relacionados son https://github.com/aio-libs/aiohttp/issues/2200 (los websockets del cliente en segundo plano se cuelgan de SIGINT), https://github.com/aio-libs/aiohttp/issues/ 1002 (La respuesta de Websocket .close () puede bloquearse indefinidamente), https://github.com/aio-libs/aiohttp/issues/376 (ProactorEventLoop se bloquea), https://github.com/aio-libs/aiohttp/issues / 3027 (ws_connect hang) y https://github.com/aio-libs/aiohttp/issues/265 (Implementar websocket del lado del cliente).

parece que client_ws.py # L204 no regresa ... ¿alguna idea?

El servidor es un decodificador SkyQ, por lo que es posible que no sea una implementación de WS totalmente compatible con RFC. Por ejemplo, no respeta los ping / poings ... Y hay ese extraño �~� en la carga útil ... No estoy seguro si es un código de operación o alguna otra peculiaridad ...

Websocket es un protocolo binario �~� codifica el tipo y la longitud de un mensaje.
Lo siento, no puedo ayudarlo sin su participación para decodificarlo y compararlo con el tamaño real de la carga útil.

�~� es 001111110111111000111111 en binario, que es: 3F7E3F en hexadecimal.

esos bytes se ven así de extremo a extremo ...
00111111
01111110
00111111

No soy un experto en RFC, pero ¿qué dice esto en términos de tipo y longitud?

¿Crees que el problema está en eso? Es decir, ¿no está de acuerdo con la longitud real de la carga útil y, por ejemplo, está esperando una trama adicional?

Curiosamente, este cliente cli de socket web , escrito en go, maneja bien la salida, incluidas las cosas binarias en la parte superior:

''
ws ws: // skyq: 9006 / as / system / status
<{
"camessage": {
"motivo": "sin mensaje",
"estado": "no disponible"
},
"drmstatus": {
"estado": "disponible"
},
"derechos": [
"ANALÍTICA",
"GRANDE",
"ETHAN_APP_1",
"HD",
"PDL",
"SKY_DRM_CE",
"SKY_DRM_MR",
"SKY_IPPV",
"ULTRA +",
"SKY +",
"GATEWAYENABLER",
"CARGA LATERAL"
],
"epginfobits": {
"epginfobits": "0xFDE5FFC0",
"máscara": "0x5FFA003F",
"estado": "disponible"
},
"gatewayservices": {
"estado": "disponible"
},
"hdmi": {
"2160p10bitCapable": falso,
"authenticatedHDCP": "NINGUNO",
"motivo": "El puerto de salida HDMI está desactivado",
"sinkHDCP": "NINGUNO",
"sinkHLG": falso,
"sinkUHD": falso,
"estado": "no disponible",
"uhdConfigured": falso
},
"la red" : {
"estado": "disponible"
},
"nssplayback": {
"estado": "disponible"
},
"pvr": {
"estado": "disponible"
},
"calendario" : {
"lastdate": "20181003",
"estado": "disponible"
},
"Lista de servicios" : {
"estado": "disponible"
},
"tarjeta electrónica" : {
"activo": verdadero,
"ramo": "4101",
"countryCode": "GBR",
"moneda": "GBP",
"cwe": cierto,
"familyid": "10947783",
"emparejado": verdadero,
"estado": "disponible",
"subramo": "1",
"transactionlimit": 65535,
"viewsCardNumber": "725325260"
},
"swupdate": {
"motivo": "IDLE",
"estado": "no disponible"
},
"actualizaciones del sistema" : {
"derechos": 2,
"instalar": 1,
"servicegenres": 1,
"tarjeta inteligente": 1
},
"updatetask": {
"motivo": "sin actualización",
"estado": "no disponible"
}
}

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

El protocolo dice: el primer byte 3F en el mensaje WS está prohibido (reservado para uso futuro).
¿Estás seguro de que no te perdiste nada?

Supongo que se está refiriendo al 2d, 3er y 4to bit en el protocolo de entramado base. En cuyo caso sí, tal vez me equivoqué. Podría ser un problema de Unicode / ASCII. Echaré otro vistazo.

Probé algunos otros convertidores en línea ... Estoy obteniendo esto ahora:
hex = ef bf bd 7e ef bf bd
binario = 11101111 10111111 10111101 01111110 11101111 10111111 10111101

Si de acuerdo con 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_.

Entonces parece que RSV1 y RSV2 se están configurando en 1, como sugiere, a menos que se esté negociando alguna extensión. No tengo idea.

El número de caracteres en el documento JSON es 1703

El RFC dice esto sobre la longitud de la carga útil:

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

Entonces, dado todo eso, ¿a dónde vamos ahora? Si el servidor tiene muerte cerebral y no cumple con RFC, entonces tengo que solucionarlo como claramente pueden hacer otros clientes ... ¿Pueden ofrecerme alguna sugerencia?

Todavía parece sospechoso.
La única extensión conocida es la compresión WebSocket que utiliza RSV1. No sé qué extensión usa RSV2.
Además, el código de operación FF también está reservado y no debe usarse.

A menos que no sepa lo que envía el servidor, no hay forma de "arreglar" un cliente.

@asvetlov Probé la biblioteca websockets y funciona ... Así que voy a seguir con eso, ya que no puedo dedicar más tiempo a depurar esta biblioteca contigo ...

aoihttp - no funciona

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

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

No hay cargo por cerrar este ticket si lo desea, pero es posible que desee mantenerlo abierto ya que parece ser un error ... O al menos una situación que no obedece a la ley de Postel.

Agradezco su ayuda para tratar de solucionar este problema.

Todavía parece sospechoso.
La única extensión conocida es la compresión WebSocket que utiliza RSV1. No sé qué extensión usa RSV2.
Además, el código de operación FF también está reservado y no debe usarse.

Hola @asvetlov

A menos que no sepa lo que envía el servidor, no hay forma de "arreglar" un cliente.

Creo que quería decir "a menos, que saben lo que envía un servidor, no se puede arreglar un cliente."

En cuyo caso, estoy completamente en desacuerdo con este punto.

¿Está familiarizado con el principio de robustez ?

"Sea conservador en lo que envíe y liberal en lo que reciba".

Independientemente de los MUST, DEBERÍAN y NO DEBE, en el RFC, creo que está interpretando y esperando que las entidades cumplan con el RFC _completamente _ ... Claramente no lo hacen, y muchos servidores y clientes de Internet, de varios tipos, han horribles medias implementaciones bastardas de protocolos.

Una buena implementación de biblioteca / protocolo debe perdonarlos en la medida de lo posible. Consulte RFC1122 . Es completamente posible escribir un cliente de protocolo para perdonar (algunas) violaciones del protocolo sin tener acceso al otro participante en la comunicación.

Siento que este no es el caso aquí, parece que está adoptando la posición de línea dura de que si un host no cumple al 100%, entonces es una lástima, no hablaremos con él. Esto es una lástima. Es como decir: 'Solo hablo el inglés de la reina, y si vienes a hablar conmigo en algún dialecto extraño del inglés, no te hablaré'.

Claramente, este no es un buen enfoque si el objetivo de la biblioteca es facilitar la comunicación.

Mi sugerencia, por lo tanto, es considerar dónde puede relajar los requisitos estrictos del protocolo (tal vez a través del método kwargs, que activa el manejo "relajado"), y ver si puede hacer que las cosas hablen incluso cuando la otra parte no se está comportando correctamente. .

Aparte de esta crítica, me gustaría agradecerles a ustedes y a los colaboradores de aio-libs por todo su trabajo para la comunidad. 👍 🥇

@asvetlov Solo una actualización sobre esto ... Me di cuenta de que mi servidor (no compatible) requiere un encabezado Upgrade: mayúsculas antes de que funcione. Solo pensé en mencionarlo en caso de que ustedes estén considerando el tema de los encabezados de Title Case ... Este es un caso real de una violación de protocolo de un servidor mal implementado que hace que el cliente websocket no funcione.

¿Fue útil esta página
0 / 5 - 0 calificaciones