概要・この記事で伝えること
チャットアプリや通知機能、リアルタイムのダッシュボードなどを作るとき、必ず登場するのが WebSocket です。「HTTPと何が違うの?」「なぜポーリングよりWebSocketの方がいいの?」という疑問に答えられるよう、以下を解説します。
- HTTPとWebSocketの根本的な違い
- ハンドシェイクの仕組み(HTTPからどう「アップグレード」するのか)
- フレームというデータの送受信単位
- Pythonでの簡単な実装例
- 筆者が実務・学習で感じた所感
読み終えると、「WebSocketがなぜ双方向通信を実現できるのか」を仕組みレベルで説明できるようになります。
基礎知識の解説
HTTPの限界:リクエストしないと何も起きない
HTTPは基本的に「クライアントがリクエストを送り、サーバーがレスポンスを返す」という一方向・単発の通信モデルです。サーバー側で新しい情報が発生しても、クライアントが次にリクエストを送るまでそれを知る方法がありません。
これを回避するために古くから使われてきたのが「ポーリング」です。一定間隔でクライアントがサーバーに「新しい情報ある?」と聞き続ける方法ですが、以下の問題があります。
- リクエストのたびにTCP接続・HTTPヘッダのオーバーヘッドが発生する
- 通知が欲しいタイミングと実際にポーリングするタイミングがズレる(遅延)
- サーバー負荷が無駄に高くなる(聞かれるたびに「特に何もない」と返すことが多い)
WebSocketは「一度繋いだら双方向に話せる」プロトコル
WebSocketは、最初の接続確立だけHTTPの仕組みを借りて、その後はTCP接続を維持したまま、クライアントとサーバーが双方向に自由にメッセージを送れるプロトコルです(RFC 6455で標準化)。
ポーリングと違い、サーバーは「言いたいことがある瞬間」にいつでもクライアントへメッセージを送れます。これが「リアルタイム通信」と呼ばれる所以です。
ハンドシェイク:HTTPからWebSocketへの「アップグレード」
WebSocketの接続確立は、実はHTTPのリクエストとして始まります。クライアントは次のようなヘッダを送ります。
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
ポイントは Upgrade: websocket と Connection: Upgrade です。「このHTTP接続をWebSocketプロトコルに切り替えてほしい」という意思表示になります。
サーバーがそれを受け入れると、こう返します。
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
ステータスコード 101 Switching Protocols がこの仕組みの核心です。「リクエストは受理したが、これ以降このTCP接続はもうHTTPではなくWebSocketとして扱う」という宣言です。
Sec-WebSocket-Key と Sec-WebSocket-Accept は、リバースプロキシなどがWebSocketと知らずに古いキャッシュ応答を返してしまう事故を防ぐための検証値で、クライアントが送ったキーに固定文字列を結合してSHA-1ハッシュを取った値をサーバーが返すルールになっています。
フレーム:WebSocketのデータ送受信単位
ハンドシェイクが完了すると、データは「フレーム」という単位でやり取りされます。HTTPのようにヘッダ+ボディの構造ではなく、もっと軽量なバイナリ構造です。フレームには次のような種類があります。
- テキストフレーム:UTF-8の文字列を送る
- バイナリフレーム:任意のバイナリデータを送る
- Ping/Pongフレーム:接続が生きているか確認する(死活監視)
- Closeフレーム:接続を正常に終了する
このフレーム構造がHTTPヘッダのような冗長な情報を持たないため、1メッセージあたりのオーバーヘッドが小さく、頻繁に小さいメッセージをやり取りする用途(チャット、ゲームの状態同期、株価のティック更新など)に向いています。
具体例・実際の動作
Pythonの websockets ライブラリを使って、簡単なエコーサーバーを書いてみます。
# server.py
import asyncio
import websockets
async def echo(websocket):
"""受信したメッセージをそのまま返すエコーサーバー"""
async for message in websocket:
print(f"受信: {message}")
await websocket.send(f"サーバーから返却: {message}")
async def main():
async with websockets.serve(echo, "localhost", 8765):
await asyncio.Future() # サーバーを永続稼働させる
if __name__ == "__main__":
asyncio.run(main())
クライアント側はこのようになります。
# client.py
import asyncio
import websockets
async def main():
uri = "ws://localhost:8765"
async with websockets.connect(uri) as websocket:
await websocket.send("こんにちは")
response = await websocket.recv()
print(f"サーバーからの応答: {response}")
if __name__ == "__main__":
asyncio.run(main())
注目してほしいのは、HTTPのように「リクエストを送って毎回新しい接続を張る」のではなく、async with websockets.connect(...) で確立した1本の接続を send / recv で何度も使い回せる点です。
ブラウザのJavaScriptであれば、さらにシンプルに書けます。
const socket = new WebSocket("ws://localhost:8765");
socket.onopen = () => {
socket.send("こんにちは");
};
socket.onmessage = (event) => {
console.log("受信:", event.data);
};
onmessage がまさに「サーバーが好きなタイミングで送ってくるメッセージ」を受け取るためのコールバックで、ポーリングのように自分から聞きに行く必要がない点が体感できると思います。
筆者の考え・所感
個人的に、WebSocketを最初に学んだときに一番感動したのは「HTTPの仕組みの上に成り立っている」という設計の巧みさです。新しいプロトコルをゼロから作るのではなく、既存のHTTPハンドシェイクに Upgrade ヘッダを乗せるだけで「途中からプロトコルを切り替える」という発想は、互換性を保ちながら新機能を導入する良い例だと感じています。ファイアウォールやプロキシの多くがポート80/443を通すという既存のインフラに乗っかれる、という実務上のメリットも大きいです。
また、実務でWebSocketを使うときに気をつけているのは「接続が切れることは前提として設計する」という点です。モバイル回線の切り替えやサーバーの再起動などで接続は普通に切れるので、クライアント側に再接続ロジック(指数バックオフでの再試行など)を必ず入れるようにしています。WebSocketは「繋がっている間は便利」なプロトコルですが、「繋がっていることを保証してくれる」プロトコルではない、という認識を持つと事故が減ると感じています。
あと地味に便利だと思うのが、Ping/Pongフレームによる死活監視です。TCP自体にはアプリケーション層でのタイムアウト検知がないため、「相手は本当にまだ生きているのか」をアプリ側で確認できる仕組みが用意されているのは、長時間接続を前提としたプロトコルらしい配慮だと感じます。
まとめ
- WebSocketはHTTPのハンドシェイク(101 Switching Protocols)を使って確立される、双方向通信が可能なプロトコルである
- 確立後は軽量な「フレーム」単位でデータをやり取りするため、ポーリングよりも低遅延・低オーバーヘッドでリアルタイム通信ができる
- 接続が切れることを前提に、再接続やPing/Pongによる死活監視を組み込んで設計するのが実務上のポイントである