Aiohttp: Websocketクライアントがハング/ブロックしていますか?

作成日 2018年09月26日  ·  12コメント  ·  ソース: aio-libs/aiohttp

短編小説

ws.receive()は、WebSocketからデータが流れているにもかかわらず、ハングしているように見えます。

期待される動作

ws.receiveは何かを出力して報告する必要があります。

実際の動作

WebSocketでハング/ブロックするだけです。

再現する手順

以下のコードは呼び出しコードです。

    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

結果のログ:

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

ただし、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] $

あなたの環境

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

更新...

Pythonのバージョンは3.7.0です

bug

全てのコメント12件

GitMate.ioは、関連する可能性のある問題はhttps://github.com/aio-libs/aiohttp/issues/2200 (バックグラウンドのクライアントWebSocketがSIGINTでハングする)、 https: //github.com/aio-libs/aiohttp/issues/であると考えています。 1002 (Websocket応答.close()は無期限にハングする可能性があります)、 https://github.com/aio-libs/aiohttp/issues/376 (ProactorEventLoopがハングします)、 https://github.com/aio-libs/aiohttp/issues / 3027 (ws_connect hang)、およびhttps://github.com/aio-libs/aiohttp/issues/265 (クライアント側のWebSocketを実装します)。

client_ws.py#L204が戻って

サーバーはSkyQセットトップボックスであるため、WSのRFC準拠の実装ではない可能性があります。 たとえば、ping / poingsを尊重しません...そしてペイロードにその奇妙な�~�があります...それがオペコードなのか他の癖なのかわかりません...

Websocketはバイナリプロトコルであり、 �~�はメッセージの種類と長さをエンコードします。
申し訳ありませんが、それをデコードして実際のペイロードサイズと比較することにあなたの参加なしにあなたを助けることはできません。

�~�は、バイナリでは001111110111111000111111です。つまり、16進数では3F7E3Fです。

これらのバイトは、このエンドツーエンドのように見えます...
00111111
01111110
00111111

私はRFCの専門家ではありませんが、タイプと長さの観点からこれは何を言っていますか?

問題はそれだと思いますか? つまり、実際のペイロードの長さに同意していません。たとえば、追加のフレームを待っているのでしょうか。

興味深いことに、goで記述されたこのWebソケットCLIクライアントは、上部にあるバイナリのものを含め、出力を

`` `
ws ws:// skyq:9006 / as / system / status
<{
"camessage":{
「理由」:「メッセージなし」、
「状態」:「利用不可」
}、
"drmstatus":{
「状態」:「利用可能」
}、
「資格」:[
「分析」、
「BIGBASIC」、
「ETHAN_APP_1」、
「HD」、
「PDL」、
「SKY_DRM_CE」、
「SKY_DRM_MR」、
「SKY_IPPV」、
「ULTRA +」、
「SKY +」、
「GATEWAYENABLER」、
「サイドロード」
]、
"epginfobits":{
"epginfobits": "0xFDE5FFC0"、
"マスク": "0x5FFA003F"、
「状態」:「利用可能」
}、
"ゲートウェイサービス":{
「状態」:「利用可能」
}、
"hdmi":{
"2160p10bitCapable":false、
"authenticatedHDCP": "NONE"、
「理由」:「HDMI出力ポートが無効になっています」、
"sinkHDCP": "NONE"、
「sinkHLG」:false、
「sinkUHD」:false、
"状態": "利用不可"、
"uhdConfigured":false
}、
"通信網" : {
「状態」:「利用可能」
}、
"nssplayback":{
「状態」:「利用可能」
}、
"pvr":{
「状態」:「利用可能」
}、
「スケジュール」:{
「最終日」:「20181003」、
「状態」:「利用可能」
}、
"サービスリスト":{
「状態」:「利用可能」
}、
"スマートカード" : {
「アクティブ」:true、
「花束」:「4101」、
"countryCode": "GBR"、
「通貨」:「GBP」、
「cwe」:true、
"householdid": "10947783"、
「ペア」:true、
「状態」:「利用可能」、
「サブブーケ」:「1」、
"transactionlimit":65535、
"viewingCardNumber": "725 325 260"
}、
"swupdate":{
「理由」:「アイドル」、
「状態」:「利用不可」
}、
"システムアップデート" : {
「資格」:2、
「インストール」:1、
「サービスジャンル」:1、
「スマートカード」:1
}、
"updatetask":{
「理由」:「更新なし」、
「状態」:「利用不可」
}
}

^ C
割り込み
✔[ brad @ bradmac :〜] $ `` `

