nginxを挟んでsocket.ioのxhr-pollingを動かすには

8月3日現在 Node stable v0.4.10, Socket.IO v0.7.7

というところでハマっていた。


nginxを使ってreverse proxyする場合、Socket.IOのWebSocket接続は

  • 表側に繋がずに裏側のportを指定して繋ぐ
  • nginx_tcp_proxy_moduleを使って通す
  • あきらめる

のどれかだと思う。
しかしSocket.IOは様々な接続方法をサポートしているので、WebSocketをあきらめてもxhr-pollingや他の方法で繋げられるはず。
ということで試してみたのだけど上手くいかなかった。

失敗事例

nginxを以下のように設定し

http {
    include       mime.types;
    default_type  application/octet-stream;

    upstream node {
        server 127.0.0.1:3000;
    }

    server {
        listen       80;
        server_name  _;

        location / {
            proxy_pass       http://node;
        }
    }
}

nodeを3000番ポートで動かす。

var server = require('http').createServer(function (req, res) {
    res.writeHeader(200, {
        'Content-Type': 'text/html'
    });
    res.end(
        '<!DOCTYPE html><html>' +
        '<head><script type="text/javascript" src="/socket.io/socket.io.js"></script>' +
        '<script type="text/javascript">' +
        'var s = io.connect(null, { transports: ["xhr-polling"] });' +
        's.on("connect", function(){ console.log("ok"); });' +
        's.on("connect_failed", function(){ console.log("ng"); });' +
        '</script>' +
        '</head><body></body></html>');
});
server.listen(3000);

require('socket.io').listen(server);

ここではwebsocketをあきらめているので最初からxhr-polling指定で。
[]http://127.0.0.1:80/[]をブラウザで叩いてみる。

$ node app.js
   info  - socket.io started
   debug - served static /socket.io.js
   debug - client authorized
   info  - handshake authorized 18151724210868660
   debug - setting request GET /socket.io/1/xhr-polling/18151724210868660?t1312366753908
   debug - setting poll timeout
   debug - client authorized for
   debug - clearing poll timeout
   debug - xhr-polling writing 1::
   debug - set close timeout for client 18151724210868660
   debug - setting request GET /socket.io/1/jsonp-polling/18151724210868660/?t=1312366763909&i=0
   debug - setting poll timeout
   debug - discarding transport
   debug - cleared close timeout for client 18151724210868660

xhr-pollingで繋いでみて失敗し、jsonp-pollingを試みるが結局ダメで接続が失敗する。
ちなみに繋ぐ先のポートを裏側に明示指定すると一発で繋がる。

var s = io.connect(null, { transports: ["xhr-polling"], port: 3000 });

↑これならok


で、試しにSocket.IO v0.6.18で似たようなことをしてみるとこれは上手くいく。

原因

何が起こっているのだろう? と調べ始め、試しに最初に繋いでいる
[]http://127.0.0.1:80/socket.io/1/xhr-polling/18151724210868660?t1312366753908[]というのを叩いてみるとこれがレスポンス返ってこない。
じゃあコイツは何を返そうとしているのか、と裏側を直接叩いてみる。

$ curl -i 'http://127.0.0.1:3000/socket.io/1/xhr-polling/18151724210868660?t1312366753908'
HTTP/1.1 200 OK
Content-Type: text/plain; charset=UTF-8
Content-Length: 7
Connection: Keep-Alive

7:::1+0

どうもこの"Keep-Alive"が原因らしく。@先生に教えていただいたところ、nginxのHttpProxyModuleは裏側のkeep-aliveには対応していない。
Module ngx_http_proxy_module
上記のようなレスポンスが来た場合はバッファにレスポンスを溜め込み接続が切れない状態になってしまうため、表側にはレスポンスが返ってこない、ということらしい。

解決策

とりあえず表のnginxでproxy_bufferingをoffに設定しておく、という手がある。

    server {
        listen       80;
        server_name  _;

        location / {
            proxy_buffering  off;
            proxy_pass       http://node;
        }
    }

こうしておくと、バッファに溜めずにすぐにレスポンスを返してくれるようになるので、結果的にSocket.IOのxhr-pollingも接続できるようになる。


もしくはSocket.IO側でレスポンスヘッダからkeep-aliveの項目を削除するようにコード書き変えるか何かする、という方法も…?nginx側でproxy_bufferingをoffにしなくても、socket.io/lib/transports/xhr-polling.jsのdoWriteメソッドでheadersを指定している部分があるので、

  var origin = this.req.headers.origin
    , headers = {
          'Content-Type': 'text/plain; charset=UTF-8'
        , 'Content-Length': data === undefined ? 0 : Buffer.byteLength(data)
        , 'Connection': 'Keep-Alive'
      };

このKeep-Aliveの行をコメントアウトするだけでも一応動くようになる。