プロトコル

WebSocketとは?仕組み・ハンドシェイクからNode.js実装・運用の判断まで実装者向けに解説

チャット、通知、共同編集、ダッシュボードの数値更新。画面が勝手に動く機能を任されたとき、実装者が最初に候補へ挙げるのがWebSocketです。ところが実際に詰まる場所はプロトコルの理解ではなく「本番のプロキシで60秒ごとに切れる」「サーバーを2台に増やしたら片方の利用者にだけ届かない」といった周辺にあります。本記事では、ハンドシェイクとフレームの動作を仕様の粒度まで分解し、ブラウザとNode.jsの実装、運用でつまずく箇所、採否の判断までを2026年7月時点の一次情報で整理します。

まとめ:WebSocketの仕組みと実装・採用判断の結論

WebSocketは、1本のTCP接続の上でクライアントとサーバーが対等にデータを送り合う通信プロトコルです。仕様はRFC 6455(2011年12月)で、URLのスキームは平文がws、TLS上がwssになります。

接続はいきなり独自プロトコルでは始まりません。まず通常のHTTPリクエストを投げ、サーバーが101 Switching Protocolsを返した時点で同じTCP接続をWebSocketへ切り替える。この2段構えにより、既存の443番ポートとHTTPSの経路をそのまま通れます。

切り替わった後の通信単位はフレームで、ヘッダーは最小2バイトまで縮みます。リクエストのたびに数百バイトを送り直すHTTPと比べると、1メッセージの固定費は2桁違う計算です。

実装で判断が要る箇所は4つ。ハンドシェイク時の認証(ブラウザAPIは独自ヘッダーを付けられない)、切断の検知と再接続、送信が詰まったときのbufferedAmount、複数台へ広げるときの配信経路です。採否の判断軸は「双方向性が本当に要るか」に絞れます。サーバーから流すだけならSSE、映像や音声ならWebRTC、モバイル網の間欠通信ならMQTTのほうが構成は軽い。

WebSocketとは何か|1本のTCP接続で双方向にやり取りする通信プロトコル

HTTPポーリング・ロングポーリングと比べたときの決定的な違い

HTTPは、クライアントが尋ねてサーバーが答える往復で成り立っています。サーバー側の出来事を画面へ反映したいとき、この形では定期的に尋ね続けるしかありません。3秒間隔・利用者1万人なら1時間に1,200万リクエストが積み上がり、大半は空振りです。ロングポーリングは変化まで応答を保持する改良ですが、通知1回ごとに接続を張り直す構造は残ります。

WebSocketはこの往復モデル自体を降ります。接続を1度張れば、どちらの側からでも相手の許可を待たずに送れる。実装者から見て変わるのは、リクエストとレスポンスの対応関係が消え、受け取ったメッセージが何に対する応答かを自前で紐づける設計になる点です。

RFC 6455と関連仕様の位置づけ|HTTP/2・HTTP/3上での扱い

WebSocketの本体はRFC 6455、ブラウザ側のAPIはWHATWGのHTML標準が定めます。圧縮はRFC 7692のpermessage-deflate拡張として後から追加されました。

HTTP/2以降での扱いは分けて覚える必要があります。Upgradeヘッダーによる切り替えはHTTP/1.1の仕組みで、HTTP/2には持ち込めません。RFC 8441(2018年9月)が拡張CONNECTで多重化を可能にし、RFC 9220(2022年6月)がこれをHTTP/3へ広げています。

ただし実装の現状は仕様の整備と一致しません。RFC 9220に対応した主要ブラウザ・サーバーの本番実装は2026年前半時点で出荷されておらず、実運用のほとんどはHTTP/1.1のUpgradeかRFC 8441の経路で成立しています。設計時は「HTTP/1.1のハンドシェイクが通る経路を確保する」前提が安全でしょう。土台のTCPソケットそのものの手順はソケット通信の仕組みとTCP・UDPの扱いで扱っています。

SSE・WebRTC・MQTTとの住み分けを一枚の表で確認する

候補が複数あるときは、通信の向きと中身で切り分けます。

技術 通信の向き 向く用途
WebSocket 双方向 チャット・共同編集・操作の即時反映
SSE サーバーから片方向 通知・進捗・株価の配信
WebRTC 双方向(P2P) 映像・音声・画面共有
MQTT 双方向(仲介あり) IoT機器の間欠通信

