WebSocket協議探究(一)


一 復習和目標

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM