【初心者向け】プログラミングの「ポーリング」vs「プッシュ」
ポーリングとプッシュ通信の違いを初心者向けに解説。それぞれの仕組み、メリット・デメリット、使い分け方を具体例とコードで詳しく紹介します
みなさん、Webアプリケーションで「新着メッセージが来たらすぐに画面に表示したい」と思ったことはありませんか?
例えば、チャットアプリやリアルタイム通知など、サーバーからの情報をタイムリーに受け取りたい場面がありますよね。
そんな時に重要になるのが「ポーリング」と「プッシュ」という2つの通信方式です。 この記事では、プログラミング初心者の方でも理解できるように、それぞれの仕組みと使い分け方を詳しく解説します。
ポーリングとプッシュの基本概念
ポーリングとは?
ポーリングとは、クライアント側が定期的にサーバーに「新しい情報はありますか?」と問い合わせる方式です。
簡単に言うと、「こちらから聞きに行く」方法です。
日常生活での例
郵便受けの確認
- 毎日決まった時間に郵便受けを確認する
- 手紙が来ているかどうかを自分でチェック
- 手紙がなくても確認作業は発生する
これがポーリングのイメージです。
プッシュとは?
プッシュとは、サーバー側が新しい情報が発生した時に、クライアントに能動的に送信する方式です。
簡単に言うと、「向こうから教えてくれる」方法です。
日常生活での例
宅配便の配達
- 荷物が届いた時にインターホンが鳴る
- 配達員が積極的に知らせてくれる
- 荷物がない時は何も起こらない
これがプッシュのイメージです。
基本的な違い
## ポーリング vs プッシュ
| 項目 | ポーリング | プッシュ ||------|------------|----------|| 通信の開始者 | クライアント | サーバー || タイミング | 定期的 | 必要な時のみ || 無駄な通信 | 発生しやすい | 少ない || 実装の複雑さ | 簡単 | やや複雑 || リアルタイム性 | 遅延あり | 即座 |
ポーリングの詳細解説
基本的な仕組み
シンプルなポーリングの例
// 基本的なポーリングの実装function startPolling() { setInterval(async () => { try { // サーバーに新着メッセージを問い合わせ const response = await fetch('/api/messages/new'); const newMessages = await response.json(); if (newMessages.length > 0) { // 新着メッセージがあれば画面に表示 displayNewMessages(newMessages); } console.log('ポーリング完了:', new Date()); } catch (error) { console.error('ポーリングエラー:', error); } }, 5000); // 5秒ごとに確認}
function displayNewMessages(messages) { const messageList = document.getElementById('messageList'); messages.forEach(message => { const messageElement = document.createElement('div'); messageElement.textContent = message.content; messageList.appendChild(messageElement); });}
// ポーリング開始startPolling();
サーバー側の実装例
# Flask でのサーバー側実装from flask import Flask, jsonifyfrom datetime import datetime
app = Flask(__name__)
# 新着メッセージを保存するリストnew_messages = []last_check_times = {}
@app.route('/api/messages/new')def get_new_messages(): client_id = request.args.get('client_id') last_check = last_check_times.get(client_id, datetime.min) # 最後にチェックした時刻以降のメッセージを取得 recent_messages = [ msg for msg in new_messages if msg['timestamp'] > last_check ] # 最終チェック時刻を更新 last_check_times[client_id] = datetime.now() return jsonify(recent_messages)
@app.route('/api/messages', methods=['POST'])def add_message(): message = request.json message['timestamp'] = datetime.now() new_messages.append(message) return jsonify({'status': 'success'})
ポーリングの種類
1. 短いポーリング(Short Polling)
特徴
- サーバーは即座にレスポンスを返す
- 新しいデータがなくても空のレスポンス
- 実装が簡単
// 短いポーリングの例async function shortPolling() { const response = await fetch('/api/status'); const data = await response.json(); if (data.hasUpdate) { updateUI(data); } // すぐに次のポーリングをスケジュール setTimeout(shortPolling, 3000);}
2. 長いポーリング(Long Polling)
特徴
- サーバーは新しいデータが来るまで待機
- 効率的な通信
- 実装がやや複雑
// 長いポーリングの例async function longPolling() { try { // サーバーは新しいデータが来るまで待機 const response = await fetch('/api/wait-for-update', { timeout: 30000 // 30秒でタイムアウト }); const data = await response.json(); updateUI(data); } catch (error) { console.log('タイムアウトまたはエラー'); } // 即座に次の長いポーリングを開始 longPolling();}
サーバー側の長いポーリング
import timefrom flask import Flask, jsonify
@app.route('/api/wait-for-update')def wait_for_update(): timeout = 30 # 30秒でタイムアウト start_time = time.time() while time.time() - start_time < timeout: # 新しいデータがあるかチェック if has_new_data(): return jsonify(get_new_data()) # 1秒待ってから再チェック time.sleep(1) # タイムアウト時は空のレスポンス return jsonify({'timeout': True})
ポーリングのメリット・デメリット
メリット
シンプルな実装
- HTTP の標準的な仕組みを使用
- ファイアウォールやプロキシに優しい
- デバッグが容易
信頼性
- ネットワーク障害からの回復が簡単
- サーバーの再起動があっても自動的に再開
デメリット
効率性の問題
## 無駄な通信の例- 5秒ごとにポーリング- 新着メッセージは1時間に1回- 720回中719回が無駄な通信
遅延の発生
## 遅延の計算ポーリング間隔: 5秒平均遅延: 2.5秒(最大5秒)リアルタイム性: 低い
サーバー負荷
- 多数のクライアントが同時にポーリング
- サーバーリソースの消費
- データベースへの頻繁なアクセス
プッシュ通信の詳細解説
基本的な仕組み
WebSocketを使ったプッシュ通信
// WebSocket クライアント側の実装const socket = new WebSocket('ws://localhost:8080');
// 接続が開かれた時socket.onopen = function(event) { console.log('WebSocket接続が開かれました'); // サーバーに認証情報を送信 socket.send(JSON.stringify({ type: 'auth', userId: 'user123' }));};
// メッセージを受信した時socket.onmessage = function(event) { const data = JSON.parse(event.data); switch(data.type) { case 'newMessage': displayNewMessage(data.message); break; case 'notification': showNotification(data.content); break; case 'userOnline': updateUserStatus(data.userId, 'online'); break; }};
// 接続が閉じられた時socket.onclose = function(event) { console.log('WebSocket接続が閉じられました'); // 再接続の試行 setTimeout(connectWebSocket, 5000);};
// エラーが発生した時socket.onerror = function(error) { console.error('WebSocketエラー:', error);};
// メッセージの送信function sendMessage(message) { if (socket.readyState === WebSocket.OPEN) { socket.send(JSON.stringify({ type: 'message', content: message })); }}
サーバー側の実装(Node.js + WebSocket)
const WebSocket = require('ws');const http = require('http');
// HTTPサーバーの作成const server = http.createServer();
// WebSocketサーバーの作成const wss = new WebSocket.Server({ server });
// 接続中のクライアントを管理const clients = new Map();
wss.on('connection', function connection(ws) { console.log('新しいクライアントが接続しました'); ws.on('message', function incoming(data) { const message = JSON.parse(data); switch(message.type) { case 'auth': // クライアントを認証・登録 clients.set(message.userId, ws); console.log(`ユーザー ${message.userId} が認証されました`); break; case 'message': // 全クライアントにメッセージをブロードキャスト broadcastMessage({ type: 'newMessage', message: message.content, timestamp: new Date().toISOString() }); break; } }); ws.on('close', function close() { // クライアントの削除 for (let [userId, client] of clients) { if (client === ws) { clients.delete(userId); console.log(`ユーザー ${userId} が切断しました`); break; } } });});
// 全クライアントにメッセージを送信function broadcastMessage(message) { clients.forEach((client, userId) => { if (client.readyState === WebSocket.OPEN) { client.send(JSON.stringify(message)); } });}
// 特定のユーザーにメッセージを送信function sendToUser(userId, message) { const client = clients.get(userId); if (client && client.readyState === WebSocket.OPEN) { client.send(JSON.stringify(message)); }}
server.listen(8080, () => { console.log('WebSocketサーバーがポート8080で起動しました');});
プッシュ通信の種類
1. WebSocket
特徴
- 双方向の通信が可能
- 低遅延
- 持続的な接続
使用例
// リアルタイムチャットsocket.onmessage = function(event) { const message = JSON.parse(event.data); addMessageToChat(message);};
// ライブコメントsocket.onmessage = function(event) { const comment = JSON.parse(event.data); displayLiveComment(comment);};
// リアルタイム共同編集socket.onmessage = function(event) { const edit = JSON.parse(event.data); applyEditToDocument(edit);};
2. Server-Sent Events (SSE)
特徴
- サーバーからクライアントへの一方向通信
- HTTP/HTTPSを使用
- 自動再接続機能
// SSE クライアント側const eventSource = new EventSource('/api/stream');
// メッセージ受信eventSource.onmessage = function(event) { const data = JSON.parse(event.data); updateUI(data);};
// 特定のイベント受信eventSource.addEventListener('notification', function(event) { const notification = JSON.parse(event.data); showNotification(notification);});
// エラーハンドリングeventSource.onerror = function(error) { console.error('SSEエラー:', error);};
SSE サーバー側(Express.js)
const express = require('express');const app = express();
app.get('/api/stream', (req, res) => { // SSE ヘッダーの設定 res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'Access-Control-Allow-Origin': '*' }); // 定期的にデータを送信 const interval = setInterval(() => { const data = { timestamp: new Date().toISOString(), message: 'Hello from server' }; res.write(`data: ${JSON.stringify(data)}
`); }, 5000); // 接続が閉じられた時の処理 req.on('close', () => { clearInterval(interval); console.log('SSE接続が閉じられました'); });});
3. Push API (ブラウザ通知)
特徴
- ブラウザが閉じていても通知可能
- サービスワーカーを使用
- モバイル対応
// サービスワーカー登録if ('serviceWorker' in navigator && 'PushManager' in window) { navigator.serviceWorker.register('/sw.js') .then(registration => { console.log('Service Worker登録成功'); return registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(publicVapidKey) }); }) .then(subscription => { // サブスクリプション情報をサーバーに送信 return fetch('/api/subscribe', { method: 'POST', body: JSON.stringify(subscription), headers: { 'Content-Type': 'application/json' } }); });}
// サービスワーカー (sw.js)self.addEventListener('push', function(event) { const data = event.data.json(); const options = { body: data.message, icon: '/icon-192x192.png', badge: '/badge-72x72.png', data: data.url }; event.waitUntil( self.registration.showNotification(data.title, options) );});
プッシュ通信のメリット・デメリット
メリット
効率性
- 必要な時のみ通信
- サーバーリソースの節約
- ネットワーク帯域の効率利用
リアルタイム性
- 即座にデータを受信
- 遅延が最小限
- ユーザー体験の向上
スケーラビリティ
- 多数のクライアントに効率的に配信
- ブロードキャスト機能
デメリット
実装の複雑さ
- 接続管理が必要
- エラーハンドリングが複雑
- 再接続ロジックの実装
ネットワーク制約
- ファイアウォールで制限される場合
- プロキシサーバーの影響
- モバイルネットワークでの制限
使い分けのガイドライン
ポーリングが適している場面
1. データ更新頻度が低い場合
例: ダッシュボードの統計データ
// 1時間ごとの売上データ更新setInterval(async () => { const salesData = await fetch('/api/sales/hourly'); const data = await salesData.json(); updateSalesChart(data);}, 60000); // 1分ごとにチェック(十分な頻度)
2. シンプルな実装が必要な場合
例: 簡単な通知システム
// 新着通知の確認async function checkNotifications() { const response = await fetch('/api/notifications'); const notifications = await response.json(); notifications.forEach(notification => { if (!notification.read) { showNotification(notification); } });}
// 30秒ごとにチェックsetInterval(checkNotifications, 30000);
3. 一時的な監視が必要な場合
例: ファイルアップロードの進捗確認
// アップロード進捗の監視async function monitorUpload(uploadId) { const checkProgress = async () => { const response = await fetch(`/api/upload/${uploadId}/progress`); const progress = await response.json(); updateProgressBar(progress.percentage); if (progress.completed) { onUploadComplete(); } else { setTimeout(checkProgress, 1000); } }; checkProgress();}
プッシュが適している場面
1. リアルタイム性が重要な場合
例: チャットアプリケーション
// 即座にメッセージを受信socket.onmessage = function(event) { const message = JSON.parse(event.data); if (message.type === 'chat') { addMessageToChat({ user: message.sender, content: message.content, timestamp: new Date(message.timestamp) }); // 音で通知 playNotificationSound(); }};
2. 高頻度でのデータ更新
例: 株価のリアルタイム表示
// 株価の瞬時更新socket.onmessage = function(event) { const priceUpdate = JSON.parse(event.data); if (priceUpdate.type === 'priceChange') { updateStockPrice(priceUpdate.symbol, priceUpdate.price); highlightPriceChange(priceUpdate.symbol, priceUpdate.change); }};
3. 多数のクライアントへの同報
例: ライブイベントの配信
// ライブイベントの情報配信socket.onmessage = function(event) { const eventData = JSON.parse(event.data); switch(eventData.type) { case 'liveComment': addLiveComment(eventData.comment); break; case 'viewerCount': updateViewerCount(eventData.count); break; case 'eventEnd': showEventEndMessage(); break; }};
ハイブリッド手法
ポーリング + プッシュの組み合わせ
class HybridCommunication { constructor() { this.websocket = null; this.pollingInterval = null; this.isWebSocketConnected = false; } start() { // WebSocket接続を試行 this.connectWebSocket(); // フォールバックとしてポーリングを開始 this.startPolling(); } connectWebSocket() { this.websocket = new WebSocket('ws://localhost:8080'); this.websocket.onopen = () => { console.log('WebSocket接続成功'); this.isWebSocketConnected = true; // WebSocket接続成功時はポーリングを停止 this.stopPolling(); }; this.websocket.onmessage = (event) => { const data = JSON.parse(event.data); this.handleMessage(data); }; this.websocket.onclose = () => { console.log('WebSocket接続断'); this.isWebSocketConnected = false; // 接続断時はポーリングにフォールバック this.startPolling(); // 再接続を試行 setTimeout(() => this.connectWebSocket(), 5000); }; } startPolling() { if (this.pollingInterval) return; console.log('ポーリング開始'); this.pollingInterval = setInterval(async () => { if (!this.isWebSocketConnected) { const response = await fetch('/api/messages'); const data = await response.json(); this.handleMessage(data); } }, 5000); } stopPolling() { if (this.pollingInterval) { console.log('ポーリング停止'); clearInterval(this.pollingInterval); this.pollingInterval = null; } } handleMessage(data) { // メッセージの処理 console.log('受信:', data); updateUI(data); }}
// 使用例const communication = new HybridCommunication();communication.start();
実装時の注意点
ポーリング実装のベストプラクティス
1. 適切な間隔の設定
class AdaptivePolling { constructor() { this.baseInterval = 5000; // 基本間隔: 5秒 this.maxInterval = 60000; // 最大間隔: 60秒 this.currentInterval = this.baseInterval; this.consecutiveEmptyResponses = 0; } async poll() { try { const response = await fetch('/api/updates'); const data = await response.json(); if (data.hasUpdates) { // データありの場合は間隔をリセット this.currentInterval = this.baseInterval; this.consecutiveEmptyResponses = 0; this.handleUpdates(data); } else { // データなしの場合は間隔を徐々に伸ばす this.consecutiveEmptyResponses++; this.currentInterval = Math.min( this.baseInterval * Math.pow(1.5, this.consecutiveEmptyResponses), this.maxInterval ); } } catch (error) { console.error('ポーリングエラー:', error); // エラー時は間隔を長くする this.currentInterval = Math.min(this.currentInterval * 2, this.maxInterval); } // 次のポーリングをスケジュール setTimeout(() => this.poll(), this.currentInterval); } start() { this.poll(); }}
2. エラーハンドリング
class RobustPolling { constructor() { this.retryCount = 0; this.maxRetries = 3; this.isActive = true; } async poll() { if (!this.isActive) return; try { const response = await fetch('/api/data', { timeout: 10000 // 10秒でタイムアウト }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); // 成功時はリトライカウントをリセット this.retryCount = 0; this.handleSuccess(data); } catch (error) { this.handleError(error); } // 次のポーリングをスケジュール this.scheduleNext(); } handleError(error) { console.error('ポーリングエラー:', error); this.retryCount++; if (this.retryCount >= this.maxRetries) { console.error('最大リトライ回数に達しました'); this.isActive = false; this.onMaxRetriesReached(); } } scheduleNext() { if (this.isActive) { const delay = this.retryCount > 0 ? 5000 * Math.pow(2, this.retryCount) // 指数バックオフ : 1000; // 通常間隔 setTimeout(() => this.poll(), delay); } }}
プッシュ通信実装のベストプラクティス
1. 再接続の実装
class ReconnectingWebSocket { constructor(url) { this.url = url; this.reconnectDelay = 1000; this.maxReconnectDelay = 30000; this.reconnectDecay = 1.5; this.timeoutInterval = 2000; this.maxReconnectAttempts = null; this.reconnectAttempts = 0; } connect() { this.ws = new WebSocket(this.url); this.ws.onopen = (event) => { console.log('WebSocket接続成功'); this.reconnectAttempts = 0; this.reconnectDelay = 1000; this.onopen(event); }; this.ws.onmessage = (event) => { this.onmessage(event); }; this.ws.onclose = (event) => { console.log('WebSocket接続閉じる'); this.onclose(event); this.scheduleReconnect(); }; this.ws.onerror = (event) => { console.error('WebSocketエラー:', event); this.onerror(event); }; } scheduleReconnect() { if (this.maxReconnectAttempts && this.reconnectAttempts >= this.maxReconnectAttempts) { console.error('最大再接続試行回数に達しました'); return; } this.reconnectAttempts++; setTimeout(() => { console.log(`再接続試行 ${this.reconnectAttempts}`); this.connect(); }, this.reconnectDelay); // 遅延時間を増加 this.reconnectDelay = Math.min( this.reconnectDelay * this.reconnectDecay, this.maxReconnectDelay ); } send(data) { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send(data); } else { console.warn('WebSocket接続が利用できません'); } } // イベントハンドラ(オーバーライド可能) onopen(event) {} onmessage(event) {} onclose(event) {} onerror(event) {}}
2. メッセージキューイング
class QueuedWebSocket { constructor(url) { this.url = url; this.messageQueue = []; this.isConnected = false; this.connect(); } connect() { this.ws = new WebSocket(this.url); this.ws.onopen = () => { console.log('接続成功'); this.isConnected = true; // キューに蓄積されたメッセージを送信 this.flushQueue(); }; this.ws.onclose = () => { console.log('接続断'); this.isConnected = false; }; this.ws.onmessage = (event) => { this.handleMessage(event.data); }; } send(message) { if (this.isConnected && this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(message)); } else { // 接続していない場合はキューに追加 this.messageQueue.push(message); console.log('メッセージをキューに追加:', message); } } flushQueue() { console.log(`${this.messageQueue.length}件のメッセージを送信`); while (this.messageQueue.length > 0) { const message = this.messageQueue.shift(); this.ws.send(JSON.stringify(message)); } } handleMessage(data) { const message = JSON.parse(data); console.log('受信:', message); }}
まとめ
ポーリングとプッシュは、それぞれ異なる特徴と適用場面を持つ通信方式です。
重要なポイント
- ポーリング: シンプルで信頼性が高いが、効率性に課題
- プッシュ: 効率的でリアルタイムだが、実装が複雑
- 使い分け: 要件に応じた適切な選択が重要
- ハイブリッド: 両方の利点を活かした組み合わせも有効
初心者の方は、まずポーリングから始めて、必要に応じてプッシュ通信を導入することをおすすめします。
どちらの方式も理解して適切に使い分けることで、ユーザーにとって快適なWebアプリケーションを作ることができるでしょう。 ぜひ実際のプロジェクトで試してみてください。