4方式のレイテンシやスループットを比べた検証はSSEとWebSocketの違いと使い分けにあります。本記事は比較ではなく、選んだ後に要る仕様と実装を担当します。

WebSocketの仕組み①|HTTPハンドシェイクで接続を切り替える手順

クライアントが送るUpgradeリクエストとSec-WebSocket-Key

クライアントは、通常のHTTP GETに4つのヘッダーを載せます。Upgradeにwebsocket、ConnectionにUpgrade、Sec-WebSocket-Versionに13、Sec-WebSocket-Keyへ16バイトの乱数をBase64にした値です。

GET /realtime HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: chat.v1
Origin: https://app.example.com

押さえたいのは、Sec-WebSocket-Keyが認証情報ではない点です。相手がWebSocketを理解しているかを確かめる使い捨ての乱数で、盗まれて困る値ではありません。ブラウザが自動生成するため、コードで触る場面もない。

サーバーが返す101 Switching Protocolsと受理条件の検証

サーバーは、受け取ったキーの末尾に仕様で固定されたGUID(258EAFA5-E914-47DA-95CA-C5AB0DC85B11)を連結し、SHA-1でハッシュしてBase64に直した値をSec-WebSocket-Acceptへ入れ、ステータス101で返します。

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat.v1

この101が返った瞬間、同じTCP接続の意味づけがHTTPからWebSocketへ切り替わります。以降はHTTPの構文が流れることはなく、両者はフレームだけをやり取りする関係になる。経路上の機器が101と非HTTPのバイト列を通せないと、接続は成立しても直後に落ちます。

トラブル時の切り分けは、この1往復を見るのが最短です。応答が200や400ならWebSocket対応のハンドラに届いておらず、403ならOrigin検証か認証で弾かれています。502や504はプロキシがUpgradeを転送していない疑いが濃い。

WebSocketの仕組み②|フレーム構造とopcode・マスキングの役割

opcodeで分かれるフレームの種類とテキスト・バイナリの使い分け

フレームの先頭は、FINビット1つ、予約ビット3つ、opcode 4ビットの計1バイトです。opcodeは6種類が定義済みで、0x1がテキスト、0x2がバイナリ、0x0が分割の続き、0x8がクローズ、0x9がPing、0xAがPong。続く1バイト目にマスクビットとペイロード長が入ります。

結果として、短いメッセージのヘッダーはサーバー送信で2バイト、クライアント送信でもマスクキーを含めて6バイトに収まります。テキストフレームはUTF-8と仕様で決まっているため、画像などのバイト列はバイナリフレームへ載せる。ブラウザ側はbinaryTypeにarraybufferを指定すると扱いが単純です。大きなデータはFINを0にした継続フレームへ分割できます。

クライアントからの送信にマスキングが必須とされている仕様上の理由

RFC 6455は、クライアントから送るフレームすべてにマスキングを義務づけています。4バイトのランダムなキーとXORするだけの処理で、暗号としての強度はありません。目的が秘匿ではないからです。

狙いは、経路上のプロキシやキャッシュを騙す攻撃の防止です。マスクがなければ、攻撃者は被害者のブラウザから「HTTPリクエストに見えるバイト列」を送り込み、WebSocketを理解しない中間装置に別のリクエストとして解釈させる余地が生まれる。送信のたびにキーが変わるマスキングは、バイト列を意図通りに固定させない措置です。サーバー発のフレームは逆にマスクしてはならず、取り違えると「ブラウザからは繋がるのに別のクライアントからはプロトコルエラー」という挙動になります。

Ping・Pongとクローズハンドシェイクによる接続状態の管理

TCP接続は、相手のプロセスが落ちてもモバイル網が切り替わっても、こちら側からは静かなままに見えます。届かなくなったことを検知する仕組みがPing・Pongです。仕様上、Pingを受け取った側は早くPongを返す義務があり、応答が戻らない接続は死んだものとして切断できます。ブラウザのJavaScript APIからPingは送れないため、生存確認はサーバー側から仕掛けます。

正常な終了はクローズフレームの往復です。片方が0x8を送り、相手も0x8を返してからTCPを閉じる。この手順を踏んだときのコードが1000です。対して1006は、クローズフレームを受け取らないまま接続が消えた場合にライブラリが内部で立てる値で、ネットワーク上を流れません。再接続処理では、この区別が分岐の基準になります。

