メインコンテンツにスキップ

🔴 WebSocket vs SSE vs ロングポーリング

📖 定義

リアルタイム通信は、サーバーとクライアント間で即座にデータ交換を可能にする技術です。WebSocketは双方向リアルタイム通信を提供し、SSE(Server-Sent Events)はサーバーからクライアントへのみデータをプッシュし、ロングポーリング(Long Polling)はHTTPを利用したリアルタイム通信方式です。それぞれに長所と短所があり、使用シナリオに応じて選択する必要があります。

🎯 例えで理解する

電話 vs ラジオ vs メッセンジャー

通常のHTTP = 郵便
あなた: "こんにちは?" (手紙を送る)
↓ (数日後)
友達: "やあ!" (返事)
↓ (数日後)
あなた: "元気?" (また手紙)

- 遅い
- 毎回新しく接続
- リアルタイム不可能

ロングポーリング = 電話(通話待機)
あなた: "何かあったら教えて!" (電話して待機)
↓ (しばらく待つ...)
友達: "今話すことがある!" (応答)
↓ (通話終了)
あなた: "また何かあったら教えて!" (再度電話)

- HTTPベース
- 接続維持 → 応答 → 再接続
- 非効率的

SSE = ラジオ放送
友達: "皆さんこんにちは!" (放送開始)
"今日の天気は..." (継続送信)
"次のニュースは..." (継続送信)
あなた: (聞くだけ)

- サーバー → クライアント(単方向)
- 接続維持
- シンプル

WebSocket = ビデオ通話
あなた: "こんにちは!" (即座に送信)
友達: "会えて嬉しい!" (即座に応答)
あなた: "何してる?" (即座に送信)
友達: "コーディング中!" (即座に応答)

- 双方向リアルタイム
- 接続維持
- 速くて効率的

レストランの注文

通常のHTTP = セルフサービス
1. カウンターに行って料理を注文
2. 待つ
3. 料理を受け取って席へ
4. カウンターに再び行って飲み物を注文
5. また待つ

ロングポーリング = ベルを押して待機
1. ベルを押してスタッフを待つ(接続維持)
2. スタッフが来る → "ご注文をどうぞ"
3. 注文してベルを再び押す
4. また待つ...

SSE = キッチンディスプレイ
キッチン: "1番のお客様、料理ができました!"
キッチン: "2番のお客様、準備中です!"
キッチン: "3番のお客様、もうすぐです!"
あなた: (聞くだけ)

WebSocket = テーブルサービス
あなた: "お水ください"
スタッフ: "はい、お持ちします"
あなた: "キムチも"
スタッフ: "すぐお持ちします"
スタッフ: "料理をお持ちしました"
あなた: "ありがとうございます"

- 自由な会話
- 素早い応答

⚙️ 動作原理

1. 通常のHTTP vs リアルタイム通信

========== 通常のHTTP(リクエスト-レスポンス) ==========

クライアント サーバー
│ │
│ 1. 接続(Request) │
│────────────────────────>│
│ │
│ │ 処理中...
│ │
│ 2. 応答(Response) │
│<────────────────────────│
│ │
│ 接続終了 │
╳ ╳

│ 3. 再接続 │
│────────────────────────>│
│ │

特徴:
- クライアントがリクエストした時だけ応答
- 毎回新しく接続
- サーバーから先に送信できない
- リアルタイム不可能

========== リアルタイム通信 ==========

クライアント サーバー
│ │
│ 接続 │
│<───────────────────────>│
│ │
│ 双方向通信維持 │
│<───────────────────────>│
│ │
│ データ交換 │
│<───────────────────────>│
│ │

特徴:
- 接続維持
- サーバーから先に送信可能
- リアルタイム可能

2. ロングポーリング(Long Polling)

クライアント                              サーバー
│ │
│ 1. リクエスト(新しいデータは?) │
│──────────────────────────────────>│
│ │
│ 接続維持(待機中...) │
│ │
│ │ データなし...
│ │ まだ待機...
│ │
│ │ 新しいデータ発生!
│ │
│ 2. 応答(データがあります) │
│<──────────────────────────────────│
│ │
│ 接続終了 │
╳ ╳
│ │
│ 3. すぐに再接続 │
│──────────────────────────────────>│
│ │
│ 再び待機... │

プロセス:
1. クライアント → サーバー: "新しいデータは?"
2. サーバー: データが発生するまで待機
3. データ発生 → 応答
4. 接続終了
5. すぐに再接続(繰り返し)

