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

🔌 WebSocketとは?

📖 定義

WebSocketは、クライアントとサーバー間の双方向リアルタイム通信を可能にするプロトコルです。HTTPと異なり、一度接続すると持続的に接続を維持し、サーバーからクライアントへ先にデータを送信できます(push)。チャット、リアルタイム通知、ゲーム、コラボレーションツールなどに使用されます。

🎯 比喩で理解する

電話 vs 郵便

HTTP (郵便)
├─ 手紙を送る (リクエスト)
├─ 返事を受け取る (レスポンス)
├─ また手紙を書く (リクエスト)
└─ 毎回新しい手紙が必要 (オーバーヘッド)

WebSocket (電話)
├─ 一度電話接続
├─ 継続的な会話可能
├─ 両方が先に話せる
└─ 切るまで接続維持

宅配 vs 直通パイプ

HTTP
┌─────────┐ リクエスト ┌─────────┐
│クライアント│ ──────────→ │ サーバー │
└─────────┘ └─────────┘
┌─────────┐ レスポンス ┌─────────┐
│クライアント│ ←────────── │ サーバー │
└─────────┘ └─────────┘
毎回新しい接続!

WebSocket
┌─────────┐ ┌─────────┐
│クライアント│ ←─────────→ │ サーバー │
└─────────┘ 持続接続 └─────────┘
↕ 双方向

⚙️ 動作原理

1. WebSocket接続プロセス

1. HTTPでハンドシェイクリクエスト
GET /chat HTTP/1.1
Upgrade: websocket
Connection: Upgrade

2. サーバー承認
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade

3. WebSocket接続確立
↕ 双方向通信開始

4. データ送信
メッセージ ←→ メッセージ ←→ メッセージ

5. 接続終了
close() 呼び出し

2. HTTP vs WebSocket

HTTP (単方向、リクエスト-レスポンス)
クライアント → サーバー: リクエスト
クライアント ← サーバー: レスポンス
[接続終了]

WebSocket (双方向、持続)
クライアント ↔ サーバー
- サーバーが先にメッセージ送信可能
- 接続維持
- リアルタイム通信

💡 実際の例

基本WebSocket (クライアント)

// ウェブブラウザで
const ws = new WebSocket('ws://localhost:8080');

// 接続成功
ws.onopen = () => {
console.log('接続完了!');
ws.send('こんにちは!'); // メッセージ送信
};

// メッセージ受信
ws.onmessage = (event) => {
console.log('受信メッセージ:', event.data);
};

// エラー処理
ws.onerror = (error) => {
console.error('エラー:', error);
};

// 接続終了
ws.onclose = () => {
console.log('接続終了');
};

// メッセージ送信
ws.send('Hello Server!');
ws.send(JSON.stringify({ type: 'chat', message: 'Hi' }));

// 接続終了
ws.close();

基本WebSocketサーバー (Node.js)

const WebSocket = require('ws');

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

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

// クライアント接続時
wss.on('connection', (ws) => {
console.log('クライアント接続!');

// ウェルカムメッセージ
ws.send('ようこそ!');

// メッセージ受信
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('エラー:', error);
});
});

Socket.io (実践チャットアプリ)

// ============ サーバー (server.js) ============
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);

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

const users = new Map(); // 接続中のユーザー

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

// ユーザー入室
socket.on('join', (username) => {
users.set(socket.id, username);

// 入室通知 (全員に)
io.emit('user-joined', {
username,
userCount: users.size
});

console.log(`${username} 入室 (合計 ${users.size}名)`);
});

// チャットメッセージ
socket.on('chat-message', (message) => {
const username = users.get(socket.id);

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

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

// 接続終了
socket.on('disconnect', () => {
const username = users.get(socket.id);
users.delete(socket.id);

// 退室通知
io.emit('user-left', {
username,
userCount: users.size
});

console.log(`${username} 退室 (残り: ${users.size}名)`);
});
});

server.listen(3000, () => {
console.log('サーバー実行中: http://localhost:3000');
});
<!-- ============ クライアント (public/index.html) ============ -->
<!DOCTYPE html>
<html>
<head>
<title>リアルタイムチャット</title>
<style>
#messages {
height: 300px;
overflow-y: auto;
border: 1px solid #ccc;
padding: 10px;
margin-bottom: 10px;
}
.message {
margin-bottom: 10px;
}
.system {
color: #999;
font-style: italic;
}
</style>
</head>
<body>
<h1>リアルタイムチャット</h1>
<div id="messages"></div>
<div id="typing"></div>
<input id="message-input" type="text" placeholder="メッセージ入力...">
<button id="send-btn">送信</button>

<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io();
const messagesDiv = document.getElementById('messages');
const messageInput = document.getElementById('message-input');
const sendBtn = document.getElementById('send-btn');
const typingDiv = document.getElementById('typing');

