Node.jsでのWebSocket活用術:リアルタイムアプリケーションの作成方法

このチュートリアルでは、ブラウザとサーバーの間で双方向、インタラクティブな通信を実現するためにNode.jsでWebSocketsを使用する方法を紹介します。ダッシュボード、チャットアプリ、マルチプレイヤーゲームなど、高速でリアルタイムのアプリケーションに不可欠な技術です。

Table of Contents

Webはリクエスト-レスポンスのHTTPメッセージに基づいています。あなたのブラウザがURLリクエストを作成し、サーバーはデータで応答します。それはさらにブラウザのリクエストやサーバーのレスポンスを画像、CSS、JavaScriptなどのために引き起こすことがありますが、サーバーはブラウザに任意のデータを送信することはできません。

ロングポーリングAjax技術は、ウェブアプリケーションがリアルタイムで更新されているかのように見えるが、プロセスは本当のリアルタイムアプリケーションには制限が大きすぎる。1秒ごとに投票すると、特定の時間には非効率的であり、他の時間には遅すぎる。

ブラウザからの初期接続に続いて、サーバー送信イベントは、サーバーからいつでもメッセージを送信できる標準(ストリーミング)HTTPレスポンスです。ただし、チャネルは一方向であり、ブラウザはメッセージを送信できません。本当の速い双方向通信を行うには、WebSocketsが必要です。

WebSocketsの概要

用語WebSocketは、ws://または安全で暗号化されたwss://上のTCP通信プロトコルを指します。HTTPとは異なりますが、非ウェブトラフィックをブロックする場所で動作するように、ポート80または443で実行できます。2012年以来リリースされたほとんどのブラウザはWebSocketプロトコルをサポートしています。

典型的なリアルタイムウェブアプリケーションでは、ウェブコンテンツ(HTML、CSS、JavaScript、画像など)を提供するための少なくとも1つのウェブサーバーと、双方向通信を処理するWebSocketサーバーが必要です。

ブラウザはまだサーバーに対して初期のWebSocketリクエストを行い、通信チャネルを開きます。その後、ブラウザまたはサーバーのどちらかがそのチャネル上にメッセージを送信でき、それによって他のデバイスでイベントが発生します。

他の接続されたブラウザとの通信

初期リクエスト後、ブラウザはWebSocketサーバーとの間でメッセージの送受信ができます。WebSocketサーバーは、接続されているクライアントブラウザのいずれかとの間でメッセージの送受信ができます。

ピアツーピア通信はできません。BrowserAは、同じデバイス上で実行されており、同じWebSocketサーバーに接続されていても、BrowserBに直接メッセージを送ることはできません。BrowserAはサーバーにメッセージを送信し、必要に応じて他のブラウザに転送されることを期待することしかできません。

WebSocketサーバーサポート

Node.jsはまだネイティブなWebSocketサポートを持っていませんが、間もなく導入されるという噂もあります。この記事では、サードパーティのwsモジュールを使用していますが、他にも数十種類あります。

組み込みのWebSocketサポートは、DenoおよびBunのJavaScriptランタイムで利用可能です。

WebSocketライブラリはPHP, Python, Rubyなどのランタイム用に利用可能です。また、PusherPubNubのような第三者SaaSオプションも、ホステッドWebSocketサービスを提供しています。

WebSocketsのデモンストレーション クイックスタート

チャットアプリはWebSocketデモンストレーションのHello, World!であるため、以下の点についてお詫び申し上げます:

  1. 独創性に欠けること。ただし、チャットアプリは概念を説明するのに非常に良い方法です。

  2. 完全なホステッドオンラインソリューションを提供できないこと。私は匿名のメッセージの流れを監視およびモデレートする必要がない方が良いと考えています!

GitHubからnode-wschatリポジトリをクローンまたはダウンロード:

git clone https://github.com/craigbuckler/node-wschat

Node.jsの依存関係をインストール:

cd node-wschat
npm install

チャットアプリケーションを開始:

npm start

