ネットワーク帯域計測プログラム

複数のソケットの入力状況を管理するクラス,つまりselect()のラッパクラスを数年前に作成したのですが使用感がイマイチだなと思って没にしたままだったので,書き直してsockmanagerと言うクラス名でCLXに入れました.最初に通信が発生したときに呼び出す関数オブジェクトをソケットとペアで登録しておいて,後はsockmanager側で必要に応じてその関数を呼び出してもらう,と言うコンセプト自体は以前に作成したものとかわりません.boost::asio::io_serviceの実装に似せようかなと思ったのですが,ソースコードを読みきれなかったので断念.

単にライブラリを紹介するのもアレなので,ネットワーク帯域を計測するプログラムをサラっと書いてみて,そのプログラムで使い方を紹介します.帯域計測と言っても,データを流してそのスループットを測ると言う単純なもので,何か(計測用トラヒック減らすための)凄いことをやってる,と言う訳じゃないです:p

httpによるダウンロード時のスループットを計測

適当なサイトからGETでデータをダウンロードして,そのダウンロード時のスループットを計測するプログラムです.まず,下準備として計測用のソケットクラスを作成します

class measure_socket : public clx::tcp::socket {
public:
    measure_socket(const clx::tcp::socket& cp);
    measure_socket(const std::string& host, int port);
    
    void add(double bytes);
    void initialize();
    void terminate();
    void putrate();

    template <class OutputStream>
    void putrate(OutputStream& sout);
};

実装部コードは省略.クライアント/サーバ兼用のソケットなので,コンストラクタは 2 種類あります.クライアント用として使うときは,ホスト名,ポート番号を指定してオブジェクトを作成します.一方,サーバ用として使うときは,clx::tcp::acceptorが,accept()に成功すると通信用のソケットを作成して返すので,そのソケットを引数にしてオブジェクトを作成します.

add()メソッドはデータの受信が発生するたびに呼び出して,合計何バイト受信したかのかを覚えておくために使用します.尚,合計受信バイト数はMByte単位で記憶しています(double型なので,1MByte以下でもOK).残りのinitialize(),terminate(),putrate()メソッドは情報(接続先,スループットなど)を出力するためのメソッドです.基本的に標準出力(std::cout)へ出力しますが,putrate()メソッドは出力用ストリームを引数に指定するとそのストリームへ出力します.putrate()メソッドは,前回putrate()メソッドが呼ばれてから今までのスループットを出力します.例えば,1秒おきにputrate()メソッドを呼び出すとその1秒間毎のスループットが出力されます.terminate()メソッドは,データ受信開始から終了までのスループットを出力します.

次に,この計測用ソケットがデータを受信するときの振る舞いを決めるクラスを作成します.

class measure_handler {
public:
    typedef measure_socket socket_type;
    
    enum { Mbyte = 1000000 };
    
    measure_handler();
    measure_handler(socket_int s);
    
    template <class T, class SockManager>
    bool operator()(T* s, SockManager& sm) {
        socket_type* sock = dynamic_cast<socket_type*>(s);
        
        char buf[Mbyte];
        std::memset(buf, 0, sizeof(buf));
        int len = sock->recv(buf, sizeof(buf));
        if (len <= 0) {
            sock->terminate();
            return false;
        }
        sock->add(len / static_cast<double>(Mbyte));
        return true;
    }
};

実装部コードは一部のみ掲載.これもクライアント/サーバ兼用にする予定なので,コンストラクタは 2 種類あります.実際の動作は()演算子の中に書いてある部分で,recv()でデータを受信して,受信したバイト数を(MByte単位で)ソケットに記録します.端末内部がボトルネックになると嫌なのでバッファは大きめに用意しています(1MByteも).ちなみに,読み込んだデータは捨てています:p通信が終了してfalseを返すと,sockmanagerはそのソケットを監視対象から外します.ソケットのクローズはデストラクタが呼ばれたときに行われるのでやらなくてもいいです.

最後にメインプログラム.

#include <iostream>
#include <string>
#include "msocket.h"
#include "clx/tokenizer.h"
#include "clx/argument.h"

int main(int argc, char* argv[]) {
    clx::argument arg(argc, argv);
    if (arg.head().empty()) {
        std::cerr << "usage mhttpclient URL [-i interval]" << std::endl;
        std::exit(-1);
    }
    
    clx::format_separator<char> f("http://%s/%s");
    clx::strftokenizer token(arg.head().at(0), f);
    if (token.size() < 2) std::exit(-1);
    
    double interval = -1.0;
    arg.assign("i,interval", interval);
	
    clx::tcp::sockmanager sm;
    
    // GETクエリを投げた後にsockmanagerに登録して通信を監視する.
    measure_socket* clt = new measure_socket(token.at(0), 80);
    std::string query = "GET /" + token.at(1) + " HTTP/1.0\r\n\r\n";
    clt->send(query);
    sm.add(clt, measure_handler());
    	
    while (1) {
        sm.start(interval);
        if (sm.empty()) break;
        else clt->putrate();
    }
    
    return 0;
}

プログラム引数としてURLを与えられると,そのURLに対してGETを行います.GETクエリを投げた後に,sockmanagerに登録してその後は任せます.start()メソッドの引数に秒数を指定すると,その秒数で監視を終了するので,それを利用して一定時間ずつスループットを出力しています.ちなみに,ソケットオブジェクトはstd::tr1::shared_ptr*1で管理しているので,deleteはしません.プログラム引数の解析にargumenttokenizerを使っていますが,詳細は各リンクを参照して下さい.

