Aiohttp: Cliente Websocket suspenso / bloqueado?

Criado em 26 set. 2018  ·  12Comentários  ·  Fonte: aio-libs/aiohttp

Longa história curta

ws.receive() parece estar travado apesar dos dados fluírem do websocket.

Comportamento esperado

ws.receive deve produzir e relatar algo.

Comportamento real

ele apenas trava / bloqueia no websocket.

Passos para reproduzir

O código abaixo é o código de chamada.

    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.

No entanto, o soquete está aberto e servindo dados como pode ser visto pelo 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] $

Seu ambiente

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

atualizar...

A versão Python é 3.7.0

bug

Todos 12 comentários

GitMate.io acredita que os problemas possivelmente relacionados são https://github.com/aio-libs/aiohttp/issues/2200 (os soquetes do cliente em segundo plano travam no SIGINT), https://github.com/aio-libs/aiohttp/issues/ 1002 (resposta do Websocket .close () pode travar indefinidamente), https://github.com/aio-libs/aiohttp/issues/376 (ProactorEventLoop trava), https://github.com/aio-libs/aiohttp/issues / 3027 (ws_connect travar) e https://github.com/aio-libs/aiohttp/issues/265 (Implementar websocket do lado do cliente).

parece que client_ws.py # L204 não está retornando ... alguma ideia?

O servidor é um decodificador SkyQ, então pode não ser uma implementação de WS totalmente em conformidade com RFC. Por exemplo, ele não respeita ping / poings ... E há aquele estranho �~� na carga útil ... Não tenho certeza se isso é um opcode ou alguma outra peculiaridade ...

Websocket é um protocolo binário que �~� codifica um tipo e comprimento de mensagem.
Desculpe, não posso ajudá-lo sem sua participação na decodificação e comparação com o tamanho real da carga útil.

�~� é 001111110111111000111111 em binário, que é: 3F7E3F em hexadecimal.

esses bytes se parecem com este de ponta a ponta ...
00111111
01111110
00111111

Não sou um especialista na RFC, mas o que isso quer dizer em termos de tipo e comprimento?

Você acha que o problema é com isso? Ou seja, não está de acordo com o comprimento real da carga útil, e a coisa esperando por um quadro extra, por exemplo?

Curiosamente, este cliente web socket cli , escrito em go, lida bem com a saída, incluindo o binário no topo:

