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: WebSocket
とConnection: 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