WebSocket 通訊過程與實現

什麼是 WebSocket ?

WebSocket 是一種標準協議,用於在客戶端和服務端之間進行雙向數據傳輸。但它跟 HTTP 沒什麼關係,它是基於 TCP 的一種獨立實現。javascript

之前客戶端想知道服務端的處理進度,要不停地使用 Ajax 進行輪詢,讓瀏覽器隔個幾秒就向服務器發一次請求,這對服務器壓力較大。另一種輪詢就是採用 long poll 的方式,這就跟打電話差很少,沒收到消息就一直不掛電話,也就是說,客戶端發起鏈接後,若是沒消息,就一直不返回 Response 給客戶端,鏈接階段一直是阻塞的。html

而 WebSocket 解決了 HTTP 的這幾個難題。當服務器完成協議升級後( HTTP -> WebSocket ),服務端能夠主動推送信息給客戶端,解決了輪詢形成的同步延遲問題。因爲 WebSocket 只須要一次 HTTP 握手,服務端就能一直與客戶端保持通訊,直到關閉鏈接,這樣就解決了服務器須要反覆解析 HTTP 協議,減小了資源的開銷。前端

websockets

隨着新標準的推動,WebSocket 已經比較成熟了,而且各個主流瀏覽器對 WebSocket 的支持狀況比較好(不兼容低版本 IE,IE 10 如下),有空能夠看看。java

瀏覽器兼容性

使用 WebSocket 的時候,前端使用是比較規範的,js 支持 ws 協議,感受相似於一個輕度封裝的 Socket 協議,只是之前須要本身維護 Socket 的鏈接,如今可以以比較標準的方法來進行。python

WebSocket 通訊過程

下面咱們就結合上圖具體來聊一下 WebSocket 的通訊過程。web

創建鏈接

客戶端請求報文 Header

客戶端請求報文:算法

GET / HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: example.com
Origin: http://example.com
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
Sec-WebSocket-Version: 13

與傳統 HTTP 報文不一樣的地方:瀏覽器

Upgrade: websocket
Connection: Upgrade

這兩行表示發起的是 WebSocket 協議。緩存

Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
Sec-WebSocket-Version: 13

Sec-WebSocket-Key 是由瀏覽器隨機生成的,提供基本的防禦,防止惡意或者無心的鏈接。安全

Sec-WebSocket-Version 表示 WebSocket 的版本,最初 WebSocket 協議太多,不一樣廠商都有本身的協議版本,不過如今已經定下來了。若是服務端不支持該版本,須要返回一個 Sec-WebSocket-Versionheader,裏面包含服務端支持的版本號。

建立 WebSocket 對象:

var ws = new websocket("ws://127.0.0.1:8001");

ws 表示使用 WebSocket 協議,後面接地址及端口

完整的客戶端代碼:

<script type="text/javascript">
    var ws;
    var box = document.getElementById('box');

    function startWS() {
        ws = new WebSocket('ws://127.0.0.1:8001');
        ws.onopen = function (msg) {
            console.log('WebSocket opened!');
        };
        ws.onmessage = function (message) {
            console.log('receive message: ' + message.data);
            box.insertAdjacentHTML('beforeend', '<p>' + message.data + '</p>');
        };
        ws.onerror = function (error) {
            console.log('Error: ' + error.name + error.number);
        };
        ws.onclose = function () {
            console.log('WebSocket closed!');
        };
    }

    function sendMessage() {
        console.log('Sending a message...');
        var text = document.getElementById('text');
        ws.send(text.value);
    }

    window.onbeforeunload = function () {
        ws.onclose = function () {};  // 首先關閉 WebSocket
        ws.close()
    };
</script>

服務端響應報文 Header

首先咱們來看看服務端的響應報文:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

咱們一行行來解釋

  1. 首先,101 狀態碼錶示服務器已經理解了客戶端的請求,並將經過 Upgrade 消息頭通知客戶端採用不一樣的協議來完成這個請求;
  2. 而後,Sec-WebSocket-Accept 這個則是通過服務器確認,而且加密事後的 Sec-WebSocket-Key
  3. 最後,Sec-WebSocket-Protocol 則是表示最終使用的協議。

Sec-WebSocket-Accept 的計算方法:

  1. Sec-WebSocket-Key 跟 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接;
  2. 經過 SHA1 計算出摘要,並轉成 base64 字符串。

注意:Sec-WebSocket-Key/Sec-WebSocket-Accept 的換算,只能帶來基本的保障,但鏈接是否安全、數據是否安全、客戶端 / 服務端是否合法的 ws 客戶端、ws 服務端,其實並無實際性的保證。

建立主線程,用於實現接受 WebSocket 創建請求:

