wssサービスの作成

質問:

Workermanはどのようにしてwssサービスを作成し、クライアントがwssプロトコルを通じて通信できるようにするのか、例えばWeChatミニプログラムでサーバーに接続する方法は?

回答:

wssプロトコルは実際にはwebsocket+SSLで、websocketプロトコルにSSLレイヤーを追加したもので、httpsの構造に似ています(http+SSL)。したがって、websocketプロトコルの基本の上でSSLを有効にすれば、wssプロトコルをサポートできます。

方法1、nginx/apacheを利用してSSLをプロキシする(推奨)

推奨理由

  • 443ポートを再利用でき、クライアントが接続するときにポートを指定する必要がない
  • SSL証明書を集中管理でき、ウェブサイトの設定を再利用できる
  • 負荷分散が可能
  • ログ監視を内蔵
  • より良い互換性

通信原理およびフロー

  1. クライアントがwss接続をnginx/apacheに開始する

  2. nginx/apacheはwssプロトコルのデータをwsプロトコルのデータに変換し、Workermanのwebsocketプロトコルポートに転送する

  3. Workermanはデータを受信し、ビジネスロジックを処理する

  4. Workermanがクライアントにメッセージを送信する場合、逆のプロセスが行われ、データはnginx/apacheを経由してwssプロトコルに変換され、クライアントに送信される

nginxの設定参考

前提条件および準備作業:

  1. nginxがインストールされており、バージョンは1.3以上であること

  2. Workermanが8282ポート(websocketプロトコル)をリッスンしていると仮定

  3. 証明書が申請済み(pem/crtファイルおよびkeyファイル)で、/etc/nginx/conf.d/sslに配置したと仮定

  4. nginxを利用して443ポートを利用してwssプロキシサービスを提供する予定(ポートは必要に応じて変更可)

  5. nginxは一般的にウェブサイトサーバーとして他のサービスを運用しているため、元のサイトの使用に影響を与えないように、ここではアドレス ドメイン名.com/wss をwssのプロキシエントリとして使用します。つまり、クライアント接続アドレスは wss://ドメイン名.com/wss となります。

nginxの設定は以下のようにします

server {
  listen 443;
  # ドメイン名の設定は省略...

  ssl on;
  ssl_certificate /etc/ssl/server.pem;
  ssl_certificate_key /etc/ssl/server.key;
  ssl_session_timeout 5m;
  ssl_session_cache shared:SSL:50m;
  ssl_protocols SSLv3 SSLv2 TLSv1 TLSv1.1 TLSv1.2;
  ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP;

  location /wss
  {
    proxy_pass http://127.0.0.1:8282;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";
    proxy_set_header X-Real-IP $remote_addr;
  }

  # location / {} サイトの他の設定...
}

テスト

// 証明書はドメイン名をチェックしますので、ドメイン名を使用して接続してください。ポートは書かないでください。
ws = new WebSocket("wss://ドメイン名.com/wss");

ws.onopen = function() {
    alert("接続成功");
    ws.send('tom');
    alert("サーバーに文字列を送信しました:tom");
};
ws.onmessage = function(e) {
    alert("サーバーからのメッセージを受信しました:" + e.data);
};

apacheを利用してwssをプロキシする

apacheを利用してwssをWorkermanに転送することもできます。

準備作業:

  1. GatewayWorkerが8282ポート(websocketプロトコル)をリッスンしている

  2. SSL証明書を申請し、/server/httpd/cert/ に配置したと仮定

  3. apacheが443ポートを指定ポート8282に転送する

  4. httpd-ssl.confが読み込まれている

  5. opensslがインストールされている

proxy_wstunnel_moduleモジュールを有効にする

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

SSLとプロキシの設定

#extra/httpd-ssl.conf
DocumentRoot "/ウェブサイト/ディレクトリ"
ServerName ドメイン名

# プロキシ設定
SSLProxyEngine on

ProxyRequests Off
ProxyPass /wss ws://127.0.0.1:8282/wss
ProxyPassReverse /wss ws://127.0.0.1:8282/wss

# SSLプロトコルのサポートを追加し、安全でないプロトコルを削除
SSLProtocol all -SSLv2 -SSLv3
# 暗号スイートを以下のように変更
SSLCipherSuite HIGH:!RC4:!MD5:!aNULL:!eNULL:!NULL:!DH:!EDH:!EXP:+MEDIUM
SSLHonorCipherOrder on
# 証明書公開鍵の設定
SSLCertificateFile /server/httpd/cert/your.pem
# 証明書秘密鍵の設定
SSLCertificateKeyFile /server/httpd/cert/your.key
# 証明書チェーンの設定
SSLCertificateChainFile /server/httpd/cert/chain.pem

