ホームページ  >  記事  >  ウェブフロントエンド  >  リアルタイム Web 通信: ロング/ショート ポーリング、WebSocket、SSE の説明 + Next.js コード

リアルタイム Web 通信: ロング/ショート ポーリング、WebSocket、SSE の説明 + Next.js コード

Linda Hamilton
Linda Hamiltonオリジナル
2024-09-23 22:30:32639ブラウズ

Real-Time Web Communication: Long/Short Polling, WebSockets, and SSE Explained + Next.js code

裏話: 面接での予期せぬ質問

数か月前、私は中堅のフロントエンドポジションの技術面接の真っ最中でした。物事は順調に進んでいたのですが、少し不意を突かれた質問を受けました。

「必要なものを取得するまで、毎秒何かをチェックするために、一定の通信形式が必要であると想像してください。

たとえば、電子商取引の設定のように、支払いが成功したかどうかを継続的に確認したいとします。これにどのようにアプローチしますか?"

私は慎重に「WebSocket を実装すればそれに対応できると思います。」と答えました。

面接官は微笑んだ。 「これは良い解決策ですが、状況に応じて、おそらくより良い他の選択肢もあります。」

そこで、ロングポーリングショートポーリングWebSocketなど、リアルタイム通信へのさまざまなアプローチについての会話に突入しました。そして最後に、 Server-Sent Events (SSE)。これはおそらく、支払いの例のような単方向データ ストリームに最適な選択肢です。

また、サーバー リソースを消耗することなく、これらの定数的でありながら軽量なリクエストを処理するための適切なデータベースの選択についても説明しました。そのような文脈で、この種のリクエストの管理におけるシンプルさと効率性で知られる Redis が登場しました。

この会話は私にとって心に残りました。 WebSocket は大きな注目を集めていますが、理解すればリアルタイム通信の管理方法を最適化できるさまざまなテクニックがあることに気づきました。今日は、これら 4 つのアプローチ、それぞれをいつ使用するか、そしてそれぞれの長所と短所を明確かつ魅力的な方法で詳しく説明したいと思います。最後までに、サーバー送信イベント (SSE) が一方向のリアルタイム通信に適している理由をしっかりと理解できるようになります。

始める前に、このチャットを実施し、数か月後に私にこの記事を書くきっかけを与えてくれた経験豊かなシニア ソフトウェア エンジニアである Marcos に多大な感謝を申し上げます。仕事は得られなかったにもかかわらず、とても感謝しています。 :)


リアルタイム通信の 4 つの方法

SSE の例に入る前に、インタビュー中に説明した 4 つの方法を詳しく見てみましょう。

1. ショートポーリング

ショートポーリングはおそらく最も簡単な方法です。これには、サーバーに定期的に「新しいデータはありますか?」というリクエストを送信することが含まれます。サーバーは、新しいものがあるかどうかに関係なく、現在の状態を返します。

利点:

  • 実装が簡単
  • 従来の HTTP リクエストで動作します

欠点:

  • リソースが大量に必要です。新しいデータが利用できない場合でも、頻繁にリクエストを行っています。
  • サーバーの負荷とネットワーク トラフィックが増加する可能性があり、支払いステータスの更新などの頻繁なチェックでは非効率になります。

最適な用途: 1 分ごとに更新する株価など、小規模で低頻度のデータ更新。

2. ロングポーリング

ロングポーリングはショートポーリングをさらに一歩進めたものです。クライアントは繰り返しサーバーに情報を要求しますが、サーバーはすぐに応答するのではなく、新しいデータが利用可能になるまで接続を保持します。データが返送されると、クライアントはすぐに新しい接続を開き、プロセスを繰り返します。

上向き:

  • サーバーは必要な場合にのみ応答するため、ショートポーリングより効率的です。非常に高速です。
  • ブラウザおよび HTTP/HTTPS プロトコルと互換性があります。

欠点:

  • 依然として接続を繰り返し再開する必要があるため、時間の経過とともに非効率になり、リソースが高価になります。
  • ショートポーリングよりも少し複雑です。

