メインコンテンツへスキップ
ブログ一覧に戻る
Web制作・運用

WebSocket入門:双方向通信と、切断後の欠落・重複を扱う設計

2025年10月23日
最終更新: 2026年10月8日
9分で読めます

この記事でわかること

WebSocketの接続と、業務上の配信保証は別です。SSEやポーリングとの選択から、再接続、欠落検知、操作の重複防止、認可と負荷まで整理します。

2026年10月8日にMDN WebSocket APIとRFC 6455を本文まで再確認した。WebSocketはブラウザーとサーバーの双方向セッションを開くが、到着速度が処理能力を超えたときのbackpressure、接続切断、再接続、重複・欠落、認証・認可、順序、退避をアプリケーションが設計する必要がある。WebSocket接続を張るだけでリアルタイム性、配信順序、メッセージ到達、スケールが保証されるとは扱わず、終了時に接続を閉じてbfcacheや資源も確認する。

30秒で要点

WebSocketは、接続を確立した後、クライアントとサーバーの双方からメッセージを送れる通信方式です。チャットや共同操作などの候補になりますが、遅延ゼロ、切断しない接続、業務処理の確実な完了を保証するものではありません。

  • 双方向の更新が必要かを確認し、SSEや定期取得とも比較します。
  • 接続、メッセージの受信、保存や注文の完了を区別します。
  • 切断、再接続、欠落、重複、遅い利用者を想定して設計します。

この記事は、動く接続のデモから、運用できるリアルタイム機能へ進む開発者向けです。本文では通信サービスを起動せず、欠落を検知する純粋関数を小さく確認します。

通信方式は、方向と遅延の要件から選ぶ

「HTTPは毎回接続を切るが、WebSocketは維持する」という比較は不正確です。HTTP/1.1の仕様では、通常は持続接続を使います。WebSocketを選ぶ理由は、アプリが継続的な双方向のメッセージ交換を必要とするかどうかにあります。

表が収まらない場合は、横にスクロールできます

方式通信の形候補となる場面運用で確認すること
定期的なHTTP取得一定間隔で要求・応答更新頻度が低い一覧や状態確認空振り要求、間隔、同時アクセス
ロングポーリング更新まで要求への応答を待つ既存HTTP基盤で通知を扱う構成待機時間、再要求、接続数
SSEHTTP上でサーバーからイベント配信通知、進捗、読み取り中心の更新接続維持、認証、再開位置、履歴
WebSocket接続後に双方からメッセージ送信チャット、共同操作、双方向の状態更新接続状態、配信の契約、負荷、認可
SSEのEventSourceには再接続の仕組みがありますが、それだけでサーバーに過去のイベントが保存されるわけではありません。WebSocketでもSSEでも、どの期間のデータを復元できるかをアプリ側で決めます。

ゲームや動画という分類だけで方式を決めず、必要な遅延、順序、信頼性、データ量を具体化します。既存のHTTP取得で目的を満たすなら、常時接続を増やさない選択もあります。

接続が開いていることと、処理の完了は違う

ブラウザのWebSocket APIは、接続中、接続済み、終了処理中、終了という状態を持ちます。send()は受信者が保存を完了したという応答ではありません。送信待ちのデータ量はbufferedAmountで観察できますが、それが0でも業務処理の成功までは分かりません。WebSockets標準は、このAPIの状態と送信処理を定めています。

表が収まらない場合は、横にスクロールできます

状態・観測分かること分からないこと
CONNECTING接続を確立している途中接続できるか、いつ開くか
OPENAPI上、メッセージを送れる状態相手の業務処理が成功するか
bufferedAmountAPIがネットワークへ未送信の待ち量相手が受信・保存したか
アプリ独自の受付応答定義した受付段階へ進んだこと応答の意味を超える処理の完了
CLOSED接続が終了したこと最後の操作が未実行か実行済みか

注文送信後に接続が切れた場合、注文が未保存なのか、保存済みだが応答が届かなかったのか分からないことがあります。「接続エラーだから同じ注文を新しく送る」という処理では、二重注文になり得ます。

重要な操作には、操作IDと、そのIDに対する結果を確認する仕組みを設計します。サーバー側で重複を検出し、保存と結果記録をどう整合させるかを決めます。通信方式だけで、業務上のexactly-onceを得られると考えないでください。

再接続は、状態を合わせ直す入口にする

ブラウザ標準のWebSocketには、アプリのための自動再接続や履歴復元が一式で用意されているわけではありません。再試行するなら、待ち時間を増やす、ばらつきを付ける、上限を設けるなど、同時再接続の集中を避ける設計が必要です。

再接続後は、最後に確認した位置から履歴を取得するか、現在の状態を取り直します。履歴が保存期限を超えていた場合も、古い状態のまま更新を再開してはいけません。

連番40の後に41を受信して反映し、切断後に43が届くと42の欠落を検知する。差分の適用を止め、整合したスナップショットと再開位置を取得してから更新を再開する
図1:接続の回復とデータの復元は別。例では1つの購読系列に連続した番号がある前提で欠落を見つけ、整合した状態を取得し直す。

連番の欠落を、小さな関数で確認する

以下は架空の公開カウンターです。1つの購読系列のイベント番号が1ずつ増える契約を前提にします。複数の系列を混ぜたり、番号の途中を意図的に配信しなかったりする仕組みには、そのまま適用できません。

