一、WebSocket 由來
WebSocket 是一個持久化的協議,通過第一次 HTTP Request 建立連線之後,再把通訊協議升級成 websocket,保持連線狀態,後續的資料交換不需要再重複請求。websocket 可以看成一種類似 TCP/IP 的 socke t技術,在 web 應用中實現、並獲得同 TCP/IP 通訊一樣的雙向通訊功能,因此客戶端既和伺服器可以傳送訊息也可以接收訊息,同時還支援多路複用的功能,由於它借用了 HTTP 協議的一些概念,所以被稱為 WebSocket。
webSocket API定義了web應用和伺服器進行通訊的公共介面,具體的建構函式建立物件、物件的屬性、方法、事件及它的意義,在上一篇《HTML5(十一)——WebSocket 基礎教程》文章中已詳細介紹。
二、WebSocket 通訊過程
WebSocket 協議可分為兩部分:握手階段和資料通訊階段。
WebSocket 為應用層協議,定義在 TCP/IP 協議棧之上,連線伺服器的 url 是以 ws 或 wss 開頭的。ws 開頭的預設TCP埠為80,wss 開頭的預設埠為443。
ws(websocket)是不安全的,容易被竊聽,只要別人知道你的ip和埠號,任何人都可以去連線通訊。
wss(web socket secure)是websocket的加密版本。
2.1、建立連線
客戶端去與伺服器建立 TCP 連線,客戶端生成 websocket 物件,然後使用 API 建立連線,程式碼如下:
let ws= new WebSocket('ws://localhost:8888') ws.onopen = function(){ console.log("連線") }
2.2、握手階段
客戶端與伺服器建立連線之後,客戶端傳送握手請求,隨後伺服器傳送握手響應即完成握手階段。
客戶端握手請求如下:
'GET / HTTP/1.1',
'Host: localhost:8888',
'Connection: Upgrade',
'Pragma: no-cache',
'Cache-Control: no-cache',
'Upgrade: websocket',
'Origin: file://',
'Sec-WebSocket-Version: 13',
'Accept-Encoding: gzip, deflate, br',
'Accept-Language: zh-CN,zh;q=0.9',
'Sec-WebSocket-Key: In1aAp/ya9Lkv+tsUtXLXQ==',
'Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits',
伺服器握手響應如下:
Status Code: 101 Switching Protocols
Connection: Upgrade
sec-websocket-Accept: HBMDBbZMiS59r3aAITpGtJ64Mfc=
Upgrade: websocket
2.3、資料通訊
WebSocket 握手連線成功之後。可以使用 send 進行傳送資料,onmessage 接收資料,如下傳送“你好”:
let ws= new WebSocket('ws://localhost:8888') ws.onopen = function(){ console.log("連線成功") ws.send("你好") } ws.onmessage = function(res){ console.log('接收到的訊息',res) }
伺服器列印接收到的資料,如:<Buffer 81 86 af 87 53 b4 4b 3a f3 51 0a 3a>。
websocket 在傳送資料時,被組織為一串資料幀,然後進行傳送。傳送的幀包含兩部分:資料幀和控制幀。資料幀可以攜帶文字資料或者二進位制資料,控制幀包含關閉幀和 Ping/Pong 幀。
- FIN :1bit ,表示是訊息的最後一幀,如果訊息只有一幀那麼第一幀也就是最後一幀。
- RSV1,RSV2,RSV3:每個1bit,必須是0,除非擴充套件定義為非零。如果接受到的是非零值但是擴充套件沒有定義,則需要關閉連線。
- Opcode:4bit,解釋Payload資料,規定有以下不同的狀態,如果是未知的,接收方必須馬上關閉連線。狀態如下:0x0(附加資料幀) 0x1(文字資料幀) 0x2(二進位制資料幀) 0x3-7(保留為之後非控制幀使用) 0xB-F(保留為後面的控制幀使用) 0x8(關閉連線幀) 0x9(ping) 0xA(pong)
- Mask:1bit,掩碼,定義payload資料是否進行了掩碼處理,如果是1表示進行了掩碼處理。Masking-key域的資料即是掩碼金鑰,用於解碼PayloadData。客戶端發出的資料幀需要進行掩碼處理,所以此位是1。
- Payload length:7位,7 + 16位,7+64位,payload資料的長度,如果是0-125,就是真實的payload長度,如果是126,那麼接著後面的2個位元組對應的16位無符號整數就是payload資料長度;如果是127,那麼接著後面的8個位元組對應的64位無符號整數就是payload資料的長度。
- Masking-key:0到4位元組,如果MASK位設為1則有4個位元組的掩碼解密金鑰,否則就沒有。
- Payload data:任意長度資料。包含有擴充套件定義資料和應用資料,如果沒有定義擴充套件則沒有此項,僅含有應用資料。
把接收到的buffer十六進位制資料轉成二進位制資料,控制幀與上述各個型別幀進行對比解析其意義。
2.4、關閉連線
任何一端可以關閉連線。客戶端關閉連線如下:
ws.close()
然後傳送關閉幀給對方,通常會帶有關閉連線的狀態碼,常見的狀態碼如下:
- 1000 連線正常關閉
- 1001 端點離線,例如伺服器down,或者瀏覽器已經離開此頁面
- 1002 端點因為協議錯誤而中斷連線
- 1003 端點因為受到不能接受的資料型別而中斷連線
- 1004 保留
- 1005 保留, 用於提示應用未收到連線關閉的狀態碼
- 1006 端點異常關閉
- 1007 端點收到的資料幀型別不一致而導致連線關閉
- 1008 資料違例而關閉連線
- 1009 收到的訊息資料太大而關閉連線
- 1010 客戶端因為伺服器未協商擴充套件而關閉
- 1011 伺服器因為遭遇異常而關閉連線
- 1015 TLS握手失敗關閉連線
三、websocket 例項
3.1、客戶端建立websocket物件,並建立連線之後傳送資料。其中 ws 地址根據後臺服務埠對應。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <script> let ws= new WebSocket('ws://localhost:8888') ws.onopen = function(){ console.log("連線") ws.send("你好") } ws.onmessage = function(res){ console.log('res',res) } </script> </body> </html>
3.2、使用 node.js 建立一個 websocket 服務,如建立一個serve.js檔案,程式碼如下:
const http = require("http") const net = require("net") //原生的websocket const crypto = require('crypto') // 安全性校驗 let serve = net.createServer(sock=>{ //只握手一次 sock.once('data',(data)=>{ console.log("hand shake start") // 開始握手 let str = data.toString(); let lines = str.split('\r\n') //捨棄第一行和最後兩行 lines = lines.slice(1,lines.length-2) let headers = {} lines.forEach(line=>{ let [key,val] = line.split(': ') headers[key.toLowerCase()] = val }) if( headers['upgrade']!= 'websocket' ){ console.log("其他協議") sock.end() }else if(headers['sec-websocket-version']!=13){ console.log("版本不對") sock.end() }else{ let key = headers['sec-websocket-key'] let mask = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' //sha1(key+mask) -> base64 =>client let hash = crypto.createHash('sha1') hash.update(key+mask) let key2 = hash.digest('base64') sock.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade:websocket\r\nConnection:Upgrade\r\nsec-websocket-Accept:${key2}\r\n\r\n` ) console.log("hand shake end") // 握手結束 //真正的資料 sock.on('data',res=>{ console.log("真正接收資料",res) //資料解析 let FIN = res[0]&0x001; let opcode = data[0]&0x0F0; let msak = data[1]&0x001; let payload = data[1]&0x0FE; }) } }) //斷開 sock.on('end',()=>{ console.log("連線已斷開") }) }) serve.listen("8888")
使用命令 node serve.js 或node serve 啟動服務,服務啟動成功之後可以使用localhost:8888訪問服務。
啟動服務之後,訪問前邊建立的html檔案訪問websocket服務。
四、websocket的優點
- 第一次通過http建立連線之後,資料互動不用傳送http請求,節省了頻寬資源。
- websocket連線是雙向通訊,伺服器和客戶端既可接受也可傳送訊息。
- websocket多路複用,幾個不同url可以複用一個websocket服務。
- 是HTML5的技術之一,有巨大應用前景。