長所:
✅ HTTPベース(既存インフラ活用)
✅ ファイアウォール問題なし
✅ 実装シンプル

短所:
❌ 非効率的(継続的な再接続)
❌ サーバー負担大
❌ ヘッダーオーバーヘッド

3. Server-Sent Events (SSE)

クライアント                              サーバー
│ │
│ 1. 接続リクエスト │
│──────────────────────────────────>│
│ │
│ 2. 接続維持(ストリーム開始) │
│<══════════════════════════════════│
│ │
│ 3. データプッシュ │
│<──────────────────────────────────│
│ │
│ 4. またデータプッシュ │
│<──────────────────────────────────│
│ │
│ 接続維持中... │
│<══════════════════════════════════│

特徴:
- サーバー → クライアント(単方向)
- 接続維持
- HTTPベース
- 自動再接続

長所:
✅ シンプル(ブラウザ内蔵)
✅ 自動再接続
✅ イベントIDで再開可能
✅ HTTP/2で効率的

短所:
❌ 単方向(サーバー → クライアント)
❌ バイナリデータ不可
❌ IE未サポート

4. WebSocket

クライアント                              サーバー
│ │
│ 1. HTTP Upgradeリクエスト │
│──────────────────────────────────>│
│ │
│ 2. Upgrade承認 │
│<──────────────────────────────────│
│ │
│ WebSocketプロトコルに切り替え │
│<══════════════════════════════════>│
│ │
│ 3. データ送信 │
│──────────────────────────────────>│
│ │
│ 4. データ受信 │
│<──────────────────────────────────│
│ │
│ 5. データ送信 │
│──────────────────────────────────>│
│ │
│ 双方向通信継続... │
│<══════════════════════════════════>│

特徴:
- 双方向リアルタイム通信
- 別プロトコル(ws://, wss://)
- 接続維持
- 低遅延

長所:
✅ 真のリアルタイム
✅ 双方向通信
✅ 低オーバーヘッド
✅ バイナリサポート

短所:
❌ 複雑
❌ プロキシ/ファイアウォール問題の可能性
❌ サーバー負担(接続維持)

💡 実例

ロングポーリングの例

// ========== サーバー(Express.js) ==========
const express = require('express');
const app = express();

let messages = [];
let waitingClients = [];

// メッセージ追加(他の場所から呼ばれる)
function addMessage(message) {
messages.push(message);

// 待機中のクライアントに即座に応答
waitingClients.forEach(client => {
client.json({ messages });
});
waitingClients = [];
}

// ロングポーリングエンドポイント
app.get('/messages', (req, res) => {
const lastId = parseInt(req.query.lastId) || 0;

// 新しいメッセージがあれば即座に応答
if (messages.length > lastId) {
return res.json({ messages: messages.slice(lastId) });
}

// なければ待機リストに追加(最大30秒)
waitingClients.push(res);

// 30秒タイムアウト
req.setTimeout(30000, () => {
const index = waitingClients.indexOf(res);
if (index > -1) {
waitingClients.splice(index, 1);
res.json({ messages: [] }); // 空の応答
}
});
});

app.listen(3000);

// ========== クライアント ==========
let lastMessageId = 0;

async function longPolling() {
while (true) {
try {
const response = await fetch(`/messages?lastId=${lastMessageId}`);
const data = await response.json();

// 新しいメッセージ処理
if (data.messages.length > 0) {
data.messages.forEach(msg => {
console.log('新しいメッセージ:', msg);
displayMessage(msg);
});
lastMessageId += data.messages.length;
}

// すぐに再接続
await longPolling();
} catch (error) {
console.error('エラー:', error);
// 3秒後に再試行
await new Promise(resolve => setTimeout(resolve, 3000));
}
}
}

// 開始
longPolling();

// ========== 問題点 ==========
/*
1. 継続的な再接続(非効率的)
2. ネットワーク使用量多い
3. サーバー負担大
4. バッテリー消費(モバイル)
*/

Server-Sent Events (SSE) の例

// ========== サーバー(Express.js) ==========
const express = require('express');
const app = express();

app.use(express.static('public'));

// SSEエンドポイント
app.get('/events', (req, res) => {
// SSEヘッダー設定
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');

// CORS(必要な場合)
res.setHeader('Access-Control-Allow-Origin', '*');

// 接続確認メッセージ
res.write('data: Connected\n\n');

// クライアントID
const clientId = Date.now();
console.log(`クライアント ${clientId} 接続されました`);

// 5秒ごとに時間を送信
const intervalId = setInterval(() => {
const data = {
time: new Date().toLocaleTimeString(),
message: 'こんにちは!'
};

// SSE形式で送信
res.write(`data: ${JSON.stringify(data)}\n\n`);
}, 5000);

// クライアント接続切断処理
req.on('close', () => {
console.log(`クライアント ${clientId} 切断されました`);
clearInterval(intervalId);
res.end();
});
});

// イベント発行API
const clients = [];

app.get('/events/stream', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');

// クライアント保存
clients.push(res);

req.on('close', () => {
const index = clients.indexOf(res);
clients.splice(index, 1);
});
});

