WebSocketでの通信内容をWiresharkで見る

webjavascriptgolang

Webで双方向通信するためのプロトコル、WebSocketでの通信内容をWiresharkで見る。

アプリケーション

サーバー

package main

import (
	"context"
	"fmt"
	"io"
	"net/http"

	"golang.org/x/net/websocket"
)

type Payload struct {
	A string
}

func Handler(ws *websocket.Conn) {
	ctx, cancel := context.WithCancel(context.Background())
	go func() {
		var payload Payload
		for {
			err := websocket.JSON.Receive(ws, &payload)
			if err != nil {
				if err == io.EOF {
					fmt.Println("connection closed")
				} else {
					fmt.Println(err)
				}
				cancel()
				break
			}
			fmt.Println(payload.A)
		}
	}()
	websocket.JSON.Send(ws, Payload{A: "a"})
	select {
	case <-ctx.Done():
	}
}

func main() {
	http.Handle("/", websocket.Handler(Handler))
	http.ListenAndServe(":12345", nil)
}

クライアント

<script>
const websocket = new WebSocket("ws://localhost:12345");
console.log(websocket)
websocket.onopen = (e) => { 
    setInterval(() => {
        console.log("send")
        websocket.send(JSON.stringify({A: "あ".repeat(50)}));
    }, 10000)
};
websocket.onmessage = (e) => { 
    console.log(`received ${e.data}`);
};
</script>

Wiresharkのインストール

GUIが立ち上がるのでtcp.port == 12345のフィルタで待ち構える。

$ brew install wireshark --with-qt
$ sudo wireshark

通信内容

WebSocketで通信するまで

まず通常のHTTP通信と同様に3-way handshakeでTCPのコネクションを張った後、 Upgrade: WebSocketConnection: Upgradeを付けたリクエストをサーバーに送ってWebSocketでの通信を要求する。

Hypertext Transfer Protocol
    GET / HTTP/1.1\r\n
    Host: localhost:12345\r\n
    Connection: Upgrade\r\n
    Pragma: no-cache\r\n
    Cache-Control: no-cache\r\n
    Upgrade: websocket\r\n
    Sec-WebSocket-Version: 13\r\n
    Sec-WebSocket-Key: vuQHXJAxDEdD9pQ3d7RtOw==\r\n
    Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\n    
    ...

それに対してサーバーはステータスコード101でレスポンスを返す。 送られてきたランダムなSec-WebSocket-Keyをもとに生成したSec-WebSocket-Accept をヘッダーに付けて、クライアントはこれを見て正しいWebSocketサーバーであることを確認する。

Hypertext Transfer Protocol
    HTTP/1.1 101 Switching Protocols\r\n
    Upgrade: websocket\r\n
    Connection: Upgrade\r\n
    Sec-WebSocket-Accept: oImbSbalm6WMVbXV5oqmRlHHhWg=\r\n

WebSocketでの通信

サーバーからクライアントへのデータ。 HTTP/1.1のヘッダーはテキストで記述も多いためデータ量が大きく、毎回それらを送る必要があり効率的ではなかったが、WebSocketではこの場合Payloadを除いた部分が2bytesしかない。

ちなみにHTTP/2では、頻出するヘッダー名と値のテーブルのインデックスを用いて圧縮するHPACKという方式で、バイナリのHEADERSフレームにすることで、この問題を改善している。正式仕様になる前は差分を送ることになっていたが、効果が薄く採用されなかったようだ。

WebSocket
    1... .... = Fin: True
    .000 .... = Reserved: 0x0
    .... 0001 = Opcode: Text (1)
    0... .... = Mask: False
    .000 1001 = Payload length: 9
    Payload

クライアントからサーバーへのデータ。Payload lengthが125を超えたため拡張されている。 それ以外にMasked payloadが含まれるのでデータ量が少し大きくなっている。これは不正なスクリプトがHTTPリクエストに偽装したデータを送ることでプロキシのキャッシュが汚染されるのを防ぐため。

WebSocket
    1... .... = Fin: True
    .000 .... = Reserved: 0x0
    .... 0001 = Opcode: Text (1)
    1... .... = Mask: True
    .111 1110 = Payload length: 126 Extended Payload Length (16 bits)
    Extended Payload length (16 bits): 158
    Masking-Key: ef17e997
    Masked payload
    Payload

参考

The WebSocket Protocol (RFC 6455) の歴史

O’Reilly Japan - Real World HTTP 第2版