複数のブラウザーやタブでhttp://localhost:3000/を開きます(クエリ文字列でチャット名を定義することもできます—例えばhttp://localhost:3000/?Craig)。どれか一つのウィンドウで何かを入力し、SENDを押すか、Enterキーを押すと、それが接続されたすべてのブラウザに表示されます。

Node.jsコードの概要

Node.jsアプリケーションのindex.jsエントリーファイルは、2つのサーバーを起動します:

  1. EJSテンプレートを使用してクライアントサイドのHTML、CSS、JavaScriptを含む単一のページを提供するhttp://localhost:3000/で実行されるExpressアプリ。ブラウザのJavaScriptはWebSocket APIを使用して初期接続を行い、メッセージの送受信を行います。

  2. A WebSocket server running at ws://localhost:3001/, which listens for incoming client connections, handles messages, and monitors disconnections. The full code:

    WebSocketサーバー
    import WebSocket, { WebSocketServer } from 'ws';
    
    const ws = new WebSocketServer({ port: cfg.wsPort });
    
    クライアント接続
    ws.on('connection', (socket, req) => {
    
      console.log(`connection from ${ req.socket.remoteAddress }`);
    
      受信メッセージ
      socket.on('message', (msg, binary) => {
    
        全クライアントへのブロードキャスト
        ws.clients.forEach(client => {
          client.readyState === WebSocket.OPEN && client.send(msg, { binary });
        });
    
      });
    
      閉じられた
      socket.on('close', () => {
        console.log(`disconnection from ${ req.socket.remoteAddress }`);
      });
    
    });

The Node.js wsライブラリ:

  • ブラウザが接続を希望するときに"connection"イベントを発生させます。ハンドラ関数は、個々のデバイスと通信するために使用されるsocketオブジェクトを受け取ります。接続の寿命全体を通じて保持されなければなりません。

  • ブラウザがメッセージを送信するときにsocket "message"イベントを発生させます。ハンドラ関数は、送信元のブラウザを含むすべての接続されたブラウザにメッセージをブロードキャストします。

  • ブラウザが切断するときにsocket "close"イベントを発生させます—通常、タブが閉じられたり更新されたりしたときです。

クライアントサイドのJavaScriptコードの概要

アプリケーションのstatic/main.jsファイルはwsInit()関数を実行し、WebSocketサーバーのアドレス(ページのドメインにHTMLページテンプレートで定義されたポート値を加えたもの)を渡します:

wsInit(`ws://${ location.hostname }:${ window.cfg.wsPort }`);

