WebSocket的通信過程與實現(xiàn)方法詳解
什么是 WebSocket ?
WebSocket 是一種標(biāo)準(zhǔn)協(xié)議,用于在客戶端和服務(wù)端之間進行雙向數(shù)據(jù)傳輸。但它跟 HTTP 沒什么關(guān)系,它是基于 TCP 的一種獨立實現(xiàn)。
以前客戶端想知道服務(wù)端的處理進度,要不停地使用 Ajax 進行輪詢,讓瀏覽器隔個幾秒就向服務(wù)器發(fā)一次請求,這對服務(wù)器壓力較大。另外一種輪詢就是采用 long poll 的方式,這就跟打電話差不多,沒收到消息就一直不掛電話,也就是說,客戶端發(fā)起連接后,如果沒消息,就一直不返回 Response 給客戶端,連接階段一直是阻塞的。
而 WebSocket 解決了 HTTP 的這幾個難題。當(dāng)服務(wù)器完成協(xié)議升級后( HTTP -> WebSocket ),服務(wù)端可以主動推送信息給客戶端,解決了輪詢造成的同步延遲問題。由于 WebSocket 只需要一次 HTTP 握手,服務(wù)端就能一直與客戶端保持通信,直到關(guān)閉連接,這樣就解決了服務(wù)器需要反復(fù)解析 HTTP 協(xié)議,減少了資源的開銷。
隨著新標(biāo)準(zhǔn)的推進,WebSocket 已經(jīng)比較成熟了,并且各個主流瀏覽器對 WebSocket 的支持情況比較好(不兼容低版本 IE,IE 10 以下),有空可以看看。
使用 WebSocket 的時候,前端使用是比較規(guī)范的,js 支持 ws 協(xié)議,感覺類似于一個輕度封裝的 Socket 協(xié)議,只是以前需要自己維護 Socket 的連接,現(xiàn)在能夠以比較標(biāo)準(zhǔn)的方法來進行。
下面我們就結(jié)合上圖具體來聊一下 WebSocket 的通信過程。
建立連接
客戶端請求報文 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
與傳統(tǒng) HTTP 報文不同的地方:
Upgrade: websocket Connection: Upgrade
這兩行表示發(fā)起的是 WebSocket 協(xié)議。
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ== Sec-WebSocket-Version: 13
Sec-WebSocket-Key 是由瀏覽器隨機生成的,提供基本的防護,防止惡意或者無意的連接。
Sec-WebSocket-Version 表示 WebSocket 的版本,最初 WebSocket 協(xié)議太多,不同廠商都有自己的協(xié)議版本,不過現(xiàn)在已經(jīng)定下來了。如果服務(wù)端不支持該版本,需要返回一個 Sec-WebSocket-Versionheader,里面包含服務(wù)端支持的版本號。
創(chuàng)建 WebSocket 對象:
var ws = new websocket("ws://127.0.0.1:8001");
ws 表示使用 WebSocket 協(xié)議,后面接地址及端口
完整的客戶端代碼:
<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 () {}; // 首先關(guān)閉 WebSocket ws.close() }; </script>
服務(wù)端響應(yīng)報文 Header
首先我們來看看服務(wù)端的響應(yīng)報文:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= Sec-WebSocket-Protocol: chat
我們一行行來解釋
- 首先,101 狀態(tài)碼表示服務(wù)器已經(jīng)理解了客戶端的請求,并將通過 Upgrade 消息頭通知客戶端采用不同的協(xié)議來完成這個請求;
- 然后,Sec-WebSocket-Accept 這個則是經(jīng)過服務(wù)器確認(rèn),并且加密過后的 Sec-WebSocket-Key;
- 最后,Sec-WebSocket-Protocol 則是表示最終使用的協(xié)議。
Sec-WebSocket-Accept 的計算方法:
- 將 Sec-WebSocket-Key 跟 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接;
- 通過 SHA1 計算出摘要,并轉(zhuǎn)成 base64 字符串。
注意:Sec-WebSocket-Key/Sec-WebSocket-Accept 的換算,只能帶來基本的保障,但連接是否安全、數(shù)據(jù)是否安全、客戶端 / 服務(wù)端是否合法的 ws 客戶端、ws 服務(wù)端,其實并沒有實際性的保證。
創(chuàng)建主線程,用于實現(xiàn)接受 WebSocket 建立請求:
def create_socket(): # 啟動 Socket 并監(jiān)聽連接 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: sock.bind(('127.0.0.1', 8001)) # 操作系統(tǒng)會在服務(wù)器 Socket 被關(guān)閉或服務(wù)器進程終止后馬上釋放該服務(wù)器的端口,否則操作系統(tǒng)會保留幾分鐘該端口。 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 狀態(tài) 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()
進行通信
服務(wù)端解析 WebSocket 報文
Server 端接收到 Client 發(fā)來的報文需要進行解析
Client 包格式
FIN: 占 1bit
0:不是消息的最后一個分片
1:是消息的最后一個分片
RSV1, RSV2, RSV3:各占 1bit
一般情況下全為 0。當(dāng)客戶端、服務(wù)端協(xié)商采用 WebSocket 擴展時,這三個標(biāo)志位可以非
0,且值的含義由擴展進行定義。如果出現(xiàn)非零的值,且并沒有采用 WebSocket 擴展,連接出錯。
Opcode: 4bit
%x0:表示一個延續(xù)幀。當(dāng) Opcode 為 0 時,表示本次數(shù)據(jù)傳輸采用了數(shù)據(jù)分片,當(dāng)前收到的數(shù)據(jù)幀為其中一個數(shù)據(jù)分片;
%x1:表示這是一個文本幀(text frame);
%x2:表示這是一個二進制幀(binary frame);
%x3-7:保留的操作代碼,用于后續(xù)定義的非控制幀;
%x8:表示連接斷開;
%x9:表示這是一個心跳請求(ping);
%xA:表示這是一個心跳響應(yīng)(pong);
%xB-F:保留的操作代碼,用于后續(xù)定義的控制幀。
Mask: 1bit
表示是否要對數(shù)據(jù)載荷進行掩碼異或操作。
0:否
1:是
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ù)的長度。
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)等問題。
Payload Data: 載荷數(shù)據(jù)
解析 WebSocket 報文代碼如下:
def read_msg(data): logging.debug(data) msg_len = data[1] & 127 # 數(shù)據(jù)載荷的長度 if msg_len == 126: mask = data[4:8] # Mask 掩碼 content = data[8:] # 消息內(nèi)容 elif msg_len == 127: mask = data[10:14] content = data[14:] else: mask = data[2:6] content = data[6:] raw_str = '' # 解碼后的內(nèi)容 for i, d in enumerate(content): raw_str += chr(d ^ mask[i % 4]) return raw_str
服務(wù)端發(fā)送 WebSocket 報文
返回時不攜帶掩碼,所以 Mask 位為 0,再按載荷數(shù)據(jù)的大小寫入長度,最后寫入載荷數(shù)據(jù)。
struct 模塊解析
struct.pack(fmt, v1, v2, ...)
按照給定的格式 fmt,把數(shù)據(jù)封裝成字符串 ( 實際上是類似于 C 結(jié)構(gòu)體的字節(jié)流 )
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 語言中的結(jié)構(gòu)體交換數(shù)據(jù),還要考慮有的 C 或 C++ 編譯器使用了字節(jié)對齊,通常是以 4 個字節(jié)為單位的 32 位系統(tǒng),故而 struct 根據(jù)本地機器字節(jié)順序轉(zhuǎn)換??梢杂酶袷街械牡谝粋€字符來改變對齊方式,定義如下:
Character | Byte order | Size | Alignment |
---|---|---|---|
@ | native | native | native |
= | native | standard | none |
< | little-endian | standard | none |
> | big-endian | standard | none |
! | network (= big-endian) | standard | none |
發(fā)送 WebSocket 報文代碼如下:
def write_msg(message): data = struct.pack('B', 129) # 寫入第一個字節(jié),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') # 寫入消息內(nèi)容 logging.debug(data) return data
總結(jié)
沒有其他能像 WebSocket 一樣實現(xiàn)全雙工傳輸?shù)募夹g(shù)了,迄今為止,大部分開發(fā)者還是使用 Ajax 輪詢來實現(xiàn),但這是個不太優(yōu)雅的解決辦法,WebSocket 雖然用的人不多,可能是因為協(xié)議剛出來的時候有安全性的問題以及兼容的瀏覽器比較少,但現(xiàn)在都有解決。如果你有這些需求可以考慮使用 WebSocket:
- 多個用戶之間進行交互;
- 需要頻繁地向服務(wù)端請求更新數(shù)據(jù)。
比如彈幕、消息訂閱、多玩家游戲、協(xié)同編輯、股票基金實時報價、視頻會議、在線教育等需要高實時的場景。
參考文章
https://www.zhihu.com/question/20215561/answer/40316953
http://fullstackpython.atjiang.com/websockets.html
http://www.52im.net/thread-1341-1-1.html
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
- 使用spring的websocket創(chuàng)建通信服務(wù)的示例代碼
- PHP實現(xiàn)websocket通信的方法示例
- android利用websocket協(xié)議與服務(wù)器通信
- C# websocket及時通信協(xié)議的實現(xiàn)方法示例
- 使用 Spring Boot 實現(xiàn) WebSocket實時通信
- Spring Boot 開發(fā)私有即時通信系統(tǒng)(WebSocket)
- .NET實現(xiàn)WebSocket服務(wù)端即時通信實例
- WebSocket+node.js創(chuàng)建即時通信的Web聊天服務(wù)器
- Python通過websocket與js客戶端通信示例分析
- 在Android中使用WebSocket實現(xiàn)消息通信的方法詳解
相關(guān)文章
JavaScript——DOM操作——Window.document對象詳解
下面小編就為大家?guī)硪黄狫avaScript——DOM操作——Window.document對象詳解。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-07-07JavaScript設(shè)置彈出式獨立窗口頁面和window的方法舉例詳解
window.open是網(wǎng)頁中經(jīng)常遇到的彈出窗口代碼,不是網(wǎng)絡(luò)中比較反感的那類彈出代碼,下面這篇文章主要給大家介紹了關(guān)于JavaScript設(shè)置彈出式獨立窗口頁面和window的方法,需要的朋友可以參考下2024-01-01