什么是 WebSocket ?
WebSocket 是一種標準協議,用于在客戶端和服務端之間進行雙向數據傳輸。但它跟 HTTP 沒什么關系,它是一種基于 TCP 的一種獨立實現。
以前客戶端想知道服務端的處理進度,要不停地使用 Ajax 進行輪詢,讓瀏覽器隔個幾秒就向服務器發一次請求,這對服務器壓力較高。另外一種輪詢就是采用 long poll 的方式,這就跟打電話差不多,沒收到消息就一直不掛電話,也就是說,客戶端發起連接后,如果沒消息,就一直不返回 Response 給客戶端,連接階段一直是阻塞的。
而 WebSocket 解決了 HTTP 的這幾個難題。首先,當服務器完成協議升級后( HTTP -> WebSocket ),服務端可以主動推送信息給客戶端,解決了輪詢造成的同步延遲問題。由于 WebSocket 只需要一次 HTTP 握手,服務端就能一直與客戶端保持通訊,直到關閉連接,這樣就解決了服務器需要反復解析 HTTP 協議,減少了資源的開銷。
隨著新標準的推進,WebSocket 已經比較成熟了,并且各個瀏覽器對 WebSocket 的支持情況比較好,有空可以看看。
使用 WebSocket 的時候,前端使用是比較規范的,js 支持 ws 協議,感覺類似于一個輕度封裝的 Socket 協議,只是以前需要自己維護 Socket 的連接,現在能夠以比較標準的方法來進行。
客戶端請求報文及實現
客戶端請求報文:
GET / HTTP/1.1Upgrade: websocketConnection: UpgradeHost: example.comOrigin: http://example.comSec-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 協議,后面接地址及端口
完整的客戶端代碼:
服務端響應報文及實現
首先我們來看看服務端的響應報文
HTTP/1.1 101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-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 字符串。
PS: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 端接收到瀏覽器發來的報文需要進行解析
瀏覽器包格式
1、FIN: 占 1 個 bit
0:不是消息的最后一個分片1:是消息的最后一個分片
2、RSV1, RSV2, RSV3:各占 1 個 bit
一般情況下全為 0。當客戶端、服務端協商采用 WebSocket 擴展時,這三個標志位可以非0,且值的含義由擴展進行定義。如果出現非零的值,且并沒有采用 WebSocket 擴展,連接出錯。
3、Opcode: 4 個 bit
%x0:表示一個延續幀。當 Opcode 為 0 時,表示本次數據傳輸采用了數據分片,當前收到的數據幀為其中一個數據分片;%x1:表示這是一個文本幀(frame);%x2:表示這是一個二進制幀(frame);%x3-7:保留的操作代碼,用于后續定義的非控制幀;%x8:表示連接斷開;%x9:表示這是一個 ping 操作;%xA:表示這是一個 pong 操作;%xB-F:保留的操作代碼,用于后續定義的控制幀。
4、Mask: 1 個 bit
表示是否要對數據載荷進行掩碼異或操作。0:否1:是
5、Payload length: 7bit or 7 + 16bit or 7 + 64bit
表示數據載荷的長度x為 0~126:數據的長度為x字節;x為 126:后續 2 個字節代表一個 16 位的無符號整數,該無符號整數的值為數據的長度;x為 127:后續 8 個字節代表一個 64 位的無符號整數(最高位為 0),該無符號整數的值為數據的長度。
6、Masking-key: 0 or 4bytes
當 Mask 為 1,則攜帶了 4 字節的 Masking-key;當 Mask 為 0,則沒有 Masking-key。PS:掩碼的作用并不是為了防止數據泄密,而是為了防止早期版本的協議中存在的代理緩存污染攻擊(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 中支持的格式如下表:
為了同 C 語言中的結構體交換數據,還要考慮有的 C 或 C++ 編譯器使用了字節對齊,通常是以 4 個字節為單位的 32 位系統,故而 struct 根據本地機器字節順序轉換。可以用格式中的第一個字符來改變對齊方式,定義如下:
發送 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、需要頻繁地向服務端請求更新數據。
比如彈幕、消息訂閱、多玩家游戲、協同編輯、股票基金實時報價、視頻會議、在線教育等需要高實時的場景。
-
通信
+關注
關注
18文章
5969瀏覽量
135850 -
TCP
+關注
關注
8文章
1349瀏覽量
78985 -
WebSocket
+關注
關注
0文章
29瀏覽量
3737
原文標題:一文讀懂 WebSocket 通信過程與實現
文章出處:【微信號:magedu-Linux,微信公眾號:馬哥Linux運維】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論