socket.io-clientを用いてExpress+Socket.IOで作ったサービスへ同時接続テストを行うすた2012年10月29日

このエントリーをはてなブックマークに追加
はてなブックマーク - socket.io-clientを用いてExpress+Socket.IOで作ったサービスへ同時接続テストを行う

Express3.x(connect2.3+)とSocket.IOでのセッション管理mochaとsuperagentでexpressを使ったサービスのログイン周りのテストの続き。

今回はsocket.ioの同時接続のテストについて。今回は

  • 1000人同時接続が出来るか
  • 正しくメッセージのやりとりが出来るか(チャットなので)
  • ログイン済みユーザーのみが接続できるか
のテストを行なってみました。

socket.io-clientを用いれば、コンソールから同時接続のテストが簡単に出来ます。
ただし、環境によってはOS自体のファイルディスクリプタの最大数を引き上げないと1000人同時接続は難しいです。


ソースコード

今回は今までのを全て含めたものをgithubに上げています。
pxsta / express-socket.io-chat-test


今回の環境

  • Mac OSX 10.7.4 Lion
  • socket.io@0.9.10
  • socket.io-client@0.9.10
  • express@3.0.0rc3


やったこと

ファイルディスクリプタの最大数の変更(環境によっては不要)

1000人同時接続のテストを行う際にはOS自体のファイルディスクリプタの最大数を引き上げる必要があります。
少なくともMacでは256などというとても低い値に設定されているのでそのままでは無理です。

Mac OSX 10.7.4 Lionの環境で1000人接続のテストを行った所、117人あたりで接続を受け付けなくなるか、以下のようなエラーで止まってしまいまいした。
[ERROR] console - Error: EBADF, Bad file descriptor
/node_modules/express/node_modules/connect/lib/middleware/errorHandler.js:68
                .replace('{style}', style)
                 ^
TypeError: Cannot call method 'replace' of undefined
    at /node_modules/express/node_modules/connect/lib/middleware/errorHandler.js:68:18
    at [object Object]. (fs.js:80:5)
    at [object Object].emit (events.js:64:17)
    at fs.js:820:12

Macの場合は
$ ulimit -n
256
$ sysctl kern.maxfilesperproc
kern.maxfilesperproc: 10240
$ ulimit -n  10240
$ ulimit -n  
10240
のようにして変更可能です。ulimitにkern.maxfilesperprocの値を設定すれば最大値まで引き上げられます。


socket.io-clientによる同時接続のテスト(1000人くらい)

socket.io-clientを用いれば同時接続のテストをコンソールから行うことが出来ます。
以下のように、接続自体は今までクライアント側で書いていたものをサーバ側でそのまま使うことが出来ます。
var io = require('socket.io-client');
var options = {
    'force new connection' : true,  //別々のコネクションとして認識させるために必要
    port : 3000
};

//接続
var socket = io.connect("",options);
socket.on('connect', function(data) {
    done("wrong behaviour");
});

//メッセージの送信
socket.emit("chat","send message test");

そのため、複数人接続のテストはmochaと組み合わせて以下のようにして簡単に出来ます。
var should = require('should')
  , expect = require('expect.js');
var options = {
    'force new connection' : true,  //別々のコネクションとして認識させるために必要
    port : 3000,
};

//socket.ioの接続テスト
describe('socket.io test', function() {
    it('1000件接続出来るかどうか。ファイルディスクリプタの最大値を大きくしないと一度に接続できない', function(done) {
        this.timeout(600000);
        var maxCount = 100;
        var clientCount = 0;
        var clients = [];
        
        //全てが接続し終わったら全て切断してから終了
        var end = function() {
            for (var k=0;k<clients.length;k++) {
                clients[k].disconnect();
            }
            done();
        }; 
        
        //maxCount個のクライアントを接続する
        for (var i = 0; i < maxCount; i++) {
            var client = helper.login({
                auth : {
                    userID : 'test' + i,
                    password : 'test'
                },
                server : {
                    host : "",
                    details : options
                }
            }, function(err, client) {
                clients.push(client);
                
                //接続出来たクライアントがmaxCount個に達するまで待つ
                client.on('connect', function(data) {
                    if ((++clientCount) == maxCount) {
                        end();
                    }
                });
            });
        }
    });
});
接続する際のパラメータを
'force new connection':true
と設定しておかないと全てが1つのコネクションとして扱われてしまいます。
パラメータさえ指定しておけば後は繋ぎたいだけforで回して接続すれば出来てしまいます。簡単!