// ユーザー名入力
const username = prompt('ユーザー名を入力してください:') || 'Anonymous';
socket.emit('join', username);

// ユーザー入室通知
socket.on('user-joined', (data) => {
addMessage(`${data.username}さんが入室しました。 (${data.userCount}名接続)`, 'system');
});

// ユーザー退室通知
socket.on('user-left', (data) => {
addMessage(`${data.username}さんが退室しました。 (${data.userCount}名接続)`, 'system');
});

// チャットメッセージ受信
socket.on('chat-message', (data) => {
const time = new Date(data.timestamp).toLocaleTimeString();
addMessage(`[${time}] ${data.username}: ${data.message}`);
});

// タイピング中表示
socket.on('user-typing', (username) => {
typingDiv.textContent = `${username}さんが入力中...`;
setTimeout(() => {
typingDiv.textContent = '';
}, 1000);
});

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

sendBtn.addEventListener('click', sendMessage);
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
sendMessage();
}
});

// タイピングイベント
let typingTimeout;
messageInput.addEventListener('input', () => {
clearTimeout(typingTimeout);
socket.emit('typing');
typingTimeout = setTimeout(() => {
// タイピング停止
}, 500);
});

// メッセージ追加
function addMessage(text, className = '') {
const messageEl = document.createElement('div');
messageEl.className = `message ${className}`;
messageEl.textContent = text;
messagesDiv.appendChild(messageEl);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
</script>
</body>
</html>

Room機能 (グループチャット)

// サーバー
io.on('connection', (socket) => {
// 部屋入室
socket.on('join-room', (roomId) => {
socket.join(roomId);
console.log(`${socket.id}${roomId}部屋に入室`);

// 同じ部屋にいる人にだけ送信
socket.to(roomId).emit('user-joined-room', {
userId: socket.id,
roomId
});
});

// 特定の部屋にメッセージ送信
socket.on('room-message', ({ roomId, message }) => {
// その部屋にのみ送信
io.to(roomId).emit('room-message', {
userId: socket.id,
message
});
});

// 部屋退室
socket.on('leave-room', (roomId) => {
socket.leave(roomId);
socket.to(roomId).emit('user-left-room', socket.id);
});
});

リアルタイム通知システム

// サーバー
const notifications = io.of('/notifications');

notifications.on('connection', (socket) => {
console.log('通知接続:', socket.id);

// ユーザー認証
const userId = socket.handshake.query.userId;

// 個人部屋に入室
socket.join(`user-${userId}`);

// 特定ユーザーに通知送信
socket.on('send-notification', ({ targetUserId, notification }) => {
notifications.to(`user-${targetUserId}`).emit('notification', notification);
});
});

// どこからでも通知送信
function sendNotification(userId, notification) {
notifications.to(`user-${userId}`).emit('notification', {
type: notification.type,
message: notification.message,
timestamp: new Date()
});
}

// 例: 新規注文通知
app.post('/api/orders', async (req, res) => {
const order = await createOrder(req.body);

// 販売者にリアルタイム通知
sendNotification(order.sellerId, {
type: 'new-order',
message: `新しい注文が入りました!`,
orderId: order.id
});

res.json(order);
});

リアルタイムコラボレーション (共同編集)

// サーバー
io.on('connection', (socket) => {
socket.on('join-document', (docId) => {
socket.join(docId);

// 現在のドキュメント内容を送信
const document = getDocument(docId);
socket.emit('document-loaded', document);
});

// ドキュメント修正
socket.on('document-change', ({ docId, changes }) => {
// ドキュメント更新
updateDocument(docId, changes);

// 他のユーザーに変更内容を送信
socket.to(docId).emit('document-updated', {
userId: socket.id,
changes
});
});

// カーソル位置共有
socket.on('cursor-move', ({ docId, position }) => {
socket.to(docId).emit('cursor-update', {
userId: socket.id,
position
});
});
});

🤔 よくある質問

Q1. WebSocket vs HTTPポーリング?

A:

// HTTPポーリング (非効率的)
// クライアントが定期的にサーバーにリクエスト

setInterval(() => {
fetch('/api/messages')
.then(res => res.json())
.then(messages => {
// 新しいメッセージ確認
});
}, 1000); // 1秒ごとにリクエスト

// 問題点:
// - 不要なリクエスト (新しいメッセージがなくてもリクエスト)
// - 遅延時間 (最大1秒)
// - サーバー負荷

// WebSocket (効率的)
// リアルタイム双方向通信

const socket = io();

socket.on('new-message', (message) => {
// 新しいメッセージ即座に受信!
});

// 利点:
// - 即座に受信 (遅延なし)
// - サーバーがpush
// - 効率的

Q2. WebSocketは常に良いですか?

A:

// ✅ WebSocketが良い場合
1. リアルタイムチャット
2. リアルタイム通知
3. コラボレーションツール (共同編集)
4. リアルタイムゲーム
5. 株価情報 (リアルタイムデータ)
6. IoTデバイス制御

// ❌ WebSocketが不要な場合
1. 一般的なウェブサイト (ブログ、ニュース)
2. REST API (CRUD)
3. 静的コンテンツ
4. リアルタイム更新不要

// HTTPの方が良い場合:
// - 簡単なリクエスト-レスポンス
// - キャッシングが必要
// - RESTful設計

Q3. Socket.io vs 純粋WebSocket?

A:

// 純粋WebSocket
const ws = new WebSocket('ws://localhost:8080');
ws.send('Hello');

// 利点: 軽量で高速
// 欠点: 追加機能なし、フォールバックなし

// Socket.io
const socket = io();
socket.emit('message', 'Hello');

// 利点:
// 1. 自動再接続
// 2. Room/Namespace サポート
// 3. フォールバック (WebSocketが使えない場合はPolling)
// 4. イベントベース
// 5. バイナリサポート

// 欠点:
// 1. 重い (ライブラリサイズ)
// 2. 純粋WebSocketと互換性なし

// 選択基準:
// シンプルなプロジェクト → 純粋WebSocket
// 複雑なプロジェクト → Socket.io

Q4. WebSocketのセキュリティは?

A:

// 1. wss:// 使用 (WebSocket Secure)
// HTTP → HTTPSのように
// ws:// → wss://

const socket = new WebSocket('wss://example.com'); // ✅ 暗号化
const socket = new WebSocket('ws://example.com'); // ❌ 平文

// 2. 認証
// Socket.io
io.use((socket, next) => {
const token = socket.handshake.auth.token;

if (isValidToken(token)) {
next();
} else {
next(new Error('認証失敗'));
}
});

// クライアント
const socket = io({
auth: {
token: 'user-token'
}
});

// 3. 権限確認
socket.on('delete-message', (messageId) => {
const userId = socket.userId;

if (canDeleteMessage(userId, messageId)) {
deleteMessage(messageId);
} else {
socket.emit('error', '権限がありません');
}
});

// 4. Rate Limiting
const rateLimit = require('socket.io-rate-limit');

io.use(rateLimit({
max: 10, // 最大10個
interval: 1000 // 1秒あたり
}));

// 5. 入力検証
socket.on('message', (message) => {
// XSS防止
const sanitized = sanitizeHtml(message);

if (sanitized.length > 1000) {
return socket.emit('error', 'メッセージが長すぎます');
}

broadcast(sanitized);
});

Q5. WebSocket接続管理は?

A:

// 1. 自動再接続 (Socket.io)
const socket = io({
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000
});

socket.on('disconnect', () => {
console.log('接続切断');
});

socket.on('reconnect', () => {
console.log('再接続成功');
});

// 2. ハートビート (接続確認)
// サーバー
setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) {
return ws.terminate(); // 応答なしの場合終了
}

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

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

