WebSocketの全体フロー
- 認証(この接続は誰のものかを確定する)
- Upgrade(通信を WebSocket に切り替える)
- Hub.Join(接続をユーザーに紐づけて管理下に入れる)
- writer goroutine(サーバーからの送信経路を確立する)
- reader ループ(クライアントからの入力を受け取る)
- 終了処理(接続を安全に片付ける)
ここでは、WebSocket 接続がサーバの管理下に入るまでの流れとして、1〜3 のステップを追っていきます。
WebSocket接続をどう捉えるか
WebSocketはHTTPの延長
WebSocket は、最初から専用のプロトコルで通信を始めるわけではなく、必ず HTTP リクエストから始まります。
ブラウザやクライアントは、通常の HTTP リクエストを送り、その中で
「この接続を WebSocket に切り替えたい」という意思表示をします。
GET /ws HTTP/1.1
Upgrade: websocket
Connection: Upgrade
サーバがこれを受け入れると、HTTP のやり取りはそこで終了し、
同じ TCP 接続のまま WebSocket プロトコルへ切り替わります。
この切り替え処理を Upgrade と呼びます。
接続が確立されるまでに何が起きているか
1. 認証(この接続は誰のものかを確定する)
WebSocket 接続では、最初に必ず認証を行います。
理由はもし認証を後回しにすると下記のような不都合が生じるからです。
- 誰の接続なのか分からない状態で接続を維持する
- 不正な接続を後から切断する必要がある
- Hub に登録された後に弾く、という中途半端な状態になる
なぜ Upgrade 前に認証するのか
WebSocket の接続は、最初は通常の HTTP リクエストとして届きます。
この段階では、
- Cookie
- Authorization ヘッダ
- セッション情報
など、HTTP の仕組みをそのまま使った認証が可能です。
一方、Upgrade が完了すると、通信は HTTP ではなく WebSocket プロトコルに切り替わります。
この時点では、
- HTTP ステータスコードを返す
- 通常のエラーレスポンスを書く
といったことができなくなります。
だからこそ、「HTTP であるうちに、HTTP として弾く」という判断が重要になります。
2. Upgrade(通信を WebSocket に切り替える)
ここで初めて、通信が HTTP から WebSocket に切り替わります。
Upgradeとは、同じ TCP 接続のまま、通信プロトコルを切り替えることです。
この段階では、サーバ(正確には Upgrader)がWebSocket プロトコルとして成立するかどうかを検証し、問題なければ接続を WebSocket に切り替えます。
例:
wsConn, err := upgrader.Upgrade(writer, request, nil)
if err != nil {
// Upgrade できなかった場合はそのまま終了
return
}
upgrader は WebSocket ライブラリが提供するヘルパーで、リクエストが WebSocket として成立するかを検証し、問題なければ接続を切り替えます。
3. Hub.Join(接続をユーザーに紐づけて管理下に入れる)
この WebSocket 接続を、特定のユーザーに紐づく接続としてサーバの管理下に登録するための処理です。
ここで Hub が行うのは、
- この接続は どの userID のものか
- この userID に対して どの接続が現在有効か
- サーバからメッセージを配送できる対象か
といった 接続の生死と配送先解決の管理です。
Upgrade は「通信方式の切り替え」ですが、
Hub.Join は **「この接続を誰のものとして扱うかを確定させる処理」**です。
例:
// 接続を表す Client を作成
client := &Client{
wsConn: wsConn,
sendChannel: make(chan []byte, sendBufSize),
}
// WebSocket 接続を userID に紐づけて Hub に登録
handler.hub.JoinUser(userID, client)
type Hub struct {
// userID ごとに現在有効な接続を管理する
mutex sync.RWMutex
connsByUser map[string]*Client
}
// JoinUser は、WebSocket 接続を
// 「userID に紐づく現在有効な接続」として Hub に登録する。
//
// Hub は部屋や用途を意識せず、
// 各 userID に対して「どの接続が有効か」を 1 つだけ管理する。
// すでに接続が存在する場合は、古い接続を終了させて差し替える。
func (hub *Hub) JoinUser(userID string, conn *Client) {
// 接続の追加・削除は並行して発生するため、排他制御を行う
hub.mutex.Lock()
defer hub.mutex.Unlock()
// 既存の接続があれば終了させる(再接続対策)
if existingConn, ok := hub.connsByUser[userID]; ok {
_ = existingConn.Close()
}
// この userID に対する現在の有効な接続として登録する
hub.connsByUser[userID] = conn
}
なぜ Upgrade 直後に Join するのか
Hub に登録する前の接続は、
- どのユーザーのものか分からない
- メッセージの配送先として解決できない
- 切断時に適切なクリーンアップができない
という 管理外の状態になります。
その状態で reader / writer を動かし始めると、
- 送信対象が不明確になる
- 再接続や多重接続時に破綻しやすい
- 切断後にゴミ接続が残る
といった問題が起きやすくなります。
だから、Upgrade が成功したら、最初に「誰の接続か」を Hub に登録するという順序になります。
まとめ
WebSocket 実装の難しさは、プロトコルの切り替えではなく、接続を 誰のものとして紐づけ、サーバ側で安全に管理できるか にあります。
認証・Upgrade・Hub への登録は、そのための最小構成です。