mcpField
W
websocket-realtime

websocket-realtime

github.com/rohitg00開発2.8k210 回インストール24 週間前に更新
リポジトリ

websocket-realtime について

リアルタイムアプリケーション開発向けのスキルです。WebSocket、Socket.io、Server-Sent Events の実装パターンに加え、再接続やセキュリティ、スケーリング、接続管理の確認項目を示します。

主な機能

  1. 01WebSocket サーバーのパターン
  2. 02Socket.io のルームと Redis アダプター
  3. 03サーバーからクライアントへの SSE 配信
  4. 04クライアントの再接続パターン
  5. 05セキュリティと信頼性のチェックリスト

活用シーン

  1. 01リアルタイムのサーバー通信実装
  2. 02一方向イベント配信での SSE 選択
  3. 03クライアントの再接続動作の追加
  4. 04認証とメッセージサイズ制限の確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/rohitg00/awesome-claude-code-toolkit --skill websocket-realtime

インストール後、Claude Code で/websocket-realtimeを入力して読み込まれたか確認します。

詳細

WebSocket とリアルタイム通信

WebSocket サーバー

import { WebSocketServer, WebSocket } from "ws";

const wss = new WebSocketServer({ port: 8080 });

const rooms = new Map<string, Set<WebSocket>>();

wss.on("connection", (ws, req) => {
  const userId = authenticateFromUrl(req.url);
  if (!userId) {
    ws.close(4001, "Unauthorized");
    return;
  }

  ws.on("message", (data) => {
    const message = JSON.parse(data.toString());

    switch (message.type) {
      case "join":
        joinRoom(message.room, ws);
        break;
      case "leave":
        leaveRoom(message.room, ws);
        break;
      case "broadcast":
        broadcastToRoom(message.room, message.payload, ws);
        break;
    }
  });

  ws.on("close", () => {
    rooms.forEach((members) => members.delete(ws));
  });

  ws.send(JSON.stringify({ type: "connected", userId }));
});

function joinRoom(room: string, ws: WebSocket) {
  if (!rooms.has(room)) rooms.set(room, new Set());
  rooms.get(room)!.add(ws);
}

function broadcastToRoom(room: string, payload: unknown, sender: WebSocket) {
  const members = rooms.get(room);
  if (!members) return;
  const message = JSON.stringify({ type: "message", room, payload });
  members.forEach((client) => {
    if (client !== sender && client.readyState === WebSocket.OPEN) {
      client.send(message);
    }
  });
}

ルームを使った Socket.io

import { Server } from "socket.io";
import { createAdapter } from "@socket.io/redis-adapter";
import { createClient } from "redis";

const io = new Server(httpServer, {
  cors: { origin: "https://app.example.com" },
  pingTimeout: 20000,
  pingInterval: 25000,
});

const pubClient = createClient({ url: "redis://localhost:6379" });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
io.adapter(createAdapter(pubClient, subClient));

io.use(async (socket, next) => {
  const token = socket.handshake.auth.token;
  try {
    socket.data.user = verifyToken(token);
    next();
  } catch {
    next(new Error("Authentication failed"));
  }
});

io.on("connection", (socket) => {
  socket.join(`user:${socket.data.user.id}`);

  socket.on("chat:join", (roomId) => {
    socket.join(`chat:${roomId}`);
    socket.to(`chat:${roomId}`).emit("chat:userJoined", socket.data.user);
  });

  socket.on("chat:message", async ({ roomId, text }) => {
    const message = await saveMessage(roomId, socket.data.user.id, text);
    io.to(`chat:${roomId}`).emit("chat:message", message);
  });

  socket.on("disconnect", () => {
    console.log(`User ${socket.data.user.id} disconnected`);
  });
});

Server-Sent Events(SSE)

app.get("/events/:userId", authenticate, (req, res) => {
  res.writeHead(200, {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive",
  });

  const sendEvent = (event: string, data: unknown) => {
    res.write(`event: ${event}\n`);
    res.write(`data: ${JSON.stringify(data)}\n\n`);
  };

  sendEvent("connected", { userId: req.params.userId });

  const interval = setInterval(() => {
    res.write(":heartbeat\n\n");
  }, 30000);

  const listener = (message: string) => {
    const event = JSON.parse(message);
    sendEvent(event.type, event.data);
  };

  redis.subscribe(`user:${req.params.userId}`, listener);

  req.on("close", () => {
    clearInterval(interval);
    redis.unsubscribe(`user:${req.params.userId}`, listener);
  });
});

SSE は、サーバーからクライアントへの一方向ストリーミングでは WebSocket よりシンプルです。特別な設定をしなくても、HTTP プロキシやロードバランサーを経由して動作します。

クライアントの再接続

class ReconnectingWebSocket {
  private ws: WebSocket | null = null;
  private retryCount = 0;
  private maxRetries = 10;

  constructor(private url: string) {
    this.connect();
  }

  private connect() {
    this.ws = new WebSocket(this.url);
    this.ws.onopen = () => { this.retryCount = 0; };
    this.ws.onclose = () => { this.scheduleReconnect(); };
    this.ws.onerror = () => { this.ws?.close(); };
  }

  private scheduleReconnect() {
    if (this.retryCount >= this.maxRetries) return;
    const delay = Math.min(1000 * 2 ** this.retryCount, 30000);
    this.retryCount++;
    setTimeout(() => this.connect(), delay);
  }

  send(data: string) {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(data);
    }
  }
}

アンチパターン

  • ハンドシェイク時に WebSocket 接続を認証しない
  • メッセージサイズの上限を設けず、無制限のペイロードを送信する
  • 古くなった接続を検出するためのハートビート/ping-pong がない
  • SSE で十分な場合(サーバーからクライアントへの一方向通信のみ)に WebSocket を使う
  • Socket.io で水平スケーリングする際に Redis アダプターを使用しない
  • メッセージの同期処理でイベントループをブロックする

チェックリスト

  • ハンドシェイク時に WebSocket 接続を認証している
  • 受信データにメッセージサイズの上限を適用している
  • ハートビートの仕組みで古くなった接続を検出して閉じている
  • クライアントで指数バックオフによる再接続を実装している
  • 複数サーバーへのデプロイで Redis pub/sub アダプターを使用している
  • サーバーからクライアントへの一方向通信のみの場合は SSE を使用している
  • 切断時にルーム/チャネルのメンバーシップをクリーンアップしている
  • メッセージの大量送信を防ぐためにレート制限を適用している

コメント