// 全クライアントにメッセージ送信
app.post('/broadcast', express.json(), (req, res) => {
const { message } = req.body;

clients.forEach(client => {
client.write(`data: ${JSON.stringify({ message })}\n\n`);
});

res.json({ success: true });
});

app.listen(3000);

// ========== クライアント(HTML) ==========
/*
<!DOCTYPE html>
<html>
<body>
<div id="messages"></div>

<script>
// SSE接続
const eventSource = new EventSource('/events');

// メッセージ受信
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('受信データ:', data);

const div = document.getElementById('messages');
div.innerHTML += `<p>${data.time}: ${data.message}</p>`;
};

// 接続開始
eventSource.onopen = () => {
console.log('SSE接続されました');
};

// エラー処理
eventSource.onerror = (error) => {
console.error('SSEエラー:', error);
if (eventSource.readyState === EventSource.CLOSED) {
console.log('SSE接続切断');
}
};

// 接続終了(ページを離れる時)
window.addEventListener('beforeunload', () => {
eventSource.close();
});
</script>
</body>
</html>
*/

// ========== 高度な機能 ==========

// 1. イベントタイプ指定
app.get('/events/typed', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');

// 複数種類のイベント
setInterval(() => {
// 一般メッセージ
res.write(`event: message\ndata: Hello\n\n`);

// 通知
res.write(`event: notification\ndata: New notification!\n\n`);

// 更新
res.write(`event: update\ndata: {"count": 10}\n\n`);
}, 5000);
});

// クライアントでイベントタイプ別処理
/*
eventSource.addEventListener('message', (e) => {
console.log('メッセージ:', e.data);
});

eventSource.addEventListener('notification', (e) => {
console.log('通知:', e.data);
});

eventSource.addEventListener('update', (e) => {
const data = JSON.parse(e.data);
console.log('更新:', data);
});
*/

// 2. イベントID(再接続時に続きから受信)
let eventId = 0;

app.get('/events/resumable', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');

const lastEventId = parseInt(req.headers['last-event-id']) || 0;
console.log('最後のイベントID:', lastEventId);

// lastEventId以降のイベントのみ送信
setInterval(() => {
eventId++;
res.write(`id: ${eventId}\ndata: Event ${eventId}\n\n`);
}, 1000);
});

// 3. 再試行時間設定
app.get('/events/retry', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');

// 5秒後に再接続
res.write('retry: 5000\n');
res.write('data: Connected\n\n');
});

WebSocketの例(Socket.io)

// ========== サーバー(Socket.io) ==========
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');

const app = express();
const server = http.createServer(app);
const io = socketIo(server, {
cors: {
origin: '*'
}
});

app.use(express.static('public'));

// 接続されたユーザー管理
const users = new Map();

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

// ユーザー情報保存
socket.on('register', (username) => {
users.set(socket.id, { username, socket });
console.log(`${username} 登録されました`);

// 全ユーザーに通知
io.emit('user-joined', {
username,
totalUsers: users.size
});
});

// メッセージ受信
socket.on('chat-message', (message) => {
const user = users.get(socket.id);
console.log(`${user.username}: ${message}`);

// 全ユーザーにブロードキャスト
io.emit('chat-message', {
username: user.username,
message,
timestamp: new Date().toISOString()
});
});

// タイピング中表示
socket.on('typing', () => {
const user = users.get(socket.id);
socket.broadcast.emit('user-typing', user.username);
});

socket.on('stop-typing', () => {
const user = users.get(socket.id);
socket.broadcast.emit('user-stop-typing', user.username);
});

// 個人メッセージ
socket.on('private-message', ({ to, message }) => {
const targetSocket = Array.from(users.values())
.find(u => u.username === to)?.socket;

if (targetSocket) {
const sender = users.get(socket.id);
targetSocket.emit('private-message', {
from: sender.username,
message
});
}
});

