設定

WebSocket入門|リアルタイム双方向通信の仕組み

ネットワーク WebSocket リアルタイム

WebSocket入門
リアルタイム双方向通信の仕組み

WebSocketの仕組み、JavaScript実装、メッセージ送受信、エラーハンドリング、Socket.IOまで、リアルタイム通信の基礎を解説します。

こんな人向けの記事です

  • WebSocketの仕組みを理解したい
  • リアルタイム通信を実装したい
  • HTTPとWebSocketの違いを知りたい

Step 1WebSocketとは?HTTPとの違い

WebSocketは、クライアントとサーバー間で双方向のリアルタイム通信を実現するプロトコルです。RFC 6455で標準化されており、チャットアプリ、リアルタイム通知、オンラインゲームなど、即時性が求められる場面で広く使われています。

従来のHTTP通信との違いを理解することが、WebSocketを学ぶ第一歩です。

比較項目HTTPWebSocket
通信方向クライアント → サーバー(リクエスト/レスポンス)双方向(サーバーからもプッシュ可能)
接続毎回接続・切断(HTTP/1.1はKeep-Alive可)一度接続したら維持(持続的接続)
オーバーヘッド毎回HTTPヘッダーを送信ハンドシェイク後は最小限のフレームヘッダーのみ
プロトコルhttp:// / https://ws:// / wss://
ユースケースWebページ取得、API呼び出しチャット、通知、株価更新、ゲーム
なぜHTTPではダメなのか?
HTTPはリクエスト/レスポンスモデルのため、サーバーからクライアントに自発的にデータを送れません。ポーリング(定期的にリクエスト)やロングポーリングで擬似的に実現できますが、無駄な通信が発生しリアルタイム性も低くなります。WebSocketはこの問題を根本的に解決します。

HTTPポーリングとWebSocketの通信量を比較してみましょう。例えば、1秒ごとにデータを取得する場合を考えます。

通信量の比較(概算)
# HTTPポーリング(1秒ごと)
# 毎回 HTTPヘッダー(約800バイト)+ レスポンスヘッダー
# 1分間: 60リクエスト × 約1.6KB = 約96KB

# WebSocket
# ハンドシェイク: 1回(約500バイト)
# データフレーム: 2〜14バイトのヘッダー + ペイロード
# 1分間: 1回のハンドシェイク + 60フレーム × 約20バイト = 約1.7KB

WebSocketは接続を維持するため、ヘッダーの繰り返し送信がなく、通信量を大幅に削減できます。

Step 2WebSocketのハンドシェイク

WebSocket接続は、最初にHTTPを使ったハンドシェイクで始まります。クライアントがHTTPリクエストでプロトコルのアップグレードを要求し、サーバーが承認すると、以降はWebSocketプロトコルで通信します。

クライアントからのリクエスト

HTTPリクエスト
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: http://example.com

重要なヘッダーを確認しましょう。

ヘッダー役割
UpgradewebsocketWebSocketへのプロトコル切り替えを要求
ConnectionUpgrade接続のアップグレードを指示
Sec-WebSocket-KeyBase64文字列セキュリティ検証用のランダムキー
Sec-WebSocket-Version13WebSocketプロトコルのバージョン

サーバーからのレスポンス

HTTPレスポンス
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

ステータスコード101 Switching Protocolsが返されると、ハンドシェイク成功です。Sec-WebSocket-Acceptはクライアントが送ったSec-WebSocket-Keyにマジック文字列を結合してSHA-1ハッシュ化し、Base64エンコードした値です。

Sec-WebSocket-Acceptの生成ロジック
# Sec-WebSocket-Accept の計算
import hashlib, base64

key = "dGhlIHNhbXBsZSBub25jZQ=="
magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"

accept = base64.b64encode(
    hashlib.sha1((key + magic).encode()).digest()
).decode()

print(accept)  # s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
ハンドシェイクの注意点
WebSocketのハンドシェイクはHTTPで行われるため、プロキシやロードバランサーがUpgradeヘッダーを除去すると接続に失敗します。Nginx等のリバースプロキシを使う場合は、WebSocket用の設定(proxy_set_header Upgrade等)が必要です。

Step 3JavaScriptでのWebSocket接続

ブラウザにはWebSocket APIが標準搭載されています。new WebSocket()でインスタンスを作成するだけで、サーバーへの接続が開始されます。

