𝕏 f B! L
案件・求人数 12,345
案件を探す(準備中) エージェントを探す(準備中) お役立ち情報 ログイン
案件・求人数 12,345
OpenAI Codex CLIでWebSocketリアルタイムアプリ開発|設計・実装・デプロイ完全ガイド

OpenAI Codex CLIでWebSocketリアルタイムアプリ開発|設計・実装・デプロイ完全ガイド

OpenAI Codex CLIWebSocketリアルタイム通信Node.js
目次

「チャット機能やリアルタイム通知を実装したいが、WebSocketの設計が複雑で手が止まる」「Socket.IOとネイティブWebSocketのどちらを使うべきかわからない」「スケールアウト時のセッション管理をどう設計すべきか迷っている」

結論から言えば、OpenAI Codex CLIを活用することでWebSocketサーバーの設計・実装・テストを自然言語で効率的に進められます。本記事では、基本概念から本番運用レベルのスケーリング設計まで解説します。

この記事を3秒でまとめると

  • Codex CLIのサンドボックス環境でWebSocketサーバーの安全なプロトタイピングが可能
  • Socket.IOアダプター設計やRedis Pub/Subによるスケーリングパターンをコード生成
  • 認証・再接続・ハートビートなど本番必須の実装パターンを自動化

Codex CLIによるWebSocketリアルタイムアプリ開発の全体像

WebSocketとは:HTTP通信との根本的な違い

WebSocketは、クライアントとサーバー間で双方向のリアルタイム通信を実現するプロトコルです。HTTPがリクエスト/レスポンスの一方向通信であるのに対し、WebSocketは一度接続を確立すると、サーバー側からもデータをプッシュできます。

比較項目HTTPWebSocket
通信方向クライアント → サーバー(一方向)双方向
接続方式リクエストごとに接続/切断持続的な接続
レイテンシリクエストごとにオーバーヘッド低レイテンシ
用途REST API、Webページ配信チャット、ゲーム、ライブ更新
プロトコルHTTP/1.1, HTTP/2ws://, wss://

SES案件でWebSocketが求められる典型的なシナリオは以下の通りです。

  • チャットアプリケーション: Slack風のリアルタイムメッセージング
  • ダッシュボード: サーバーメトリクスやビジネスKPIのリアルタイム表示
  • 通知システム: Push通知のサーバーサイド基盤
  • コラボレーションツール: Google Docs風のリアルタイム共同編集
  • IoTデータストリーミング: センサーデータのリアルタイム可視化

Codex CLIでWebSocketサーバーを構築する

プロジェクト初期化

Codex CLIのサンドボックス環境を活用して、安全にプロジェクトを初期化します。

codex "Node.js + TypeScript + Socket.IOでWebSocketサーバープロジェクトを初期化して。Express統合、CORS設定、基本的なイベントハンドリングを含めて"

Codex CLIが生成するプロジェクト構造:

websocket-server/
├── src/
│   ├── server.ts
│   ├── socket/
│   │   ├── handlers.ts
│   │   ├── middleware.ts
│   │   └── events.ts
│   ├── services/
│   │   └── message.service.ts
│   ├── types/
│   │   └── socket.types.ts
│   └── config/
│       └── socket.config.ts
├── tests/
│   └── socket.test.ts
├── package.json
├── tsconfig.json
└── .env.example

基本的なSocket.IOサーバー実装

codex "Socket.IOサーバーを実装して。ルーム機能、認証ミドルウェア、型安全なイベント定義を含めて"
// src/types/socket.types.ts
export interface ServerToClientEvents {
  message: (data: MessagePayload) => void;
  userJoined: (data: UserInfo) => void;
  userLeft: (data: UserInfo) => void;
  typing: (data: TypingPayload) => void;
  error: (data: ErrorPayload) => void;
}

export interface ClientToServerEvents {
  sendMessage: (data: SendMessageInput, callback: (res: AckResponse) => void) => void;
  joinRoom: (roomId: string, callback: (res: AckResponse) => void) => void;
  leaveRoom: (roomId: string) => void;
  startTyping: (roomId: string) => void;
  stopTyping: (roomId: string) => void;
}