// 切断
socket.on('disconnect', () => {
const user = users.get(socket.id);
if (user) {
console.log(`${user.username} 切断されました`);
users.delete(socket.id);

// 全ユーザーに通知
io.emit('user-left', {
username: user.username,
totalUsers: users.size
});
}
});
});

server.listen(3000, () => {
console.log('サーバー実行中: http://localhost:3000');
});

// ========== クライアント(HTML + Socket.io) ==========
/*
<!DOCTYPE html>
<html>
<head>
<script src="/socket.io/socket.io.js"></script>
</head>
<body>
<div id="chat">
<div id="messages"></div>
<input id="username" placeholder="名前" />
<input id="message" placeholder="メッセージ" />
<button onclick="sendMessage()">送信</button>
</div>

<script>
// Socket.io接続
const socket = io('http://localhost:3000');

// 接続成功
socket.on('connect', () => {
console.log('接続されました:', socket.id);
});

// ユーザー登録
function register() {
const username = document.getElementById('username').value;
socket.emit('register', username);
}

// メッセージ送信
function sendMessage() {
const message = document.getElementById('message').value;
socket.emit('chat-message', message);
document.getElementById('message').value = '';
}

// メッセージ受信
socket.on('chat-message', (data) => {
const div = document.getElementById('messages');
div.innerHTML += `
<p><strong>${data.username}:</strong> ${data.message}</p>
`;
});

// ユーザー参加
socket.on('user-joined', (data) => {
console.log(`${data.username} 参加 (合計 ${data.totalUsers}人)`);
});

// ユーザー退出
socket.on('user-left', (data) => {
console.log(`${data.username} 退出 (合計 ${data.totalUsers}人)`);
});

// タイピング中
let typingTimeout;
document.getElementById('message').addEventListener('input', () => {
socket.emit('typing');

clearTimeout(typingTimeout);
typingTimeout = setTimeout(() => {
socket.emit('stop-typing');
}, 1000);
});

socket.on('user-typing', (username) => {
console.log(`${username}がタイピング中...`);
});

// 切断
socket.on('disconnect', () => {
console.log('切断されました');
});

// 再接続
socket.on('reconnect', () => {
console.log('再接続されました');
});
</script>
</body>
</html>
*/

ネイティブWebSocket API

// ========== サーバー(wsライブラリ) ==========
const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws) => {
console.log('クライアント接続されました');

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

// 全クライアントにブロードキャスト
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(message.toString());
}
});
});

// 切断
ws.on('close', () => {
console.log('クライアント切断されました');
});

// エラー
ws.on('error', (error) => {
console.error('WebSocketエラー:', error);
});

// ウェルカムメッセージ
ws.send('サーバーに接続されました!');
});

console.log('WebSocketサーバー実行中: ws://localhost:8080');

// ========== クライアント(ブラウザ) ==========

// WebSocket接続
const ws = new WebSocket('ws://localhost:8080');

// 接続開始
ws.addEventListener('open', (event) => {
console.log('WebSocket接続されました');
ws.send('こんにちは!');
});

// メッセージ受信
ws.addEventListener('message', (event) => {
console.log('サーバーから:', event.data);
});

// エラー
ws.addEventListener('error', (error) => {
console.error('WebSocketエラー:', error);
});

// 接続切断
ws.addEventListener('close', (event) => {
console.log('WebSocket切断されました');
if (event.code === 1000) {
console.log('正常終了');
} else {
console.log('異常終了:', event.code);
}
});

// メッセージ送信
function sendMessage(message) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(message);
} else {
console.error('WebSocketが開いていません');
}
}

// 接続終了
function closeConnection() {
ws.close(1000, '正常終了');
}

// ========== バイナリデータ転送 ==========

// ファイル転送
async function sendFile(file) {
const arrayBuffer = await file.arrayBuffer();
ws.send(arrayBuffer);
}

// サーバーでバイナリ受信
ws.addEventListener('message', (event) => {
if (event.data instanceof ArrayBuffer) {
console.log('バイナリデータ受信:', event.data.byteLength, 'bytes');
} else {
console.log('テキストデータ:', event.data);
}
});

// ========== Ping/Pong(接続維持) ==========

// サーバー
wss.on('connection', (ws) => {
ws.isAlive = true;

ws.on('pong', () => {
ws.isAlive = true;
});
});

// 30秒ごとにPing
setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) {
return ws.terminate(); // 応答なしなら接続終了
}

ws.isAlive = false;
ws.ping();
});
}, 30000);

// ========== 自動再接続 ==========

let ws;
let reconnectAttempts = 0;
const maxReconnectAttempts = 10;