基本的な接続

JavaScript
// WebSocket接続の作成
const ws = new WebSocket('ws://localhost:8080/chat');

// 接続が開いたとき
ws.onopen = function(event) {
  console.log('WebSocket接続が確立されました');
  console.log('readyState:', ws.readyState); // 1 (OPEN)
};

// 接続が閉じたとき
ws.onclose = function(event) {
  console.log('接続が閉じました');
  console.log('code:', event.code);       // 1000 = 正常終了
  console.log('reason:', event.reason);   // 終了理由
  console.log('wasClean:', event.wasClean); // 正常に閉じたか
};

// エラーが発生したとき
ws.onerror = function(event) {
  console.error('WebSocketエラー:', event);
};

WebSocketオブジェクトのreadyStateプロパティで、現在の接続状態を確認できます。

定数名状態
0CONNECTING接続中(ハンドシェイク中)
1OPEN接続済み(通信可能)
2CLOSING切断処理中
3CLOSED切断済み

セキュアな接続(wss://)

JavaScript
// 本番環境ではwss://(WebSocket Secure)を使う
const ws = new WebSocket('wss://example.com/chat');

// HTTPSページからws://に接続するとブラウザがブロックする
// Mixed Contentエラーになるため、本番では必ずwss://を使用
ws:// と wss:// の使い分け
ws://は暗号化なし、wss://はTLS暗号化ありのWebSocket接続です。HTTPSページからはwss://のみ接続可能です。開発環境ではws://localhostを使い、本番環境では必ずwss://を使用してください。

Step 4メッセージの送受信(send, onmessage)

WebSocketの最も重要な機能はメッセージの送受信です。send()でデータを送信し、onmessageイベントで受信します。

テキストメッセージの送受信

JavaScript(クライアント側)
const ws = new WebSocket('ws://localhost:8080/chat');

ws.onopen = function() {
  // テキストメッセージを送信
  ws.send('こんにちは!');

  // JSONデータを送信(よく使うパターン)
  ws.send(JSON.stringify({
    type: 'message',
    user: 'taro',
    text: 'WebSocketで通信中!',
    timestamp: Date.now()
  }));
};

// メッセージを受信
ws.onmessage = function(event) {
  console.log('受信データ:', event.data);

  // JSONの場合はパース
  try {
    const data = JSON.parse(event.data);
    console.log('type:', data.type);
    console.log('text:', data.text);
  } catch (e) {
    // プレーンテキストの場合
    console.log('テキスト:', event.data);
  }
};

バイナリデータの送受信

JavaScript
// バイナリデータの受信形式を指定
ws.binaryType = 'arraybuffer'; // または 'blob'(デフォルト)

// ArrayBufferで送信
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setUint32(0, 12345);
ws.send(buffer);

// Blobで送信(ファイル送信など)
const blob = new Blob(['バイナリデータ'], { type: 'application/octet-stream' });
ws.send(blob);

// バイナリデータの受信
ws.onmessage = function(event) {
  if (event.data instanceof ArrayBuffer) {
    const view = new DataView(event.data);
    console.log('受信した数値:', view.getUint32(0));
  } else if (typeof event.data === 'string') {
    console.log('テキスト:', event.data);
  }
};

Node.jsサーバー側の実装例

server.js(Node.js + ws)
const WebSocket = require('ws');
const server = new WebSocket.Server({ port: 8080 });

server.on('connection', function(ws) {
  console.log('クライアントが接続しました');

  // メッセージを受信
  ws.on('message', function(message) {
    console.log('受信:', message.toString());

    // 受信したメッセージを全クライアントにブロードキャスト
    server.clients.forEach(function(client) {
      if (client.readyState === WebSocket.OPEN) {
        client.send(message.toString());
      }
    });
  });

  // 接続時にウェルカムメッセージを送信
  ws.send(JSON.stringify({
    type: 'system',
    text: '接続しました。チャットを開始できます。'
  }));

  // 切断時の処理
  ws.on('close', function() {
    console.log('クライアントが切断しました');
  });
});
send()の呼び出しタイミング
ws.send()は接続がOPEN状態のときのみ使用できます。onopenイベントが発火する前にsend()を呼ぶとエラーになるため、必ずonopen内か、readyState === WebSocket.OPENを確認してから送信してください。

Step 5エラーハンドリングと再接続

WebSocket接続はネットワーク障害やサーバー再起動によって切断される可能性があります。実運用では自動再接続の仕組みが不可欠です。

基本的なエラーハンドリング

JavaScript
const ws = new WebSocket('ws://localhost:8080/chat');

ws.onerror = function(event) {
  // onerrorではエラーの詳細情報にアクセスできない(セキュリティ上の理由)
  console.error('WebSocket接続エラーが発生しました');
};

ws.onclose = function(event) {
  // oncloseで終了コードと理由を確認
  switch (event.code) {
    case 1000:
      console.log('正常終了');
      break;
    case 1001:
      console.log('サーバーが停止 or ページ遷移');
      break;
    case 1006:
      console.log('異常切断(ネットワークエラー)');
      break;
    case 1011:
      console.log('サーバー内部エラー');
      break;
    default:
      console.log('切断コード:', event.code);
  }
};

主要な終了コードを一覧で確認しましょう。

コード名前説明
1000Normal Closure正常に接続が終了
1001Going Awayサーバー停止またはページ遷移
1002Protocol Errorプロトコルエラー
1003Unsupported Dataサポートされないデータ型を受信
1006Abnormal Closure異常切断(コードなしで切断)
1011Internal Errorサーバー内部エラー

指数バックオフ付き自動再接続

JavaScript
class ReconnectingWebSocket {
  constructor(url, options = {}) {
    this.url = url;
    this.maxRetries = options.maxRetries || 10;
    this.baseDelay = options.baseDelay || 1000;  // 初回待機: 1秒
    this.maxDelay = options.maxDelay || 30000;    // 最大待機: 30秒
    this.retryCount = 0;
    this.onmessage = null;
    this.onopen = null;
    this.connect();
  }

  connect() {
    this.ws = new WebSocket(this.url);

    this.ws.onopen = (event) => {
      console.log('接続成功');
      this.retryCount = 0;  // リトライカウントをリセット
      if (this.onopen) this.onopen(event);
    };

    this.ws.onmessage = (event) => {
      if (this.onmessage) this.onmessage(event);
    };

    this.ws.onclose = (event) => {
      if (event.code === 1000) return; // 正常終了なら再接続しない

      if (this.retryCount < this.maxRetries) {
        // 指数バックオフ: 1s, 2s, 4s, 8s, ... 最大30s
        const delay = Math.min(
          this.baseDelay * Math.pow(2, this.retryCount),
          this.maxDelay
        );
        console.log(`${delay}ms後に再接続... (${this.retryCount + 1}/${this.maxRetries})`);
        setTimeout(() => this.connect(), delay);
        this.retryCount++;
      } else {
        console.error('最大リトライ回数に達しました');
      }
    };

    this.ws.onerror = () => {
      console.error('接続エラー');
    };
  }

  send(data) {
    if (this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(data);
    } else {
      console.warn('接続が開いていません。readyState:', this.ws.readyState);
    }
  }

  close() {
    this.ws.close(1000, '正常終了');
  }
}

// 使い方
const ws = new ReconnectingWebSocket('ws://localhost:8080/chat');
ws.onmessage = (event) => console.log('受信:', event.data);
ws.onopen = () => ws.send('Hello!');
再接続時のデータ整合性
再接続すると、切断中に送信されたメッセージは失われます。重要なデータを扱う場合は、メッセージにシーケンス番号を付与し、再接続時に未受信分を再取得する仕組みを実装しましょう。

ハートビート(Ping/Pong)

JavaScript
// アプリケーションレベルのハートビート
class HeartbeatWebSocket extends ReconnectingWebSocket {
  constructor(url, options = {}) {
    super(url, options);
    this.heartbeatInterval = options.heartbeatInterval || 30000; // 30秒
    this.heartbeatTimer = null;
  }

  connect() {
    super.connect();

    this.ws.onopen = (event) => {
      this.startHeartbeat();
      if (this.onopen) this.onopen(event);
    };
  }

  startHeartbeat() {
    this.stopHeartbeat();
    this.heartbeatTimer = setInterval(() => {
      if (this.ws.readyState === WebSocket.OPEN) {
        this.ws.send(JSON.stringify({ type: 'ping' }));
      }
    }, this.heartbeatInterval);
  }

  stopHeartbeat() {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer);
      this.heartbeatTimer = null;
    }
  }
}