最適な用途: リアルタイム通信が必要だが、WebSocket/SSE が過剰になる可能性がある状況 (チャット アプリケーションなど)。

3. WebSocket

WebSocket は、クライアントとサーバー間の全二重通信を提供する、より最新のソリューションです。接続が確立されると、双方とも接続を再確立することなく自由にデータを送信できます。これが双方向通信を定義します。

上向き:

  • 遅延を最小限に抑えた真のリアルタイム通信。
  • 双方向通信 (リアルタイム ゲーム、チャット アプリなど) に最適です。

欠点:

  • ポーリングや SSE よりも実装が複雑です。
  • WebSocket は、オープンな接続を維持することでリソースを消費する可能性があるため、一方向通信や更新頻度の低い更新には必ずしも最適ではありません。
  • ファイアウォールの構成が必要な場合があります。

最適な用途: マルチプレイヤー ゲーム、共同ツール、チャット アプリケーション、リアルタイム通知など、継続的な双方向通信を必要とするアプリケーション。

4. サーバー送信イベント (SSE)

最後に、支払い例の主人公である Server-Sent Events (SSE) について説明します。 SSE は、サーバーがクライアントに更新を送信する一方向の接続を作成します。 WebSocket とは異なり、これは一方向であり、クライアントはデータを送り返しません。

上向き:

  • ニュース フィード、株価表示、支払いステータスの更新などの一方向のデータ ストリームに最適です。
  • WebSocket よりも軽量で実装が簡単です。
  • 既存の HTTP 接続を使用するため、サポートが充実しており、ファイアウォールにも適しています。

欠点:

  • 双方向通信には適していません。
  • 一部のブラウザ (特に古いバージョンの IE) は SSE を完全にはサポートしていません。

最適な用途: クライアントがライブスコア、通知、支払いステータスの例などのデータを受信するだけで済むリアルタイム更新。


SSE の動作: Next.js を使用したリアルタイムの支払いステータス

本題に入りましょう。 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>
  );
}

バックエンド: Next.js での SSE 実装

サーバー側では、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 のシンプルさが完璧に適合します。
  • 軽量: SSE は単一の HTTP 接続を使用して更新をストリーミングするため、全二重通信を維持する WebSocket と比較してリソース効率が高くなります。
  • ファイアウォールに優しい: SSE は、通常はファイアウォール内で開かれている HTTP 上で実行されるため、さまざまなネットワーク環境での操作が容易ですが、WebSocket 接続では問題が発生する場合があります。
  • ブラウザのサポート: SSE は WebSocket ほど広くサポートされていませんが、最新のブラウザでサポートされているため、一方向データが必要なほとんどのユースケースで信頼性が高くなります。

結論: ツールを知る

そのインタビューの質問は、ロング ポーリング、ショート ポーリング、WebSocket、SSE の微妙な違いに目を開かせ、素晴らしい学習体験になりました。各方法には時間と場所があり、どの方法をいつ使用するかを理解することは、リアルタイム通信を最適化するために重要です。

SSE は WebSocket ほど魅力的ではないかもしれませんが、電子商取引の支払いの例のように、効率的で一方向の通信という点では、この仕事に最適なツールです。次回、リアルタイム更新が必要なものを構築するときは、デフォルトで WebSocket を使用するだけでなく、そのシンプルさと効率性を考慮して SSE を検討してください。

リアルタイム コミュニケーション テクニックを深く掘り下げて、次のプロジェクトや面接での難しい質問に備えられることを願っています。


Mettons-nous les mains dans le cambouis

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/

Références

voici quelques sources et références faisant autorité que vous pourriez explorer pour des informations plus approfondies :

Documentation WebSockets et SSE :

MDN Web Docs : l'API WebSockets
MDN Web Docs : Utilisation des événements envoyés par le serveur

Itinéraires API suivants

Next.js : Routes API


Connectons-nous

Je partagerai mes réseaux sociaux pertinents au cas où vous souhaiteriez vous connecter :
Github
LinkedIn
Portefeuille

以上がリアルタイム Web 通信: ロング/ショート ポーリング、WebSocket、SSE の説明 + Next.js コードの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。