export interface InterServerEvents {
  ping: () => void;
}

export interface SocketData {
  userId: string;
  username: string;
  rooms: Set<string>;
}

export interface MessagePayload {
  id: string;
  roomId: string;
  userId: string;
  username: string;
  content: string;
  timestamp: number;
}

export interface UserInfo {
  userId: string;
  username: string;
  roomId: string;
}

export interface TypingPayload {
  userId: string;
  username: string;
  roomId: string;
}

export interface SendMessageInput {
  roomId: string;
  content: string;
}

export interface AckResponse {
  success: boolean;
  error?: string;
}

export interface ErrorPayload {
  code: string;
  message: string;
}
// src/server.ts
import express from 'express';
import { createServer } from 'http';
import { Server } from 'socket.io';
import { authMiddleware } from './socket/middleware';
import { registerHandlers } from './socket/handlers';
import type {
  ServerToClientEvents,
  ClientToServerEvents,
  InterServerEvents,
  SocketData,
} from './types/socket.types';

const app = express();
const httpServer = createServer(app);

const io = new Server<
  ClientToServerEvents,
  ServerToClientEvents,
  InterServerEvents,
  SocketData
>(httpServer, {
  cors: {
    origin: process.env.CORS_ORIGIN || 'http://localhost:3000',
    methods: ['GET', 'POST'],
    credentials: true,
  },
  pingTimeout: 60000,
  pingInterval: 25000,
  maxHttpBufferSize: 1e6, // 1MB
  connectionStateRecovery: {
    maxDisconnectionDuration: 2 * 60 * 1000, // 2分
    skipMiddlewares: true,
  },
});

// 認証ミドルウェア
io.use(authMiddleware);

// 接続ハンドリング
io.on('connection', (socket) => {
  console.log(`User connected: ${socket.data.userId}`);
  registerHandlers(io, socket);
});

// ヘルスチェック
app.get('/health', (_req, res) => {
  const connectedSockets = io.engine.clientsCount;
  res.json({
    status: 'ok',
    connections: connectedSockets,
    uptime: process.uptime(),
  });
});

const PORT = process.env.PORT || 8080;
httpServer.listen(PORT, () => {
  console.log(`WebSocket server running on port ${PORT}`);
});

export { io, httpServer };

認証ミドルウェア

codex "JWT認証ミドルウェアを実装して。トークン検証、ユーザー情報の注入、不正トークン時の切断処理を含めて"
// src/socket/middleware.ts
import type { Socket } from 'socket.io';
import jwt from 'jsonwebtoken';
import type { SocketData } from '../types/socket.types';

const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';

export const authMiddleware = (
  socket: Socket<any, any, any, SocketData>,
  next: (err?: Error) => void
) => {
  const token =
    socket.handshake.auth.token ||
    socket.handshake.headers.authorization?.replace('Bearer ', '');

  if (!token) {
    return next(new Error('Authentication required'));
  }

  try {
    const decoded = jwt.verify(token, JWT_SECRET) as {
      userId: string;
      username: string;
    };

    socket.data.userId = decoded.userId;
    socket.data.username = decoded.username;
    socket.data.rooms = new Set();

    next();
  } catch (err) {
    next(new Error('Invalid token'));
  }
};

イベントハンドラーの実装

// src/socket/handlers.ts
import { v4 as uuidv4 } from 'uuid';
import type { Server, Socket } from 'socket.io';
import type {
  ServerToClientEvents,
  ClientToServerEvents,
  InterServerEvents,
  SocketData,
  AckResponse,
  SendMessageInput,
} from '../types/socket.types';

type IOServer = Server<
  ClientToServerEvents,
  ServerToClientEvents,
  InterServerEvents,
  SocketData
>;
type IOSocket = Socket<
  ClientToServerEvents,
  ServerToClientEvents,
  InterServerEvents,
  SocketData
>;