ブラウザ側の実装|WebSocket APIの接続・送受信・切断の書き方

ブラウザから接続してメッセージを受信・切断するまでの最小コード

クライアント側のAPIは小さく、コンストラクタにURLを渡すと接続が始まります。あとは4つのイベント(open、message、error、close)を捌くだけです。

const ws = new WebSocket('wss://example.com/realtime', 'chat.v1');
ws.binaryType = 'arraybuffer';

ws.addEventListener('open', () => {
  ws.send(JSON.stringify({ type: 'subscribe', room: 'orders' }));
});

ws.addEventListener('message', (ev) => {
  const msg = JSON.parse(ev.data);
  applyToView(msg);
});

ws.addEventListener('close', (ev) => {
  // 1000 は正常終了、1006 は相手からの通知がない異常切断
  if (ev.code !== 1000) scheduleReconnect();
});

ここで最初にぶつかるのが認証の置き場所です。ブラウザのWebSocket APIは、fetchと違ってリクエストヘッダーを付けられません。Authorizationヘッダーが使えないため、選択肢は3つ。Cookieの自動送信を使う、短命のワンタイムチケットをHTTP APIで先に発行してクエリ文字列へ載せる、サブプロトコル欄に載せる。クエリ文字列はログに残るため、数十秒で失効するチケットに限る条件が付きます。

切断を前提に再接続とハートビートを組み込むクライアント側の設計

実運用のWebSocketは切れます。電波が変わる、端末がスリープする、経路のプロキシが無通信を打ち切る。切れた後に静かに戻る作りにします。

再接続は指数バックオフが基本形で、1秒、2秒、4秒と広げて上限を30秒程度にする。要点は乱数のゆらぎです。サーバー再起動で1万接続が同時に落ちたとき、全員が同じ秒数で戻ると再起動直後に同規模の接続嵐が届きます。

もう1つ、再接続後の状態合わせを設計に含めます。切れていた間のメッセージは届かないため、繋ぎ直した直後に「最後に受け取ったID以降」をHTTP APIで取りに行く経路を用意しておく。この差分取得がないと、画面は繋がっているのに中身だけ古いという不整合が残ります。

サーバー側の実装|Node.jsのwsで接続を受けて認証まで通す構成

ここではNode.jsの標準的な選択肢であるwsライブラリ(2026年7月時点の最新は8.21系)で骨格を示します。

wsで接続を受ける最小サーバーとハンドシェイク時の認証の入れ方

本番構成で採るべき形は、WebSocketServerをnoServerモードで作り、HTTPサーバーのupgradeイベントを自分で受けるやり方です。接続を確立する前に認証を挟めるため、権限のない相手にフレームを一切流さずに済みます。

import { WebSocketServer } from 'ws';
import http from 'node:http';

const server = http.createServer();
const wss = new WebSocketServer({ noServer: true });

server.on('upgrade', (req, socket, head) => {
  const user = verifyTicket(req);        // 失効の短いチケットを検証
  if (!user || !isAllowedOrigin(req)) {
    socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
    socket.destroy();
    return;
  }
  wss.handleUpgrade(req, socket, head, (ws) => {
    wss.emit('connection', ws, req, user);
  });
});

wss.on('connection', (ws, req, user) => {
  ws.isAlive = true;
  ws.on('pong', () => { ws.isAlive = true; });
  ws.on('message', (data, isBinary) => handle(user, data, isBinary));
});

setInterval(() => {
  for (const client of wss.clients) {
    if (!client.isAlive) { client.terminate(); continue; }
    client.isAlive = false;
    client.ping();
  }
}, 30000);

server.listen(3000);

30秒ごとのPingは、死んだ接続の回収と経路のアイドルタイムアウト対策を兼ねます。terminateとcloseの違いも押さえておきたい。closeは穏当な終了、terminateは即座にソケットを破棄する強制終了で、応答が返らない相手には後者を使います。

送信が詰まったときの挙動とbufferedAmountによる背圧の制御

sendは、相手に届いたことを保証しません。送信データはバッファへ積まれ、回線の速度で吐き出されます。受け手が遅い相手へ秒間数十件を送り続けると、このバッファだけが膨らむ。