`` `
ws ws: // skyq: 9006 / as / sistema / status
<{
"camessage": {
"motivo": "sem mensagem",
"state": "indisponível"
},
"drmstatus": {
"state": "disponível"
},
"direitos": [
"ANALYTICS",
"BIGBASIC",
"ETHAN_APP_1",
"HD",
"PDL",
"SKY_DRM_CE",
"SKY_DRM_MR",
"SKY_IPPV",
"ULTRA +",
"SKY +",
"GATEWAYENABLER",
"SIDELOAD"
],
"epginfobits": {
"epginfobits": "0xFDE5FFC0",
"máscara": "0x5FFA003F",
"state": "disponível"
},
"gatewayservices": {
"state": "disponível"
},
"hdmi": {
"2160p10bitCapable": falso,
"authenticatedHDCP": "NONE",
"motivo": "a porta de saída HDMI está desativada",
"sinkHDCP": "NONE",
"sinkHLG": falso,
"sinkUHD": falso,
"state": "indisponível",
"uhdConfigured": false
},
"rede" : {
"state": "disponível"
},
"nssplayback": {
"state": "disponível"
},
"pvr": {
"state": "disponível"
},
"cronograma" : {
"lastdate": "20181003",
"state": "disponível"
},
"servicelist": {
"state": "disponível"
},
"cartão inteligente" : {
"ativo": verdadeiro,
"bouquet": "4101",
"countryCode": "GBR",
"moeda": "GBP",
"cwe": verdadeiro,
"agregado familiar": "10947783",
"emparelhado": verdadeiro,
"state": "disponível",
"subbouquet": "1",
"limite de transação": 65535,
"viewCardNumber": "725 325 260"
},
"swupdate": {
"razão": "IDLE",
"state": "indisponível"
},
"atualizações do sistema" : {
"direitos": 2,
"instalar": 1,
"servicegenres": 1,
"smartcard": 1
},
"updatetask": {
"motivo": "sem atualização",
"state": "indisponível"
}
}

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

O protocolo diz: o primeiro 3F byte na mensagem WS é proibido (reservado para uso futuro).
Tem certeza de que não perdeu nada?

Presumo que você esteja se referindo ao 2d, 3o e 4o bits no protocolo de enquadramento de base. Nesse caso, sim, talvez eu tenha entendido errado. Pode ser um problema Unicode / ASCII. Vou dar outra olhada.

Tentei alguns outros conversores online ... Estou entendendo agora:
hex = ef bf bd 7e ef bf bd
binário = 11101111 10111111 10111101 01111110 11101111 10111111 10111101

Se de acordo com o 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_.

Então, parece que RSV1 e RSV2 estão sendo definidos como 1, como você sugere, a menos que alguma tensão esteja sendo negociada? Eu não faço ideia.

O número de caracteres no documento JSON é 1703

O RFC diz o seguinte sobre o comprimento da 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".

Então, diante de tudo isso, para onde vamos a seguir? Se o servidor tiver morte cerebral e não for compatível com RFC, preciso contornar isso, como outro cliente claramente pode fazer ... Você pode dar alguma sugestão?

Ainda parece suspeito.
A única extensão conhecida é a compactação WebSocket que utiliza RSV1. Não sei qual extensão usa RSV2.
Além disso, opcode FF também é reservado e não deve ser usado.

A menos que você não saiba o que o servidor envia, não há como "consertar" um cliente.

@asvetlov Eu tentei a biblioteca websockets e funciona ... Então vou continuar com ela, já que não posso perder mais tempo depurando esta biblioteca com você ...

aoihttp - não 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())

Sem taxa para fechar este tíquete se desejar, mas você pode querer mantê-lo aberto, pois parece ser um bug ... Ou pelo menos uma situação que não obedece a lei de Postel.

Agradeço sua ajuda para tentar resolver isso.

Ainda parece suspeito.
A única extensão conhecida é a compactação WebSocket que utiliza RSV1. Não sei qual extensão usa RSV2.
Além disso, opcode FF também é reservado e não deve ser usado.

Oi @asvetlov

A menos que você não saiba o que o servidor envia, não há como "consertar" um cliente.

Eu acho que você quis dizer "a menos que, você sabe o que um servidor envia, você não pode consertar um cliente."

Nesse caso, discordo totalmente deste ponto.

Você está familiarizado com o princípio de robustez ?

"Seja conservador no que envia e liberal no que recebe."

Independentemente das OBRIGAÇÕES e OBRIGAÇÕES, na RFC, acho que você está interpretando e esperando que as entidades cumpram a RFC _completamente _... É claro que não, e muitos servidores de internet e clientes por aí, de vários tipos, têm horrendas implementações meio-bastardizadas de protocolos.

Uma boa implementação de biblioteca / protocolo deve perdoá-los na medida do possível. Veja RFC1122 . É completamente possível escrever um cliente de protocolo para perdoar (algumas) violações de protocolo sem ter acesso ao outro participante na comunicação.

Eu sinto que este não é o caso aqui, você parece estar assumindo a visão linha-dura de que se um host não for 100% compatível, então isso é ruim para ele, nós não falaremos com ele. Isso é muito ruim. É como dizer: 'Eu só falo o inglês da rainha, e se você vier falar comigo em algum dialeto esquisito de inglês, eu não falarei com você.'

Claramente, esta não é uma boa abordagem se todo o objetivo da biblioteca é facilitar a comunicação.

Minha sugestão, portanto, é considerar onde você pode relaxar os requisitos rigorosos do protocolo (talvez através do método kwargs, que ativa o manuseio "relaxado") e ver se você pode fazer as coisas falarem mesmo quando o outro lado não está se comportando corretamente .

Além dessa crítica, gostaria de agradecer a você e aos colaboradores de aio-libs por todo o seu trabalho para a comunidade. 👍 🥇

@asvetlov Apenas uma atualização sobre isso ... Eu descobri que meu servidor (não compatível) requer um cabeçalho Upgrade: maiúsculo antes de funcionar. Apenas pensei em mencioná-lo no caso de a questão dos cabeçalhos de caixa de título estar sendo considerada por vocês ... Este é um caso real de uma violação de protocolo de um servidor mal implementado que faz com que o cliente do websocket não funcione.

Esta página foi útil?
0 / 5 - 0 avaliações