テスト

// 証明書はドメイン名をチェックしますので、ドメイン名を使用して接続してください。ポートはありません。
ws = new WebSocket("wss://ドメイン名.com/wss");

ws.onopen = function() {
    alert("接続成功");
    ws.send('tom');
    alert("サーバーに文字列を送信しました:tom");
};
ws.onmessage = function(e) {
    alert("サーバーからのメッセージを受信しました:" + e.data);
};

方法2、Workermanを直接使ってSSLを有効にする(推奨しません)

注意
nginx/apacheによるSSLプロキシとWorkermanによるSSL設定のいずれかを選択してください。両方を同時に有効にはできません。

準備作業:

  1. Workermanバージョン>=3.3.7

  2. PHPにopenssl拡張がインストールされている

  3. 証明書(pem/crtファイルおよびkeyファイル)をディスクの任意のディレクトリに配置する

コード:

<?php
use Workerman\Worker;
use Workerman\Connection\TcpConnection;
require_once __DIR__ . '/vendor/autoload.php';

// 証明書は申請した証明書を使用するのが望ましい
$context = array(
    // 詳しいSSLオプションについてはマニュアルを参照してください http://php.net/manual/zh/context.ssl.php
    'ssl' => array(
        // 絶対パスを使用してください
        'local_cert'        => 'ディスクパス/server.pem', // crtファイルでも可
        'local_pk'          => 'ディスクパス/server.key',
        'verify_peer'       => false,
        'allow_self_signed' => true, // 自己サイン証明書の場合はこのオプションを有効にしてください
    )
);
// ここではwebsocketプロトコルを設定します(ポートは任意ですが、他のプログラムに占有されていない必要があります)
$worker = new Worker('websocket://0.0.0.0:8282', $context);
// transportを設定してsslを有効にし、websocket+sslすなわちwssとなる
$worker->transport = 'ssl';
$worker->onMessage = function(TcpConnection $con, $msg) {
    $con->send('ok');
};

Worker::runAll();

これらのコードによって、Workermanはwssプロトコルをリッスンし、クライアントはwssプロトコルを通じてWorkermanに接続して安全なリアルタイムコミュニケーションを実現できます。

テスト

Chromeブラウザを開き、F12を押してデバッグコンソールを開き、Console欄に以下を入力するか、下記のコードをhtmlページに挿入してjsを実行してください。

// 証明書はドメイン名をチェックしますので、ドメイン名を使用して接続し、こちらにはポート番号があります
ws = new WebSocket("wss://ドメイン名.com:8282");
ws.onopen = function() {
    alert("接続成功");
    ws.send('tom');
    alert("サーバーに文字列を送信しました:tom");
};
ws.onmessage = function(e) {
    alert("サーバーからのメッセージを受信しました:" + e.data);
};

一般的なエラー
SSL routines:SSL23_GET_CLIENT_HELLO:http request
原因は、クライアントがws://ドメイン名.comを使ってアクセスしたためです。正しいアクセスアドレスはwss://ドメイン名.comで、wssで始める必要があります。この問題が発生する大多数のシナリオは、元々wsのポートがwssに突然変更され、クライアントページが更新されずに未だにws方式でアクセスするためにエラーが発生するケースです。このエラーは無視可能で、正常なwss接続に影響しません。

注意:

  1. 443ポートを使用する必要がある場合は、上記の最初の方法nginx/apacheプロキシ方式でwssを実現してください。

  2. wssポートはwssプロトコルでのみアクセス可能で、wsではアクセスできません。

  3. 証明書は通常、ドメイン名にバインドされているため、テスト時にはクライアントがドメイン名を使用して接続してください。IPアドレスで接続しないでください。

  4. アクセスできない場合は、サーバーファイアウォールを確認してください。

  5. この方法はPHPバージョン>=5.6を必要とします。なぜならWeChatミニプログラムはtls1.2を要求しており、PHP5.6未満のバージョンはtls1.2をサポートしていないからです。

関連記事:
プロキシ越しにクライアントの実際のIPを取得
WorkermanのSSLコンテキストオプション参考