// 3. メモリ管理
const MAX_CONNECTIONS = 1000;

wss.on('connection', (ws) => {
if (wss.clients.size > MAX_CONNECTIONS) {
ws.close(1008, 'サーバーが満杯です');
return;
}

// 接続処理...
});

// 4. Graceful Shutdown
process.on('SIGTERM', () => {
console.log('サーバー終了中...');

// すべての接続に終了通知
wss.clients.forEach((ws) => {
ws.send(JSON.stringify({ type: 'server-shutdown' }));
ws.close();
});

wss.close(() => {
console.log('WebSocketサーバー終了');
process.exit(0);
});
});

🎓 次のステップ

WebSocketを理解したら、次を学習してみましょう:

  1. Node.jsとは? - WebSocketサーバー構築
  2. Reactとは? - リアルタイムUI実装
  3. Dockerとは? (ドキュメント作成予定) - WebSocketサーバーデプロイ

実践してみる

# Socket.ioチャットアプリを作る

# 1. プロジェクト初期化
mkdir chat-app
cd chat-app
npm init -y

# 2. パッケージインストール
npm install express socket.io

# 3. サーバー作成 (上記の例を参照)
# server.js

# 4. 実行
node server.js

# 5. http://localhost:3000 にアクセス

🎬 まとめ

WebSocketはリアルタイムWebの核心技術です:

  • 双方向通信: サーバー ↔ クライアント
  • 持続接続: 一度接続、継続使用
  • リアルタイム: 即座なデータ送信
  • 効率的: Pollingよりはるかに効率的

リアルタイム機能が必要ならWebSocketを使いましょう! 🔌✨