ApacheにWebSocketサーバーをリバースプロキシでつないでみる

2024年11月11日 Posted 野々瀨(フロントエンドエンジニア)

今回は直接WebSocketサーバーと直接通信するのではなく、Apacheのリバースプロキシを使用して、Apache経由でWebSocketと通信する方法をご紹介しようと思います。

環境

今回は次の環境で説明を行います。

WebSocketのサーバー準備

WebSocketのサーバーはPHPやPython、Node.jsなどで構築することができます。
ここでは次のようにNode.jsのwsモジュールを使用して構築します。

const WebSocket = require('ws');

const server = new WebSocket.Server({
  host: 'localhost',
  port: 3000
});

server.on('connection', socket => {
  socket.on('message', message => {
    console.log('受信: ', message.toString());

    server.clients.forEach(client => {
      client.send('受け取ったよ');
    });
  });

  socket.on('close', id => {
    console.log('切断した: ', id);
  });

  socket.on('error', err => {
    console.log('エラー: ', err);
  });
});

次のコマンドでWebSocketサーバーを起動しておきます。

node server.js

Apacheの設定

a2enmodコマンドを使用してリバースプロキシのモジュールを有効にします。

sudo a2enmod proxy proxy_wstunnel
モジュール名指定値説明
proxy mod_proxy Apacheでリバースプロキシを利用するためのモジュール。
proxy_wstunnel mod_proxy_wstunnel リバースプロキシでWebSocketへの通信を可能にするためのモジュール。

なお、httpd.confファイルの場合は、次の二つの項目からコメントアウトを外します。

LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_wstunnel_module modules/mod_proxy_wstunnel.so

続いてホストに割り当てるリバースプロキシを設定します。
ここではバーチャルホストで設定する例をご紹介します。

「/etc/apache2/sites-available/000-default.conf」ファイルを編集します。
次の内容を記述します。

<VirtualHost *:80>
  ServerName sample.localhost
  DocumentRoot /var/www/html/sample

  ProxyRequests Off
  ProxyPreserveHost On

  ProxyPass /api/foo/ ws://localhost:3000/
  ProxyPassReverse /api/foo/ ws://localhost:3000/
</virtualhost>

SSLの場合も同様です。

<VirtualHost *:443>
  # ...
  # 省略
  # ...

  SSLProxyEngine On

  ProxyRequests Off
  ProxyPreserveHost On

  ProxyPass /api/foo/ wss://localhost:3000/
  ProxyPassReverse /api/foo/ wss://localhost:3000/
</VirtualHost>
ディレクティブ説明
ProxyRequests フォワードプロキシを有効にするかどうか。
ProxyPreserveHost 転送先に転送元のヘッダー情報を保持するかどうか。
ProxyPass どこからどこへ転送するか。
例えば「wss://sample.localhost/api/foo/でアクセスしてきたときに、「wss://localhost:3000/」へ転送する。
ProxyPassReverse HTTPヘッダーのLocationを確認し、指定した値に書き換える。ProxyPassと同じで問題ない。
SSLProxyEngine プロキシのSSL/TLSを有効にするかどうか。

次のコマンドでApacheの設定を再読み込みします。

sudo systemctl reload apache2

クライアントの準備

今回はブラウザーで確認するため、HTMLページを用意します。
次のように、入力フォームとサーバーから受け取った情報を視覚的に表示するためのHTMLを用意します。

<form>
  <input name="keyword" autocomplete="off">
  <button>送信</button>
</form>
<div id="logs"></div>

次にやり取りを行うためのJavaScriptを用意します。

(() => {
  /**
   * WebSocketサーバーのパス
   */
  const API_PATH = 'ws://sample.localhost/api/foo/';

  // 接続
  const socket = new WebSocket(API_PATH);

  // サーバーの接続に成功したとき
  socket.addEventListener('open', () => {
    console.log('サーバー接続開始');
  });

  // サーバーの接続に失敗したとき
  socket.addEventListener('error', () => {
    console.log('サーバー接続失敗');
  });

  // サーバーが切断されたとき
  socket.addEventListener('close', () => {
    console.log('サーバー切断');
  });

  window.addEventListener('DOMContentLoaded', () => {
    const formElem = document.forms[0];
    const logsElem = document.getElementById('logs');

    // 送信
    formElem.addEventListener('submit', event => {
      event.preventDefault();

      const sendValue = formElem.keyword.value;

      if (sendValue) {
        formElem.keyword.value = '';
        socket.send(sendValue);
      }
    });

    // サーバーからメッセージを受け取る
    socket.addEventListener('message', event => {
      const logElem        = document.createElement('p');
      const firstChildElem = logsElem.firstChild;

      logElem.textContent = decodeURIComponent(event.data);

      if (firstChildElem) {
        logsElem.insertBefore(logElem, firstChildElem);
      } else {
        logsElem.appendChild(logElem);
      }
    });
  });

  window.addEventListener('unload', () => {
    socket.close();
  });
})();

ブラウザーで確認

用意したページをブラウザーで開きます。
入力欄に適当に入力し、「送信」ボタンを押します。

サーバー側には次の画像のように表示されます。

ブラウザー側は次の画像のように表示されます。

終わりに

今回はWebSocketサーバーをApacheのリバースプロキシを経由してやり取りをする方法をご紹介しました。
Apacheのリバースプロキシは、別のプログラムをApacheを経由して処理することができますので、対応していればさまざまなプログラムをWebサーバから動かすことができるようになります。