// WebSocket通信の処理
function wsInit(wsServer) {

  const ws = new WebSocket(wsServer);

  // サーバーへの接続
  ws.addEventListener('open', () => {
    sendMessage('entered the chat room');
  });

openイベントは、ブラウザがWebSocketサーバーに接続するとトリガーされます。ハンドラ関数はsendMessage()を呼び出してチャットルームに入りましたというメッセージを送信します:

// メッセージの送信
function sendMessage(setMsg) {

  let
    name = dom.name.value.trim(),
    msg =  setMsg || dom.message.value.trim();

  name && msg && ws.send( JSON.stringify({ name, msg }) );

}

sendMessage()関数は、HTMLフォームからユーザーの名前とメッセージを取得しますが、setMsg引数によってメッセージを上書きできます。値はJSONオブジェクトに変換され、ws.send()メソッドを使用してWebSocketサーバーに送信されます。

WebSocketサーバーは受信したメッセージを受け取り、"message"ハンドラ(上記参照)をトリガーし、すべてのブラウザにそれをブロードキャストします。これにより、各クライアントで"message"イベントがトリガーされます:

// メッセージの受信
ws.addEventListener('message', e => {

  try {

    const
      chat = JSON.parse(e.data),
      name = document.createElement('div'),
      msg  = document.createElement('div');

    name.className = 'name';
    name.textContent = (chat.name || 'unknown');
    dom.chat.appendChild(name);

    msg.className = 'msg';
    msg.textContent = (chat.msg || 'said nothing');
    dom.chat.appendChild(msg).scrollIntoView({ behavior: 'smooth' });

  }
  catch(err) {
    console.log('invalid JSON', err);
  }

});

ハンドラは、イベントオブジェクトの.dataプロパティで送信されたJSONデータを受け取ります。関数はそれをJavaScriptオブジェクトにパースし、チャットウィンドウを更新します。

最後に、フォームの"submit"ハンドラがトリガーされるたびに、sendMessage()関数を使用して新しいメッセージが送信されます。

// フォーム送信
dom.form.addEventListener('submit', e => {
  e.preventDefault();
  sendMessage();
  dom.message.value = '';
  dom.message.focus();
}, false);

エラー処理

WebSocket通信が失敗すると、"error"イベントが発生します。これはサーバー上で処理できます:

// サーバー上でのWebSocketエラー処理
socket.on('error', e => {
  console.log('WebSocket error:', e);
});

そして/またはクライアント上で:

// クライアント上でのWebSocketエラー処理
ws.addEventListener('error', e => {
  console.log('WebSocket error:', e);
})

クライアントのみが、new WebSocket()コンストラクタを再実行することで接続を再確立できます。

接続の切断

どちらのデバイスも、接続の.close()メソッドを使用していつでもWebSocketを閉じることができます。オプションでcode整数とreason文字列(最大123バイト)の引数を提供でき、これらは切断前に他のデバイスに送信されます。

高度なWebSocket

Node.jsでのWebSocketの管理は簡単です:1つのデバイスは.send()メソッドを使用してメッセージを送信し、これにより他方で"message"イベントが発生します。各デバイスがそれらのメッセージをどのように作成し、応答するかはより挑戦的です。以下のセクションでは、検討する必要がある問題について説明します。

WebSocketのセキュリティ

WebSocketプロトコルは認可や認証を扱いません。着信通信要求があなたのウェブアプリケーションにログインしているブラウザやユーザーから来ていると保証できません――特に、ウェブサーバーとWebSocketサーバーが異なるデバイス上にある場合です。初期接続はクッキーを含むHTTPヘッダーとサーバーOriginを受け取りますが、これらの値を偽装する可能性があります。

次のテクニックを使用することで、WebSocket通信を認証されたユーザーに限定できます:

  1. WebSocketの初期リクエストを行う前に、ブラウザはHTTPウェブサーバーに連絡します(Ajaxを使用する場合もあります)。

  2. サーバーはユーザーの資格情報をチェックし、新しい認証チケットを返します。チケットは通常、ユーザーのID、IPアドレス、リクエスト時間、セッションの有効期限、その他必要なデータを含むデータベースレコードを参照します。

  3. ブラウザは初期のWebSocketハンドシェイクでチケットをWebSocketサーバーに渡します。

  4. WebSocketサーバーはチケットを検証し、IPアドレス、有効期限などの要因をチェックしてから接続を許可します。チケットが無効な場合、WebSocketの.close()メソッドを実行します。

  5. WebSocketサーバーは、ユーザーセッションが有効な状態を保つために、時々データベースレコードを再確認する必要がある場合があります。

重要なのは、受け入れられたデータを常に検証することです。

  • HTTPと同様に、WebSocketサーバーはSQLインジェクションやその他の攻撃に対して脆弱です。

  • クライアントは、DOMに生の値を注入したり、JavaScriptコードを評価したりしてはいけません。

シングルWebSocketサーバーとマルチWebSocketサーバーインスタンス

オンラインマルチプレイヤーゲームを考えてみましょう。このゲームには、ユニバースが複数あり、それぞれがゲームの別のインスタンスをプレイしています:universeAuniverseB、そしてuniverseC。プレイヤーは1つのユニバースに接続します。

  • universeAplayer1player2player3が参加
  • universeBplayer99が参加

以下のように実装できます。

  1. A separate WebSocket server for each universe.

    A player action in universeA would never be seen by those in universeB. However, launching and managing separate server instances could be difficult. Would you stop universeC because it has no players, or continue to manage that resource?

  2. すべてのゲームユニバースに対して単一のWebSocketサーバーを使用する。

    これはリソースを節約し、管理が容易ですが、WebSocketサーバーは各プレイヤーがどのユニバースに参加するかを記録しなければなりません。player1が行動を起こすと、player2player3にはブロードキャストする必要がありますが、player99にはブロードキャストしません。

複数のWebSocketサーバー

この例のチャットアプリケーションは数百人の同時ユーザーに対応できますが、人気やメモリ使用量が臨界点を超えるとクラッシュします。最終的にさらにサーバーを追加して水平方向にスケーリングする必要があります

各WebSocketサーバーは自分の接続されたクライアントのみを管理できます。ブラウザからserverXに送信されたメッセージは、serverYに接続されたものには広播できません。バックエンドのパブリッシャー‐サブスクライバー(pub-sub)メッセージングシステムを実装する必要が生じることもあります。例えば:

  1. WebSocket serverXはすべてのクライアントにメッセージを送信したい。それはpub-subシステムでメッセージを公開します。

  2. pub-subシステムにサブスクライブしているすべてのWebSocketサーバー(serverXを含む)が新しいメッセージイベントを受信します。それぞれがメッセージを処理し、接続されたクライアントに広播します。

WebSocketメッセージングの効率

WebSocket通信は高速ですが、サーバーはすべての接続されたクライアントを管理しなければなりません。特にマルチプレイヤーアクションゲームを構築する際には、メッセージのメカニクスと効率性を考慮する必要があります:

  • プレイヤーのアクションをすべてのクライアントデバイスに同期させるにはどうすればよいですか?

  • もしplayer1player2と異なる場所にいる場合、player2に見えないアクションに関する情報を送信する必要がありますか?

  • ネットワークラグにどのように対処しますか―または通信の遅延ですか?高速のマシンと接続を持つ人は不公平な利点を持つでしょうか?

高速ゲームは妥協を伴います。ゲームをローカルデバイスでプレイしているかのように考えてくださいが、一部のオブジェクトは影響を受けています他の人の活動によって。常にすべてのオブジェクトの正確な位置を送信する代わりに、ゲームは通常、よりシンプルで頻繁でないメッセージを送信します。例えば:

  • objectXはpointXに現れました
  • objectYは新しい方向と速度を持っています
  • objectZは破壊されました

各クライアントゲームが空隙を埋める。objectZが爆発すると、爆発が各デバイスで異なって見えるかどうかは問題ではありません。

結論

Node.jsはWebSocketsを扱うのを簡単にします。リアルタイムアプリケーションの設計やコーディングを必ずしも容易にするわけではありませんが、技術があなたを妨げることはありません!

主な欠点:

  • WebSocketsは独自のサーバーインスタンスを必要とします。Ajax Fetch()リクエストとサーバから送信されるイベントは、既に実行しているウェブサーバーで処理できます。

  • WebSocketサーバーは独自のセキュリティと認証チェックを必要とします。

  • 切断されたWebSocket接続は手動で再確立する必要があります。

しかし、それによってあなたをおびえさせないでください!

WebSocketsとサーバから送信されるイベントを使用したリアルタイムアプリに関するよくある質問(FAQ)

WebSocketsはHTTPと比較してパフォーマンスと機能の面でどのように異なりますか?

WebSocketsは、単一のTCP接続を介して全二重通信チャネルを提供し、データの送受信を同時に行えることを意味します。これは、各リクエストが新しい接続を必要とするHTTPよりも大きな改善です。WebSocketsはまた、リアルタイムデータ転送を可能にするため、チャットアプリやライブスポーツの更新など、即時更新が必要なアプリケーションに最適です。一方、HTTPはステートレスであり、各リクエスト-レスポンスペアは独立しており、リアルタイムの更新が不要なアプリケーションにはより適しています。

WebSocket接続のライフサイクルを説明できますか?

WebSocket接続のライフサイクルは、HTTP接続をWebSocket接続にアップグレードするハンドシェイクから始まります。接続が確立されると、クライアントとサーバーの間でデータを送受信できます。どちらかの当事者が接続を閉じることを決定するまでです。接続は、クライアントまたはサーバーがクローズフレームを送信し、次に相手当事者がクローズフレームを確認することによって閉じることができます。

AndroidアプリケーションでWebSocketsを実装する方法を教えてください。

AndroidアプリケーションでWebSocketsを実装するには、WebSocketサーバーに接続できるWebSocketクライアントを作成する必要があります。OkHttpやScarletなどのライブラリを使用してこれを行うことができます。クライアントが設定されると、サーバーに接続を開き、メッセージを送受信し、接続の開始、メッセージの受信、接続の終了などの異なるイベントを処理できます。

サーバーアップデートイベントとは何ですか、そしてそれらはWebSocketsとどのように比較されますか?

Server-Sent Events(SSE)は、サーバーがクライアントにHTTP経由で更新をプッシュできるようにする標準です。WebSocketsとは異なり、SSEは一方向性であり、サーバーからクライアントへのデータ送信のみを可能にします。これにより、双方向通信が必要なアプリケーションには適していませんが、サーバーからの更新のみが必要なアプリケーションにはよりシンプルで効率的なソリューションになる可能性があります。

WebSocketsとServer-Sent Eventsの一般的な使用例は何ですか?

WebSocketsは、チャットアプリ、マルチプレイヤーゲーム、共同作業ツールなど、リアルタイムの双方向通信が必要なアプリケーションで一般的に使用されます。一方、Server-Sent Eventsは、サーバーからのリアルタイムの更新が必要なアプリケーション、例えばライブニュース更新、株価更新、または長時間実行されるタスクの進行状況レポートなどでよく使用されます。

Spring BootアプリケーションでWebSocket接続を処理するにはどうすればよいですか?

Spring Bootは、Spring WebSocketモジュールを通じてWebSocket通信をサポートしています。@EnableWebSocketアノテーションを使用してWebSocketサポートを有効にし、WebSocketHandlerを定義して接続のライフサイクルとメッセージ処理を管理できます。また、SimpMessagingTemplateを使用して接続されたクライアントにメッセージを送信することもできます。

WebSocketsを使用する際のセキュリティ上の考慮事項は何ですか?

他のウェブ技術と同様に、WebSocketsはCross-Site WebSocket Hijacking (CSWSH)やDenial of Service (DoS)攻撃など、様々なセキュリティリスクに対して脆弱です。これらのリスクを軽減するためには、常にセキュアなWebSockets接続(wss://)を使用し、入力データの検証とサニタイズを行う必要があります。また、WebSocketサーバへのアクセスを制御するために認証と認可メカニズムを使用することを検討すべきです。

REST APIとWebSocketsを併用できますか?

はい、REST APIと併用してWebSocketsを使用できます。REST APIは状態非依存のリクエスト-レスポンス通信には優れていますが、WebSocketsはリアルタイムの双方向通信に使用できます。これは、チャットアプリやライブスポーツアップデートなど、即時更新が必要なアプリケーションに特に有用です。

WebSocketサーバをどのようにテストできますか?

WebSocket.orgのEcho TestやPostmanなど、WebSocketサーバをテストするための複数のツールが利用可能です。これらのツールを使用すると、サーバーへのWebSocket接続を開き、メッセージを送信し、応答を受信できます。また、JestやMochaなどのライブラリを使用して、WebSocketサーバの自動テストを書くこともできます。

WebSocketsとServer-Sent Eventsの制限は何ですか?

WebSocketsとServer-Sent Eventsはリアルタイム通信のための強力な機能を提供しますが、それらにも制限があります。例えば、すべてのブラウザやネットワークがこれらの技術をサポートしておらず、適切に管理されていない場合、かなりのリソースを消費する可能性があります。さらに、従来のHTTP通信と比較して、実装と管理がより複雑になることがあります。

Source:
https://www.sitepoint.com/real-time-apps-websockets-server-sent-events/