🏀

WebSocket 少し深掘り

2023/08/07に公開

以前websocketを使ったプロダクトと関ったことがキッカケで少し気になったので調べてみた。
websocketの用途等はいろんな記事でわかりやすい説明されているので省略。

大まかな流れは以下の図に示す通りだと思われる。

  • クライアントからprotocol切り替え依頼(http GET upgrade)
  • サーバ承認(protocol切り替え)
  • websocket protocolを使った通信開始

パケットの調査

以下のような単純なサーバとクライアントを実行とキャプチャして内容を確認した。
クライアントはHi!を送り、受信後にサーバはYo!を返す。 これを三回繰り返す。
ほぼこの例文の通り。
https://github.com/nhooyr/websocket#examples
双方向のメリットを引き出せてないが一旦無視。

  • server

    package main
    
    import (
    	"context"
    	"fmt"
    	"log"
    	"net/http"
    	"time"
    
    	"nhooyr.io/websocket"
    )
    
    func main() {
    	wsh := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    		c, err := websocket.Accept(w, r, nil)
    		if err != nil {
    			// ...
    		}
    		defer c.Close(websocket.StatusInternalError, "the sky is falling")
    		for {
    			ctx, cancel := context.WithTimeout(context.Background(), time.Minute*10)
    			defer cancel()
    
    			_, message, err := c.Read(ctx)
    			if err != nil {
    				break
    			}
    			log.Printf("Received %s", message)
    
    			err = c.Write(ctx, websocket.MessageText, []byte("Yo!"))
    			if err != nil {
    				break
    			}
    		}
    	})
    	http.Handle("/", wsh)
    	fmt.Println("start ws")
    	log.Fatal(http.ListenAndServe(":8080", nil))
    }
    
  • client

    package main
    
    import (
    	"context"
    	"fmt"
    	"time"
    
    	"nhooyr.io/websocket"
    	"nhooyr.io/websocket/wsjson"
    )
    
    func main() {
    	ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
    	defer cancel()
    
    	c, _, err := websocket.Dial(ctx, "ws://localhost:8080", nil)
    	if err != nil {
    		// ...
    	}
    	defer c.Close(websocket.StatusInternalError, "the sky is falling")
    
    	for i := 0; i < 3; i++ {
    		wsjson.Write(ctx, c, "Hi!")
    		_, b, _ := c.Read(ctx)
    		fmt.Printf("respons is %s \n", string(b))
    		time.Sleep(time.Second * 2)
    	}
    
    	c.Close(websocket.StatusNormalClosure, "")
    }
    

調査結果

  Client: FIN=0, opcode=0x1, msg="Hi!"
  Client: FIN=1, opcode=0x0, msg=""

  • websocketハンドシェイク時のclient:Sec-WebSocket-Keyとserver:Sec-WebSocket-Acceptの関係
    以下に詳しく書いてあるがクライアントが送ったキーSec-WebSocket-Keyを取得したサーバは
    sha-1-hash(Sec-WebSocket-Key,"258EAFA5-E914-47DA-95CA-C5AB0DC85B11")してbase64でエンコードした値をSec-WebSocket-Acceptに入れて返す。
    そしてクライアントは自分が送ったキーと一致するかどうかを検証する。(セキュリティ?)
    という構図らしい。

    ここにわかりやすく書いてあった。
    https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#server_handshake_response
    サーバ側の実装。
    https://github.com/nhooyr/websocket/blob/v1.8.7/accept.go#L364
    クライアント側の実装
    https://github.com/nhooyr/websocket/blob/v1.8.7/dial.go#L205
    若干なんで"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"なのか気になった。

  • データ量
    サーバーがYo!を送信することに焦点を当てて比較。
    以下のように、httpの方はYo!をGetのレスポンスとして返すのに185byte使っているが、websocketは73byteで送れている。(ただwebsocketコネクション時に200byteぐらい使うがそれは無視)

    • http response
    • websocket

    このことからwebsoketは細かいデータを頻繁に送受信するような構成に向いていることが予想できる。

興味がある部分だけ実装してみる

Websocket クライアント側のみ実装

その他

Discussion