websocket學習(二)
阿新 • • 發佈:2018-11-23
websocket客戶端,服務端通訊的最小單位是幀,由1個或者多個幀組成一個條完成的訊息(message)
- 傳送端,將訊息切割成多個幀,併發送給接收端
- 接收端,接受訊息幀,並將關聯的症組裝成完整的訊息
websocket資料幀格式
單位是位元,比如FIN,RSV1各佔一個bit,opcode佔據四個bit。
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-------+-+-------------+-------------------------------+ |F|R|R|R| opcode|M| Payload len | Extended payload length | |I|S|S|S| (4) |A| (7) | (16/64) | |N|V|V|V| |S| | (if payload len==126/127) | | |1|2|3| |K| | | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + | Extended payload length continued, if payload len == 127 | + - - - - - - - - - - - - - - - +-------------------------------+ | |Masking-key, if MASK set to 1 | +-------------------------------+-------------------------------+ | Masking-key (continued) | Payload Data | +-------------------------------- - - - - - - - - - - - - - - - + : Payload Data continued ... : + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Payload Data continued ... | +---------------------------------------------------------------+
- FIN: 一個bit,如果是1,表這是message的最後一個分片(fragment),如果是0,表示不是message的最後一個分片。
- RSV1,RSV2,RSV3:各佔一個bit,一般情況寫全部為0.當客戶端,服務端協商採用Websocket擴充套件是,這三個標誌可以非0,且值的含義可以自由擴充套件進行定義。如果出現非0值,且並沒有採用Websocket擴充套件,連結出錯
- opcode:4個bit,操作碼。opcode的值決定了應該如何解析後續的資料(data payload)。 如果操作程式碼是不認識的,那麼接收端應該斷開連結
- %x0:表示一個延續幀。當Opcode為0時,表示本次資料傳輸採用了資料分片,當前收到的資料幀為其中一個數據分片。
- %x1:表示這是一個文字幀(frame)
- %x2:表示這是一個二進位制幀(frame)
- %x3-7:保留的操作程式碼,用於後續定義的非控制幀。
- %x8:表示連線斷開。
- %x9:表示這是一個ping操作。
- %xA:表示這是一個pong操作。
- %xB-F:保留的操作程式碼,用於後續定義的控制幀。
- Mask:一個bit,表示是否要對資料載荷進行掩碼操作
- 客戶端像服務端傳送資料時,需要對資料進行掩碼操作,從服務端像客戶端傳送資料是,不需要對資料進行掩碼操作,如果服務端接收到的資料沒有進行掩碼操作,伺服器需要斷開連線。
- 如果Mask是1,那麼在Masking-key中會頂一個一個掩碼鍵(masking key)。並用這個掩碼鍵來對資料進行反掩碼。所有的客戶端傳送到服務端的資料正,mask都是1
- Payload length: 資料載荷的長度。單位是位元組。為7為,或者7+16為,或7+64為。
- Payload length=x為0~125:資料的長度為x位元組。
- Payload length=x為126:後續2個位元組代表一個16位的無符號整數,該無符號整數的值為資料的長度。
- Payload length=x為127:後續8個位元組代表一個64位的無符號整數(最高位為0),該無符號整數的值為資料的長度。
- 如果payload length佔用了多個位元組的話,payload length的二進位制表達採用網路序(big endian,重要的位在前)
- Masking-key: 或4位元組(32位) 所有從客戶端傳送到服務端的資料幀,資料載荷都進行了掩碼操作,Mask為1,且攜帶了4位元組的Masking-key。如果Mask為0,則沒有Masking-key。載荷資料的長度,不包括mask key的長度
- Payload data:(x+y) 位元組
- 載荷資料:包括了擴充套件資料、應用資料。其中,擴充套件資料x位元組,應用資料y位元組。
- 擴充套件資料:如果沒有協商使用擴充套件的話,擴充套件資料資料為0位元組。所有的擴充套件都必須宣告擴充套件資料的長度,或者可以如何計算出擴充套件資料的長度。此外,擴充套件如何使用必須在握手階段就協商好。如果擴充套件資料存在,那麼載荷資料長度必須將擴充套件資料的長度包含在內。
- 應用資料:任意的應用資料,在擴充套件資料之後(如果存在擴充套件資料),佔據了資料幀剩餘的位置。載荷資料長度 減去 擴充套件資料長度,就得到應用資料的長度。
掩碼演算法
掩碼鍵(Masking-key)是由客戶端挑選出來的32位的隨機數。掩碼操作不會影響資料載荷的長度。掩碼、反掩碼操作都採用如下演算法:
- 對索引i模以4得到j,因為掩碼一共就是四個位元組
- 對原來的索引進行異或對應的掩碼位元組
- 異或就是兩個數的二進位制形式,按位對比,相同取0,不同取1
function unmask(buffer, mask) {
const length = buffer.length;
for (let i = 0; i < length; i++) {
buffer[i] ^= mask[i & 3];
}
}
栗子
這裡使用tcp實現聊天室功能
-
新建目錄,新建index.html, server.js
-
客戶端:index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <div>hello webscoket</div> <script> // 建立一個socket物件,相當於買了一部手機 // socket 插座 let socket = new WebSocket('ws://localhost:9999') // 如果伺服器連結成功,會觸發此事件 socket.onopen = function () { console.log('連結已經建立好了') socket.send(('你好')) } // 如果客戶端接收到訊息,會觸發這個事件 socket.onmessage = (e) => { console.log(e.data) } </script> </body> </html>
-
服務端:server.js
let net = require('net') let code = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; let crypto = require('crypto') let express = require('express') let app = express() app.use(express.static(__dirname)) app.listen(8080) let server = net.createServer(( socket ) => { // 監聽客戶端發來的資訊 socket.once('data', data => { data = data.toString(); // data 內容大概如下 /* 請求頭 Request Method: GET Connection: Upgrade // 請求升級協議 Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits Sec-WebSocket-Key: BdHBU6Vo/0OMycVshRE3iQ== Sec-WebSocket-Version: 13 // 升級的版本 Upgrade: websocket // 升級的型別 */ let lines = data.split('\r\n') let headers = lines.slice(1, -2) // 處理字串為一個物件 獲取請求頭 headers = headers.reduce((headers, line) => { let [ key, val ] = line.split(': ') headers[key] = val return headers }, {}); // console.log('headers', headers) // 打印出來的header如下 /* { Host: 'localhost:9999', Connection: 'Upgrade', Pragma: 'no-cache', 'Cache-Control': 'no-cache', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36', Upgrade: 'websocket', Origin: 'http://localhost:8080', 'Sec-WebSocket-Version': '13', 'Accept-Encoding': 'gzip, deflate, br', 'Accept-Language': 'zh,zh-CN;q=0.9,en;q=0.8,en-AU;q=0.7', Cookie: 'visit=10; visit.sig=VVIZcIDkunpRwQDicJGbWKrQhTE; hello=bf5b7610-d783-11e8-aa65-354f8f3423d3', 'Sec-WebSocket-Key': 'qmuFW2gfEl4prcOwCqg7tA==', 'Sec-WebSocket-Extensions': 'permessage-deflate; client_max_window_bits' } */ // 判斷是否升級協議 // 如果Upgrade的知識websocket的話,則需要升級協議 if (headers.Upgrade === 'websocket') { let key = headers['Sec-WebSocket-Key'] let accept = crypto.createHash('sha1').update(key + code).digest('base64') // 處理響應頭 let response = [ "HTTP/1.1 101 Switching Protocols", "Upgrade: websocket", "Connection: Upgrade", `Sec-WebSocket-Accept: ${accept}`, '\r\n' ].join('\r\n') socket.write(response) // 切換協議成功,開始監聽客戶端發來的訊息 socket.on('data', function (bufs) { let FIN = (bufs[0] & 0b10000000) === 0b10000000 // 是否最後一幀 let opcode = bufs[0] & 0b00001111 // 操作碼 1文字, let isMasked = (bufs[1] & 0b100000000) === 0b100000000 // 對方是否掩碼 let payloadLength = bufs[1] & 0b01111111;//取得負載資料的長度 let mask = bufs.slice(2, 6);//掩碼 4個位元組 let payload = bufs.slice(6);//負載資料 // console.log('FIN', FIN) // true // console.log('opcode', opcode) // 1 // console.log(FIN, opcode, isMasked, payloadLength, mask, payload) // true 1 false 6 <Buffer 04 6e 75 fe> <Buffer e0 d3 d5 1b a1 d3> for (let i = 0; i < payload.length; i++) { payload[i] ^= mask[i % 4] // 為運算 相同是0, 不想同是1 } // 處理返回值 // console.log(FIN, opcode, isMasked, payloadLength, mask, payload.toString()) let response = Buffer.alloc( 2 + payload.length ) response[0] = 0b10000001 response[1] = payload.length // 將payload buf物件從下標2開始拷貝到response中 payload.copy(response, 2) socket.write(response) }) } }) }) server.listen(9999)
總結
其實我也不是很懂,就是勉勉強強知道絲絲,還要學習呀。
嗯麼麼,附上完成程式碼