WebSocket/SSE

SvelteKitでWebSocketとServer-Sent Events (SSE)を使用したリアルタイム通信の実装方法について解説します。

リアルタイム通信は、チャットアプリケーション、通知システム、協働編集ツール、ライブダッシュボードなど、現代のWebアプリケーションに不可欠な機能です。このページでは、SvelteKitで利用可能な複数のリアルタイム通信技術と、それぞれの実装方法を詳しく解説します。

リアルタイム通信の選択肢

リアルタイム通信を実現する技術にはいくつかの選択肢があり、それぞれ異なる特性と適用場面があります。アプリケーションの要件に応じて最適な技術を選択することが重要です。

通信方式の比較

方式双方向プロトコルユースケース
WebSocketws/wssチャット、ゲーム、コラボレーション
SSE× (サーバー→クライアント)HTTP/HTTPS通知、ライブフィード、進捗表示
Long PollingHTTP/HTTPSレガシー環境対応

WebSocketは双方向通信が必要な場合に最適で、クライアントとサーバーの両方からリアルタイムでメッセージを送受信できます。SSEはサーバーからクライアントへの一方向通信に特化しており、シンプルな実装で済むため、通知やライブフィードに適しています。

技術選択のフローチャート

以下の図は、要件に応じて最適なリアルタイム通信技術を選択するためのガイドです。

ダイアグラムを読み込み中...

Server-Sent Events (SSE)

SSE(Server-Sent Events)は、HTTPプロトコルを使用してサーバーからクライアントへ継続的にデータをプッシュする技術です。標準のHTTP接続を使用するため、プロキシやファイアウォールとの互換性が高く、実装も比較的簡単です。

SSEの通信フロー

以下の図は、SSE接続の確立からデータ送信、切断までの流れを示しています。EventSource APIがサーバーとの接続を管理し、データを自動的に受信します。

ダイアグラムを読み込み中...

SSEの実装

以下の実装例では、サーバーが1秒ごとにランダムな値をクライアントに送信します。接続が確立されると、クライアントは継続的にデータを受信し、画面に表示します。

サーバー側 (+server.ts)

ReadableStreamを使用してSSEストリームを作成します。text/event-streamコンテンツタイプと適切なヘッダーを設定することで、ブラウザがSSE接続として認識します。クライアントが切断した場合、request.signalabortイベントでクリーンアップを行います。

// src/routes/api/events/+server.ts
import type { RequestHandler } from './$types';

export const GET: RequestHandler = async ({ request }) => {
  const stream = new ReadableStream({
    start(controller) {
      const encoder = new TextEncoder();
      
      // 初期メッセージ
      controller.enqueue(
        encoder.encode('data: {"type":"connected","timestamp":' + Date.now() + '}\n\n')
      );
      
      // 定期的にデータを送信
      const interval = setInterval(() => {
        const data = {
          type: 'update',
          timestamp: Date.now(),
          value: Math.random()
        };
        
        controller.enqueue(
          encoder.encode(`data: ${JSON.stringify(data)}\n\n`)
        );
      }, 1000);
      
      // クライアント切断時のクリーンアップ
      request.signal.addEventListener('abort', () => {
        clearInterval(interval);
        controller.close();
      });
    }
  });
  
  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive'
    }
  });
};
typescript

クライアント側 (+page.svelte)

クライアント側では、ブラウザ標準のEventSource APIを使用してSSE接続を確立します。Svelte 5の$effectを使用することで、コンポーネントのマウント時に接続を開始し、アンマウント時に自動的にクリーンアップできます。

受信したイベントは配列に追加され、最新10件のみを保持することでメモリ使用量を抑えています。

<script lang="ts">
  type EventData = {
    type: string;
    timestamp: number;
    value?: number;
  };

  let events = $state<EventData[]>([]);
  let eventSource: EventSource | null = null;

  // EventSourceの初期化とクリーンアップ
  $effect(() => {
    eventSource = new EventSource('/api/events');

    eventSource.onmessage = (event) => {
      const data: EventData = JSON.parse(event.data);
      events = [...events, data];

      // 最新10件のみ保持
      if (events.length > 10) {
        events = events.slice(-10);
      }
    };

    eventSource.onerror = (error) => {
      console.error('SSE error:', error);
      eventSource?.close();
    };

    return () => {
      eventSource?.close();
    };
  });
</script>