防ぎ方は、送信前にbufferedAmount(未送信のバイト数)を見て、しきい値を超えていたら送るのをやめる単純な判断です。数値更新なら古い値を捨てて最新だけ送る、ログなら一定件数をまとめて1メッセージにする間引きが効きます。しきい値は1MB前後から始め、実測しながら調整するのが現実的でしょう。

ブラウザ側にも同名のプロパティがあります。標準化が進むWebSocketStreamはStreams APIで背圧を自動的に扱えますが、対応が単一のレンダリングエンジンにとどまるため、2026年7月時点の本番コードは自前の判定を持つ前提で組みます。

運用でつまずく箇所|プロキシのタイムアウトと水平分散の設計方針

アイドルタイムアウトで切断される原因とプロキシ側の設定の勘所

ローカルでは安定していた接続が、本番で規則的に切れる。原因の大半は経路上の機器が持つ無通信タイマーです。nginxのproxy_read_timeoutは既定60秒、AWSのApplication Load Balancerのアイドルタイムアウトも既定60秒で、無通信が続けば接続を畳みます。

対処は2方向あり、実務では両方を入れます。30秒間隔のPingで無通信をなくす方法と、経路側のタイムアウト値を延ばす方法です。nginxを手前に置く場合、HTTP/1.1への固定とUpgrade系ヘッダーの転送も要り、抜けていると101すら返りません。

location /realtime {
  proxy_pass http://app_upstream;
  proxy_http_version 1.1;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection "upgrade";
  proxy_read_timeout 300s;
  proxy_send_timeout 300s;
}

CDNを挟む構成では、通過可否と対象パスをキャッシュから外す設定を先に確認します。企業ネットワークには遮断するプロキシも残っており、ポーリングへ落とす退避経路の要否は要件段階で決めます。

複数台へスケールさせるときのセッション共有とPub/Subの構成

WebSocketは接続が張られた1台に固定されます。ここが、リクエストごとに任意のサーバーへ振り分けられるHTTPとの決定的な差です。利用者Aが1号機、Bが2号機に繋がる状態でAの発言をBへ届けるには、1号機から2号機へ伝える経路が別途要ります。

定番はRedisなどのPub/Subを挟む形です。各サーバーは受け取ったメッセージをチャンネルへ発行し、全サーバーが購読して自分が抱える接続へ配る。到達保証や配信の遅延が要件に入るなら、専用のメッセージブローカーへ移す判断も出てきます。

手前に置く分散装置は、L7で長時間接続を扱えるものを選びます。方式ごとの向き不向きはロードバランサーの仕組みと振り分け方式で整理していますが、WebSocketの観点で確認すべきは、維持中に構成変更が起きたときの挙動と1台あたりの同時接続数の上限です。1接続で数十KBのメモリとファイルディスクリプタを消費するため、数万接続を目指すならOS側の上限値の調整も要ります。

セキュリティ|wssの必須化とOrigin検証・CSWSHへの対処方法

外せないのは暗号化と接続元の確認です。本番でwsを使う理由はありません。平文のWebSocketは中身が読めるうえ、経路上の機器に書き換えられる余地も残る。TLSで包んだwssなら443番を通るため、企業プロキシの通過率という実務上の利点も付きます。

次に、ハンドシェイクへ同一生成元ポリシーが効かない性質を理解しておきます。ブラウザは別ドメインのページからのWebSocket接続をCORSのようには止めず、そのうえでCookieを自動送信します。つまり悪意のあるサイトを開いた利用者のブラウザから、ログイン済みのセッションで自社サーバーへ繋がれる余地がある。これがCSWSH(クロスサイトWebSocketハイジャック)で、対策はOriginヘッダーを許可リストと突き合わせて弾くことです。チケット方式にすれば、この経路は原理的に塞がります。

残る観点は3つ。接続後のメッセージにはHTTPのようなルーティングやミドルウェアの層がないため、型と権限の確認を各ハンドラで書くこと。メッセージ毎秒数とペイロード最大長に上限を設けること。認証前の接続をIP単位で絞り、連打による枯渇を防ぐことです。

WebSocketを採用してよい3条件と、見送るべき3つの場面

WebSocketを採用してよい3つの条件を実装コストから言い切る

第1に、クライアントからサーバーへの送信が高頻度で発生すること。共同編集のカーソル位置やゲームの入力のように送る側が両方なら、1接続に集約する価値が実装の複雑さを上回ります。片方向の通知だけならSSEで足り、再接続もブラウザが面倒を見てくれる。

