🔌 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);
});
});