export const registerHandlers = (io: IOServer, socket: IOSocket) => {
  // ルーム参加
  socket.on('joinRoom', async (roomId: string, callback: (res: AckResponse) => void) => {
    try {
      await socket.join(roomId);
      socket.data.rooms.add(roomId);

      // ルーム内の他ユーザーに通知
      socket.to(roomId).emit('userJoined', {
        userId: socket.data.userId,
        username: socket.data.username,
        roomId,
      });

      callback({ success: true });
    } catch (error) {
      callback({ success: false, error: 'Failed to join room' });
    }
  });

  // メッセージ送信
  socket.on('sendMessage', (data: SendMessageInput, callback: (res: AckResponse) => void) => {
    const { roomId, content } = data;

    if (!socket.data.rooms.has(roomId)) {
      return callback({ success: false, error: 'Not in room' });
    }

    if (!content || content.trim().length === 0) {
      return callback({ success: false, error: 'Empty message' });
    }

    if (content.length > 5000) {
      return callback({ success: false, error: 'Message too long' });
    }

    const message = {
      id: uuidv4(),
      roomId,
      userId: socket.data.userId,
      username: socket.data.username,
      content: content.trim(),
      timestamp: Date.now(),
    };

    io.to(roomId).emit('message', message);
    callback({ success: true });
  });

  // タイピングインジケーター
  socket.on('startTyping', (roomId: string) => {
    socket.to(roomId).emit('typing', {
      userId: socket.data.userId,
      username: socket.data.username,
      roomId,
    });
  });

  // ルーム退出
  socket.on('leaveRoom', async (roomId: string) => {
    await socket.leave(roomId);
    socket.data.rooms.delete(roomId);

    socket.to(roomId).emit('userLeft', {
      userId: socket.data.userId,
      username: socket.data.username,
      roomId,
    });
  });

  // 切断処理
  socket.on('disconnect', (reason) => {
    console.log(`User disconnected: ${socket.data.userId} (${reason})`);

    // 全ルームに退出通知
    for (const roomId of socket.data.rooms) {
      socket.to(roomId).emit('userLeft', {
        userId: socket.data.userId,
        username: socket.data.username,
        roomId,
      });
    }
  });
};

スケーリング設計:Redis Pub/Subアダプター

本番環境で複数のWebSocketサーバーインスタンスを運用する場合、Redis Pub/Subアダプターが必要です。

codex "Socket.IOのRedisアダプターを設定して。複数インスタンス間でメッセージブロードキャストが同期されるようにして"
// src/config/socket.config.ts
import { createClient } from 'redis';
import { createAdapter } from '@socket.io/redis-adapter';
import type { Server } from 'socket.io';

export const configureRedisAdapter = async (io: Server) => {
  const pubClient = createClient({
    url: process.env.REDIS_URL || 'redis://localhost:6379',
    socket: {
      reconnectStrategy: (retries) => {
        if (retries > 10) {
          console.error('Redis: max reconnection attempts reached');
          return new Error('Max reconnection attempts reached');
        }
        return Math.min(retries * 100, 3000);
      },
    },
  });

  const subClient = pubClient.duplicate();

  pubClient.on('error', (err) => console.error('Redis Pub Error:', err));
  subClient.on('error', (err) => console.error('Redis Sub Error:', err));

  await Promise.all([pubClient.connect(), subClient.connect()]);

  io.adapter(createAdapter(pubClient, subClient));

  console.log('Redis adapter configured');
  return { pubClient, subClient };
};

スケーリング構成図

                    ┌─────────────────┐
                    │   Load Balancer  │
                    │  (sticky session)│
                    └────────┬────────┘

              ┌──────────────┼──────────────┐
              │              │              │
     ┌────────▼────┐  ┌─────▼──────┐  ┌────▼────────┐
     │ WS Server 1 │  │ WS Server 2│  │ WS Server 3 │
     │ (Socket.IO) │  │ (Socket.IO)│  │ (Socket.IO)  │
     └────────┬────┘  └─────┬──────┘  └────┬────────┘
              │              │              │
              └──────────────┼──────────────┘

                    ┌────────▼────────┐
                    │   Redis Cluster  │
                    │   (Pub/Sub)      │
                    └─────────────────┘

