WebSocket 入門及開源庫aiowebsocket

轉載自公眾號:FightingCoder

WebSocket 協議和知識

WebSocket是一種在單個TCP連接上進行全雙工通信的協議。WebSocket通信協議于2011年被IETF定為標準RFC 6455,并由RFC7936補充規范。WebSocket API也被W3C定為標準。

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

為什么會有 WebSocket

以前,很多網站為了實現推送技術,所用的技術都是輪詢。輪詢是在特定的的時間間隔(如每1秒),由瀏覽器對服務器發出HTTP請求,然后由服務器返回最新的數據給客戶端的瀏覽器。這種傳統的模式帶來很明顯的缺點,即瀏覽器需要不斷的向服務器發出請求,然而HTTP請求可能包含較長的頭部,其中真正有效的數據可能只是很小的一部分,顯然這樣會浪費很多的帶寬等資源。
而比較新的技術去做輪詢的效果是Comet。這種技術雖然可以雙向通信,但依然需要反復發出請求。而且在Comet中,普遍采用的長鏈接,也會消耗服務器資源。
在這種情況下,HTML5定義了WebSocket協議,能更好的節省服務器資源和帶寬,并且能夠更實時地進行通訊。

WebSocket 有什么優點

開銷少、時時性高、二進制支持完善、支持擴展、壓縮更優。

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

  • 更強的實時性。由于協議是全雙工的,所以服務器可以隨時主動給客戶端下發數據。相對于HTTP請求需要等待客戶端發起請求服務端才能響應,延遲明顯更少;即使是和Comet等類似的長輪詢比較,其也能在短時間內更多次地傳遞數據。
    保持連接狀態。與HTTP不同的是,Websocket需要先創建連接,這就使得其成為一種有* 狀態的協議,之后通信時可以省略部分狀態信息。而HTTP請求可能需要在每個請求都攜帶狀態信息(如身份認證等)。

  • 更好的二進制支持。Websocket定義了二進制幀,相對HTTP,可以更輕松地處理二進制內容。

  • 可以支持擴展。Websocket定義了擴展,用戶可以擴展協議、實現部分自定義的子協議。如部分瀏覽器支持壓縮等。

  • 更好的壓縮效果。相對于HTTP壓縮,Websocket在適當的擴展支持下,可以沿用之前內容的上下文,在傳遞類似的數據時,可以顯著地提高壓縮率。

握手是怎么回事?

WebSocket 是獨立的、創建在 TCP 上的協議。

Websocket 通過HTTP/1.1 協議的101狀態碼進行握手。

為了創建Websocket連接,需要通過瀏覽器發出請求,之后服務器進行回應,這個過程通常稱為“握手”(handshaking)。

WebSocket 協議規范

WebSocket 是一個通信協議,它規定了一些規范和標準。它的協議標準為 RFC 6455,具體的協議內容可以在tools.ietf.org中查看。

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

雙端交互流程

客戶端與服務端交互流程如下所示:

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

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

握手 - 客戶端

關于握手標準,在協議中有說明:

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 協議,而是 HTTP 協議,握手時發出的請求可以叫做升級請求。客戶端在握手階段通過:
Upgrade: websocket Connection: Upgrade
Connection 和 Upgrade 這兩個頭域告知服務端,要求將通信的協議轉換為 websocket。其中 Sec-WebSocket-Version、Sec-WebSocket-Protocol 這兩個頭域表明通信版本和協議約定, Sec-WebSocket-Key 則作為一個防止無端連接的保障(其實并沒有什么保障作用,因為 key 的值完全由客戶端控制,服務端并無驗證機制),其他幾個頭域則與 HTTP
協議的作用一致。

握手 - 服務端

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

服務端需要回復什么內容呢?
Status Code: 101 Web Socket Protocol Handshake Sec-WebSocket-Accept: T5ar3gbl3rZJcRmEmBT8vxKjdDo= Upgrade: websocket Connection: Upgrade