実行例は以下のようになります.適当なサイズのファイルがなかったので,SourceForge.netからboost-jam(1MByteくらい)をダウンロードしてみました.

$ ./mhttpclient http://nchc.dl.sourceforge.net/sourceforge/boost/boost-jam-3.1.16.tgz -i 1.0
211.79.60.17:80 establish
1.00 sec   0.102 Mbps ( from 211.79.60.17:80 )
2.00 sec   0.498 Mbps ( from 211.79.60.17:80 )
3.00 sec   1.226 Mbps ( from 211.79.60.17:80 )
4.00 sec   1.279 Mbps ( from 211.79.60.17:80 )
5.00 sec   1.253 Mbps ( from 211.79.60.17:80 )
6.00 sec   1.279 Mbps ( from 211.79.60.17:80 )
7.00 sec   1.268 Mbps ( from 211.79.60.17:80 )
8.00 sec   1.274 Mbps ( from 211.79.60.17:80 )
211.79.60.17:80 close ( 8.85 sec 1.159 Mbytes 1.048 Mbps )

iperfを用いてスループットを計測

mhttpclientは,監視するソケットは結局1つだけだったので,今度はサーバ側の計測用プログラムを作成してみます.TCPスループットを計測する際にはiperfと言うツールがよく使われるのですが,このサーバとして使えるものを作成します.作成すると言っても,measure_socketとmeasure_handlerは流用するので書くのはメインプログラムだけです.

#include <iostream>
#include "msocket.h"
#include "clx/sockmanager.h"
#include "clx/argument.h"

typedef clx::tcp::accept_handler<measure_socket, measure_handelr> accept_handler;

int main(int argc, char* argv[]) {
    clx::argument arg(argc, argv);
    if (arg.head().empty()) {
        std::cerr << "usage mserver port [-i interval]" << std::endl;
        std::exit(-1);
    }
    
    double interval = -1.0;
    arg("i,interval", interval);
    
    // クライアントの接続要求受け付け用ソケットを登録する.
    clx::tcp::sockmanager sm;
    clx::tcp::acceptor* serv = new clx::tcp::acceptor(arg.head().at(0));
    socket_int accid = serv->socket();
    sm.add(serv, accept_handler());
    
    while (1) {
        sm.start(interval);
        for (clx::tcp::sockmanager::iterator pos = sm.begin();
            pos != sm.end(); pos++) {
            if (sm.socket(pos)->socket() == accid) continue;
            measure_socket* s = dynamic_cast<measure_socket*>(sm.socket(pos));
            s->putrate();
        }
    }
    
    return 0;
}

clx::tcp::accept_handler<Socket, Service>は,CLXにデフォルトで入れているハンドラクラスです.クライアントからの接続要求が発生するとその要求を受け付けて通信用ソケットを作成し,Serviceで指定した関数オブジェクト(ハンドラ)とのペアでsockmanagerに登録するという動きをします.サーバ用のソケットをsockmanagerに登録した後は,基本的にsockmanagerに任せます.ただし,このプログラムも一定時間ごとに監視を中断して,通信しているソケットがある場合はスループットを出力しています.

実行例は以下の通り.クライアント側は,iperfを使って実験しています.iperfのクライアント側の実行方法は以下のような形になります.

iperf -c 接続先 [-p ポート番号] [-i スループット出力間隔(秒)] [-t 実行時間]

デフォルトの設定だと,ポート番号は5001,スループット情報の出力は通信終了時のみ,実行時間は10秒になっています.

$ ./mserver 5001 -i 1.0
192.168.0.3:2571 establish
0.91 sec   0.361 Mbps ( from 192.168.0.3:2571 )
127.0.0.1:2821 establish
0.07 sec   1.959 Mbps ( from 127.0.0.1:2821 )
1.96 sec   0.313 Mbps ( from 192.168.0.3:2571 )
1.13 sec   9.537 Mbps ( from 127.0.0.1:2821 )
2.98 sec   0.386 Mbps ( from 192.168.0.3:2571 )
2.15 sec   4.408 Mbps ( from 127.0.0.1:2821 )
4.02 sec   0.315 Mbps ( from 192.168.0.3:2571 )
3.18 sec   9.400 Mbps ( from 127.0.0.1:2821 )
5.03 sec   0.326 Mbps ( from 192.168.0.3:2571 )
192.168.0.3:2571 close ( 5.19 sec 0.221 Mbytes 0.341 Mbps )
4.20 sec   4.718 Mbps ( from 127.0.0.1:2821 )
127.0.0.1:2821 close ( 5.08 sec 4.661 Mbytes 7.346 Mbps )

2つのホスト(localhostと192.168.0.3)からiperfを実行してみました.サンプルプログラムでは全て標準出力に出力していますが,コネクション毎に別のファイルに出力する,などと言ったことをするともっと見やすくなりそうです.

ネットワーク通信に関するプログラムが楽に早く書けるようになると思うのですが,どうでしょう.

*1:なければboost::shared_ptrで代替します.ただし,そのときはCLX_USE_BOOSTオプション付きでコンパイルして下さい.