スケーリング時の重要なポイントは以下です。

  • スティッキーセッション: WebSocket接続は特定のサーバーインスタンスに紐づくため、ロードバランサーでスティッキーセッションを有効化
  • Redis Pub/Sub: 異なるインスタンスに接続しているクライアント間のメッセージ同期
  • 接続状態の復旧: Socket.IOのconnectionStateRecoveryで短時間の切断からの自動復帰

クライアントサイドの実装

codex "React + TypeScriptでSocket.IOクライアントを実装して。カスタムフック形式で再利用可能にして。自動再接続、エラーハンドリング付き"
// hooks/useSocket.ts
import { useEffect, useRef, useState, useCallback } from 'react';
import { io, Socket } from 'socket.io-client';
import type {
  ServerToClientEvents,
  ClientToServerEvents,
} from '../types/socket.types';

type TypedSocket = Socket<ServerToClientEvents, ClientToServerEvents>;

interface UseSocketOptions {
  url: string;
  token: string;
  autoConnect?: boolean;
}

interface UseSocketReturn {
  socket: TypedSocket | null;
  isConnected: boolean;
  error: string | null;
  connect: () => void;
  disconnect: () => void;
}

export const useSocket = ({
  url,
  token,
  autoConnect = true,
}: UseSocketOptions): UseSocketReturn => {
  const socketRef = useRef<TypedSocket | null>(null);
  const [isConnected, setIsConnected] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const connect = useCallback(() => {
    if (socketRef.current?.connected) return;

    const socket = io(url, {
      auth: { token },
      transports: ['websocket', 'polling'],
      reconnection: true,
      reconnectionAttempts: 10,
      reconnectionDelay: 1000,
      reconnectionDelayMax: 10000,
      timeout: 20000,
    }) as TypedSocket;

    socket.on('connect', () => {
      setIsConnected(true);
      setError(null);
    });

    socket.on('disconnect', (reason) => {
      setIsConnected(false);
      if (reason === 'io server disconnect') {
        setError('Server disconnected');
      }
    });

    socket.on('connect_error', (err) => {
      setError(err.message);
      setIsConnected(false);
    });

    socketRef.current = socket;
  }, [url, token]);

  const disconnect = useCallback(() => {
    socketRef.current?.disconnect();
    socketRef.current = null;
    setIsConnected(false);
  }, []);

  useEffect(() => {
    if (autoConnect) connect();
    return () => disconnect();
  }, [autoConnect, connect, disconnect]);

  return {
    socket: socketRef.current,
    isConnected,
    error,
    connect,
    disconnect,
  };
};

テスト自動化

codex "WebSocketサーバーの統合テストを書いて。接続・メッセージ送受信・ルーム機能・認証エラーのテストケースを含めて"
// tests/socket.test.ts
import { createServer } from 'http';
import { Server } from 'socket.io';
import { io as Client, Socket as ClientSocket } from 'socket.io-client';
import jwt from 'jsonwebtoken';

const JWT_SECRET = 'test-secret';

const createToken = (userId: string, username: string) =>
  jwt.sign({ userId, username }, JWT_SECRET);