Step 6WebSocketライブラリ(Socket.IO, ws)

生のWebSocket APIでも十分ですが、実運用では便利な機能を備えたライブラリを使うことが多いです。代表的なライブラリを比較しましょう。

ライブラリ環境特徴
wsNode.js(サーバー)軽量で高速。生のWebSocketに近いAPI
Socket.IONode.js + ブラウザ自動再接続、ルーム、名前空間、フォールバック
WebSocket APIブラウザ(標準)追加ライブラリ不要。シンプルだが機能は最小限

ws(Node.js向け軽量ライブラリ)

ターミナル
# wsのインストール
npm install ws
server.js(ws)
const { WebSocketServer } = require('ws');

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', function connection(ws, req) {
  const ip = req.socket.remoteAddress;
  console.log(`接続元: ${ip}`);

  ws.on('message', function message(data, isBinary) {
    // 全クライアントにブロードキャスト
    wss.clients.forEach(function each(client) {
      if (client !== ws && client.readyState === 1) {
        client.send(data, { binary: isBinary });
      }
    });
  });

  // サーバー側のPing/Pong(30秒間隔)
  const interval = setInterval(() => {
    if (ws.readyState === 1) ws.ping();
  }, 30000);

  ws.on('close', () => clearInterval(interval));
});

Socket.IO(フル機能ライブラリ)