function connect() {
ws = new WebSocket('ws://localhost:8080');

ws.addEventListener('open', () => {
console.log('接続されました');
reconnectAttempts = 0;
});

ws.addEventListener('close', (event) => {
console.log('切断されました');

// 自動再接続
if (reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++;
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
console.log(`${delay}ms後に再接続試行...`);
setTimeout(connect, delay);
} else {
console.error('再接続失敗');
}
});

ws.addEventListener('error', (error) => {
console.error('エラー:', error);
ws.close();
});
}

connect();

🤔 よくある質問

Q1. どの方式を選択すべきですか?

A:

✅ ロングポーリングを選択する場合:
├─ WebSocket/SSEサポートされない環境
├─ レガシーシステム
├─ シンプルな通知
└─ 例: 古いブラウザサポート、シンプルなポーリング

使用例:
- 株価(更新頻度が低い)
- メール確認
- シンプルな通知

✅ SSEを選択する場合:
├─ サーバー → クライアント単方向
├─ リアルタイム更新
├─ シンプルな実装を希望
├─ 自動再接続が必要
└─ 例: ニュースフィード、通知、ダッシュボード

使用例:
- ニュースフィード(リアルタイム更新)
- 株価チャート(サーバーからプッシュ)
- 進行状況表示
- ログストリーミング
- サーバーモニタリング

✅ WebSocketを選択する場合:
├─ 双方向リアルタイム通信
├─ 低遅延が重要
├─ 頻繁なメッセージ交換
├─ バイナリデータ
└─ 例: チャット、ゲーム、コラボレーションツール

使用例:
- チャットアプリケーション
- マルチプレイヤーゲーム
- 共同ドキュメント編集(Google Docs)
- ビデオ会議
- IoTリアルタイム制御

📊 比較表:

特性 | ロングポーリング | SSE | WebSocket
------------|----------------|----------|----------
方向性 | 双方向 | 単方向 | 双方向
プロトコル | HTTP | HTTP | WebSocket
遅延時間 | 高い | 低い | 非常に低い
オーバーヘッド| 高い | 普通 | 低い
ブラウザ | 全て | ほとんど | 全て
複雑度 | 低い | 低い | 高い
再接続 | 手動 | 自動 | 手動
バイナリ | 可能 | 不可 | 可能

推奨:
- シンプルなリアルタイム: SSE
- チャット/ゲーム: WebSocket
- レガシーサポート: ロングポーリング

Q2. パフォーマンスの違いは?

A:

// ========== ベンチマーク例 ==========

// 1. ロングポーリング
// リクエストあたり:
// - HTTPヘッダー: ~800 bytes
// - 再接続時間: ~50ms
// - サーバーリソース: 接続あたり1スレッド

// 100同時接続時:
// - 秒あたりリクエスト: 100回
// - データ転送: 80KB/s(ヘッダーのみ)
// - サーバー負担: 高い

// 2. SSE
// 接続あたり:
// - HTTPヘッダー: 初回のみ(~800 bytes)
// - メッセージオーバーヘッド: ~10 bytes
// - サーバーリソース: Keep-Alive接続

// 100同時接続時:
// - 初回接続: 80KB
// - メッセージあたり: 1KB(ヘッダーなし)
// - サーバー負担: 普通

// 3. WebSocket
// 接続あたり:
// - Upgradeヘッダー: 1回のみ(~500 bytes)
// - フレームオーバーヘッド: 2~6 bytes
// - サーバーリソース: 接続維持

// 100同時接続時:
// - 初回接続: 50KB
// - メッセージあたり: ~0.002KB(フレームのみ)
// - サーバー負担: 低い

// ========== 実測定 ==========

// テスト: 100人が秒あたり1メッセージ送信

// ロングポーリング
const longPollingBenchmark = {
秒あたりリクエスト: 100,
平均遅延時間: '200ms',
帯域幅: '8MB/分',
サーバーCPU: '70%',
メモリ: '500MB'
};

// SSE
const sseBenchmark = {
秒あたりリクエスト: 0, // 接続維持
平均遅延時間: '10ms',
帯域幅: '600KB/分',
サーバーCPU: '30%',
メモリ: '200MB'
};

// WebSocket
const webSocketBenchmark = {
秒あたりリクエスト: 0, // 接続維持
平均遅延時間: '2ms',
帯域幅: '100KB/分',
サーバーCPU: '15%',
メモリ: '100MB'
};

// ========== 実践的最適化 ==========

// 1. 接続数制限
const MAX_CONNECTIONS = 1000;