describe('WebSocket Server', () => {
  let httpServer: ReturnType<typeof createServer>;
  let ioServer: Server;
  let clientSocket: ClientSocket;
  let port: number;

  beforeAll((done) => {
    httpServer = createServer();
    ioServer = new Server(httpServer);
    httpServer.listen(() => {
      const addr = httpServer.address();
      port = typeof addr === 'object' ? addr!.port : 0;
      done();
    });
  });

  afterAll(() => {
    ioServer.close();
    httpServer.close();
  });

  afterEach(() => {
    clientSocket?.disconnect();
  });

  test('認証済みユーザーが接続できること', (done) => {
    const token = createToken('user1', 'テストユーザー');
    clientSocket = Client(`http://localhost:${port}`, {
      auth: { token },
    });

    clientSocket.on('connect', () => {
      expect(clientSocket.connected).toBe(true);
      done();
    });
  });

  test('不正なトークンで接続拒否されること', (done) => {
    clientSocket = Client(`http://localhost:${port}`, {
      auth: { token: 'invalid-token' },
    });

    clientSocket.on('connect_error', (err) => {
      expect(err.message).toBe('Invalid token');
      done();
    });
  });

  test('ルーム参加してメッセージを受信できること', (done) => {
    const token1 = createToken('user1', 'ユーザー1');
    const token2 = createToken('user2', 'ユーザー2');

    const client1 = Client(`http://localhost:${port}`, { auth: { token: token1 } });
    const client2 = Client(`http://localhost:${port}`, { auth: { token: token2 } });

    client1.on('connect', () => {
      client1.emit('joinRoom', 'room1', (res) => {
        expect(res.success).toBe(true);

        client2.on('connect', () => {
          client2.emit('joinRoom', 'room1', () => {
            client1.on('message', (msg) => {
              expect(msg.content).toBe('テストメッセージ');
              client1.disconnect();
              client2.disconnect();
              done();
            });

            client2.emit('sendMessage', {
              roomId: 'room1',
              content: 'テストメッセージ',
            }, () => {});
          });
        });
      });
    });
  });
});

本番デプロイのベストプラクティス

Nginx WebSocketプロキシ設定

codex "NginxのWebSocketプロキシ設定を作成して。SSL終端、ヘルスチェック、接続タイムアウト設定を含めて"
upstream websocket_backend {
    ip_hash;  # スティッキーセッション
    server ws-server-1:8080;
    server ws-server-2:8080;
    server ws-server-3:8080;
}

server {
    listen 443 ssl http2;
    server_name ws.ses-base.com;

    ssl_certificate /etc/ssl/certs/ses-base.com.pem;
    ssl_certificate_key /etc/ssl/private/ses-base.com.key;

    location /socket.io/ {
        proxy_pass http://websocket_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_read_timeout 86400s;
        proxy_send_timeout 86400s;
        proxy_connect_timeout 7s;

        proxy_buffering off;
        proxy_cache off;
    }

    location /health {
        proxy_pass http://websocket_backend;
    }
}

SES案件でのWebSocketスキル需要

リアルタイム通信のスキルは、以下のSES案件で特に需要が高まっています。

案件カテゴリ具体例要求スキル
チャット/コミュニケーション業務用メッセンジャー開発Socket.IO, WebSocket, Redis
ダッシュボード管理画面のリアルタイム更新SSE, WebSocket, Chart.js
IoTセンサーデータ可視化MQTT, WebSocket, Time Series DB
ゲームブラウザゲーム開発WebSocket, Canvas API, WebGL
金融リアルタイム株価表示WebSocket, データストリーミング

まとめ:Codex CLIでWebSocket開発を加速しよう

OpenAI Codex CLIは、WebSocketアプリケーション開発の全工程をサポートする強力なツールです。サンドボックス環境での安全なプロトタイピングから、Redis Pub/Subを使ったスケーリング設計、Nginx設定まで、自然言語で効率的にコード生成できます。

SESエンジニアとして、リアルタイム通信の設計・実装スキルは差別化ポイントになります。Codex CLIを活用して効率的に学習・実装を進めましょう。

Codex CLIの基本はOpenAI Codex CLI完全ガイドを、API開発はAPI開発ガイドをご覧ください。テスト自動化はテスト自動化ガイド、TypeScript連携はTypeScript開発ガイドが参考になります。

SES案件をお探しですか?

SES記事をもっと読む →
🏗️

SES BASE 編集長

SES業界歴10年以上のメンバーが在籍する編集チーム。SES企業での営業・エンジニア経験、フリーランス独立経験を持つメンバーが、業界のリアルな情報をお届けします。

📊 業界データに基づく記事制作 🔍 IPA・経済産業省データ参照 💼 SES実務経験者が執筆・監修