Socket.IOは単なるWebSocketラッパーではなく、独自のプロトコルを持つライブラリです。WebSocketが使えない環境ではHTTPロングポーリングに自動フォールバックします。

ターミナル
# Socket.IOのインストール
npm install socket.io          # サーバー側
npm install socket.io-client   # クライアント側
server.js(Socket.IO サーバー)
const { Server } = require('socket.io');
const io = new Server(3000, {
  cors: { origin: '*' }
});

io.on('connection', (socket) => {
  console.log('ユーザー接続:', socket.id);

  // カスタムイベントでメッセージ受信
  socket.on('chat message', (msg) => {
    console.log('メッセージ:', msg);
    // 全クライアントに送信(送信者含む)
    io.emit('chat message', msg);
  });

  // ルーム機能: 特定のグループにだけ送信
  socket.on('join room', (room) => {
    socket.join(room);
    socket.to(room).emit('system', `${socket.id}が入室しました`);
  });

  socket.on('disconnect', (reason) => {
    console.log('切断:', reason);
  });
});
client.js(Socket.IO クライアント)
import { io } from 'socket.io-client';

const socket = io('http://localhost:3000');

// 接続成功
socket.on('connect', () => {
  console.log('接続:', socket.id);

  // ルームに参加
  socket.emit('join room', 'general');

  // メッセージ送信
  socket.emit('chat message', {
    user: 'taro',
    text: 'Socket.IOで通信中!'
  });
});

// メッセージ受信
socket.on('chat message', (msg) => {
  console.log(`${msg.user}: ${msg.text}`);
});

// 自動再接続(Socket.IOはデフォルトで有効)
socket.on('reconnect', (attempt) => {
  console.log(`再接続成功(${attempt}回目)`);
});

socket.on('reconnect_error', (error) => {
  console.error('再接続失敗:', error);
});
Socket.IOと生WebSocketの互換性
Socket.IOは独自プロトコルのため、Socket.IOサーバーにnew WebSocket()で接続することはできません(その逆も同様)。サーバーとクライアントの両方でSocket.IOを使うか、両方で生WebSocketを使うかを統一する必要があります。

ライブラリ選択の指針

  • シンプルな双方向通信のみ → 生WebSocket API + ws
  • 自動再接続・ルーム・名前空間が必要 → Socket.IO
  • レガシーブラウザ対応が必要 → Socket.IO(フォールバック機能あり)
  • パフォーマンス重視(ゲーム等) → 生WebSocket API + ws(オーバーヘッド最小)
  • Django/Pythonバックエンド → Django Channels(WebSocket対応)
まとめ
WebSocketはHTTPのリクエスト/レスポンスモデルの制約を超え、サーバーとクライアント間で効率的な双方向通信を実現するプロトコルです。基本的な接続・送受信・エラーハンドリングを理解し、用途に合ったライブラリを選択することで、チャット、通知、リアルタイムダッシュボードなど様々なアプリケーションを構築できます。