第2に、遅延の要求が1秒を切ること。人が待てる範囲が数秒なら、5秒間隔のポーリングで足ります。対象は、取引画面の板情報や複数人が触る在庫のように、100ミリ秒台の反映が体験の質を決める場面です。

第3に、同時接続数の見積もりが立っていること。費用はリクエスト数ではなく接続数に比例します。ピーク時に何本張られ、1本あたり何KB使うかを掛け算し、ノード数とメモリが現実的な範囲に収まるかを先に確かめる。飛ばすと、利用者が増えた局面で構成を作り直すことになります。

WebSocketを見送るべき3つの場面と代わりに選ぶ手段の指針

1つ目は、サーバーから流すだけの通知です。進捗表示、完了通知、ダッシュボードの更新はSSEで組めます。HTTPのままなので既存の認証もプロキシも通り、再接続も標準で備わっている。

2つ目は、映像や音声そのものを運ぶ場面です。WebSocketはTCPの上にあるため、パケットが欠けたときの再送待ちで映像が止まります。欠落を許して遅延を詰めるWebRTCが構造的に有利でしょう。

3つ目は、モバイル回線や電池で動く機器から間欠的に送るデータです。接続を張り続ける前提は電池と通信量に効きます。MQTTのように軽いヘッダーと明示的なQoSを持つプロトコルのほうが扱いやすい。

採用が固まった後にもう1段の選択があります。素のプロトコルで書くか、ライブラリに寄せるかです。再接続、ルーム、確認応答、複数台への配信を既製の実装で賄うならSocket.IOの仕組みとWebSocketとの違いが候補です。クライアントが他社システムや組込み機器を含むなら、上乗せ仕様のない素のWebSocketのほうが相互接続で困りません。

リアルタイム基盤の開発を外部に委ねるとき見積書で確かめる5項目

発注する場合、見積書の粒度で品質の差が出ます。確かめるのは5項目です。第1に想定同時接続数とピークの前提が数値で書かれているか。第2に再接続と差分取得の設計が含まれているか(抜けた見積もりは、切断時に画面が古くなる欠陥をそのまま納品します)。第3に認証方式がCookieかチケットか。第4に複数台構成での配信手段が入っているか。第5に負荷試験の合格条件が定義されているか。

いずれも後から追加すると構成の作り直しになりやすい項目です。要件定義の段階で通信方式の妥当性から詰めたい場合は、API開発・システム連携で相談を受け付けています。WebSocketで組む範囲とHTTP APIに残す範囲の線引きから設計します。

よくある質問

WebSocketとHTTPの違いは何ですか?

HTTPは往復型で、1回のやり取りごとにヘッダーを送り直します。WebSocketは1度の接続確立の後、どちらの側からでも自由に送れる状態が続き、1メッセージあたりのヘッダーは最小2バイトです。接続の入り口はHTTPで、101 Switching Protocolsの応答を境に切り替わります。

WebSocketのデメリットは何ですか?

接続を持ち続けるため、サーバー費用が接続数に比例して増える点が第1です。次に、経路のプロキシやロードバランサーの設定が要る点、複数台へ広げるとき配信経路が別途必要になる点。加えて、切断と再接続、送信バッファの詰まりへの対処をアプリケーション側で書く必要があります。

wsとwssはどちらを使うべきですか?

本番環境ではwssを使ってください。TLSで暗号化されるため中身の盗み見と書き換えを防げるうえ、443番ポートを通るため企業プロキシを越えやすくなります。wsは開発中のローカル環境に限る、と決めておくのが安全です。

WebSocketの接続が本番環境だけ切れるのはなぜですか?

経路上の機器が持つ無通信タイマーが最も多い原因です。nginxのproxy_read_timeout、ロードバランサーのアイドルタイムアウトはいずれも既定60秒程度で、無通信が続くと接続を閉じます。サーバー側から30秒間隔でPingを送り、あわせて経路側の値を延ばす対処になります。

WebSocketは同時に何接続くらいまで耐えられますか?

1接続あたりのメモリとファイルディスクリプタの消費量で決まるため、実装と設定次第です。目安として1接続で数十KBを使う前提で計算し、OS側の上限とポート枯渇の条件を調整したうえで負荷試験で実測してください。台数を増やす場合は、配信の横流しをどう作るかを先に決める必要があります。

関連記事

資料請求

RELATED POSTS 関連記事