socket.ioの認証まわりのテスト

socket.io-clientではcookieをセットすることができません。

socket.ioの接続認証はexpressのセッションIDをcookieより取得、さらにそのセッションIDを元にsessionStoreからセッションデータを取得、正しいセッションデータがあれば接続を認める、という流れになっています。

そのため、cookieが無いとsocket.ioの認証をそもそも受けられないのですが、
expressやsocket.ioのテストはこんな感じで書いてます、というお話 – アルパカDiary
という素晴らしいエントリーが合ったので参考にさせていただきました。

socket.io-client/lib/socket.js内のio.Socket.prototype.handshakeをsupport/helper.js内でオーバーライドしてしまい、
cookieをx-set-cookieというキーでヘッダに格納しているようです。

support/helper.jsに補助的なメソッドを追加&テスト用にcookieをセットする、という手法を使わせて頂き、
socket.ioの認証テストは以下のように。

var should = require('should')
  , expect = require('expect.js')
  , helper = require('./support/helper')
  , app = require('../app.js')

var options = {
    transports : ['websocket'],
    'force new connection' : true,  //別々のコネクションとして認識させるために必要
    port : app.get('port'),
};


//socket.ioの接続テスト
describe('socket.io test', function() {
    before(function(done) {
        helper.initDataStore(done);
    });
    
    it('ログイン前はsocket.ioで接続できないはず', function(done) {
        this.timeout(10000);
        helper.login({
            auth : {
                userID : 'test',
                password : 'wrong password'
            },
            server : {
                host : "",
                details : options
            }
        }, function(err, client) {
            //接続できてはいけない
            client.on('connect', function(data) {
                done("wrong behaviour");
            });
            
            //エラーが起こるはず
            client.on('error', function(data) {
                //ハンドシェイクのエラーのはず
                should.equal(data.toString(),"handshake error");
                done();
            });
        });
    });
    it('ログインした後はsocket.ioで接続できるはず', function(done) {
        this.timeout(10000);
        helper.login({
            auth : {
                userID : 'test',
                password : 'test'
            },
            server : {
                host : "",
                details : options
            }
        }, function(err, client) {
            client.on('connect', function(data) {
                //接続完了するはず
                done();
            });
        });
    });
});
helper.loginではログインしてexpressのセッションの発行を受けたのち、コールバック関数でsocket.io-clientのsocketインスタンスを返しています。

socket.ioの認証でエラーが起きるとerrorイベントが発火するためclient.on(‘error’, function(data) {})の用にしてそのエラーをキャッチするようにしたいます。
testUserはログイン済みのため接続できてconnectionイベントが発火しますが、invalidUserはログインしていないため接続できずにerrorイベントが発火します。


メッセージ交換が出来るかどうかのテストも以下のように簡単に行うことが出来ます。
    it('メッセージの送受信がclient0とclient1でできるかどうか', function(done) {
        this.timeout(10000);
        var maxCount = 2;
        var clientCount = 0;
        var clients = [];
        
        var connectCompleat = function() {
            clients[0].on("message",function(message){
                //client0が受信したメッセージはclient1が送信したメッセージなはず
                should.equal(message, clients[1].userID + ' ' + "message");
                
                //テスト終了前に切断する
                clients[0].disconnect();
                clients[1].disconnect();
                done();
            });
            clients[1].emit("chat","message");
        }; 
        
        for (var i = 0; i < maxCount; i++) {
            (function(userID) {
                var client = helper.login({
                    auth : {
                        userID : userID,
                        password : 'test'
                    },
                    server : {
                        host : "",
                        details : options
                    }
                }, function(err, client) {
                    client.userID = userID;
                    clients.push(client);
                    
                    //client0とclient1が接続するまで待つ
                    client.on('connect', function(data) {
                        if ((++clientCount) == maxCount) {
                            connectCompleat();
                        }
                    });
                });
            })('test' + i);
        }
    });

1000人同時接続のテストの際にファイルディスクリプタ最大数が原因だと気づかずにはまってしまいましたが、
それさえ忘れなければ普段クライアントで利用しているコードがサーバ側でも使えるのは便利ですね。


前々回・前回・今回のサンプルコードを全て含めたものはpxsta / express-socket.io-chat-testです。

このエントリーをはてなブックマークに追加
はてなブックマーク - socket.io-clientを用いてExpress+Socket.IOで作ったサービスへ同時接続テストを行う

関連する記事

Leave a Reply

Dansette