<div class="events">
  <h2>リアルタイムイベント</h2>
  {#each events as event}
    <div class="event">
      <span>{event.type}</span>
      <span>{new Date(event.timestamp).toLocaleTimeString()}</span>
      {#if event.value !== undefined}
        <span>{event.value.toFixed(3)}</span>
      {/if}
    </div>
  {/each}
</div>
svelte

WebSocket実装

WebSocketは、クライアントとサーバー間で双方向の永続的な接続を提供します。リアルタイムチャット、オンラインゲーム、協働編集など、双方向通信が必要なアプリケーションに最適です。

SvelteKitでWebSocketを実装する方法はいくつかありますが、開発環境ではViteプラグインを使用し、本番環境ではカスタムサーバーを使用するのが一般的です。

WebSocketの通信フロー

以下の図は、WebSocket接続の確立から双方向メッセージング、切断までの流れを示しています。クライアントからサーバーへ、サーバーからクライアントへ、双方向でメッセージを送受信できます。

ダイアグラムを読み込み中...

Viteプラグインを使用したWebSocket

開発環境では、Viteプラグインを使用してWebSocketサーバーを統合できます。この方法により、開発サーバーと同じプロセス内でWebSocketを実行でき、開発体験が向上します。

vite.config.tsの設定

以下の設定では、ポート5174でWebSocketサーバーを起動し、接続されたすべてのクライアントにメッセージをブロードキャストする基本的な実装を示しています。wsライブラリを使用して、標準のWebSocket仕様に準拠したサーバーを構築します。

import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import type { ViteDevServer } from 'vite';
import { WebSocketServer } from 'ws';

function webSocketPlugin() {
  let wss: WebSocketServer;
  
  return {
    name: 'websocket-plugin',
    configureServer(server: ViteDevServer) {
      wss = new WebSocketServer({ port: 5174 });
      
      wss.on('connection', (ws) => {
        console.log('Client connected');
        
        ws.on('message', (data) => {
          const message = JSON.parse(data.toString());
          
          // すべてのクライアントにブロードキャスト
          wss.clients.forEach((client) => {
            if (client.readyState === 1) {
              client.send(JSON.stringify({
                type: 'broadcast',
                ...message,
                timestamp: Date.now()
              }));
            }
          });
        });
        
        ws.on('close', () => {
          console.log('Client disconnected');
        });
      });
    }
  };
}

export default defineConfig({
  plugins: [sveltekit(), webSocketPlugin()]
});
typescript

クライアント側の実装

クライアント側では、ブラウザ標準のWebSocket APIを使用して接続を確立します。接続状態を$stateで管理し、UIに反映させることで、ユーザーに接続状況をフィードバックします。

この実装では、簡単なチャットアプリケーションを構築しています。メッセージの送信、受信、接続状態の表示など、WebSocketアプリケーションの基本的な要素をすべて含んでいます。

<script lang="ts">
  type Message = {
    id: string;
    user: string;
    text: string;
    timestamp: number;
  };

  let messages = $state<Message[]>([]);
  let inputText = $state('');
  let username = $state('User' + Math.floor(Math.random() * 1000));
  let ws: WebSocket | null = null;
  let connected = $state(false);

  // WebSocketの初期化とクリーンアップ
  $effect(() => {
    ws = new WebSocket('ws://localhost:5174');

    ws.onopen = () => {
      connected = true;
      console.log('WebSocket connected');
    };

    ws.onmessage = (event) => {
      const message: Message = JSON.parse(event.data);
      messages = [...messages, message];
    };

    ws.onerror = (error) => {
      console.error('WebSocket error:', error);
      connected = false;
    };

    ws.onclose = () => {
      connected = false;
      console.log('WebSocket disconnected');
    };

    return () => {
      ws?.close();
    };
  });
  
  function sendMessage() {
    if (!ws || !inputText.trim()) return;
    
    const message = {
      id: crypto.randomUUID(),
      user: username,
      text: inputText
    };
    
    ws.send(JSON.stringify(message));
    inputText = '';
  }
</script>

<div class="chat">
  <div class="status" class:connected>
    {connected ? '接続中' : '切断'}
  </div>
  
  <div class="messages">
    {#each messages as message}
      <div class="message">
        <strong>{message.user}:</strong>
        <span>{message.text}</span>
        <time>{new Date(message.timestamp).toLocaleTimeString()}</time>
      </div>
    {/each}
  </div>
  
  <form onsubmit={sendMessage}>
    <input
      bind:value={inputText}
      placeholder="メッセージを入力..."
      disabled={!connected}
    />
    <button type="submit" disabled={!connected}>
      送信
    </button>
  </form>
</div>

<style>
  .status {
    padding: 0.5rem;
    background: #f44336;
    color: white;
  }
  
  .status.connected {
    background: #4caf50;
  }
  
  .messages {
    height: 300px;
    overflow-y: auto;
    border: 1px solid #ddd;
    padding: 1rem;
  }
  
  .message {
    margin-bottom: 0.5rem;
  }
  
  time {
    font-size: 0.8em;
    color: #666;
    margin-left: 0.5rem;
  }
</style>
svelte

Socket.IOの統合

Socket.IOは、WebSocketをベースとした高レベルのライブラリで、自動再接続、ルーム機能、イベントベースの通信など、多くの便利な機能を提供します。プロダクションレベルのリアルタイムアプリケーションでは、Socket.IOを使用することで開発効率が大幅に向上します。

Socket.IOの通信フロー

以下の図は、Socket.IOを使用したルームベースのチャットシステムの動作を示しています。接続確立、ルーム参加、メッセージ配信の流れを確認できます。

ダイアグラムを読み込み中...

Socket.IO実装例

Socket.IOは、標準のWebSocketに比べて、より豊富な機能セットを提供します。ルーム管理、ネームスペース、自動再接続、フォールバックなど、複雑なリアルタイムアプリケーションに必要な機能が組み込まれています。

サーバー側設定

以下の実装では、ルームベースのチャットシステムを構築しています。ユーザーは複数のルームに参加でき、各ルーム内でのみメッセージが共有されます。この実装パターンは、チームチャット、ゲームロビー、コラボレーションツールなど、多くのアプリケーションに応用できます。

// vite.config.ts
import { createServer } from 'http';
import { Server } from 'socket.io';

function socketIOPlugin() {
  return {
    name: 'socket-io',
    configureServer(server: ViteDevServer) {
      const httpServer = createServer();
      const io = new Server(httpServer, {
        cors: {
          origin: 'http://localhost:5173',
          methods: ['GET', 'POST']
        }
      });
      
      io.on('connection', (socket) => {
        console.log('User connected:', socket.id);
        
        socket.on('join-room', (roomId) => {
          socket.join(roomId);
          socket.to(roomId).emit('user-joined', socket.id);
        });
        
        socket.on('leave-room', (roomId) => {
          socket.leave(roomId);
          socket.to(roomId).emit('user-left', socket.id);
        });
        
        socket.on('message', (data) => {
          io.to(data.room).emit('message', {
            ...data,
            timestamp: Date.now()
          });
        });
        
        socket.on('disconnect', () => {
          console.log('User disconnected:', socket.id);
        });
      });
      
      httpServer.listen(5174);
    }
  };
}
typescript

クライアント側の実装

Socket.IOクライアントは、socket.io-clientパッケージを使用して実装します。標準のWebSocket APIよりも高レベルのAPIを提供し、イベントベースの通信やルーム管理が容易になります。

以下の実装では、ユーザーが異なるルーム間を切り替えることができ、ルームに参加/退出する際にサーバーに通知します。

<script lang="ts">
  import io from 'socket.io-client';

  let socket: ReturnType<typeof io>;
  let currentRoom = $state('general');
  let rooms = $state(['general', 'tech', 'random']);

  // Socket.IOの初期化とクリーンアップ
  $effect(() => {
    socket = io('http://localhost:5174');

    socket.on('connect', () => {
      console.log('Connected to Socket.IO');
      socket.emit('join-room', currentRoom);
    });

    socket.on('message', (data) => {
      // メッセージ処理
    });

    socket.on('user-joined', (userId) => {
      console.log('User joined:', userId);
    });

    return () => {
      socket.disconnect();
    };
  });
  
  function switchRoom(room: string) {
    socket.emit('leave-room', currentRoom);
    currentRoom = room;
    socket.emit('join-room', room);
  }
</script>
svelte

プロダクション環境での考慮事項

リアルタイム通信をプロダクション環境で運用する際は、スケーラビリティ、セキュリティ、パフォーマンスなど、多くの要素を考慮する必要があります。

スケーラビリティ

複数のサーバーインスタンスでWebSocketアプリケーションを実行する場合、Redisなどのメッセージブローカーを使用して、インスタンス間でメッセージを共有する必要があります。Socket.IOはRedis Adapterを提供しており、これを使用することで水平スケーリングが可能になります。

以下の実装により、複数のサーバーインスタンスが協調して動作し、どのインスタンスに接続しているクライアントにも同じメッセージが配信されます。

// Redis Pub/Subを使用した水平スケーリング
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';

const pubClient = createClient({ url: 'redis://localhost:6379' });
const subClient = pubClient.duplicate();

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

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

認証とセキュリティ

WebSocket接続では、HTTP接続と同様に認証とセキュリティが重要です。Socket.IOでは、ミドルウェアを使用して接続時に認証を行うことができます。

以下の実装では、JWTトークンを使用してユーザーを認証しています。トークンが有効でない場合、接続は拒否されます。認証されたユーザー情報はsocket.dataに保存され、後続の処理で使用できます。

// JWTトークンによる認証
import jwt from 'jsonwebtoken';

io.use((socket, next) => {
  const token = socket.handshake.auth.token;
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET!);
    socket.data.user = decoded;
    next();
  } catch (err) {
    next(new Error('Authentication error'));
  }
});
typescript

レート制限

WebSocket接続でも、悪意のあるユーザーからの過剰なメッセージ送信を防ぐため、レート制限を実装する必要があります。以下の実装では、各クライアントが1分間に送信できるメッセージ数を制限しています。

この実装は、メモリベースのシンプルなレート制限ですが、本番環境ではRedisなどの永続ストレージを使用することを推奨します。

// レート制限の実装
const rateLimiter = new Map<string, number[]>();

function checkRateLimit(socketId: string): boolean {
  const now = Date.now();
  const timestamps = rateLimiter.get(socketId) || [];
  
  // 1分以内のリクエストをフィルタ
  const recentRequests = timestamps.filter(t => now - t < 60000);
  
  if (recentRequests.length >= 100) {
    return false; // レート制限に達した
  }
  
  recentRequests.push(now);
  rateLimiter.set(socketId, recentRequests);
  return true;
}
typescript

デプロイメント戦略

リアルタイム通信のデプロイメントは、選択するプラットフォームによって大きく異なります。WebSocketは永続的な接続を必要とするため、すべてのホスティングプラットフォームがサポートしているわけではありません。

Adapterの選択

各プラットフォームのWebSocket対応状況を理解し、要件に応じて適切なプラットフォームを選択することが重要です。

プラットフォームWebSocket対応推奨Adapter
Vercel△ (制限あり)@sveltejs/adapter-vercel
Netlify×SSEを使用
Node.js@sveltejs/adapter-node
Cloudflare○ (Durable Objects)@sveltejs/adapter-cloudflare

Vercelは制限付きでWebSocketをサポートしていますが、本格的なリアルタイムアプリケーションにはNode.jsCloudflare Workersが推奨されます。NetlifyではWebSocketがサポートされていないため、SSEを使用する必要があります。

Node.js Adapterでの実装

Node.js Adapterを使用する場合、カスタムサーバーを作成してSvelteKitとSocket.IOを統合できます。この方法により、完全な制御と柔軟性が得られます。

// server.js (カスタムサーバー)
import { handler } from './build/handler.js';
import express from 'express';
import { createServer } from 'http';
import { Server } from 'socket.io';

const app = express();
const server = createServer(app);
const io = new Server(server);

// Socket.IOのセットアップ
io.on('connection', (socket) => {
  // WebSocket処理
});

// SvelteKitハンドラー
app.use(handler);

server.listen(3000, () => {
  console.log('Server running on port 3000');
});
typescript

まとめ

SvelteKitでリアルタイム通信を実装する際の選択肢と、それぞれの適用場面を理解することが重要です。

  • SSE: シンプルで、サーバーからクライアントへの一方向通信に最適

    • 通知システム、ライブフィード、進捗表示
    • HTTP接続を使用するため、プロキシとの互換性が高い
    • 実装が簡単で、ブラウザ標準API(EventSource)で利用可能
  • WebSocket: 双方向通信が必要な場合に使用

    • チャットアプリケーション、オンラインゲーム、協働編集
    • 低レイテンシーで、リアルタイム性が高い
    • 標準のWebSocket APIまたはSocket.IOで実装
  • Socket.IO: WebSocketベースの高レベルライブラリ

    • ルーム管理、自動再接続、フォールバックなどの高度な機能
    • プロダクションレベルのアプリケーションに適している
    • Redis Adapterによる水平スケーリングが可能

プロダクション環境では、以下の点を考慮する必要があります。

  • スケーラビリティ: Redis Pub/Subによる複数サーバーインスタンスの連携
  • セキュリティ: JWT認証、レート制限、入力検証
  • デプロイメント: プラットフォームのWebSocket対応状況を確認
  • 監視: 接続数、メッセージレート、エラー率の監視

適切な技術を選択し、セキュリティとスケーラビリティを考慮した実装により、堅牢なリアルタイムアプリケーションを構築できます。

次のステップ

リアルタイム通信の実践的な使用例は以下をご覧ください。

さらに学ぶ

Last update at: 2025/10/01 05:30:17