プロトコルによると、WSメッセージの最初の3Fバイトは禁止されています(将来の使用のために予約されています)。
あなたは何かを逃していなかったと確信していますか?

基本フレーミングプロトコルの2d、3、4番目のビットを参照していると思います。 その場合はそうです、多分私はそれを間違えました。 Unicode / ASCIIの問題である可能性があります。 もう一度見てみましょう。

私は他のいくつかのオンラインコンバーターを試しました...私は今これを手に入れています:
16進数= ef bf bd 7e ef bf bd
バイナリ= 11101111 10111111 10111101 01111110 11101111 10111111 10111101

RFC6455に準拠している場合

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

それから、あなたが提案するように、 RSV1RSV2が1に設定されているように見えます。 何も思いつきません。

JSONドキュメントの文字数は1703

RFCは、ペイロードの長さについて次のように述べています。

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

それで、それをすべて考えると、次にどこに行きますか? サーバーが脳死していてRFCに準拠していない場合は、他のクライアントが明らかにできるように、サーバーを回避する必要があります...何か提案がありますか?

まだ疑わしいようです。
唯一の既知の拡張機能は、RSV1を利用するWebSocket圧縮です。 どの拡張機能がRSV2を使用しているかわかりません。
さらに、オペコードFFも予約されているため、使用しないでください。

サーバーが何を送信するかわからない限り、クライアントを「修正」する方法はありません。

@asvetlov websocketsライブラリを試しましたが、動作します...このライブラリのデバッグにこれ以上時間を費やすことができないので、これを使用します...

aoihttp -機能しません

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 -動作します

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

必要に応じてこのチケットを閉じるのは無料ですが、バグのように見えるので開いたままにしておくことをお勧めします...または少なくともPostelの法則に従わない状況。

これに対処するためにあなたの助けに感謝します。

まだ疑わしいようです。
唯一の既知の拡張機能は、RSV1を利用するWebSocket圧縮です。 どの拡張機能がRSV2を使用しているかわかりません。
さらに、オペコードFFも予約されているため、使用しないでください。

こんにちは@asvetlov

サーバーが何を送信するかわからない限り、クライアントを「修正」する方法はありません。

「サーバーが何を送信するかを知らない限り、クライアントを修正することはできません」という意味だと思います。

その場合、私はこの点に完全に同意しません。

あなたはロバストネス原則に精通していますか?

「送信するものは保守的に、受信するものは寛大にしてください。」

必須事項、必須事項、必須事項に関係なく、RFCでは、エンティティがRFCに完全に準拠することを解釈および期待していると思います...明らかに準拠しておらず、さまざまなタイプの多くのインターネットサーバーやクライアントがプロトコルの恐ろしいろくでなしの半分の実装。

優れたライブラリ/プロトコルの実装は、可能な範囲でこれらを許容する必要があります。 RFC1122を参照してください。 通信の他の参加者にアクセスすることなく、(一部の)プロトコル違反を許容するプロトコルクライアントを作成することは完全に可能です。

ここではそうではないと思います。ホストが100%準拠していない場合は、ホストに対してあまりにも悪いとは言えないという強硬な見方をしているようです。 これは残念です。 「私は女王の英語しか話せません。あなたが奇妙な英語の方言で私に話しに来たら、私はあなたに話しません。」と言っているようなものです。

ライブラリの全体的な目的がコミュニケーションを促進することである場合、明らかにこれは良いアプローチではありません。

したがって、私の提案は、プロトコルの厳しい要件をどこで緩和できるかを検討し(おそらく、「緩和された」処理をオンにするメソッドkwargsを介して)、反対側が適切に動作していない場合でも物事を話せるかどうかを確認することです。 。

この批評とは別に、コミュニティのためにあなたのすべての仕事をしてくれたaio-libsからのあなたと貢献者に感謝したいと思います。 👍🥇

@asvetlovこれに関する単なる更新...私の(非準拠の)サーバーが機能する前に大文字のUpgrade:ヘッダーが必要であることがわかりました。 Title Caseヘッダーの問題が皆さんによって検討されている場合に備えて、言及したいと思います...これは、実装が不十分なサーバーからのプロトコル違反の実際のケースであり、WebSocketクライアントが機能しなくなります。

このページは役に立ちましたか?
0 / 5 - 0 評価