WebSocket入門
リアルタイム双方向通信の仕組み
WebSocketの仕組み、JavaScript実装、メッセージ送受信、エラーハンドリング、Socket.IOまで、リアルタイム通信の基礎を解説します。
こんな人向けの記事です
- WebSocketの仕組みを理解したい
- リアルタイム通信を実装したい
- HTTPとWebSocketの違いを知りたい
Step 1WebSocketとは?HTTPとの違い
WebSocketは、クライアントとサーバー間で双方向のリアルタイム通信を実現するプロトコルです。RFC 6455で標準化されており、チャットアプリ、リアルタイム通知、オンラインゲームなど、即時性が求められる場面で広く使われています。
従来のHTTP通信との違いを理解することが、WebSocketを学ぶ第一歩です。
| 比較項目 | HTTP | WebSocket |
|---|---|---|
| 通信方向 | クライアント → サーバー(リクエスト/レスポンス) | 双方向(サーバーからもプッシュ可能) |
| 接続 | 毎回接続・切断(HTTP/1.1はKeep-Alive可) | 一度接続したら維持(持続的接続) |
| オーバーヘッド | 毎回HTTPヘッダーを送信 | ハンドシェイク後は最小限のフレームヘッダーのみ |
| プロトコル | http:// / https:// | ws:// / wss:// |
| ユースケース | Webページ取得、API呼び出し | チャット、通知、株価更新、ゲーム |
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プロトコルで通信します。
クライアントからのリクエスト
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: http://example.com
重要なヘッダーを確認しましょう。
| ヘッダー | 値 | 役割 |
|---|---|---|
Upgrade | websocket | WebSocketへのプロトコル切り替えを要求 |
Connection | Upgrade | 接続のアップグレードを指示 |
Sec-WebSocket-Key | Base64文字列 | セキュリティ検証用のランダムキー |
Sec-WebSocket-Version | 13 | WebSocketプロトコルのバージョン |
サーバーからのレスポンス
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 の計算
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=
Upgradeヘッダーを除去すると接続に失敗します。Nginx等のリバースプロキシを使う場合は、WebSocket用の設定(proxy_set_header Upgrade等)が必要です。
Step 3JavaScriptでのWebSocket接続
ブラウザにはWebSocket APIが標準搭載されています。new WebSocket()でインスタンスを作成するだけで、サーバーへの接続が開始されます。
基本的な接続
// 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プロパティで、現在の接続状態を確認できます。
| 値 | 定数名 | 状態 |
|---|---|---|
0 | CONNECTING | 接続中(ハンドシェイク中) |
1 | OPEN | 接続済み(通信可能) |
2 | CLOSING | 切断処理中 |
3 | CLOSED | 切断済み |
セキュアな接続(wss://)
// 本番環境ではwss://(WebSocket Secure)を使う
const ws = new WebSocket('wss://example.com/chat');
// HTTPSページからws://に接続するとブラウザがブロックする
// Mixed Contentエラーになるため、本番では必ずwss://を使用
ws://は暗号化なし、wss://はTLS暗号化ありのWebSocket接続です。HTTPSページからはwss://のみ接続可能です。開発環境ではws://localhostを使い、本番環境では必ずwss://を使用してください。
Step 4メッセージの送受信(send, onmessage)
WebSocketの最も重要な機能はメッセージの送受信です。send()でデータを送信し、onmessageイベントで受信します。
テキストメッセージの送受信
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);
}
};
バイナリデータの送受信
// バイナリデータの受信形式を指定
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サーバー側の実装例
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('クライアントが切断しました');
});
});
ws.send()は接続がOPEN状態のときのみ使用できます。onopenイベントが発火する前にsend()を呼ぶとエラーになるため、必ずonopen内か、readyState === WebSocket.OPENを確認してから送信してください。
Step 5エラーハンドリングと再接続
WebSocket接続はネットワーク障害やサーバー再起動によって切断される可能性があります。実運用では自動再接続の仕組みが不可欠です。
基本的なエラーハンドリング
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);
}
};
主要な終了コードを一覧で確認しましょう。
| コード | 名前 | 説明 |
|---|---|---|
1000 | Normal Closure | 正常に接続が終了 |
1001 | Going Away | サーバー停止またはページ遷移 |
1002 | Protocol Error | プロトコルエラー |
1003 | Unsupported Data | サポートされないデータ型を受信 |
1006 | Abnormal Closure | 異常切断(コードなしで切断) |
1011 | Internal Error | サーバー内部エラー |
指数バックオフ付き自動再接続
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)
// アプリケーションレベルのハートビート
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でも十分ですが、実運用では便利な機能を備えたライブラリを使うことが多いです。代表的なライブラリを比較しましょう。
| ライブラリ | 環境 | 特徴 |
|---|---|---|
| ws | Node.js(サーバー) | 軽量で高速。生のWebSocketに近いAPI |
| Socket.IO | Node.js + ブラウザ | 自動再接続、ルーム、名前空間、フォールバック |
| WebSocket API | ブラウザ(標準) | 追加ライブラリ不要。シンプルだが機能は最小限 |
ws(Node.js向け軽量ライブラリ)
# wsのインストール
npm install 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 # クライアント側
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);
});
});
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);
});
new WebSocket()で接続することはできません(その逆も同様)。サーバーとクライアントの両方でSocket.IOを使うか、両方で生WebSocketを使うかを統一する必要があります。
ライブラリ選択の指針
- シンプルな双方向通信のみ → 生WebSocket API + ws
- 自動再接続・ルーム・名前空間が必要 → Socket.IO
- レガシーブラウザ対応が必要 → Socket.IO(フォールバック機能あり)
- パフォーマンス重視(ゲーム等) → 生WebSocket API + ws(オーバーヘッド最小)
- Django/Pythonバックエンド → Django Channels(WebSocket対応)