数か月前、私は中堅のフロントエンドポジションの技術面接の真っ最中でした。物事は順調に進んでいたのですが、少し不意を突かれた質問を受けました。
「必要なものを取得するまで、毎秒何かをチェックするために、一定の通信形式が必要であると想像してください。
たとえば、電子商取引の設定のように、支払いが成功したかどうかを継続的に確認したいとします。これにどのようにアプローチしますか?"
私は慎重に「WebSocket を実装すればそれに対応できると思います。」と答えました。
面接官は微笑んだ。 「これは良い解決策ですが、状況に応じて、おそらくより良い他の選択肢もあります。」
そこで、ロングポーリング、ショートポーリング、WebSocketなど、リアルタイム通信へのさまざまなアプローチについての会話に突入しました。そして最後に、 Server-Sent Events (SSE)。これはおそらく、支払いの例のような単方向データ ストリームに最適な選択肢です。
また、サーバー リソースを消耗することなく、これらの定数的でありながら軽量なリクエストを処理するための適切なデータベースの選択についても説明しました。そのような文脈で、この種のリクエストの管理におけるシンプルさと効率性で知られる Redis が登場しました。
この会話は私にとって心に残りました。 WebSocket は大きな注目を集めていますが、理解すればリアルタイム通信の管理方法を最適化できるさまざまなテクニックがあることに気づきました。今日は、これら 4 つのアプローチ、それぞれをいつ使用するか、そしてそれぞれの長所と短所を明確かつ魅力的な方法で詳しく説明したいと思います。最後までに、サーバー送信イベント (SSE) が一方向のリアルタイム通信に適している理由をしっかりと理解できるようになります。
始める前に、このチャットを実施し、数か月後に私にこの記事を書くきっかけを与えてくれた経験豊かなシニア ソフトウェア エンジニアである Marcos に多大な感謝を申し上げます。仕事は得られなかったにもかかわらず、とても感謝しています。 :)
SSE の例に入る前に、インタビュー中に説明した 4 つの方法を詳しく見てみましょう。
ショートポーリングはおそらく最も簡単な方法です。これには、サーバーに定期的に「新しいデータはありますか?」というリクエストを送信することが含まれます。サーバーは、新しいものがあるかどうかに関係なく、現在の状態を返します。
利点:
欠点:
最適な用途: 1 分ごとに更新する株価など、小規模で低頻度のデータ更新。
ロングポーリングはショートポーリングをさらに一歩進めたものです。クライアントは繰り返しサーバーに情報を要求しますが、サーバーはすぐに応答するのではなく、新しいデータが利用可能になるまで接続を保持します。データが返送されると、クライアントはすぐに新しい接続を開き、プロセスを繰り返します。
上向き:
欠点:
最適な用途: リアルタイム通信が必要だが、WebSocket/SSE が過剰になる可能性がある状況 (チャット アプリケーションなど)。
WebSocket は、クライアントとサーバー間の全二重通信を提供する、より最新のソリューションです。接続が確立されると、双方とも接続を再確立することなく自由にデータを送信できます。これが双方向通信を定義します。
上向き:
欠点:
最適な用途: マルチプレイヤー ゲーム、共同ツール、チャット アプリケーション、リアルタイム通知など、継続的な双方向通信を必要とするアプリケーション。
最後に、支払い例の主人公である Server-Sent Events (SSE) について説明します。 SSE は、サーバーがクライアントに更新を送信する一方向の接続を作成します。 WebSocket とは異なり、これは一方向であり、クライアントはデータを送り返しません。
上向き:
欠点:
最適な用途: クライアントがライブスコア、通知、支払いステータスの例などのデータを受信するだけで済むリアルタイム更新。
本題に入りましょう。 Server-Sent Events (SSE) を使用してリアルタイムの支払いプロセスをシミュレートするシンプルな Next.js アプリを構築しました。これは、一方向の通信を設定して支払いのステータスを確認し、支払いが成功または失敗したときにユーザーに通知する方法を正確に示しています。
Next 用に設定するのは少し面倒です。プレーンな JS とは動作が少し異なるため、後ほど感謝します。
セットアップは次のとおりです:
次のコンポーネントには、実際のゲートウェイ API (Pix、Stripe、失敗したクレジット カード支払い) からのさまざまなタイプのトランザクションをシミュレートするためのボタンを表示するシンプルな UI があります。これらのボタンは、SSE を通じてリアルタイムの支払いステータスの更新をトリガーします。
ここで SSE の魔法が起こります。支払いがシミュレートされると、クライアントは SSE 接続を開き、サーバーからの更新をリッスンします。保留中、転送中、支払い済み、失敗などのさまざまなステータスを処理します。
"use client"; import { useState } from "react"; import { PAYMENT_STATUSES } from "../utils/payment-statuses"; const paymentButtons = [ { id: "pix", label: "Simulate payment with Pix", bg: "bg-green-200", success: true, }, { id: "stripe", label: "Simulate payment with Stripe", bg: "bg-blue-200", success: true, }, { id: "credit", label: "Simulate failing payment", bg: "bg-red-200", success: false, }, ]; type transaction = { type: string; amount: number; success: boolean; }; const DOMAIN_URL = process.env.NEXT_PUBLIC_DOMAIN_URL; export function TransactionControl() { const [status, setStatus] = useState<string>(""); const [isProcessing, setIsProcessing] = useState<boolean>(false); async function handleTransaction({ type, amount, success }: transaction) { setIsProcessing(true); setStatus("Payment is in progress..."); const eventSource = new EventSource( `${DOMAIN_URL}/payment?type=${type}&amount=${amount}&success=${success}` ); eventSource.onmessage = (e) => { const data = JSON.parse(e.data); const { status } = data; console.log(data); switch (status) { case PAYMENT_STATUSES.PENDING: setStatus("Payment is in progress..."); break; case PAYMENT_STATUSES.IN_TRANSIT: setStatus("Payment is in transit..."); break; case PAYMENT_STATUSES.PAID: setIsProcessing(false); setStatus("Payment completed!"); eventSource.close(); break; case PAYMENT_STATUSES.CANCELED: setIsProcessing(false); setStatus("Payment failed!"); eventSource.close(); break; default: setStatus(""); setIsProcessing(false); eventSource.close(); break; } }; } return ( <div> <div className="flex flex-col gap-3"> {paymentButtons.map(({ id, label, bg, success }) => ( <button key={id} className={`${bg} text-background rounded-full font-medium py-2 px-4 disabled:brightness-50 disabled:opacity-50`} onClick={() => handleTransaction({ type: id, amount: 101, success }) } disabled={isProcessing} > {label} </button> ))} </div> {status && <div className="mt-4 text-lg font-medium">{status}</div>} </div> ); }
サーバー側では、SSE を通じてステータス更新を定期的に送信することで、支払いプロセスをシミュレートします。トランザクションが進行するにつれて、クライアントは支払いがまだ保留中であるか、完了したか、失敗したかに関する最新情報を受け取ります。
import { NextRequest, NextResponse } from "next/server"; import { PAYMENT_STATUSES } from "../utils/payment-statuses"; export const runtime = "edge"; export const dynamic = "force-dynamic"; // eslint-disable-next-line @typescript-eslint/no-unused-vars export async function GET(req: NextRequest, res: NextResponse) { const { searchParams } = new URL(req.url as string); const type = searchParams.get("type") || null; const amount = parseFloat(searchParams.get("amount") || "0"); const success = searchParams.get("success") === "true"; if (!type || amount < 0) { return new Response(JSON.stringify({ error: "invalid transaction" }), { status: 400, headers: { "Content-Type": "application/json", }, }); } const responseStream = new TransformStream(); const writer = responseStream.writable.getWriter(); const encoder = new TextEncoder(); let closed = false; function sendStatus(status: string) { writer.write( encoder.encode(`data: ${JSON.stringify({ status, type, amount })}\n\n`) ); } // Payment gateway simulation async function processTransaction() { sendStatus(PAYMENT_STATUSES.PENDING); function simulateSuccess() { setTimeout(() => { if (!closed) { sendStatus(PAYMENT_STATUSES.IN_TRANSIT); } }, 3000); setTimeout(() => { if (!closed) { sendStatus(PAYMENT_STATUSES.PAID); // Close the stream and mark closed to prevent further writes writer.close(); closed = true; } }, 6000); } function simulateFailure() { setTimeout(() => { if (!closed) { sendStatus(PAYMENT_STATUSES.CANCELED); // Close the stream and mark closed to prevent further writes writer.close(); closed = true; } }, 3000); } if (success === false) { simulateFailure(); return; } simulateSuccess(); } await processTransaction(); // Return the SSE response return new Response(responseStream.readable, { headers: { "Access-Control-Allow-Origin": "*", Connection: "keep-alive", "X-Accel-Buffering": "no", "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-cache, no-transform", "Content-Encoding": "none", }, }); }
また、次の内容を含む .env.local ファイルを必ず追加してください:
NEXT_PUBLIC_DOMAIN_URL='http://localhost:3000'
実装方法を説明しましたが、なぜこれに WebSocket ではなく SSE を使用するのか疑問に思われるかもしれません。その理由は次のとおりです:
そのインタビューの質問は、ロング ポーリング、ショート ポーリング、WebSocket、SSE の微妙な違いに目を開かせ、素晴らしい学習体験になりました。各方法には時間と場所があり、どの方法をいつ使用するかを理解することは、リアルタイム通信を最適化するために重要です。
SSE は WebSocket ほど魅力的ではないかもしれませんが、電子商取引の支払いの例のように、効率的で一方向の通信という点では、この仕事に最適なツールです。次回、リアルタイム更新が必要なものを構築するときは、デフォルトで WebSocket を使用するだけでなく、そのシンプルさと効率性を考慮して SSE を検討してください。
リアルタイム コミュニケーション テクニックを深く掘り下げて、次のプロジェクトや面接での難しい質問に備えられることを願っています。
Next.js + Dépôt d'exemples TypeScript : https://github.com/brinobruno/sse-next
Exemple de déploiement Next.js + TypeScript : https://sse-next-one.vercel.app/
voici quelques sources et références faisant autorité que vous pourriez explorer pour des informations plus approfondies :
MDN Web Docs : l'API WebSockets
MDN Web Docs : Utilisation des événements envoyés par le serveur
Next.js : Routes API
Je partagerai mes réseaux sociaux pertinents au cas où vous souhaiteriez vous connecter :
Github
LinkedIn
Portefeuille
以上がリアルタイム Web 通信: ロング/ショート ポーリング、WebSocket、SSE の説明 + Next.js コードの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。