Socket.IO
1. Socket.IOとは?
Socket.IOは、Webクライアントとサーバー間のリアルタイムで双方向、かつイベントベースの通信を可能にする強力なJavaScriptライブラリです。あらゆるプラットフォーム、ブラウザ、デバイスで動作するように設計されており、信頼性とスピードの両方に重点を置いています。
1.1 主な特徴
- リアルタイム双方向通信: クライアントとサーバー間での即時のデータ転送を実現します。
- 自動再接続: 切断が発生した際の再接続を自動的に処理します。
- ルーム(Room)サポート: グループ通信用のチャネルを簡単に作成できます。
- バイナリサポート: ArrayBuffer、Blob、Fileなどのバイナリデータを送受信可能です。
- マルチプレクシング(Multiplexing): ネームスペースを使用して複数のソケットを管理できます。
- フォールバックオプション: WebSocketが利用できない場合、自動的にHTTPロングポーリングに切り替えます。
1.2 ユースケース
- リアルタイムチャットアプリケーション
- ライブ通知(プッシュ通知)
- 共同編集ツール
- オンラインゲーム
- リアルタイム分析(アナリティクス)
- ドキュメントの同時編集
- リアルタイムダッシュボード
- IoTアプリケーション
Socket.IOは、ブラウザで動作するクライアントサイドライブラリと、Node.js用のサーバーサイドライブラリの2つの部分で構成されています。
2. Socket.IOのインストール
2.1 サーバーサイドのインストール
npmまたはyarnを使用して、Node.jsプロジェクトにSocket.IOをインストールします。
# npmを使用する場合
npm install socket.io
# Yarnを使用する場合
yarn add socket.io2.2 クライアントサイドのセットアップ
クライアントライブラリを組み込むには、以下のいずれかの方法を選択してください。
オプション 1: CDN(クイックスタート)
<script src="https://cdn.socket.io/4.5.0/socket.io.min.js"></script>オプション 2: NPM(本番環境推奨)
# クライアントライブラリをインストール
npm install socket.io-client
# または Yarn を使用
yarn add socket.io-clientオプション 3: ES6モジュールの使用
import { io } from 'socket.io-client';2.3 バージョンの互換性
| Socket.IO バージョン | Node.js バージョン | ブラウザサポート |
|---|---|---|
| v4.x | v12.22.0+ | Chrome 49+, Firefox 53+, Safari 10+ |
| v3.x | v10.0.0+ | Chrome 49+, Firefox 53+, Safari 10+ |
| v2.x | v6.0.0+ | Chrome 5+, Firefox 6+, Safari 5.1+ |
注意: 本番環境では、クライアントとサーバーで同じバージョンを使用することをお勧めします。
3. Socket.IOを使用したシンプルなチャットアプリの作成
Node.jsとSocket.IOを使用して、シンプルなリアルタイムチャットアプリケーションを構築しましょう。この例ではログイン機能は不要で、基本的な機能のデモンストレーションに焦点を当てます。
3.1 サーバーの作成 (app.js)
app.js という名前の新しいファイルを作成し、以下の内容を記述します。
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const path = require('path');
const app = express();
const server = http.createServer(app);
const io = new Server(server);
// 静的ファイルの提供
app.use(express.static(path.join(__dirname, 'public')));
// シンプルなルート設定
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
// Socket.IO 接続ハンドラー
io.on('connection', (socket) => {
console.log('ユーザーが接続しました');
// 新しいメッセージの処理
socket.on('chat message', (msg) => {
console.log('メッセージを受信:', msg);
// 接続されているすべてのクライアントにメッセージをブロードキャスト
io.emit('chat message', msg);
});
// 切断時の処理
socket.on('disconnect', () => {
console.log('ユーザーが切断しました');
});
});
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`サーバーがポート ${PORT} で起動しました`);
});3.2 クライアントの作成 (public/index.html)
public ディレクトリを作成し、その中に以下の内容の index.html ファイルを追加します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>シンプルチャット</title>
<style>
body { margin: 0; padding: 20px; font-family: Arial, sans-serif; }
#messages {
list-style-type: none; margin: 0; padding: 0; margin-bottom: 20px;
border: 1px solid #ddd; padding: 10px; height: 400px; overflow-y: auto;
}
#messages li { padding: 8px 16px; border-bottom: 1px solid #eee; }
#messages li:last-child { border-bottom: none; }
#form { display: flex; margin-top: 10px; }
#input { flex-grow: 1; padding: 10px; font-size: 16px; }
button {
padding: 10px 20px; background: #4CAF50; color: white;
border: none; cursor: pointer; margin-left: 10px;
}
button:hover { background: #45a049; }
</style>
</head>
<body>
<h1>シンプルチャット</h1>
<ul id="messages"></ul>
<form id="form" action="#">
<input id="input" autocomplete="off" placeholder="メッセージを入力..." />
<button>送信</button>
</form>
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io();
const form = document.getElementById('form');
const input = document.getElementById('input');
const messages = document.getElementById('messages');
// フォーム送信時の処理
form.addEventListener('submit', (e) => {
e.preventDefault();
const message = input.value.trim();
if (message) {
// サーバーにメッセージを送信(Emit)
socket.emit('chat message', message);
// 入力欄をクリア
input.value = '';
}
});
// メッセージ受信時の処理
socket.on('chat message', (msg) => {
const item = document.createElement('li');
item.textContent = msg;
messages.appendChild(item);
// 最下部までスクロール
messages.scrollTop = messages.scrollHeight;
});
</script>
</body>
</html>3.3 アプリケーションの実行
- サーバーを起動します:
node app.js - ブラウザを開き、
http://localhost:3000にアクセスします。 - 複数のブラウザウィンドウを開き、リアルタイムに更新される様子を確認してください。
3.4 動作の仕組み
- サーバーは Express を使用して静的ファイルを提供し、Socket.IO 接続を処理します。
- クライアントが接続すると、メッセージを送信できるようになります。そのメッセージはすべての接続済みクライアントにブロードキャストされます。
- クライアントサイドの JavaScript は、メッセージの送受信をリアルタイムで処理します。
4. 次のステップ
基本バージョンの動作を確認したら、以下の機能の追加を検討してください:
- 各メッセージへのユーザー名の付与
- ユーザーの入室/退室通知
- 異なるチャットルームの作成
- メッセージの永続化(データベース保存)
- ユーザー認証
注意: これはデモンストレーション用の基本的な例です。本番環境では、適切なエラーハンドリング、入力バリデーション、およびセキュリティ対策を追加する必要があります。
5. ユーザー名の追加
メッセージにユーザー名を追加してチャットを強化しましょう。まず、サーバー側を修正してユーザー名を扱えるようにします。
// app.js 内の接続ハンドラーを修正
io.on('connection', (socket) => {
console.log('ユーザーが接続しました');
// ソケットにユーザー名を保存(デフォルトは Anonymous)
socket.username = 'Anonymous';
// ユーザー名付きで新しいメッセージを処理
socket.on('chat message', (msg) => {
io.emit('chat message', {
username: socket.username,
message: msg,
timestamp: new Date().toISOString()
});
});
// ユーザー名の設定を処理
socket.on('set username', (username) => {
const oldUsername = socket.username;
socket.username = username || 'Anonymous';
io.emit('user joined', {
oldUsername: oldUsername,
newUsername: socket.username
});
});
// 切断時の処理
socket.on('disconnect', () => {
console.log('ユーザーが切断しました');
io.emit('user left', { username: socket.username });
});
});次に、クライアント側を更新してユーザー名を処理できるようにします。
<div id="username-container">
<input type="text" id="username-input" placeholder="ユーザー名を入力" />
<button id="set-username">名前を設定</button>
</div>
<script>
// ユーザー名処理の追加
const usernameInput = document.getElementById('username-input');
const setUsernameBtn = document.getElementById('set-username');
let currentUsername = 'Anonymous';
setUsernameBtn.addEventListener('click', () => {
const newUsername = usernameInput.value.trim();
if (newUsername) {
socket.emit('set username', newUsername);
currentUsername = newUsername;
usernameInput.value = '';
}
});
// ユーザー名を表示するようにメッセージ表示を更新
socket.on('chat message', (data) => {
const item = document.createElement('li');
item.innerHTML = `<strong>${data.username}:</strong> ${data.message}`;
messages.appendChild(item);
messages.scrollTop = messages.scrollHeight;
});
// ユーザー入室通知の処理
socket.on('user joined', (data) => {
const item = document.createElement('li');
item.className = 'system-message';
if (data.oldUsername === 'Anonymous') {
item.textContent = `${data.newUsername} がチャットに参加しました`;
} else {
item.textContent = `${data.oldUsername} は ${data.newUsername} に名前を変更しました`;
}
messages.appendChild(item);
messages.scrollTop = messages.scrollHeight;
});
// ユーザー退室通知の処理
socket.on('user left', (data) => {
const item = document.createElement('li');
item.className = 'system-message';
item.textContent = `${data.username} が退出しました`;
messages.appendChild(item);
messages.scrollTop = messages.scrollHeight;
});
</script>
<style>
.system-message {
color: #666;
font-style: italic;
font-size: 0.9em;
}
</style>6. チャットルームの追加
異なるチャットルームを作成して参加できる機能を追加します。サーバーを更新します:
// app.js にルーム処理を追加
const rooms = new Set(['general', 'random']);
io.on('connection', (socket) => {
// ... 既存のコード ...
// ルームへの参加
socket.on('join room', (room) => {
// デフォルト以外のすべてのルームから退出
socket.rooms.forEach(r => {
if (r !== socket.id) {
socket.leave(r);
socket.emit('left room', r);
}
});
// 新しいルームに参加
socket.join(room);
socket.emit('joined room', room);
// ルーム内の他のユーザーに通知
socket.to(room).emit('room message', {
username: 'System',
message: `${socket.username} がルームに参加しました`,
timestamp: new Date().toISOString()
});
});
// ルームの作成処理
socket.on('create room', (roomName) => {
if (!rooms.has(roomName)) {
rooms.add(roomName);
io.emit('room created', roomName);
}
});
// メッセージハンドラーをルーム宛に送信するように変更
socket.on('chat message', (data) => {
const room = Array.from(socket.rooms).find(r => r !== socket.id) || 'general';
io.to(room).emit('chat message', {
username: socket.username,
message: data.message,
timestamp: new Date().toISOString(),
room: room
});
});
});7. ユーザーリストとタイピングインジケーターの追加
ユーザーリストと「入力中...」の表示を追加して、ユーザー体験を向上させましょう。
7.1 サーバー側:状態管理
// app.js でユーザーとタイピング状態を追跡
const usersInRooms = new Map();
const typingUsers = new Map();
io.on('connection', (socket) => {
// ルーム参加時の初期化
socket.on('join room', (room) => {
// ... 既存のルーム参加コード ...
if (!usersInRooms.has(room)) {
usersInRooms.set(room, new Map());
typingUsers.set(room, new Set());
}
usersInRooms.get(room).set(socket.id, {
username: socket.username,
id: socket.id
});
updateUserList(room);
});
// タイピング状態の処理
socket.on('typing', (isTyping) => {
const room = Array.from(socket.rooms).find(r => r !== socket.id);
if (!room) return;
if (isTyping) {
typingUsers.get(room).add(socket.username);
} else {
typingUsers.get(room).delete(socket.username);
}
io.to(room).emit('typing users', Array.from(typingUsers.get(room)));
});
// 切断時の処理
socket.on('disconnect', () => {
Array.from(usersInRooms.entries()).forEach(([room, users]) => {
if (users.has(socket.id)) {
users.delete(socket.id);
typingUsers.get(room)?.delete(socket.username);
updateUserList(room);
}
});
});
});
function updateUserList(room) {
const users = Array.from(usersInRooms.get(room)?.values() || []);
io.to(room).emit('user list', {
room: room,
users: users.map(u => ({
username: u.username,
isTyping: typingUsers.get(room)?.has(u.username) || false
}))
});
}8. クライアントサイドAPIの概要
クライアントサイドの Socket.IO API は以下のメソッドを提供します:
io(): サーバーに接続します。socket.emit(): サーバーにイベントを送信します。socket.on(): サーバーからのイベントをリッスン(受信)します。socket.disconnect(): サーバーから切断します。
8.1 Socket.IO イベント
Socket.IO は通信にイベントベースのアーキテクチャを使用します。
ビルトインイベント:
connect: 接続時に発生disconnect: 切断時に発生error: エラー発生時に発生reconnect: 再接続成功時に発生reconnect_attempt: 再接続の試行中に発生
9. Socket.IO ミドルウェア
Socket.IO では、認証などの目的でミドルウェア関数を定義できます。
const io = new Server(server);
// 認証用ミドルウェア
io.use((socket, next) => {
const token = socket.handshake.auth.token;
if (!token) {
return next(new Error('認証エラー: トークンがありません'));
}
// トークンの検証 (JWTの例)
try {
const user = jwt.verify(token, 'your-secret-key');
socket.user = user;
next();
} catch (error) {
next(new Error('認証エラー: 無効なトークンです'));
}
});
io.on('connection', (socket) => {
console.log(`認証済みユーザーが接続しました: ${socket.user.username}`);
});10. Socket.IO vs ネイティブ WebSocket
| 機能 | Socket.IO | ネイティブ WebSocket |
|---|---|---|
| フォールバック機構 | あり (HTTPロングポーリング等) | なし |
| 自動再接続 | あり | なし (手動実装が必要) |
| ブロードキャスト | 標準機能 | 手動実装が必要 |
| ルーム/ネームスペース | 標準機能 | 手動実装が必要 |
| ブラウザサポート | すべてのブラウザ | モダンブラウザのみ |
| パケットサイズ | 大きい (プロトコルオーバーヘッド) | 小さい |
| バイナリデータ | サポート済み | サポート済み |
Socket.IO は、信頼性、互換性、および高度な機能が必要な場合に適しています。一方、ネイティブ WebSocket はより軽量でオーバーヘッドが少ないのが特徴です。