io.on('connection', (socket) => {
if (io.engine.clientsCount > MAX_CONNECTIONS) {
socket.disconnect();
return;
}
});

// 2. メッセージ圧縮
const io = socketIo(server, {
perMessageDeflate: {
threshold: 1024 // 1KB以上のみ圧縮
}
});

// 3. バッチ処理
const messageQueue = [];

setInterval(() => {
if (messageQueue.length > 0) {
io.emit('batch', messageQueue);
messageQueue.length = 0;
}
}, 100); // 100msごとにバッチ送信

// 4. ルーム使用
socket.join('room1');
io.to('room1').emit('message', data); // room1のみ

// 5. バイナリ転送
// WebSocketはバイナリで効率的
const buffer = Buffer.from('Hello');
socket.send(buffer); // テキストより速い

// 6. ハートビート最適化
const HEARTBEAT_INTERVAL = 25000; // 25秒
const HEARTBEAT_TIMEOUT = 30000; // 30秒

setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) {
return ws.terminate();
}
ws.isAlive = false;
ws.ping();
});
}, HEARTBEAT_INTERVAL);

Q3. モバイルアプリでは?

A:

// ========== モバイル考慮事項 ==========

// 1. バッテリー消費
// ロングポーリング: ❌ 高い(継続的な再接続)
// SSE: ⚠️ 普通
// WebSocket: ✅ 低い(接続維持)

// 2. ネットワーク切り替え
// WiFi <-> モバイルデータ切り替え時

// 自動再接続
let ws;

function connect() {
ws = new WebSocket('wss://api.example.com');

ws.onclose = () => {
// 指数バックオフ
const delay = Math.min(1000 * Math.pow(2, attempts), 30000);
setTimeout(connect, delay);
};
}

// ネットワーク状態検出
window.addEventListener('online', () => {
console.log('オンラインになりました - 再接続');
connect();
});

window.addEventListener('offline', () => {
console.log('オフライン');
ws.close();
});

// 3. バックグラウンド処理
// アプリがバックグラウンドになったら?

// React Native例
import { AppState } from 'react-native';

AppState.addEventListener('change', (nextAppState) => {
if (nextAppState === 'background') {
// バックグラウンド → 接続維持または終了
ws.close();
} else if (nextAppState === 'active') {
// フォアグラウンド → 再接続
connect();
}
});

// 4. データ節約モード
// ユーザー設定に応じて

const isDataSaverMode = await getDataSaverSetting();

if (isDataSaverMode) {
// ロングポーリング(頻度少なく)
setInterval(poll, 60000); // 1分ごと
} else {
// WebSocket(リアルタイム)
connect();
}

// 5. プッシュ通知連動
// WebSocket + FCM/APNs組み合わせ

// アプリ実行中: WebSocket
if (appIsActive) {
useWebSocket();
}

// バックグラウンド: プッシュ通知
if (appIsBackground) {
usePushNotification();
}

// ========== React Native例 ==========
import React, { useEffect, useState } from 'react';
import { View, Text } from 'react-native';

function ChatScreen() {
const [ws, setWs] = useState(null);
const [messages, setMessages] = useState([]);
const [isConnected, setIsConnected] = useState(false);

useEffect(() => {
const websocket = new WebSocket('wss://api.example.com');

websocket.onopen = () => {
console.log('接続されました');
setIsConnected(true);
};

websocket.onmessage = (event) => {
const message = JSON.parse(event.data);
setMessages(prev => [...prev, message]);
};

websocket.onerror = (error) => {
console.error('WebSocketエラー:', error);
};

websocket.onclose = () => {
console.log('切断されました');
setIsConnected(false);

// 3秒後に再接続
setTimeout(() => {
// 再接続ロジック
}, 3000);
};

setWs(websocket);

// クリーンアップ
return () => {
websocket.close();
};
}, []);

const sendMessage = (text) => {
if (ws && isConnected) {
ws.send(JSON.stringify({ text }));
}
};

return (
<View>
<Text>接続: {isConnected ? 'はい' : 'いいえ'}</Text>
{messages.map((msg, i) => (
<Text key={i}>{msg.text}</Text>
))}
</View>
);
}

Q4. セキュリティはどうすればいいですか?

A:

// ========== 1. HTTPS/WSS使用 ==========

// ❌ HTTP/WS(平文送信)
const ws = new WebSocket('ws://api.example.com');

// ✅ HTTPS/WSS(暗号化)
const ws = new WebSocket('wss://api.example.com');

// ========== 2. 認証 ==========

