WebSocket レッスン5
WebSocket総合演習
Socket.IOでリアルタイムダッシュボードを作ろう
プロジェクト概要
この演習では、リアルタイムダッシュボードを Socket.IOで構築します。サーバーがランダムにデータを生成し、 接続中のクライアントにリアルタイムで表示するアプリケーションです。
機能一覧
- サーバーからのリアルタイムデータ配信
- 接続状態の表示(接続中/切断中)
- ライブデータのグラフ表示
- 接続ユーザー数の表示
使用技術
- Socket.IO(サーバー + クライアント)
- Express(HTTPサーバー)
- React(フロントエンド)
- TypeScript
Step 1: サーバーのイベント配信
サーバー側で定期的にダッシュボードデータを生成し、 接続中の全クライアントに配信します。
// server.ts
import express from "express";
import { createServer } from "http";
import { Server } from "socket.io";
const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, {
cors: { origin: "http://localhost:3000" },
});
// ダッシュボードデータを生成
function generateMetrics() {
return {
cpu: Math.round(Math.random() * 100),
memory: Math.round(40 + Math.random() * 50),
requests: Math.round(Math.random() * 500),
errors: Math.round(Math.random() * 10),
responseTime: Math.round(50 + Math.random() * 200),
timestamp: Date.now(),
};
}
// 接続ユーザー数を管理
let connectedUsers = 0;
io.on("connection", (socket) => {
connectedUsers++;
console.log(`接続: ${socket.id} (合計: ${connectedUsers})`);
// 接続ユーザー数を全員に通知
io.emit("dashboard:users", { count: connectedUsers });
// 初回データを送信
socket.emit("dashboard:metrics", generateMetrics());
// 切断時
socket.on("disconnect", () => {
connectedUsers--;
io.emit("dashboard:users", { count: connectedUsers });
console.log(`切断: ${socket.id} (合計: ${connectedUsers})`);
});
});
// 2秒ごとにメトリクスを全員に配信
setInterval(() => {
const metrics = generateMetrics();
io.emit("dashboard:metrics", metrics);
}, 2000);
// アラートイベントをランダムに配信
setInterval(() => {
if (Math.random() > 0.7) {
const alerts = [
{ level: "warning", message: "CPU使用率が80%を超えました" },
{ level: "error", message: "APIレスポンスタイムが遅延しています" },
{ level: "info", message: "新しいデプロイが完了しました" },
];
const alert = alerts[Math.floor(Math.random() * alerts.length)];
io.emit("dashboard:alert", {
...alert,
timestamp: Date.now(),
});
}
}, 5000);
httpServer.listen(3001, () => {
console.log("ダッシュボードサーバー起動: http://localhost:3001");
});Step 2: Reactクライアント
React側でSocket.IOに接続し、受信したデータをリアルタイムで表示します。 カスタムフックでSocket.IOの接続を管理します。
// hooks/useSocket.ts
import { useEffect, useRef, useState } from "react";
import { io, Socket } from "socket.io-client";
export function useSocket(url: string) {
const socketRef = useRef<Socket | null>(null);
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
const socket = io(url, {
reconnection: true,
reconnectionAttempts: 10,
reconnectionDelay: 1000,
});
socketRef.current = socket;
socket.on("connect", () => setIsConnected(true));
socket.on("disconnect", () => setIsConnected(false));
return () => {
socket.disconnect();
};
}, [url]);
return { socket: socketRef.current, isConnected };
}
// hooks/useDashboard.ts
import { useEffect, useState } from "react";
import { useSocket } from "./useSocket";
interface Metrics {
cpu: number;
memory: number;
requests: number;
errors: number;
responseTime: number;
timestamp: number;
}
interface Alert {
level: "info" | "warning" | "error";
message: string;
timestamp: number;
}
export function useDashboard() {
const { socket, isConnected } = useSocket("http://localhost:3001");
const [metrics, setMetrics] = useState<Metrics | null>(null);
const [metricsHistory, setMetricsHistory] = useState<Metrics[]>([]);
const [userCount, setUserCount] = useState(0);
const [alerts, setAlerts] = useState<Alert[]>([]);
useEffect(() => {
if (!socket) return;
socket.on("dashboard:metrics", (data: Metrics) => {
setMetrics(data);
setMetricsHistory((prev) => [...prev.slice(-29), data]);
});
socket.on("dashboard:users", (data) => {
setUserCount(data.count);
});
socket.on("dashboard:alert", (data: Alert) => {
setAlerts((prev) => [data, ...prev].slice(0, 10));
});
return () => {
socket.off("dashboard:metrics");
socket.off("dashboard:users");
socket.off("dashboard:alert");
};
}, [socket]);
return { metrics, metricsHistory, userCount, alerts, isConnected };
}Step 3: ライブデータの可視化
受信したメトリクスデータをカード形式で表示し、 履歴データをシンプルなバーチャートで可視化します。
// components/Dashboard.tsx
import { useDashboard } from "../hooks/useDashboard";
export function Dashboard() {
const { metrics, metricsHistory, userCount, alerts, isConnected } =
useDashboard();
return (
<div className="p-6 max-w-6xl mx-auto">
{/* ヘッダー */}
<div className="flex items-center justify-between mb-6">
<h1 className="text-2xl font-bold">
リアルタイムダッシュボード
</h1>
<ConnectionStatus isConnected={isConnected} />
</div>
{/* メトリクスカード */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<MetricCard
label="CPU使用率"
value={`${metrics?.cpu ?? 0}%`}
color={metrics?.cpu ?? 0 > 80 ? "red" : "green"}
/>
<MetricCard
label="メモリ使用率"
value={`${metrics?.memory ?? 0}%`}
color={metrics?.memory ?? 0 > 80 ? "red" : "yellow"}
/>
<MetricCard
label="リクエスト/s"
value={`${metrics?.requests ?? 0}`}
color="blue"
/>
<MetricCard
label="接続ユーザー"
value={`${userCount}`}
color="purple"
/>
</div>
{/* CPUヒストリーチャート */}
<div className="bg-gray-800 rounded-lg p-4 mb-6">
<h2 className="text-lg font-semibold mb-3">
CPU使用率(直近30件)
</h2>
<div className="flex items-end gap-1 h-32">
{metricsHistory.map((m, i) => (
<div
key={i}
className="flex-1 bg-violet-500 rounded-t"
style={{ height: `${m.cpu}%` }}
title={`${m.cpu}%`}
/>
))}
</div>
</div>
{/* アラート一覧 */}
<div className="bg-gray-800 rounded-lg p-4">
<h2 className="text-lg font-semibold mb-3">アラート</h2>
{alerts.length === 0 ? (
<p className="text-gray-500 text-sm">
アラートはありません
</p>
) : (
<ul className="space-y-2">
{alerts.map((alert, i) => (
<AlertItem key={i} alert={alert} />
))}
</ul>
)}
</div>
</div>
);
}Step 4: 接続状態の表示
ユーザーに接続状態を視覚的に伝えることで、 リアルタイムデータが正しく受信できているか確認できるようにします。
// components/ConnectionStatus.tsx
interface ConnectionStatusProps {
isConnected: boolean;
}
export function ConnectionStatus({ isConnected }: ConnectionStatusProps) {
return (
<div className="flex items-center gap-2">
<div
className={`w-3 h-3 rounded-full ${
isConnected ? "bg-green-500 animate-pulse" : "bg-red-500"
}`}
/>
<span className="text-sm text-gray-400">
{isConnected ? "接続中" : "切断中"}
</span>
</div>
);
}
// components/MetricCard.tsx
interface MetricCardProps {
label: string;
value: string;
color: string;
}
export function MetricCard({ label, value, color }: MetricCardProps) {
const colorMap: Record<string, string> = {
red: "border-red-500/30 text-red-400",
green: "border-green-500/30 text-green-400",
blue: "border-blue-500/30 text-blue-400",
yellow: "border-yellow-500/30 text-yellow-400",
purple: "border-violet-500/30 text-violet-400",
};
return (
<div className={`p-4 rounded-lg border bg-gray-900 ${
colorMap[color] ?? colorMap.blue
}`}>
<p className="text-xs text-gray-500 mb-1">{label}</p>
<p className="text-2xl font-bold">{value}</p>
</div>
);
}
// components/AlertItem.tsx
interface Alert {
level: "info" | "warning" | "error";
message: string;
timestamp: number;
}
export function AlertItem({ alert }: { alert: Alert }) {
const levelStyles = {
info: "border-blue-500/30 text-blue-400",
warning: "border-yellow-500/30 text-yellow-400",
error: "border-red-500/30 text-red-400",
};
const levelLabels = {
info: "INFO",
warning: "WARN",
error: "ERROR",
};
const time = new Date(alert.timestamp).toLocaleTimeString("ja-JP");
return (
<li className={`flex items-center gap-3 p-2 rounded border ${
levelStyles[alert.level]
} bg-gray-900`}>
<span className="text-xs font-bold w-12">
{levelLabels[alert.level]}
</span>
<span className="text-sm flex-1">{alert.message}</span>
<span className="text-xs text-gray-500">{time}</span>
</li>
);
}発展課題
基本のダッシュボードが完成したら、以下の機能を追加して発展させてみましょう。
1. アラートのフィルタリング
レベル(info/warning/error)でアラートを絞り込める機能を追加する。
2. メトリクスの一時停止
データ受信の一時停止/再開ボタンを実装する。socket.off/onを切り替える。
3. ルーム別ダッシュボード
複数のサーバーをルームで分離し、切り替えて表示できるようにする。
4. JWT認証の追加
レッスン4で学んだJWT認証をダッシュボードに組み込む。
まとめ
- Socket.IOサーバーで定期的にメトリクスデータを生成し全クライアントに配信
- Reactカスタムフックで接続管理とデータの状態管理を分離
- メトリクスカード、バーチャート、アラート一覧でデータをリアルタイム可視化
- 接続状態インジケーターでユーザーに接続状況をフィードバック
- ルーム、認証、フィルタリングなどの発展課題で実践力を高める