WebSocket 從入門到寫出開源庫

      網(wǎng)友投稿 830 2025-03-31

      前言


      我已經(jīng) 2 個月沒有發(fā)文了,看到有人問: '那個專注爬蟲小奎因去哪了?',我就趕緊跳出來了。

      另外說明一下,德瑪西亞之翼-奎因這個 ID 現(xiàn)在換成了 AsyncIns

      我計劃在今年的夏天去帝都,在去之前我需要做好技術(shù)準(zhǔn)備,所以最近一直是在學(xué)習(xí)。我的學(xué)習(xí)方式很簡單明了:看文檔、讀源碼、造輪子。造輪子是我認(rèn)為能讓人進(jìn)步的最快、最有效的方法。

      前段時間需要通過 websocket 爬取一些數(shù)據(jù),網(wǎng)文介紹中都是使用 websocket-client 這個庫。但我的項目是異步的,我希望 websocket 數(shù)據(jù)讀取也能夠是異步的,然后我在 github 上搜索到了 websockets 這個庫,在使用和源碼閱讀中,我發(fā)現(xiàn) websockets 仍然不是我認(rèn)為理想的庫,所以我決定自己開發(fā)一個異步的 WebSocket 連接客戶端(async websocket client)。

      這一次我就跟大家分享 WebSocket 協(xié)議知識以及介紹我的開源庫 aiowebsocket。

      WebSocket 協(xié)議和知識

      WebSocket是一種在單個TCP連接上進(jìn)行全雙工通信的協(xié)議。WebSocket通信協(xié)議于2011年被IETF定為標(biāo)準(zhǔn)RFC 6455,并由RFC7936補(bǔ)充規(guī)范。WebSocket API也被W3C定為標(biāo)準(zhǔn)。

      WebSocket使得客戶端和服務(wù)器之間的數(shù)據(jù)交換變得更加簡單,允許服務(wù)端主動向客戶端推送數(shù)據(jù)。在WebSocket API中,瀏覽器和服務(wù)器只需要完成一次握手,兩者之間就直接可以創(chuàng)建持久性的連接,并進(jìn)行雙向數(shù)據(jù)傳輸。

      為什么會有 WebSocket

      以前,很多網(wǎng)站為了實現(xiàn)推送技術(shù),所用的技術(shù)都是輪詢。輪詢是在特定的的時間間隔(如每1秒),由瀏覽器對服務(wù)器發(fā)出HTTP請求,然后由服務(wù)器返回最新的數(shù)據(jù)給客戶端的瀏覽器。這種傳統(tǒng)的模式帶來很明顯的缺點,即瀏覽器需要不斷的向服務(wù)器發(fā)出請求,然而HTTP請求可能包含較長的頭部,其中真正有效的數(shù)據(jù)可能只是很小的一部分,顯然這樣會浪費很多的帶寬等資源。

      而比較新的技術(shù)去做輪詢的效果是Comet。這種技術(shù)雖然可以雙向通信,但依然需要反復(fù)發(fā)出請求。而且在Comet中,普遍采用的長鏈接,也會消耗服務(wù)器資源。

      在這種情況下,HTML5定義了WebSocket協(xié)議,能更好的節(jié)省服務(wù)器資源和帶寬,并且能夠更實時地進(jìn)行通訊。

      WebSocket 有什么優(yōu)點

      開銷少、時時性高、二進(jìn)制支持完善、支持?jǐn)U展、壓縮更優(yōu)。

      較少的控制開銷。在連接創(chuàng)建后,服務(wù)器和客戶端之間交換數(shù)據(jù)時,用于協(xié)議控制的數(shù)據(jù)包頭部相對較小。在不包含擴(kuò)展的情況下,對于服務(wù)器到客戶端的內(nèi)容,此頭部大小只有2至10字節(jié)(和數(shù)據(jù)包長度有關(guān));對于客戶端到服務(wù)器的內(nèi)容,此頭部還需要加上額外的4字節(jié)的掩碼。相對于HTTP請求每次都要攜帶完整的頭部,此項開銷顯著減少了。

      更強(qiáng)的實時性。由于協(xié)議是全雙工的,所以服務(wù)器可以隨時主動給客戶端下發(fā)數(shù)據(jù)。相對于HTTP請求需要等待客戶端發(fā)起請求服務(wù)端才能響應(yīng),延遲明顯更少;即使是和Comet等類似的長輪詢比較,其也能在短時間內(nèi)更多次地傳遞數(shù)據(jù)。

      保持連接狀態(tài)。與HTTP不同的是,Websocket需要先創(chuàng)建連接,這就使得其成為一種有* 狀態(tài)的協(xié)議,之后通信時可以省略部分狀態(tài)信息。而HTTP請求可能需要在每個請求都攜帶狀態(tài)信息(如身份認(rèn)證等)。

      更好的二進(jìn)制支持。Websocket定義了二進(jìn)制幀,相對HTTP,可以更輕松地處理二進(jìn)制內(nèi)容。

      可以支持?jǐn)U展。Websocket定義了擴(kuò)展,用戶可以擴(kuò)展協(xié)議、實現(xiàn)部分自定義的子協(xié)議。如部分瀏覽器支持壓縮等。

      更好的壓縮效果。相對于HTTP壓縮,Websocket在適當(dāng)?shù)臄U(kuò)展支持下,可以沿用之前內(nèi)容的上下文,在傳遞類似的數(shù)據(jù)時,可以顯著地提高壓縮率。

      握手是怎么回事?

      WebSocket 是獨立的、創(chuàng)建在 TCP 上的協(xié)議。

      Websocket 通過HTTP/1.1 協(xié)議的101狀態(tài)碼進(jìn)行握手。

      為了創(chuàng)建Websocket連接,需要通過瀏覽器發(fā)出請求,之后服務(wù)器進(jìn)行回應(yīng),這個過程通常稱為“握手”(handshaking)。

      WebSocket 協(xié)議規(guī)范

      WebSocket 是一個通信協(xié)議,它規(guī)定了一些規(guī)范和標(biāo)準(zhǔn)。它的協(xié)議標(biāo)準(zhǔn)為 RFC 6455,具體的協(xié)議內(nèi)容可以在tools.ietf.org中查看。

      協(xié)議共有 14 個部分,其中包括協(xié)議背景與介紹、握手、設(shè)計理念、術(shù)語約定、雙端要求、掩碼以及連接關(guān)閉等內(nèi)容。

      雙端交互流程

      客戶端與服務(wù)端交互流程如下所示:

      客戶端 - 發(fā)起握手請求 - 服務(wù)器接到請求后返回信息 - 連接建立成功 - 消息互通

      所以,要解決的第一個問題就是握手問題。

      WebSocket 從入門到寫出開源庫

      關(guān)于握手標(biāo)準(zhǔn),在協(xié)議中有說明:

      The opening handshake is intended to be compatible with HTTP-based

      server-side software and intermediaries, so that a single port can be

      used by both HTTP clients talking to that server and WebSocket

      clients talking to that server. ?To this end, the WebSocket client's

      handshake is an HTTP Upgrade request:

      GET?/chat?HTTP/1.1

      Host:?server.example.com

      Upgrade:?websocket

      Connection:?Upgrade

      Sec-WebSocket-Key:?dGhlIHNhbXBsZSBub25jZQ==

      Origin:?http://example.com

      Sec-WebSocket-Protocol:?chat,?superchat

      Sec-WebSocket-Version:?13

      In compliance with [RFC2616], header fields in the handshake may be

      sent by the client in any order, so the order in which different

      header fields are received is not significant.

      WebSocket 握手時使用的并不是 WebSocket 協(xié)議,而是 HTTP 協(xié)議,握手時發(fā)出的請求可以叫做升級請求。客戶端在握手階段通過:

      Upgrade: websocket ? Connection: Upgrade

      Connection 和 Upgrade 這兩個頭域告知服務(wù)端,要求將通信的協(xié)議轉(zhuǎn)換為 websocket。其中 Sec-WebSocket-Version、Sec-WebSocket-Protocol 這兩個頭域表明通信版本和協(xié)議約定, Sec-WebSocket-Key 則作為一個防止無端連接的保障(其實并沒有什么保障作用,因為 key 的值完全由客戶端控制,服務(wù)端并無驗證機(jī)制),其他幾個頭域則與 HTTP

      協(xié)議的作用一致。

      ##### 握手 - 服務(wù)端

      剛才只是客戶端發(fā)出一個 HTTP 請求,表明想要握手,服務(wù)端需要對信息進(jìn)行驗證,確認(rèn)以后才算握手成功(連接建立成功,可以雙向通信),然后服務(wù)端會給客戶端回復(fù):"小老弟你好,沒有內(nèi)鬼,連接達(dá)成!"

      服務(wù)端需要回復(fù)什么內(nèi)容呢?

      Status Code: 101 Web Socket Protocol Handshake ? Sec-WebSocket-Accept: T5ar3gbl3rZJcRmEmBT8vxKjdDo= ? Upgrade: websocket ? Connection: Upgrade

      首先,服務(wù)端會給出狀態(tài)碼,101 狀態(tài)碼表示服務(wù)器已經(jīng)理解了客戶端的請求,并且回復(fù) Connection 和 Upgrade 表示已經(jīng)切換成 websocket 協(xié)議。Sec-WebSocket-Accept 則是經(jīng)過服務(wù)器確認(rèn),并且加密過后的 Sec-WebSocket-Key。

      這樣,客戶端與服務(wù)端就完成了握手操作,達(dá)成一致,使用 WebSocket 協(xié)議進(jìn)行通信。

      你來我往 - 數(shù)據(jù)交流

      雙方握手成功并確認(rèn)協(xié)議后,就可以互相發(fā)送信息了。它們的信息是如何發(fā)送的呢?難道是:

      client:?Hello,?server?boy

      server:?Hello,?client?girl

      跟我們在微信和 QQ 中發(fā)信息是一樣的嗎?

      雖然我們看到的信息是這樣的,但是在傳輸過程中可不是這樣子的。傳輸這部也有相應(yīng)的規(guī)定:

      In the WebSocket Protocol, data is transmitted using a sequence of

      frames. ?To avoid confusing network intermediaries (such as

      intercepting proxies) and for security reasons that are further

      discussed in Section 10.3, a client MUST mask all frames that it

      sends to the server (see Section 5.3 for further details). ?(Note

      that masking is done whether or not the WebSocket Protocol is running

      over TLS.) ?The server MUST close the connection upon receiving a

      frame that is not masked. ?In this case, a server MAY send a Close

      frame with a status code of 1002 (protocol error) as defined in

      Section 7.4.1. ?A server MUST NOT mask any frames that it sends to

      the client. ?A client MUST close a connection if it detects a masked

      frame. ?In this case, it MAY use the status code 1002 (protocol

      error) as defined in Section 7.4.1. ?(These rules might be relaxed in

      a future specification.)

      The base framing protocol defines a frame type with an opcode, a

      payload length, and designated locations for "Extension data" and

      "Application data", which together define the "Payload data".

      Certain bits and opcodes are reserved for future expansion of the

      protocol.

      協(xié)議中規(guī)定傳輸時并不是直接使用 unicode 編碼進(jìn)行傳輸,而是使用幀(frame),數(shù)據(jù)幀協(xié)議定義了帶有操作碼的幀類型,有效載荷長度,以及“擴(kuò)展數(shù)據(jù)”和的指定位置應(yīng)用程序數(shù)據(jù)”,它們共同定義“有效載荷數(shù)據(jù)”。某些位和操作碼保留用于將來的擴(kuò)展協(xié)議。

      數(shù)據(jù)幀的格式如圖所示:

      幀由以下幾部分組成:

      FIN、RSV1、RSV2、RSV3、opcode、MASK、Payload length、Masking-key、Payload-Data。它們的含義和作用如下:

      1.FIN: 占 1bit

      0:不是消息的最后一個分片

      1:是消息的最后一個分片

      2.RSV1, RSV2, RSV3:各占 1bit

      一般情況下全為 0。當(dāng)客戶端、服務(wù)端協(xié)商采用 WebSocket 擴(kuò)展時,這三個標(biāo)志位可以非 0,且值的含義由擴(kuò)展進(jìn)行定義。如果出現(xiàn)非零的值,且并沒有采用 WebSocket 擴(kuò)展,連接出錯。

      3.Opcode: 4bit

      %x0:表示一個延續(xù)幀。當(dāng)?Opcode?為?0?時,表示本次數(shù)據(jù)傳輸采用了數(shù)據(jù)分片,當(dāng)前收到的數(shù)據(jù)幀為其中一個數(shù)據(jù)分片;

      %x1:表示這是一個文本幀(text?frame);

      %x2:表示這是一個二進(jìn)制幀(binary?frame);

      %x3-7:保留的操作代碼,用于后續(xù)定義的非控制幀;

      %x8:表示連接斷開;

      %x9:表示這是一個心跳請求(ping);

      %xA:表示這是一個心跳響應(yīng)(pong);

      %xB-F:保留的操作代碼,用于后續(xù)定義的控制幀。

      4.Mask: 1bit

      表示是否要對數(shù)據(jù)載荷進(jìn)行掩碼異或操作。

      0:否

      1:是

      5.Payload length: 7bit or (7 + 16)bit or (7 + 64)bit

      表示數(shù)據(jù)載荷的長度。

      0~126:數(shù)據(jù)的長度等于該值;

      126:后續(xù)?2?個字節(jié)代表一個?16?位的無符號整數(shù),該無符號整數(shù)的值為數(shù)據(jù)的長度;

      127:后續(xù)?8?個字節(jié)代表一個?64?位的無符號整數(shù)(最高位為?0),該無符號整數(shù)的值為數(shù)據(jù)的長度。

      6.Masking-key: 0 or 4bytes

      當(dāng)?Mask?為?1,則攜帶了?4?字節(jié)的?Masking-key;

      當(dāng)?Mask?為?0,則沒有?Masking-key。

      掩碼算法:按位做循環(huán)異或運算,先對該位的索引取模來獲得?Masking-key?中對應(yīng)的值?x,然后對該位與?x?做異或,從而得到真實的?byte?數(shù)據(jù)。

      注意:掩碼的作用并不是為了防止數(shù)據(jù)泄密,而是為了防止早期版本的協(xié)議中存在的代理緩存污染攻擊(proxy cache poisoning attacks)等問題。

      7.Payload Data: 載荷數(shù)據(jù)

      雙端接收到數(shù)幀之后,就可以根據(jù)數(shù)據(jù)幀各個位置的值進(jìn)行處理或信息提取。

      這里要注意的是從客戶端向服務(wù)端發(fā)送數(shù)據(jù)時,需要對數(shù)據(jù)進(jìn)行掩碼操作;從服務(wù)端向客戶端發(fā)送數(shù)據(jù)時,不需要對數(shù)據(jù)進(jìn)行掩碼操作。如果服務(wù)端接收到的數(shù)據(jù)沒有進(jìn)行過掩碼操作,服務(wù)端需要斷開連接。如果Mask是1,那么在Masking-key中會定義一個掩碼鍵(masking key),并用這個掩碼鍵來對數(shù)據(jù)載荷進(jìn)行反掩碼。所有客戶端發(fā)送到服務(wù)端的數(shù)據(jù)幀,Mask都是1。

      保持連接

      剛才提到 WebSocket 協(xié)議是雙向通信的,那么一旦連接上,就不會斷開了嗎?

      事實上確實是這樣,但是服務(wù)端不可能讓所有的連接都一直保持,所以服務(wù)端通常會在一個定期的時間給客戶端發(fā)送一個 ping 幀,而客戶端收到 Ping 幀后則回復(fù)一個 Pong 幀,如果客戶端不響應(yīng),那么服務(wù)端就會主動斷開連接。

      opcode 幀為 0x09 則代表這是一個 Ping ,為 0x0A 則代表這是一個 Pong。

      WebSocket 協(xié)議學(xué)習(xí)小結(jié)

      WebSocket 的協(xié)議寫得比較規(guī)范,比較容易閱讀和理解。只要遵循協(xié)議中的規(guī)定,就可以實現(xiàn)穩(wěn)定的通信連接和數(shù)據(jù)傳輸。

      aiowebsocket 設(shè)計

      基于對協(xié)議的學(xué)習(xí),我編了一個開源的異步 WebSocket 庫 - aiowebsocket,它的文件結(jié)構(gòu)和類的設(shè)計如下圖所示:

      aiowebsocket

      aiowebsocket 是一個比同類型庫更快、更輕、更靈活的 WebSocket 客戶端,它基于 asyncio 開并具備了與 websocket-client 和 websockets 庫簡單易用的特點。這是我用 7 天時間學(xué)習(xí) WebSocket 知識以及 Python 文檔 Stream 知識的成果。

      安裝與使用

      安裝:跟其他庫一樣,你可以通過 pip 進(jìn)行安裝:pip install aiowebsocket,也可以在 github 上 clone 到本地使用。

      使用:WebSocket 協(xié)議的簡寫是 ws,它與 http/https 類似,具有更安全的協(xié)議 wss。使用上的區(qū)別并不大,只需要在創(chuàng)建連接時打開 ssl 即可。

      ws 協(xié)議示例代碼:

      import?asyncio

      import?logging

      from?datetime?import?datetime

      from?aiowebsocket.converses?import?AioWebSocket

      async?def?startup(uri):

      async?with?AioWebSocket(uri)?as?aws:

      converse?=?aws.manipulator

      message?=?b'AioWebSocket?-?Async?WebSocket?Client'

      while?True:

      await?converse.send(message)

      print('{time}-Client?send:?{message}'

      .format(time=datetime.now().strftime('%Y-%m-%d?%H:%M:%S'),?message=message))

      mes?=?await?converse.receive()

      print('{time}-Client?receive:?{rec}'

      .format(time=datetime.now().strftime('%Y-%m-%d?%H:%M:%S'),?rec=mes))

      if?__name__?==?'__main__':

      remote?=?'ws://echo.websocket.org'

      try:

      asyncio.get_event_loop().run_until_complete(startup(remote))

      except?KeyboardInterrupt?as?exc:

      logging.info('Quit.')

      運行后就會得到如下結(jié)果:

      2019-03-04?15:11:25-Client?send:?b'AioWebSocket?-?Async?WebSocket?Client'

      2019-03-04?15:11:25-Client?receive:?b'AioWebSocket?-?Async?WebSocket?Client'

      2019-03-04?15:11:25-Client?send:?b'AioWebSocket?-?Async?WebSocket?Client'

      2019-03-04?15:11:25-Client?receive:?b'AioWebSocket?-?Async?WebSocket?Client'

      這代表客戶端與服務(wù)連接成功并正常通信。

      wss 協(xié)議示例代碼:

      #?開啟?ssl?即可

      import?asyncio

      import?logging

      from?datetime?import?datetime

      from?aiowebsocket.converses?import?AioWebSocket

      async?def?startup(uri):

      async?with?AioWebSocket(uri,?ssl=True)?as?aws:

      converse?=?aws.manipulator

      message?=?b'AioWebSocket?-?Async?WebSocket?Client'

      while?True:

      await?converse.send(message)

      print('{time}-Client?send:?{message}'

      .format(time=datetime.now().strftime('%Y-%m-%d?%H:%M:%S'),?message=message))

      mes?=?await?converse.receive()

      print('{time}-Client?receive:?{rec}'

      .format(time=datetime.now().strftime('%Y-%m-%d?%H:%M:%S'),?rec=mes))

      if?__name__?==?'__main__':

      remote?=?'wss://echo.websocket.org'

      try:

      asyncio.get_event_loop().run_until_complete(startup(remote))

      except?KeyboardInterrupt?as?exc:

      logging.info('Quit.')

      運行結(jié)果與上方運行結(jié)果類似。除此之外,aiowebsocket 還允許自定義請求頭,在連接一些需要校驗 origin、user-agent 和 host 頭域信息的網(wǎng)站時,自定義請求頭就非常有用了:

      import?asyncio

      import?logging

      from?datetime?import?datetime

      from?aiowebsocket.converses?import?AioWebSocket

      async?def?startup(uri,?header):

      async?with?AioWebSocket(uri,?headers=header)?as?aws:

      converse?=?aws.manipulator

      message?=?b'AioWebSocket?-?Async?WebSocket?Client'

      while?True:

      await?converse.send(message)

      print('{time}-Client?send:?{message}'

      .format(time=datetime.now().strftime('%Y-%m-%d?%H:%M:%S'),?message=message))

      mes?=?await?converse.receive()

      print('{time}-Client?receive:?{rec}'

      .format(time=datetime.now().strftime('%Y-%m-%d?%H:%M:%S'),?rec=mes))

      if?__name__?==?'__main__':

      remote?=?'ws://123.207.167.163:9010/ajaxchattest'

      header?=?[

      'GET?/ajaxchattest?HTTP/1.1',

      'Connection:?Upgrade',

      'Host:?123.207.167.163:9010',

      'Origin:?http://coolaf.com',

      'Sec-WebSocket-Key:?RmDgZzaqqvC4hGlWBsEmwQ==',

      'Sec-WebSocket-Version:?13',

      'Upgrade:?websocket',

      ]

      try:

      asyncio.get_event_loop().run_until_complete(startup(remote,?header))

      except?KeyboardInterrupt?as?exc:

      logging.info('Quit.')

      ws://123.207.167.163:9010/ajaxchattest 是一個免費的、開放的 WebSocket 連接測試接口,它在握手階段會校驗 origin 頭域,如果不符合規(guī)范則不允許客戶端連接。

      項目 Github 地址為

      https://github.com/asyncins/aiowebsocket

      歡迎各位前去 star ,如果能給出建議或者發(fā)現(xiàn) bug 那就更美了。

      python

      版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實的內(nèi)容,請聯(lián)系我們jiasou666@gmail.com 處理,核實后本網(wǎng)站將在24小時內(nèi)刪除侵權(quán)內(nèi)容。

      版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實的內(nèi)容,請聯(lián)系我們jiasou666@gmail.com 處理,核實后本網(wǎng)站將在24小時內(nèi)刪除侵權(quán)內(nèi)容。

      上一篇:excel中生成目錄的教程
      下一篇:IPIDEA使用方式
      相關(guān)文章
      亚洲av不卡一区二区三区| 亚洲AV午夜福利精品一区二区| 亚洲综合日韩久久成人AV| 亚洲第一综合天堂另类专| 亚洲欧洲日产国码www| 亚洲色爱图小说专区| 亚洲免费视频一区二区三区| 亚洲精品国产精品乱码不卞| 亚洲av永久无码精品秋霞电影秋| 亚洲男人的天堂久久精品| 国产成人亚洲精品| 亚洲jjzzjjzz在线播放| 亚洲免费闲人蜜桃| 亚洲 欧洲 自拍 另类 校园| 亚洲综合中文字幕无线码| 456亚洲人成在线播放网站| 亚洲色大成网站www尤物| 亚洲av永久无码精品网址| 国产精品亚洲二区在线| 亚洲美女高清一区二区三区| 亚洲伊人成无码综合网| 亚洲国产激情一区二区三区| 老司机亚洲精品影院在线观看| 日韩亚洲国产二区| MM131亚洲国产美女久久| 亚洲AV香蕉一区区二区三区| 亚洲精品免费网站| 亚洲三级视频在线观看| 国产精品久久亚洲不卡动漫| 亚洲精品精华液一区二区| www亚洲精品久久久乳| 在线视频亚洲一区| 亚洲区视频在线观看| 亚洲成aⅴ人在线观看| 国产亚洲国产bv网站在线| 学生妹亚洲一区二区| 久久久久亚洲精品无码网址色欲| 亚洲第一福利网站在线观看| 奇米影视亚洲春色| 亚洲AV永久精品爱情岛论坛| 亚洲bt加勒比一区二区|