// 方法1: URLにJWT(シンプルだが推奨しない)
const token = getJWTToken();
const ws = new WebSocket(`wss://api.example.com?token=${token}`);
// ⚠️ URLにトークン露出のリスク

// 方法2: 接続後認証(推奨)
const ws = new WebSocket('wss://api.example.com');

ws.onopen = () => {
// 認証メッセージ送信
ws.send(JSON.stringify({
type: 'auth',
token: getJWTToken()
}));
};

// サーバー
io.use((socket, next) => {
const token = socket.handshake.auth.token;

try {
const decoded = jwt.verify(token, SECRET_KEY);
socket.userId = decoded.userId;
next();
} catch (error) {
next(new Error('認証失敗'));
}
});

// 方法3: Cookie(HttpOnly)
// クライアント
const ws = new WebSocket('wss://api.example.com');
// Cookieは自動的に送信される

// サーバー
io.use((socket, next) => {
const cookies = parseCookies(socket.request.headers.cookie);
const sessionId = cookies.sessionId;

if (isValidSession(sessionId)) {
next();
} else {
next(new Error('認証失敗'));
}
});

// ========== 3. CORS設定 ==========

const io = socketIo(server, {
cors: {
origin: 'https://myapp.com', // 特定ドメインのみ
credentials: true
}
});

// 複数ドメイン
const allowedOrigins = ['https://myapp.com', 'https://admin.myapp.com'];

const io = socketIo(server, {
cors: {
origin: (origin, callback) => {
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('CORS拒否'));
}
},
credentials: true
}
});

// ========== 4. Rate Limiting ==========

const rateLimitMap = new Map();

io.on('connection', (socket) => {
socket.on('message', (data) => {
const userId = socket.userId;
const now = Date.now();

// ユーザーごとのメッセージ数追跡
if (!rateLimitMap.has(userId)) {
rateLimitMap.set(userId, []);
}

const userMessages = rateLimitMap.get(userId);

// 最近1分間のメッセージフィルタリング
const recentMessages = userMessages.filter(
timestamp => now - timestamp < 60000
);

// 分あたり10個制限
if (recentMessages.length >= 10) {
socket.emit('error', 'メッセージ送信制限超過');
return;
}

recentMessages.push(now);
rateLimitMap.set(userId, recentMessages);

// メッセージ処理
handleMessage(data);
});
});

// ========== 5. 入力検証 ==========

socket.on('message', (data) => {
// タイプ検証
if (typeof data !== 'object') {
return socket.emit('error', '無効なデータ形式');
}

// 必須フィールド検証
if (!data.type || !data.content) {
return socket.emit('error', '必須フィールド不足');
}

// 長さ検証
if (data.content.length > 1000) {
return socket.emit('error', 'メッセージが長すぎます');
}

// XSS防止
const sanitizedContent = sanitizeHtml(data.content);

// 処理
broadcastMessage(sanitizedContent);
});

// ========== 6. ネームスペースおよびRoom ==========

// ネームスペースで分離
const chatNamespace = io.of('/chat');
const adminNamespace = io.of('/admin');

chatNamespace.on('connection', (socket) => {
// 一般チャット
});

adminNamespace.use(authenticateAdmin);
adminNamespace.on('connection', (socket) => {
// 管理者専用
});

// Roomで権限制御
socket.on('join-room', (roomId) => {
if (canAccessRoom(socket.userId, roomId)) {
socket.join(roomId);
} else {
socket.emit('error', 'ルームアクセス権限なし');
}
});

// ========== 7. DDoS防止 ==========

// IPあたりの接続数制限
const MAX_CONNECTIONS_PER_IP = 5;
const connectionsByIP = new Map();

io.on('connection', (socket) => {
const ip = socket.handshake.address;

const count = connectionsByIP.get(ip) || 0;

if (count >= MAX_CONNECTIONS_PER_IP) {
socket.disconnect();
return;
}

connectionsByIP.set(ip, count + 1);

socket.on('disconnect', () => {
const newCount = connectionsByIP.get(ip) - 1;
if (newCount <= 0) {
connectionsByIP.delete(ip);
} else {
connectionsByIP.set(ip, newCount);
}
});
});

Q5. エラー処理は?

A:

// ========== クライアントエラー処理 ==========

class WebSocketClient {
constructor(url) {
this.url = url;
this.ws = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 10;
this.reconnectDelay = 1000;
}

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

this.ws.onopen = () => {
console.log('接続成功');
this.reconnectAttempts = 0;
this.onConnected();
};

this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.onMessage(data);
} catch (error) {
console.error('メッセージパースエラー:', error);
}
};