def create_socket():
    # 啓動 Socket 並監聽鏈接
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    try:
        sock.bind(('127.0.0.1', 8001))

        # 操做系統會在服務器 Socket 被關閉或服務器進程終止後立刻釋放該服務器的端口,不然操做系統會保留幾分鐘該端口。
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        sock.listen(5)
    except Exception as e:
        logging.error(e)
        return
    else:
        logging.info('Server running...')

    # 等待訪問
    while True:
        conn, addr = sock.accept()  # 此時會進入 waiting 狀態

        data = str(conn.recv(1024))
        logging.debug(data)

        header_dict = {}
        header, _ = data.split(r'\r\n\r\n', 1)
        for line in header.split(r'\r\n')[1:]:
            key, val = line.split(': ', 1)
            header_dict[key] = val

        if 'Sec-WebSocket-Key' not in header_dict:
            logging.error('This socket is not websocket, client close.')
            conn.close()
            return

        magic_key = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
        sec_key = header_dict['Sec-WebSocket-Key'] + magic_key
        key = base64.b64encode(hashlib.sha1(bytes(sec_key, encoding='utf-8')).digest())
        key_str = str(key)[2:30]
        logging.debug(key_str)

        response = 'HTTP/1.1 101 Switching Protocols\r\n' \
                   'Connection: Upgrade\r\n' \
                   'Upgrade: websocket\r\n' \
                   'Sec-WebSocket-Accept: {0}\r\n' \
                   'WebSocket-Protocol: chat\r\n\r\n'.format(key_str)
        conn.send(bytes(response, encoding='utf-8'))

        logging.debug('Send the handshake data')

        WebSocketThread(conn).start()

進行通訊

服務端解析 WebSocket 報文

Server 端接收到 Client 發來的報文須要進行解析

Client 包格式

Client 包格式

  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: 載荷數據

解析 WebSocket 報文代碼以下:

def read_msg(data):
    logging.debug(data)

    msg_len = data[1] & 127  # 數據載荷的長度
    if msg_len == 126:
        mask = data[4:8]  # Mask 掩碼
        content = data[8:]  # 消息內容
    elif msg_len == 127:
        mask = data[10:14]
        content = data[14:]
    else:
        mask = data[2:6]
        content = data[6:]

    raw_str = ''  # 解碼後的內容
    for i, d in enumerate(content):
        raw_str += chr(d ^ mask[i % 4])
    return raw_str

服務端發送 WebSocket 報文

返回時不攜帶掩碼,因此 Mask 位爲 0,再按載荷數據的大小寫入長度,最後寫入載荷數據。

struct 模塊解析

struct.pack(fmt, v1, v2, ...)

按照給定的格式 fmt,把數據封裝成字符串 ( 其實是相似於 C 結構體的字節流 )

struct 中支持的格式以下表:

Format C Type Python type Standard size
x pad byte no value
c char bytes of length 1 1
b signed char integer 1
B unsigned char integer 1
? _Bool bool 1
h short integer 2
H unsigned short integer 2
i int integer 4
I unsigned int integer 4
l long integer 4
L unsigned long integer 4
q long long integer 8
Q unsigned long long integer 8
n ssize_t integer
N size_t integer
e -7 float 2
f float float 4
d double float 8
s char[] bytes
p char[] bytes
P void * integer
爲了同 C 語言中的結構體交換數據,還要考慮有的 C 或 C++ 編譯器使用了字節對齊,一般是以 4 個字節爲單位的 32 位系統,故而 struct 根據本地機器字節順序轉換。能夠用格式中的第一個字符來改變對齊方式,定義以下:
Character Byte order Size Alignment
@ native native native
= native standard none
< little-endian standard none
> big-endian standard none
! network (= big-endian) standard none

發送 WebSocket 報文代碼以下:

def write_msg(message):
    data = struct.pack('B', 129)  # 寫入第一個字節,10000001

    # 寫入包長度
    msg_len = len(message)
    if msg_len <= 125:
        data += struct.pack('B', msg_len)
    elif msg_len <= (2 ** 16 - 1):
        data += struct.pack('!BH', 126, msg_len)
    elif msg_len <= (2 ** 64 - 1):
        data += struct.pack('!BQ', 127, msg_len)
    else:
        logging.error('Message is too long!')
        return

    data += bytes(message, encoding='utf-8')  # 寫入消息內容
    logging.debug(data)
    return data

總結

沒有其餘能像 WebSocket 同樣實現全雙工傳輸的技術了,迄今爲止,大部分開發者仍是使用 Ajax 輪詢來實現,但這是個不太優雅的解決辦法,WebSocket 雖然用的人很少,多是由於協議剛出來的時候有安全性的問題以及兼容的瀏覽器比較少,但如今都有解決。若是你有這些需求能夠考慮使用 WebSocket:

  1. 多個用戶之間進行交互;
  2. 須要頻繁地向服務端請求更新數據。

好比彈幕、消息訂閱、多玩家遊戲、協同編輯、股票基金實時報價、視頻會議、在線教育等須要高實時的場景。

參考文章

https://www.zhihu.com/questio...

http://fullstackpython.atjian...

http://www.52im.net/thread-13...

相關文章
相關標籤/搜索