Apache2.4の後ろでsocket.ioを動かした話

あらすじ

node.jsになんとなく手を出してみて、WebSocketなるものの存在を知り、socket.ioなるパッケージで手軽に扱えることがわかったので、Apacheのバージョンを2.4に上げて(これは省略する。いずれ書くかもしれないが)設定をいろいろ試してなんとかした話。

サーバ・サイド(node.js側)

素人なのでnpmでサクッとexpress-generatorを入れてテストプログラムを生成した。

var io = require('socket.io').listen(server);
io.sockets.on('connection', function(socket) {
    console.log("connection");
    socket.on('message', function(data) {
        console.log('message');
        io.sockets.emit('message', {value: data.value});
    });
    socket.on('disconnect', function() {
        console.log('disconnect');
    });
});

クライアント・サイド

<script src="socket.io/socket.io.js"></script>
<script type="text/javascript">
    var socket = io.connect('http://192.168.0.43', {resource: 'nodejs/socket.io'});
    socket.on('connect', function(msg) {
        console.log("connect");
        document.getElementById("connectId").innerHTML = "ID: " + socket.socket.transport.sessid;
        document.getElementById("type").innerHTML = "type: " + socket.socket.transport.name;
    });
    socket.on('message', function(msg) {
        document.getElementById("receivedMessage").innerHTML = msg.value;
    });
    function sendMessage() {
        var msg = document.getElementById("message").value;
        socket.emit('message', {value: msg});
    }
    function disconnect() {
        var msg = socket.socket.transport.sessid + " has disconnected.";
        socket.emit('message', {value:msg});
        socket.disconnect();
    }
</script>

上のjavascriptはbin/wwwに、下はviews/index.ejsに書いた(それぞれ抜粋)。参考にしたのはこちら: WebSocket 事始め by Node.js + Socket.IO - Qiita:

HTML側の第3行、io.connect()の第2引数については後述する。

サーバ・サイド(Apache側)

いつまでも3000番で待っても仕方がない。しかし80番にはApacheが既にいて仕事をしている。そこでApacheから3000番へと飛ばしてくれないかというのがリバース・プロキシという仕組みのようだが筆者は詳しくない。以下はhttpd.confの抜粋である。

ProxyRequests Off
ProxyPassMatch ^/nodejs/socket\.io/(.*)/websocket/(.*)$ ws://localhost:3000/socket.io/$1/websocket/$2
ProxyPass /nodejs http://localhost:3000 timeout=600 keepalive=On
ProxyPassReverse /nodejs ws://localhost:3000
ProxyPassReverse /nodejs http://localhost:3000

1行目はフォワード・プロキシの無効化設定で、あまり関係がない。3行目と5行目がHTTPリクエストのリバース・プロキシ設定で、ここでは/nodejsに来たリクエストを3000番ポートへ寄越してくれないかと要請している。より詳しくは、3行目が/nodejs→3000番で、5行目が3000番→/nodejs(すなわちサーバからのリダイレクションの書き替え)を表している。

2行目と4行目はWebSocketリクエストのリバース・プロキシを要請している。といっても4行目は5行目と同じことをしているだけなので単純である(4行目と5行目は1対多ではなく多対1の対応を表していることに注意)。問題は2行目であるが、ここも正規表現によって見た目が複雑なだけで、第2引数を見ればさほど複雑ではない。しかし、ここを上手くやらないとWebSocket通信には失敗する。

これはsocket.ioがWebSocket通信を開始する手順による。まず上記HTMLの1行目にてnodejs/socket.io/socket.io.jsがリクエストされ、これはリバース・プロキシ3行目によって3000番ポートのsocket.io/socket.io.jsへと送られる。次に、返ってきたjavascriptによって定義された変数ioに対するconnect()呼び出しによって、nodejs/socket.io/1/へとHTTPのリクエストが送られる。この応答によって、最後にnodejs/socket.io/1/websocket/connection_id へのWebSocketリクエストが発生する。

従って、リバース・プロキシのルールは以下の条件をみたす必要がある:

  • nodejs/socket.io/1/websocket/ 以下をws://localhost:3000/socket.io/1/websocket/ 以下へ送る
  • nodejs/socket.io/1/ をhttp://localhost:3000/socket.io/1/ へ送る
  • nodejs/ をhttp://localhost:3000/ へ送る

実際には、条件の第2と第3とは統合して書くことができ(3・5行目)、第2の特殊ケースとして第1(2・4行目)を書けば上記の設定となる。

こうなりましてん

あとはテストプログラムのディレクトリへ移動してnpm startと打てばサーバが動作する。
スクリーンショット
あたかもチャットかのように見せているが実際は最後に送信されたメッセージしか表示されない。しかしサンプルとしては十分である。

補足

io.connect()の第2引数について

これはクライアントサイドのsocket.io.jsがどこを向けて通信を行うかを指定している。デフォルトでは"socket.io"となっているのだが、この場合上述のリバース・プロキシ3条件の第1と第2がそれぞれ

  • socket.io/1/websocket/ 以下をws://localhost:3000/socket.io/1/websocket/ 以下へ送る
  • socket.io/1/ をhttp://localhost:3000/socket.io/1/ へ送る

に変更される。こちらにしなかったのは、設定が多少煩雑になることと、3000番への転送をnodejs/以下に限定したかったことによる。

モジュールについて

いうまでもなく、リバース・プロキシ機能を利用するためにはhttpd.confに上記以外にも設定を行う必要がある。

LoadModule proxy_module lib64/httpd/modules/mod_proxy.so
LoadModule proxy_http_module lib64/httpd/modules/mod_proxy_http.so
LoadModule proxy_wstunnel_module lib64/httpd/modules/mod_proxy_wstunnel.so

特に3行目はWebSocketの転送を行うのに必要である。