首先,服務端會給出狀態碼,101 狀態碼表示服務器已經理解了客戶端的請求,并且回復 Connection 和 Upgrade 表示已經切換成 websocket 協議。Sec-WebSocket-Accept 則是經過服務器確認,并且加密過后的 Sec-WebSocket-Key。

這樣,客戶端與服務端就完成了握手操作,達成一致,使用 WebSocket 協議進行通信。

你來我往 - 數據交流

雙方握手成功并確認協議后,就可以互相發送信息了。它們的信息是如何發送的呢?難道是:

client: Hello, server boy

server: Hello, client girl

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

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

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.

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

數據幀的格式如圖所示:

image

幀由以下幾部分組成:
FIN、RSV1、RSV2、RSV3、opcode、MASK、Payload length、Masking-key、Payload-Data。它們的含義和作用如下:

1.FIN: 占 1bit

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

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

一般情況下全為 0。當客戶端、服務端協商采用 WebSocket 擴展時,這三個標志位可以非 0,且值的含義由擴展進行定義。如果出現非零的值,且并沒有采用 WebSocket 擴展,連接出錯。

3.Opcode: 4bit

%x0:表示一個延續幀。當 Opcode 為 0 時,表示本次數據傳輸采用了數據分片,當前收到的數據幀為其中一個數據分片;

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

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

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

%x8:表示連接斷開;

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

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

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

4.Mask: 1bit

表示是否要對數據載荷進行掩碼異或操作。

0:否
1:是

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

表示數據載荷的長度。

0~126:數據的長度等于該值;

126:后續 2 個字節代表一個 16 位的無符號整數,該無符號整數的值為數據的長度;

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

6.Masking-key: 0 or 4bytes

當 Mask 為 1,則攜帶了 4 字節的 Masking-key;

當 Mask 為 0,則沒有 Masking-key。

掩碼算法:按位做循環異或運算,先對該位的索引取模來獲得 Masking-key 中對應的值 x,然后對該位與 x 做異或,從而得到真實的 byte 數據。

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

7.Payload Data: 載荷數據

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

掩碼

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

保持連接

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

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

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

WebSocket 協議學習小結

WebSocket 的協議寫得比較規范,比較容易閱讀和理解。只要遵循協議中的規定,就可以實現穩定的通信連接和數據傳輸。

aiowebsocket 設計

基于對協議的學習,我編了一個開源的異步 WebSocket 庫 - aiowebsocket,它的文件結構和類的設計如下圖所示:

image

aiowebsocket

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

image

安裝與使用

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

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

ws 協議示例代碼:

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.')

運行后就會得到如下結果:

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'

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

wss 協議示例代碼:

# 開啟 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.')

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

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 頭域,如果不符合規范則不允許客戶端連接。

項目 Github 地址為

https://github.com/asyncins/aiowebsocket

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,621評論 2 380

推薦閱讀更多精彩內容

  • 一、內容概覽 WebSocket的出現,使得瀏覽器具備了實時雙向通信的能力。本文由淺入深,介紹了WebSocket...
    Calvin李閱讀 2,539評論 2 10
  • 第一章 我可能上了一個“假大學” [第一節] 還沒準備好的開始 盛夏時節,太陽有些刺眼,熱風吹...
    攝影師楊書坤閱讀 201評論 5 2
  • 作為一個近乎PPT小白,每到寫工作總結的時候就苦惱萬分,要花費好長時間,做出來的還是小學水平。在朋友圈看...
    inc_linda閱讀 366評論 0 2
  • 今天是什么日子 起床:7:40 就寢:9:45 天氣:多云?,有些冷 心情:還不錯 任務清單 習慣養成:簡書日更 ...
    Tobelinda閱讀 191評論 0 0
  • 19.3.8作業區筆跡解讀 一、從七個維度筆跡分析 1.書寫壓力:從該書寫者字跡整體觀看來判斷,筆壓輕重不算重也不...
    Collue閱讀 272評論 0 2