sequence.mjsとして保存します。状態はアプリ内部で作り、メッセージはJSONから得た値を想定しています。Node.js 24.18.0で検証した純粋関数であり、接続処理や実サーバーは含みません。

export function applyEvent(state, message) {
  if (!message || typeof message !== 'object' ||
      Array.isArray(message) ||
      message.type !== 'counter.updated' ||
      !Number.isSafeInteger(message.seq) || message.seq < 1 ||
      !Number.isSafeInteger(message.value) || message.value < 0) {
    throw new Error('Invalid event');
  }
  if (state.needsSync) return state;
  if (message.seq <= state.lastSeq) return state;
  if (message.seq !== state.lastSeq + 1) {
    return { ...state, needsSync: true };
  }
  return {
    lastSeq: message.seq,
    value: message.value,
    needsSync: false,
  };
}

同じ場所にcheck.mjsを保存し、node check.mjsで確認します。

import assert from 'node:assert/strict';
import { applyEvent } from './sequence.mjs';

const initial = { lastSeq: 40, value: 7, needsSync: false };
const event41 = { type: 'counter.updated', seq: 41, value: 8 };
const next = applyEvent(initial, event41);
assert.equal(next.value, 8);
assert.equal(applyEvent(next, event41), next);
const gap = applyEvent(next, { ...event41, seq: 43, value: 10 });
assert.equal(gap.needsSync, true);
assert.equal(gap.lastSeq, 41);
assert.equal(applyEvent(gap, { ...event41, seq: 42, value: 9 }), gap);
assert.throws(() => applyEvent(initial, { ...event41, seq: '41' }));
console.log('sequence checks passed');

41を二度受け取った場合は反映を繰り返しません。41の後に43が来た場合は、欠落した42を推測せず、同期が必要な状態へ移します。一度その状態になったら、遅れて42が来ても差分の適用を再開しません。

再開には、サーバーから整合したスナップショットと対応する連番を取得する手順が必要です。取得中に発生した更新を失わないよう、履歴の再送、購読開始位置、または受信済みイベントの一時保管などを組み合わせます。この例はその取得プロトコルを実装していません。単に数値をリセットすれば復元が完成するわけではありません。

Socket.IOを選ぶ場合も、配信条件を決める

Socket.IOは、接続管理やイベントなどを提供するライブラリです。現行のv4の説明にはHTTPロングポーリング、WebSocket、WebTransportが挙げられています。SSEへのフォールバックを持つという説明は適切ではありません。また、独自のプロトコルがあるため、素のWebSocketクライアントとはそのまま接続できません。

配信保証の説明では、到達するイベントの順序と、イベントが到達するかどうかを分けています。標準の到達条件はat-most-onceで、切断中のサーバーイベントが自動的に永続保存・再送されるわけではありません。

再送と確認応答を追加しても、受信者の処理後に応答だけが失われれば重複する可能性があります。永続的なイベントID、保存先、最後に確認した位置、重複時の扱いを、必要な保証に合わせて設計します。特定ライブラリの復元機能を使う場合も、保存期間や対応する配信構成の制約を確認してください。

長く続く接続に、権限と資源の上限を設ける

WebSocketを公開するときは、TLSを使うwss、接続時の認証、操作ごとの認可を組み合わせます。Cookieを伴うブラウザ接続では、想定外のサイトから接続されないよう、Originの明示的な許可リストなどを確認します。Origin確認は認証の代わりではなく、ブラウザ以外のクライアントの身元も証明しません。

ログイン時に許可された接続でも、セッションの期限や権限は後から変わります。ログアウト、失効、権限変更を長時間接続へ反映する方法を用意します。受信したルーム名や利用者IDだけで、他人の情報を購読させてはいけません。OWASPのWebSocketガイドは、Origin、認可、入力、資源制限を扱っています。

遅い利用者へ無制限に送り続けると、待ち行列が増えます。更新をまとめて最新値だけ送ってよいのか、取りこぼしてはいけない履歴なのかを分け、上限を超えた場合の切断や再同期を決めます。メッセージのサイズ、送信頻度、接続数にも用途に合った制限を設けます。

配信基盤と障害を、導入前に確かめる

複数台へ分散する構成では、どのサーバーへ接続している利用者にも必要な更新を届ける仕組みが必要です。接続情報、イベントの配布、履歴保存、再開位置を誰が担当するかを決めます。1台で動く無認可の全員向け配信例を、そのまま本番構成にしないでください。

プロキシやロードバランサーの接続維持、アイドル時間、メッセージ制限も確認します。リリース時に既存接続をどう終了し、利用者がどう復旧するかを試します。常時接続数、接続時間、メッセージ量、転送量、保存・監視費を含めて費用を見積もります。

表が収まらない場合は、横にスクロールできます

試す条件確認する結果
操作を送った直後の切断結果不明を表示し、同じ操作を二重確定しないか
履歴の保存期間を超えた再接続現在の状態へ安全に合わせ直せるか
更新が速く、受信・描画が遅い端末メモリが増え続けず、復帰方法があるか
セッション失効・権限変更接続と購読権限へ反映されるか
サーバーの切替再接続が集中せず、欠落と重複を扱えるか

監視では接続成功だけでなく、再接続率、同期に戻るまでの時間、遅延、キューの量、失敗した操作を見ます。メッセージ全文や認証情報をログへ残さず、原因を追える識別子と必要な状態を選びます。本稿の純粋関数の合格は、これらの実接続・負荷・ブラウザ動作の試験を代替しません。

よくある質問(FAQ)