WebSocket協議探究(一)
阿新 • • 發佈:2018-12-10
一 複習和目標
1 複習
- 上一節使用wireshark抓包分析了WebSocket流量
- 包含連線的建立:HTTP協議升級WebSocket協議
- 使用建立完成的WebSocket協議傳送資料
2 目標
協議對比
- 初始握手和計算響應鍵值
- 訊息格式
關閉握手
注:WebSocket伺服器使用《HTML5 WebSocket權威指南》3.4節中使用nodejs實現,WebSocket客戶端使用Chrome瀏覽器實現。
二 協議對比
特性 | TCP | HTTP | WebSocket |
---|---|---|---|
定址 | IP地址和埠 | URL | URL |
併發傳輸 | 全雙工 | 半雙工 | 全雙工 |
內容 | 位元組流 | MIME訊息 | 文字和二進位制訊息 |
訊息定界 | 否 | 是 | 是 |
連線定向 | 是 | 否 | 是 |
注:
- TCP傳送位元組流,訊息定界由高層協議來表現。
- WebSocket中,多位元組的訊息作為整體、按照順序到達。.
三 初始握手
1 HTTP請求升級協議和協議升級成功響應
- HTTP請求
GET ws://localhost:9999/echo HTTP/1.1
Host: localhost:9999
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: UjxPJpGjxC4JH5+0znrYBg==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
- HTTP響應
HTTP/1.1 101 Web Socket Protocol Handshake Upgrade: WebSocket Connection: Upgrade sec-websocket-accept: NTeDlW+9/P48+pMOtotMmM1m/J0=
注:響應不帶Sec-WebSocket-Extensions代表該伺服器不支援請求中的拓展
2 計算響應鍵值
(1)概述
響應中的sec-websocket-accept
等於base64(sha1(請求中的Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11))
(2)nodejs版本實現
var KEY_SUFFIX = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
function(key){
var sha1 = crypto.createHash('sha1');
sha1.update(key+KEY_SUFFIX,'ascii');
return sha1.digest('base64');
}
(3)java版本
public class MessageDigestUtils {
private final static String KEY_SUFFIX = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
public static String generateFinalKey(String key) {
String seckey = key.trim() + KEY_SUFFIX;
MessageDigest sha1;
try {
sha1 = MessageDigest.getInstance( "SHA1" );
} catch ( NoSuchAlgorithmException e ) {
throw new IllegalStateException( e );
}
return Base64.getEncoder().encodeToString(sha1.digest(seckey.getBytes()));
}
}
(4)其他首部
首部欄位 | 描述 |
---|---|
Sec-WebSocket-Key | 用於初始握手,避免跨協議攻擊。 |
Sec-WebSocket-Accept | 用於初始握手,伺服器確認WebSocket協議。 |
Sec-WebSocket-Extensions | 用於初始握手,伺服器確認客戶端的拓展。 |
Sec-WebSocket-Protocol | 用於初始握手,伺服器子協議選擇。 |
Sec-WebSocket-Version | 用於初始握手,對於RFC 6455對應為13。 |
四 訊息格式
1 幀和訊息
- 幀:最小的通訊單位,包含可變長度的幀首部和淨荷部分,淨荷可能包含完整或部分應用訊息。
- 訊息:一系列幀,與應用訊息對等。
2 幀格式
- FIN:表示當前幀是否為訊息的最後一幀;可能一條訊息就只有一幀。
- 操作碼(4位):表示被傳輸幀的型別
- 1:文字
- 2:二進位制
- 8:關閉連線
- 9:呼叫,ping
- 10:迴應,pong
- 掩碼位:淨荷是否有掩碼(只適用客戶端傳送給伺服器的訊息)
- 淨荷長度:
- 0~125:表示長度
- 126:接下來2個位元組的16位無符號整數才是該幀的長度
- 127:接下來8個位元組的64位無符號整數才是該幀的長度,高位必須為0。
- 掩碼鍵:包含32位,用於給淨荷加掩護
- 淨荷包含應用資料,如果客戶端和伺服器在建立連線時協商過,也可以包含自定義的擴充套件資料。
注:WebSocket的隊首阻塞:如果一個大訊息被分成多個WebSocket 幀,就會阻塞其他訊息的幀。
3 資料抓包
3.1 基礎資訊
- 客戶端:
- IP:192.168.1.10
- Port:3263
- 伺服器:
- IP:192.168.1.10
- Port:9999
注:如果使用localhost,wireshark無法抓包,因為流量走的時loop back介面。
# 管理員執行route add 本機IP地址 mask 掩碼 閘道器IP地址
route add 192.168.1.10 mask 255.255.255.255 192.168.1.1
3.2 客戶端 -> 伺服器(長度小於125的小包)
(1)wireshark抓包
WebSocket
1... .... = Fin: True
.000 .... = Reserved: 0x0
.... 0001 = Opcode: Text (1)
1... .... = Mask: True
.000 0101 = Payload length: 5
# [Extended Payload length (16 bits): 40200] 如果length超過125
Masking-Key: 0b4b5535
Masked payload
63 2e 39 59 64
(2)掩碼解析:nodejs
// maskBytes為0b4b5535 data為632e395964
// 結果為:68656c6c6f ==> hello
var unmask = function (maskBytes, data) {
var payload = new Buffer(data.length);
for (var i = 0; i < data.length; i++) {
payload[i] = maskBytes[i % 4] ^ data[i];
}
return payload;
}
(3)掩碼解析:java
public static String unmask(byte[] maskBytes,byte[] data){
byte[] payload = new byte[data.length];
for (int i = 0; i < data.length; i++) {
payload[i] = (byte)(maskBytes[i % 4] ^ data[i]);
}
return new String(payload);
}
(4)資料解析:nodejs
WebSocketConnection.prototype._processBuffer = function () {
var buf = this.buffer;
if (buf.length < 2) return;
var b1 = buf.readUInt8(0);
var fin = b1 & 0x80;
var opcode = b1 & 0x0f;
var b2 = buf.readUInt8(1);
var mask = b2 & 0x80;
var length = b2 & 0x7f;
var idx = 2; // 索引
if (length > 125) {
if (buf.length < 8) return;
if (length == 126) {
length = buf.readUInt16BE(2);
idx += 2;
} else if (length == 127) {
var highBits = buf.readUInt32BE(2);
if (highBits != 0) this.close(1009, "");// 高位必須為0
length = buf.readUInt32BE(6);
idx += 8;
}
}
// 4個位元組的掩碼
if (buf.length < idx + 4 + length) {
return;
}
maskBytes = buf.slice(idx, idx + 4);
idx += 4;
var payload = buf.slice(idx, idx + length);
payload = unmask(maskBytes, payload);
this._handleFrame(opcode, payload);
this.buffer = buf.slice(idx + length); // buffer置空
return true;
}
注:java版本的資料解析太麻煩,後期考慮補上。
3.3 伺服器 -> 客戶端 (長度小於125的小包)
- 伺服器發給客戶端不需要掩碼,直接傳送即可。
WebSocket
1... .... = Fin: True
.000 .... = Reserved: 0x0
.... 0001 = Opcode: Text (1)
0... .... = Mask: False
.000 0101 = Payload length: 5
Payload
hello
五 關閉握手
1 關閉握手異常代號
代號 | 描述 | 使用場景 |
---|---|---|
1000 | 正常關閉 | 會話正常完成時 |
1001 | 離開 | 應用離開且不期望後續連線的嘗試而關閉連線時 |
1002 | 協議錯誤 | 因協議錯誤而關閉連線時 |
1003 | 不可接受的資料型別 | 非二進位制或文字型別時 |
1007 | 無效資料 | 文字格式錯誤,如編碼錯誤 |
1008 | 訊息違反政策 | 當應用程式由於其他代號不包含的原因時 |
1009 | 訊息過大 | 當接收的訊息太大,應用程式無法處理時(幀的載荷最大為64位元組) |
1010 | 需要拓展 | |
1011 | 意外情況 |
2 其他代號
代號 | 描述 | 使用情況 |
---|---|---|
0~999 | 禁止 | |
1000~2999 | 保留 | |
3000~3999 | 需要註冊 | 用於程式庫、框架和應用程式 |
4000~4999 | 私有 | 應用程式自由使用 |
3 抓包分析
(1)客戶端發起關閉
WebSocket
1... .... = Fin: True
.000 .... = Reserved: 0x0
.... 1000 = Opcode: Connection Close (8)
1... .... = Mask: True
.000 0000 = Payload length: 0
Masking-Key: 461086e0
(2)伺服器響應關閉
WebSocket
1... .... = Fin: True
.000 .... = Reserved: 0x0
.... 1000 = Opcode: Connection Close (8)
0... .... = Mask: False
.000 0000 = Payload length: 0
參考:
- 《Web效能權威指南》
- 《HTML5 WebSocket權威指南》
- RFC 6455