websocketとは
はじめに
近年の Web アプリケーションでは、チャット、オンラインゲーム、コラボレーションツール、通知配信など、リアルタイム性 を求められる場面が増えている。従来の HTTP 通信は「リクエストを送る → レスポンスを受け取る」という一方向の問い合わせ型であり、双方向のリアルタイム通信を実現するには WebSocket を使用する必要がある。
WebSocket は、一度だけ HTTP で「プロトコルのアップグレード」を行い、その後はクライアントとサーバーが 双方向にデータを送り合える 常時接続の通信路を確立する仕組みを提供する。
本記事では、WebSocket の仕組みと通信の流れを解説し、TypeScript を使った具体的な実装例 も紹介する。
双方向通信については、以下の記事にまとめてあるので興味がある方はぜひ。
1. WebSocket は何を解決するプロトコルなのか
従来の Web は、HTTP による一問一答型の通信モデルを持つ。
このため、リアルタイム性が求められる場面では次のような課題があった。
- サーバーから即時通知を送れない
- クライアントは最新状態を知るために Polling(定期的な問い合わせ) を行う必要がある
- Long Polling でも疑似リアルタイムであり本質的に双方向ではない
- リクエストごとにヘッダーのオーバーヘッドが大きい
これらを根本的に解決するために、WebSocket は「常時接続」かつ「双方向」通信というモデルを提供している。
2. WebSocket の通信は 2 段階で構成される
WebSocket の通信は次の 2 ステップで構成される。
- HTTP を利用したオープニングハンドシェイク
- WebSocket フレームによる双方向通信
3. オープニングハンドシェイクの仕組み
3.1 クライアント → サーバー:Upgrade リクエスト
クライアント(ブラウザやアプリ)は、まず 通常の HTTP GET を送信する。
ヘッダーには「WebSocket にアップグレードしたい」という情報を含める。
例:
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://example.com
Sec-WebSocket-Protocol: chat, superchat
重要なポイントは次のとおりである。
-
Upgrade: websocket
プロトコルを WebSocket に切り替えたいという宣言。 -
Connection: Upgrade
Upgrade ヘッダーを有効にする。 -
Sec-WebSocket-Version: 13
現行の WebSocket プロトコルバージョン。 -
Sec-WebSocket-Key
クライアントが生成したランダムな鍵。
この鍵を使ってサーバー側が「正当な WebSocket 応答」であることを証明する。 -
Sec-WebSocket-Protocol
上位プロトコル(サブプロトコル)を要求するためのヘッダー。
3.2 サーバー → クライアント:101 Switching Protocols
サーバーが WebSocket 接続を受理すると、次のレスポンスを返す。
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
ここで重要なのが Sec-WebSocket-Accept である。
3.3 Sec-WebSocket-Key / Accept の仕組み
サーバーは、クライアントが送ってきた Sec-WebSocket-Key に対して特定の文字列を付け足し、SHA-1 → Base64 で加工する。
クライアント側は同じ計算を行い、値が一致すれば 正しい WebSocket 応答である と判断する。
3.4 ハンドシェイクが終わった後
101 が返り、Sec-WebSocket-Accept の検証に成功すると、
通信は HTTP を離れ WebSocket フレーム通信へ遷移 する。
以後は同じ TCP コネクションを維持しながら双方向通信を行う。
4. WebSocket フレームの仕組み
WebSocket はハンドシェイク後、フレーム(frame) と呼ばれる単位でデータを送受信する。
フレームはおおまかに次の構成を持つ。
- ヘッダー(FIN ビット、OPCODE、MASK など)
- ペイロード(実際のデータ)
詳しい構成については、RFCを参照してほしい。
5. WebSocket のライフサイクル
WebSocket 接続は次のように進行する。
- HTTP GET による Upgrade リクエスト
- 101 Switching Protocols
- WebSocket フレーム通信への切り替え
- テキスト・バイナリフレームの双方向通信
- Ping/Pong による死活監視
- Close フレームで接続終了
6. TypeScript で WebSocket を実装する
6.1 クライアント(ブラウザ)
type MessageType = "chat" | "system";
interface ChatMessage {
type: MessageType;
text: string;
sentAt: string;
}
const socket = new WebSocket("wss://example.com/chat");
socket.addEventListener("open", () => {
console.log("[WebSocket] connected");
const hello: ChatMessage = {
type: "system",
text: "Hello from client",
sentAt: new Date().toISOString(),
};
socket.send(JSON.stringify(hello));
});
socket.addEventListener("message", (event) => {
const data = event.data;
const msg = JSON.parse(data) as ChatMessage;
console.log("[WebSocket] message:", msg);
});
socket.addEventListener("close", (event) => {
console.log("[WebSocket] closed:", event.code, event.reason);
});
6.2 サーバー(Node.js + ws)
インストール
npm install ws
npm install -D @types/ws
実装例
import { WebSocketServer, WebSocket } from "ws";
interface ChatMessage {
type: "chat" | "system";
text: string;
sentAt: string;
}
const wss = new WebSocketServer({ port: 8080 });
wss.on("connection", (socket: WebSocket) => {
console.log("[Server] client connected");
const welcome: ChatMessage = {
type: "system",
text: "Welcome!",
sentAt: new Date().toISOString(),
};
socket.send(JSON.stringify(welcome));
socket.on("message", (data) => {
const msg = JSON.parse(data.toString()) as ChatMessage;
console.log("[Server] received:", msg);
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(msg));
}
});
});
});
参考資料
Discussion