this.ws.onerror = (error) => {
console.error('WebSocketエラー:', error);
this.onError(error);
};

this.ws.onclose = (event) => {
console.log('接続切断:', event.code, event.reason);

// 正常終了(1000)
if (event.code === 1000) {
console.log('正常終了');
return;
}

// 再接続試行
this.reconnect();
};
} catch (error) {
console.error('接続失敗:', error);
this.reconnect();
}
}

reconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('再接続断念');
this.onReconnectFailed();
return;
}

this.reconnectAttempts++;
const delay = Math.min(
this.reconnectDelay * Math.pow(2, this.reconnectAttempts),
30000
);

console.log(`${delay}ms後に再接続試行 (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);

setTimeout(() => {
this.connect();
}, delay);
}

send(data) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
try {
this.ws.send(JSON.stringify(data));
} catch (error) {
console.error('送信エラー:', error);
}
} else {
console.error('WebSocketが開いていません');
// メッセージキューに保存
this.queueMessage(data);
}
}

close() {
if (this.ws) {
this.ws.close(1000, 'クライアント終了');
}
}

// イベントハンドラ(オーバーライド)
onConnected() {}
onMessage(data) {}
onError(error) {}
onReconnectFailed() {}

queueMessage(data) {
// オフラインメッセージキュー実装
}
}

// 使用
const client = new WebSocketClient('wss://api.example.com');

client.onConnected = () => {
console.log('接続されました!');
};

client.onMessage = (data) => {
console.log('メッセージ:', data);
};

client.onError = (error) => {
console.error('エラー発生:', error);
// UIにエラー表示
showErrorNotification('接続エラー');
};

client.onReconnectFailed = () => {
// 再接続失敗処理
showErrorModal('サーバーに接続できません');
};

client.connect();

// ========== サーバーエラー処理 ==========

io.on('connection', (socket) => {
// エラーハンドラ
socket.on('error', (error) => {
console.error('ソケットエラー:', error);
});

// メッセージ処理エラー
socket.on('message', async (data) => {
try {
// 入力検証
if (!isValidMessage(data)) {
throw new Error('無効なメッセージ');
}

// 処理
await processMessage(data);
} catch (error) {
console.error('メッセージ処理エラー:', error);

// クライアントにエラー送信
socket.emit('error', {
code: 'MESSAGE_PROCESSING_ERROR',
message: error.message
});
}
});

// 切断処理
socket.on('disconnect', (reason) => {
console.log('切断:', reason);

// ユーザークリーンアップ
cleanupUser(socket.userId);

// 他のユーザーに通知
socket.broadcast.emit('user-left', socket.userId);
});
});

// グローバルエラーハンドラ
io.engine.on('connection_error', (error) => {
console.error('接続エラー:', error);
});

// ========== タイムアウト処理 ==========

socket.on('message', (data, callback) => {
// タイムアウト設定(5秒)
const timeout = setTimeout(() => {
callback({
error: 'TIMEOUT',
message: '応答タイムアウト'
});
}, 5000);

// 処理
processMessage(data)
.then(result => {
clearTimeout(timeout);
callback({ success: true, data: result });
})
.catch(error => {
clearTimeout(timeout);
callback({ error: error.message });
});
});

🎓 次のステップ

リアルタイム通信を理解したら、次を学習してください:

  1. HTTPの基本(文書作成予定) - HTTPプロトコルの理解
  2. APIとは?(文書作成予定) - API基本概念
  3. REST API vs GraphQL - API設計

実践してみる

# ========== 1. Socket.ioチャットアプリ ==========

mkdir chat-app
cd chat-app
npm init -y
npm install express socket.io

# server.js作成後
node server.js

# ========== 2. SSEリアルタイム通知 ==========

mkdir sse-demo
cd sse-demo
npm install express

# server.js作成
# index.html作成

node server.js
# http://localhost:3000 にアクセス

# ========== 3. WebSocketゲーム ==========

mkdir websocket-game
cd websocket-game
npm install ws express

# マルチプレイヤーゲーム実装
node server.js

🎬 まとめ

リアルタイム通信は現代ウェブのコア技術です:

  • ロングポーリング: レガシーサポート、HTTPベース
  • SSE: サーバープッシュ、シンプルな実装
  • WebSocket: 双方向リアルタイム、最高のパフォーマンス
  • 選択基準: 要件、環境、複雑度

プロジェクトに合ったリアルタイム通信方式を選択して、優れたユーザー体験を作りましょう! 🔴