ICMPパケットを利用したパケット間隔ベースの帯域計測方法

今日は,少し真面目な帯域計測方法について.

スループットが100Mbps出るとは?

スループットが100Mbps出るとは,(1個のデータパケットのサイズが1,500Byteの場合)1秒間にパケットを533,333個(100M / (1,500 * 8))受信できることを意味しますが,これを2つパケットの送受信間隔に目を向けると,最低でも120μ秒間隔*1でパケットを送受信することができる(1,500 * 8 / 100M)と捉えることができます*2.ここから,送受信間隔を観測することでスループット(≒自分が利用できる帯域)を推測できるのではないかと言う考えが生まれます.

ICMPパケットを利用して2パケットの受信間隔を観測するプログラム

あるホストへICMP ECHO REQUESTパケットを送信すると,そのホストはパケットを受信すると(多分)即座にICMP ECHO REPLYパケットを返します.この機能を利用して,送信時に適当な間隔でICMP ECHO REQUESTパケットを送信し,対応するICMP ECHO REPLYパケットの受信間隔がどうなるかを観測するプログラムを作成してみます.

ソースコードは,msocket_20081106.tar.gz (with clx_0.13.2.tar.gz)になります(ネットワーク帯域計測プログラムで作成したプログラムも含まれていますが,必要なのはcheck_interval.cppとicmp_msocket.hだけです).今回は,説明する事はこれまでに説明したプログラムとあまり差がないので,ソースコードの記載&説明は省きます.ライブラリの使い方などは,

辺りを参考にして下さい.

#!/bin/sh

addr=$1
file=$2
trial=80

i=10
while test $i -le 40
do
    echo "./check_interval $addr -i $i -n $trial >> $file"
    ./check_interval $addr -i $i -n $trial >> $file
    let i=i+2
done

作成したプログラムを上記のシェルスクリプトで走らせてICMPパケットの送受信間隔を観測した結果が以下のグラフになります.図の点は40サンプルの平均値で,エラーバーは95%信頼区間です.

ホストは,LAN内のPC(192.168.0.3)とインターネット越しにあるレンタルサーバ(clx.cielquis.net)を選んでみました.192.168.0.3の場合は,送信間隔と受信間隔がずっと同じなのが分かります.これは,LAN内なら100Mbps近く出る(120μs間隔でパケットを送受信できる)ので,10msがそれと比較して十分余裕のある間隔だからです.一方,clx.cielquis.netの場合は17ms辺りが限界で,それより小さい間隔で送信したとしても受信間隔は17msに広がっています.パケット間隔が17msだと,約700kbps(1,500 * 8 / 0.017)と言うことになります.Radish Networkspeed testingを用いて計測してみたところ,上り710.6kbps,下り1.226Mbpsだったので,恐らく送信時の最初の1ホップがボトルネックになっているのだろうと予想されます.

図を見ても分かりますが,限界を超える(今回の例だと17ms以下)とほぼ限界近くの受信間隔が観測されるようになるので,できるだけ小さな間隔でICMP ECHO REQUESTパケットを送信して,対応するICMP ECHO REPLYパケットの受信間隔からスループットを推測する,と言う方法でもそれなりに有用な値を取得することができます.実験した限りだと,10パケット程度でも有用と思われる程度の観測結果が得られたので(観測される受信間隔のバラつきが小さかった),普段やるような計測方法(数百KB〜数MBのファイルをアップロード/ダウンロードしてスループットを計測する)よりもずっと少ないパケット数で計測を行うことができそうです.また,この結果と実際にデータ転送をしたときのスループットを比較することで,スループットが受信間隔から推測されるスループットに比べて著しく低い場合は,バッファが小さすぎてまったく通信できてない空白時間があるのでは,とか,あんまり大量のデータ転送をしてどっかで制限かけられたのでは:p,などと言ったスループットが出ない原因追求の補助として利用することもできます.

パケット間隔ベースの計測手法の基本はこんな感じで,後はいかに“賢く”送信間隔を決定するかとか,いかに計測パケット数を抑えるか,と言ったことを考えながら皆あれこれ提案しています.

注意事項

注意事項というか何と言うか.

まず,1つ目はパケット間隔ベースの計測プログラムを作るときには,パケット送信のスケジューリングや時刻取得にかなりの細かい粒度を求められる,と言うことです.作成したプログラムは,パケット送信のスケジューリングにはselect(),時刻取得にはQueryPerformanceCounter()(UNIX系の場合はgettimeofday())を利用して一応μ秒単位で操作できるようにしたのですが,テストしたところ1ms以下だと精度があやしいようです.パケット間隔1msは12Mbps相当なので,10Mbps以上スループットが出せるような環境だと信頼性がかなり落ちそうな予感がします.

2つ目は,実験結果からも分かりますが,ADSLのようにアップロード帯域とダウンロード帯域が違う場合,(多くはアップロード帯域が抑えられているので)アップロード帯域の値が計測結果として得られてしまいます.計測したいのはダウンロード時のスループット,と言うことが多いのでこれはネックとなりそうです.ICMPを使うんじゃなくて,httpによるダウンロード時にパケット間隔を観測する方法を組み込む方法が(パケットの送信間隔を弄るとかができないけど)現実解なのかな,とちょっと感じています.

最後は,ping floodに関すること.多くのサーバは,短い時間に大量のICMP ECHO REQUESTパケットが到着すると弾くように設定されています(そもそも,ICMPを受け付けていない場合もありますが).最初,10秒単位くらいで見て秒間1パケット送ってなかったら大丈夫だろう,と甘く考えてたのですが,バースト的に送ってしまうとその時点でアウトだったりするようです.現在のプログラムでは,2パケット送ったら4秒スリープする(0.5パケット/秒相当)ように設定しています.これはプログラムオプションでは変更できないようにしています.defineしているのでプログラム見ればすぐ変更できますが,あんまり大量のパケットをバースト的に送信して怒られても知りません:p

Download

Usage

usage: check_interval hostname [options]

options:
  -i # (msec)    sending interval
  -l # (byte)    data length
  -n #           number of probing packets
  -t # (sec)     timeout value
  -v             print debug information

測定したいホスト名を最初に与えます.オプションは5つで,iオプションはICMP ECHO REQUESTの送信間隔をミリ秒単位で指定します.lオプションは,ICMPパケットのデータ長を指定します.nオプションは,計測に使用するパケット数を指定します.2パケットで1個の結果を得るようになっているので,得られる結果はn/2個となります.tオプションは,ICMP ECHO REQUSTを送信してから最大で何秒待つか指定します.ここで指定された時間までにICMP ECHO REPLYが返ってこなかった場合は,計測を(一時)中断します.最後のvオプションは,付属的な情報を標準出力へ出力します.主にデバッグ用です.

*1:パケットの受信間隔が120μ秒より小さくても,パケットをまったく受信しなかった時間が存在して100Mbpsになった,と言うことが考えられるため

*2:パケット間隔が120μ秒以上だと1秒間に533,333個のパケットを受信することができないので,スループットは100Mbpsを下回る

手抜きPINGプログラム

現在,ICMPパケットを扱うソケットクラスを作成しているのですが,そのテストプログラムとしてPINGっぽいプログラムを作成したところ,正常に動作したので取り合えず公開してみます.bkブログ: シンプル=バッドシグナル説と言う記事があったので,シンプルと言わず“手抜き”と明言してみます.動作確認は,cygwin gcc 4.1.2とVisual Studio 2005.

#include <iostream>
#include <string>
#include <cstring>
#include <memory>
#include "clx/icmp.h"
#include "clx/timer.h"
#include "clx/argument.h"
#include "clx/format.h"

int main(int argc, char* argv[]) {
    clx::argument arg(argc, argv);
    if (arg.head().empty()) {
        std::cerr << "usage " << argv[0] << " hostname [-l data_bytes]" << std::endl;
        return -1;
    }
    
    /* bufは送受信兼用バッファ.
     *   -send()メソッド: payload + ICMPヘッダ長
     *   -recv()メソッド: payload + IPヘッダ長 + ICMPヘッダ長
     * 以上を考慮して大きめに領域を確保する.
     */
    clx::icmp::packet_header hdr;
    int payload = 56;
    arg("l,length", payload);
    int packetsize = payload + hdr.icmp_size();
    int buffsize = packetsize + 1024;
    std::auto_ptr<char> buf(new char[buffsize]);
    
    clx::icmp::socket s(arg.head().at(0));
    std::cout << clx::format("ICMP ECHO %s (%s): %d data bytes")
        % arg.head().at(0) % s.to().ipaddr() % payload
    << std::endl;
    
    int seq = 0;
    while (1) {
        std::memset(buf.get(), 'a', packetsize);
        
        // sending ICMP echo request
        hdr.reset();
        hdr.icmp()->type = ICMP_ECHO_REQUEST;
        hdr.icmp()->sequence = seq;
        std::memcpy(buf.get(), (char*)hdr.icmp(), hdr.icmp_size());
        s.send(buf.get(), packetsize);
        clx::timer t;
        
        // receiving ICMP echo packet
        std::memset(buf.get(), 0, buffsize);
        int len = s.recv(buf.get(), buffsize);
        if (len < 0) return -1;
        double recv = t.total_elapsed();
        
        // print information
        hdr = buf.get();
        if (hdr.icmp()->type == ICMP_ECHO_REPLY) {
            std::cout <<
                clx::format("%d bytes from %s: icmp_seq=%d ttl=%d time=%d ms")
                % len % s.from().ipaddr()
                % static_cast<int>(hdr.icmp()->sequence)
                % static_cast<int>(hdr.ip()->ttl)
                % static_cast<int>(recv * 1000)
            << std::endl;
            seq = hdr.icmp()->sequence + 1;
        } else {
            std::cerr << clx::format("received ICMP packet (type: %d) from %s")
                % static_cast<int>(hdr.icmp()->type) % s.from().ipaddr()
            << std::endl;
        }
        
        clx::sleep(1.0);
    }
    
    return 0;
}

指定できるものは,PINGを送るホストとパケットのデータ長のみです.動作は,ICMP echo requestパケットを送って,ICMP echoパケットを受け取って,1秒スリープ,と言う事を永遠に繰り返します.タイムアウト処理などを入れていないので,何らかの問題でパケットが返って来ない場合は永遠にブロックされてしまいます.

以下が実行結果(オリジナルのPINGと比較してみる).

$ ./echotest yahoo.com -l 1000
ICMP ECHO yahoo.com (68.180.206.184): 1000 data bytes
1028 bytes from 68.180.206.184: icmp_seq=0 ttl=46 time=156 ms
1028 bytes from 68.180.206.184: icmp_seq=1 ttl=46 time=159 ms
1028 bytes from 68.180.206.184: icmp_seq=2 ttl=46 time=168 ms
1028 bytes from 68.180.206.184: icmp_seq=3 ttl=46 time=171 ms
1028 bytes from 68.180.206.184: icmp_seq=4 ttl=46 time=162 ms
1028 bytes from 68.180.206.184: icmp_seq=5 ttl=46 time=158 ms

$ ping yahoo.com 1000
PING yahoo.com (68.180.206.184): 1000 data bytes
1008 bytes from 68.180.206.184: icmp_seq=0 ttl=46 time=163 ms
1008 bytes from 68.180.206.184: icmp_seq=1 ttl=46 time=172 ms
1008 bytes from 68.180.206.184: icmp_seq=2 ttl=46 time=171 ms
1008 bytes from 68.180.206.184: icmp_seq=3 ttl=46 time=163 ms
1008 bytes from 68.180.206.184: icmp_seq=4 ttl=46 time=178 ms
1008 bytes from 68.180.206.184: icmp_seq=5 ttl=46 time=162 ms

受信バイト数が異なっていますね.ICMPソケットで通信する場合,recv()メソッドで受け取るデータにはIPヘッダが付与されているのですが,オリジナルのPINGプログラムはそのサイズ分は省いて表示しているようです.それと,(オリジナルのPINGがどうやっているのかは追っていないので知りませんが)テストプログラムは,RTTを計測する際にタイムスタンプは使わず,send()が成功した直後からrecv()が成功した直後までの時間を表示しているのですが,これはそこそこいい値で測れているようです.

ICMPソケットクラスを作ったのは,ICMP ECHOを利用して(ちょっと賢い)帯域計測プログラムを作るためで,そちらも完成したら公開できればと思います.時刻取得関数にかなり厳しい精度を要求されるので(100μ秒程度),きちんと使えるかどうかは怖いところですが・・・

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

複数のソケットの入力状況を管理するクラス,つまり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オプション付きでコンパイルして下さい.

streambufのカスタマイズ

ネットワーク関連のライブラリでsockstreamだけ良く分からないまま放置していたので,真面目に調べてみました.ソケットを介したデータ通信をC++のiostream(std::cinとかstd::cout)のインターフェースで行おうとするとbasic_streambuf<Ch, Tr>をカスタマイズすることになります.カスタマイズするときは,basic_streambuf<Ch, Tr>を継承してvirtual宣言されてあるメソッド群をオーバーライドします.詳細は,iostreamの拡張あたりで.

さて,ここで問題になったのが,出力用バッファを用意するかどうかということでした.ソケットを介したデータ通信をiostreamのように見せかけるためには,underflow()が呼ばれたときにrecv()システムコールで受信し,overflow()が呼ばれたときにsend()システムコールで送信するように実装するのですが,出力用バッファがない状態だと1文字ずつsend()してしまうため明らかに効率が悪いです.

ただ,streambufの規格を調べたり実際に実験してみた結果,出力バッファはなくてもいけるんじゃないかなという感触が得られました.streambufの出力の動きは,以下のようになります.

  1. <<演算子で文字列(など)がストリームへ流されると,sputn(s, n)が呼び出される(sは文字列,nは文字列数).
  2. sputn(s, n)は,単にxsputn(s, n)を呼び出すだけという実装になっている.
  3. xsputn(s, n)はデフォルトの実装では,sputc(c)がn回呼び出される.
  4. sputc(c)は出力バッファがあればそこへ格納する.ないか出力用バッファがいっぱいのときはoverflow()メソッドを呼び出す.
  5. xsputn(s, n)は派生クラスで別の実装を行っても良いことになっている.

と言う訳で,xsputn(s, n)は,(1回の<<演算子で渡された)文字列を全部持っているため,xsputn(s, n)の中でsend()システムコールで送ってしまえば出力用バッファは要らないことになります.ただし,その実装にした場合,n文字全部送れなかったときにはbadbitが立ってしまうので注意する(n文字送れるまでsend()し続けるとか)必要があります.

出力用バッファを用意しないときの動きの特徴は,<<演算子で文字列(など)を渡すと,その文字列だけですぐに送信してしまうということでしょうか.出力バッファがある状態だと,バッファがいっぱいになるか,std::flushを流さない限りは通信は(多分:p)発生しません.出力バッファがあった方がいいような気もしますが,中間バッファを介さない分速くなるかなぁとかいろいろ考えるとどっちがいいのか分からないところです.現在の実装は,出力バッファなんていらないよ派.

今まで書いたライブラリコードは大学内のスペースにアップロードしていたのですが,新たにスペースを取得してライブラリのWebページと実装コード部だけは切り離すことにしました.新しいWebページはCLX C++ Libraries.同時にSourceForge.JPにもアカウントを作って,コードはSourceForge.JP内のSubversionにぶち込んでおくことに.

文字列リテラルとポインタ

C/C++のポインタの機能--変数の場所(アドレス)という記事が一時お祭り状態になっていたようです.内容云々に関しては様々な人が指摘しているので置いておいて,今回はポインタ関連で理解に時間がかかった事例を一つ紹介してみようと思います.

// sample1
#include <stdio.h>

int main() {
    char* hello = "Hello, world.";
    printf("%s\n", hello);
    return 0;
}

Cでは,上記のプログラムは正常に動作します.しかし,このプログラムを最初に見たとき2行目の記述に違和感を覚えました.具体的に言うと,領域を確保しないままcharポインタに直接文字列を挿入してるけど大丈夫なのかな?というものです.私の想像でしかありませんが,上記のコードが,先の記事のように,ポインタに対して値を代入すると自動的にその値を記憶するための領域が確保されると言った誤解(記事のコードを見る限りはそう言った誤解をしているように見えたのだけど,実際はどうだったのだろう?)を招く原因の一つになっているのではと思います.

また,当初は以下のコードとの違いも良く分かりませんでした.

// sample2
#include <stdio.h>

int main() {
    char hello[] = "Hello, world.";
    printf("%s\n", hello);
    return 0;
}

そこで,上記2つのコード(sample1, sample2)いろいろ書き換えているうちにようやくどのような動きをしているのかが分かってきました.

まず,文字列リテラル(上記のコードだと"Hello, world.")を記述すると,システムはその文字列を記憶するための領域を確保して文字列を記憶した後,その記憶領域の先頭アドレスを返します.sample1は,特別な初期化を行っている訳ではなく,ポインタ変数にその先頭アドレスを代入しているにすぎません.そのため,以下のようなコードも正常に動作します.

// sample3
#include <stdio.h>

int main() {
    char* hello;
    hello = "Hello, world.";
    printf("%s\n", hello);
    return 0;
}

一方で,sample2のように記述した場合には(恐らくシステムが文字列リテラルのための領域を確保してその先頭アドレスを返した後に)配列の初期化処理が始まり,文字列リテラル(上記のコードだと"Hello, world.")のコピーが行われます.配列の場合の変数名(上記のコードだとhello)は内容を参照する場合(*hello, hello[1]など)はポインタ変数と同じように扱うことができますが,その変数の値を変更することはできません(hello++など).そのため,次のようなコードはコンパイルエラーとなります.

// sample4
#include <stdio.h>

int main() {
    char hello[16];
    hello = "Hello, world.";
    printf("%s\n", hello);
    return 0;
}
コンパイル結果
$ gcc -Wall -o test sample4.c
sample4.c: In function `main':
sample4.c:5: error: incompatible types in assignment

また,sample1の場合,ポインタは文字列リテラルを記憶している領域(の先頭アドレス)を指しているため,ポインタの指している内容を変更しようとすると予期せぬエラーが発生します.例えば,テストした環境(cygwin + gcc 3.4.4)では以下のコードはコアを吐いて異常終了しました.

// sample5
#include <stdio.h>

int main() {
    char* hello = "Hello, world.";
    hello[0] = 'h';
    printf("%s\n", hello);
    return 0;
}
実行結果
$ ./test
Segmentation fault (core dumped)

しかし,sample2の場合は(内容を変更可能な)配列を用意してその配列に文字列リテラルをコピーしたため,ポインタが指している先の内容を変更しても正常に動作します.

// sample6
#include <stdio.h>

int main() {
    char hello[] = "Hello, world.";
    hello[0] = 'h';
    printf("%s\n", hello);
    return 0;
}
実行結果
$ ./test
hello, world.

以上です.私自身も理解があやふやな状態で実際に実行するまで分かっていなかった部分もありました.それと,このテストをしていて分かったことですが,sample5のコードは-Wallオプションをつけても警告なしでコンパイル完了するんですね.私の環境だと実行時にコアを吐いて終了しましたが,場合によっては普通に動いてしまうこともあるようです.そんな訳で,今回のテストで得た教訓は,文字列リテラルのポインタにはきちんとconstを付けましょうでした.

// sample7
#include <stdio.h>

int main() {
    const char* hello = "Hello, world.";
    hello[0] = 'h';
    printf("%s\n", hello);
    return 0;
}
コンパイル結果
$ gcc -Wall -o test sample7.c
sample7.c: In function `main':
sample7.c:5: error: assignment of read-only location

プログラミングをしていると,その言語毎に定石と呼ばれているようなことに数多く遭遇しますが(C++だとEffective C++に載っているような事でしょうか.Cだと何だろ?K&qmp;R本?),その多くは実際に自分がつまづくまでその恩恵が理解できないことも多いように思います(知識として知ってはいてもうまく使いこなせないと言った方が正しいでしょうか).その意味では,ガンガン書いてたくさんつまづくしかないのかな,と感じました.

マーフィーに微笑まれないクリティカルな場面で躓かないことを祈りつつ.

マルチプラットフォームなソケットクラスの実装に関する覚書

2,3年前に初めてsocket関連のシステムコールのラッパクラスをC++で作成してその後何度か修正を加えていたのですが,ようやく完成かなと思える位になったのでこれまでの修正の覚書として,socketクラスの実装時の注意事項を何点か記述します.マルチプラットフォームと言ってもPOSIX socketとwinsockのみですが.

コピーコンストラクタの処理

socketクラスを設計する場合,ユーザからclose()処理などの煩わしさを除くなどの目的で,コンストラクタが呼ばれたときにsocket(),デストラクタが呼ばれたときにclose()(winsockだとclosesocket())を呼ぶようにすることが多いのですが,この設計の場合,コピーコンストラクタ(および代入演算子)が呼ばれたときに問題が発生します.何も対策をしていない場合,コピーコンストラクタ(および代入演算子)が呼ばれると単純にsocket_type型(POSIXではint,winsockではSOCKET型とtypedefされている)の変数が持つ値をコピーしてしまうので,どちらか一方のデストラクタが呼ばれた時点でもう一方のsocketクラスも通信ができなくなってしまうためです.

この問題に対する解決策として,コピーコンストラクタ(および代入演算子)が呼ばれた場合にはdescriptorを複写することが挙げられます.descriptorを複写するための関数は,POSIXではdup(),windowsではDuplicateHandleという関数が用意されているようで,具体的には,以下のようなコードになります.

template <int Type, int Family>
class basic_rawsocket {
public:
    ...
    
    basic_rawsocket& assign(const basic_rawsocket& cp) {
        if (&cp == this) return *this;
        this->close();
#ifdef CLX_WIN32
        if (cp.s_ != (socket_type)-1) {
            ::DuplicateHandle(::GetCurrentProcess(), (HANDLE)cp.s_,
                ::GetCurrentProcess(), (LPHANDLE)&s_,
                0, TRUE, DUPLICATE_SAME_ACCESS);
            }
#else
        if (cp.s_ != -1) s_ = ::dup(cp.s_);
#endif
        return *this;
    }
    ...
    
private:
    socket_type s_;
}

WSAStartup/WSACleanupの処理

POSIX socketとwinsockを同じインターフェースで提供しようとしたときに,問題になることの一つがWSAStartup()およびWSACleanup()の処理だろうと思います.winsockの場合,socketを使用する前には一度WSAStartup()を呼び,またプログラムが終了する前にWSACleanup()を呼ぶ必要があります.(確認できた限りの)多くのライブラリでは,それぞれに対応するメソッド(initialize()/terminate()など)を定義しておき,ユーザ側に呼び出してもらうという方法で対策しているようなのですが,使用する側から見るとこの方法には煩わしさを感じます.また,コンストラクタでWSAStartup()を呼び,デストラクタでWSACleanup()を呼ぶという方法も検討したのですが,この方法でもうまくいかない場合があるようです.

そこでboost::asioの実装を見てみると,boost::asioではコンストラクタでWSAStartup()を呼び,デストラクタでWSACleanup()を呼ぶようなクラスを別途作成し,さらにそのクラスの静的変数(インスタンス?)を定義しておき,winsockが使用されると分かると内部でその静的変数が定義されているヘッダファイルをインクルードすることによって解決しているようです.具体的なコードは以下のようになります.

class winsock_init {
public:
    winsock_init() {
        WSADATA dat;
        ::WSAStartup(MAKEWORD(2, 0), &dat);
    }
    	
    ~winsock_init() {
        ::WSACleanup();
    }
private:
    explicit winsock_init(const winsock_init& cp);
    winsock_init& operator=(const winsock_init& cp);
};

static winsock_init ws_init_;

このように記述することで,インクルードされたときに一度だけWSAStartup()が呼ばれ,そのクラスのデストラクタはプログラムが終了する時点に一度だけ呼ばれるので,ユーザはWSAStartup/WSACleanupの処理を気にせずsocketクラスを利用することができます.

References - CLX C++ Libraries

以上,socketクラスを作成する上で注意すべきと感じたことのまとめでした.ソースコードの取得,使用例などその他の説明は以下のリンクから.名前空間やクラス名などは,boost::asioに習って付けている部分が多いです.

  1. resolver ..... 名前解決を行うためのクラス
  2. socket ..... 各種ソケットクラスの基底となるクラス
  3. sockaddress ..... ソケット用アドレス(IPアドレス・ポート番号の組)を扱うためのクラス
  4. udp::socket ..... UDPで通信を行うためのクラス
  5. tcp::socket ..... TCPで通信を行うためのクラス
  6. tcp::acceptor ..... TCPのサーバ(ウェルカムソケット)用のクラス
  7. sockstream ..... データ通信をstd::cinやstd::coutのように扱うためのクラス

拡張 lexical_cast

C++で数字文字列<->数値の変換を行うときには専らlexical_castを使っているのですが, 16進数や8進数などに変換できないことに気づきました. そこで,strtolなどの関数を見てみるとbaseと言う引数で基数を指定できるように なっていたので,これに習ってlexical_castを拡張してみました. 場合によってsetf()するだけなので,拡張と言うほど大げさなものでもないですが.

lexical_castの第二引数には,基数としてstd::ios::dec,std::ios::hex,std::ios::octを指定することができます.基数を指定した場合,lexical_castは基数にしたがって数字文字列をそれぞれ10進数,16進数,8進数として変換を行います.尚,基数を省略した場合は10進数(std::ios::dec)として変換が行われるので, 普段はboost::lexical_castと同じ感覚で使うことができます.

また,第二引数にstd::ios::showbaseを指定した場合には,文字列に応じた変換がなされます.つまり,0xで始まる数字文字列であるならば16進数として,0で始まる数字文字列であるならば8進数として,それ以外の数字文字列は10進数として変換が行われます.

以下がサンプルプログラム.lexical_cast.hはCLX C++ Librariesからダウンロードして下さい.

#include <iostream>
#include <string>
#include "clx/lexical_cast.h"

int main(int argc, char* argv[]) {
    std::string s1 = "326";
    std::string s2 = "0xf2a";
    std::string serr = "nonum";
    
    try {
        int a = clx::lexical_cast<int>(s1, std::ios::oct);
        int b = clx::lexical_cast<int>(s2, std::ios::showbase);
        
        std::cout << "str " << s1 << ", val " << a << std::endl;
        std::cout << "str " << s2 << ", val " << b << std::endl;
        
        int c = clx::lexical_cast<int>(serr);
        std::cout << "str " << serr << ", val " << c << std::endl;
    }
    catch (clx::bad_lexical_cast e) {
        std::cerr << "exeption: " << e.what() << std::endl;
        std::exit(-1);
    }
    
    return 0;
}
Result
str 326, val 214
str 0xf2a, val 3882
exeption: bad lexical cast

String Algorithm

プログラムを書いていると,ちょっとした文字列操作が必要になる場合が多々あるのですが,C++だと若干の不便さを感じます.Boostを使っても良いのですが,せっかくなので一通りの文字列操作関数を記述してみました.実装する機能および関数名は,Rubyを参考にしました(一部,異なるものもありますが).機能ごとにできるだけ1ファイルで収まるように記述したので,該当のヘッダファイル(+ predicate.h)をコピーするだけと言う手軽さが嬉しいときもあるかな,と.

以下に,実装した機能の宣言一覧を列挙.基本的には,xxx_copy(),xxx_if(),xxx_copy_if()という関数も同時に定義してあります.また,template < ... >の部分は省略しましたが,単語の先頭が大文字になっているものはテンプレート引数として渡される型です(ただし,Stringはstd::basic_string<CharT, Traits>という形になっています).

adjust.h

String& ljust(String& s, unsigned int n, CharT c = ' ');
String& rjust(String& s, unsigned int n, CharT c = ' ');
String& center(String& s, unsigned int n, CharT c = ' ');

それぞれ,文字列を左詰め,右詰め,センタリングして,余白を指定された文字で埋めます.これらの関数には,xxx_copy(),xxx_if(),xxx_copy_if()という関数は存在しません.

case_conv.h

String& upcase(String& s, const std::locale& loc = std::locale());
String& downcase(String& s, const std::locale& loc = std::locale());
String& swapcase(String& s, const std::locale& loc = std::locale());
String& capitalize(String& s, const std::locale& loc = std::locale());

upcase,downcaseは,文字列中のアルファベットをそれぞれ,大文字,小文字にします.swapcaseは,大文字は小文字に,小文字は大文字に変換します.capitalizeは,文字列中の先頭の文字を大文字に変換します.

remove.h

String& remove(String& s, CharT c);
String& unique(String& s);
String& squeeze(String& s, CharT c);

remove,およびuniqueは,それぞれSTL Algorithmで定義されているstd::remove,std::uniqueのラッパ関数です.STL Algorithmでは,文字列を実際には消去しないため後処理まで行うようにしています(参考:Standard Template Library プログラミング on the Web:アルゴリズム(Algorithm)).uniqueは,文字列中において連続している全ての文字を一文字にします.これに対してsqueezeは,文字列中において連続している文字cを一文字にします.

replace.h

String& replace(String& s, const String& sch, const String& rep,
    unsigned int nth = 1);
String& replace(String& s, const CharT* sch, const CharT* rep,
    unsigned int nth = 1);
String& replace_all(String& s, const String& sch, const String& rep);
String& replace_all(String& s, const CharT* sch, const CharT* rep);

replaceは,文字列中に存在するnth番目の文字列schを文字列repに置換します.これに対してreplace_allは,文字列中に存在する全ての文字列schを文字列repに置換します.これらの関数には,xxx_copy(),xxx_if(),xxx_copy_if()という関数は存在しません.尚,文字列中におけるある文字を別の文字で置き換える操作についてはSTL Algorithmを使用します.

std::replace_if(s.begin(), s.end(), std::bind2nd(std::equal_to<CharT>(), sch), rep)

split.h

Container& split(const String& s, Container& result,
    bool x = false, const std::locale& loc = std::locale());
String& join(const Container& v, String& result, const String& delim);
String& join(const Container& v, String& result, const CharT* delim);

splitは,空白文字を区切り文字として文字列を分割し,結果をContainerに格納します.そのため,Containerにはstd::vector<String>など,分割した文字列を格納できるものを渡す必要があります.joinはその逆関数であり,Containerに格納されている文字列群を文字列delimを区切り文字として結合し,一つの文字列にします.splitには,split_copy(),split_copy_if()が,joinには,join_copy(),join_if(),join_copy_if()が存在しません.

strip.h

String& lstrip(String& s, const std::locale& loc = std::locale());
String& rstrip(String& s, const std::locale& loc = std::locale());
String& strip(String& s, const std::locale& loc = std::locale());
String& chop(String& s);
String& chomp(String& s);

stripは,文字列の前後に存在する空白文字を除去します.lstrip,rstripはそれぞれ,文字列の先頭,末尾に存在する空白文字を除去します.chopは,末尾の文字を1文字除去し,chompは,末尾の改行文字を除去します.

階層的CSSコンバータ

CSSの記述テクニック 階層宣言コーディングから.エイプリルフールネタを掘り起こすのはどうかとも思いましたが,意外と便利そうな記述方法でしたので,コンバータを記述してみました.以下が,階層的CSSを解析するためのクラスのプロトタイプ宣言.

namespace clx {
    template <
        class CharT,
        class Traits = std::char_traits<CharT>
    >
    class basic_hierarchical_css {
    public:
        typedef CharT char_type;
        typedef unsigned int size_type;
        typedef std::basic_string<CharT, Traits> string_type;
        typedef std::vector<string_type> selector_list;
        typedef std::vector<string_type> declaration_list;
        
        basic_hierarchical_css();
                
        template <class InputIterator>
        explicit basic_hierarchical_css(InputIterator first, InputIterator last);
        explicit basic_hierarchical_css(const char_type* filename);
        explicit basic_hierarchical_css(const string_type& filename);
        
        template <class InputIterator>
        basic_hierarchical_css& assign(InputIterator first, InputIterator last);
        basic_hierarchical_css& assign(const char_type* filename);        
        basic_hierarchical_css& assign(const string_type& filename);
        
        size_type size() const;
        const string_type& selector(size_type i) const;
        const declaration_list& declarations(size_type i) const;
    };
    
    typedef basic_hierarchical_css<char> hcss;
}

コンストラクタ,またはassign()メソッドに,ファイル名,または入力ストリームのイテレータを渡すと階層的CSSの解析を行い,結果を配列(vector)に格納します.解析結果にアクセスするためのメソッドは,selector()およびdeclarations()メソッドで,それぞれ添え字に対応するセレクタおよび宣言リストを返します.declarations()で返されるのは配列(vector)であるため,さらにat()メソッドでそれぞれの宣言にアクセスします.

例外処理は今の所ごく簡易なもので,以下の場合のみ例外を投げます.

  • 指定されたファイルが存在しない場合.
  • コメント記号の対("/*","*/")の数が合わない場合.
  • 中括弧の対("{","}")の数が合わない場合.

制限,その他注意事項としては,以下の通り(いくつかは修正するかも).

  • @規則が存在すると解析に失敗する.
  • コメントは全て読み捨てる.
  • 中括弧(“{”,“}”),セミコロン(“;”),コメント記号(“/*”,“*/”)の直後に空白や改行文字が挿入されていないと,解析に失敗する.

さて,このクラスを使用した簡単なコンバータのプログラム例を書いてみました.

#include <cstdlib>
#include <iostream>
#include "hcss.h"

int main(int argc, char* argv[]) {
    if (argc < 2) {
        std::cerr << "usage: hcssconv filename" << std::endl;
        std::exit(-1);
    }
    
    try {
        clx::hcss hc;
        hc.assign(argv[1]);
        
        // print
        for (size_t i = 0; i < hc.size(); i++) {
            if (hc.declarations(i).empty()) continue;
            std::cout << hc.selector(i) << " {" << std::endl;
            for (size_t j = 0; j &lt; hc.declarations(i).size(); j++) {
                std::cout << "  " << hc.declarations(i).at(j) << std::endl;
            }
            std::cout << '}' << std::endl;
            std::cout << std::endl;
        }
    }
    catch (std::runtime_error& e) {
        std::cerr << e.what() << std::endl;
        std::exit(-1);
    }
    
    return 0;
}

このプログラムは引数としてファイル名を渡すと,そのファイル中に記述されている階層的CSSを解析し,結果を標準出力へ出力します.以下のサンプルで試してみます.尚,現在のところ,コンパイルgccでのみ確認.

body {
	p.caution {
 		color: red;
 		font-weight: bold;
	}

	div.entry {
 		background-color: #eee;
		p.caution {
			border: 1px solid green;
			margin: 1em;
		}
	}
}

実行結果は,以下の通り.

p.caution {
  color: red;
  font-weight: bold;
}

div.entry {
  background-color: #eee;
}

div.entry p.caution {
  border: 1px solid green;
  margin: 1em;
}

なかなか面白いかもしれません.hcss_20070406.tar.gz

追記

hcssクラスをもうすこし真面目に書き直したバージョン(with clx).最新の説明は,こちら

Stream Iterator

しばしば,実験データから何らかの統計値(平均,分散,...)を求めたい場合があります.今回も,RMSE(Root Mean Square Error:平均2乗平方根誤差)を求めなければならない場面に遭遇しました.それで,せっかくなので汎用的に使える関数として残しておこうと思い,以下のような関数を書きました.

template <class Type, class InputIterator, class ValueT>
Type mse(InputIterator first,
    const InputIterator& last, ValueT correct) {
    Type sum = 0;
    Type n = 0;
    while (first != last) {
        Type tmp = static_cast<Type>(*first++)
            - static_cast<Type>(correct);
        sum += std::pow(tmp, 2);
        n++;
    }
    return sum / n;
}

template <class Type, class InputIterator, class ValueT>
Type rmse(InputIterator first,
    const InputIterator& last, ValueT correct) {
    return std::sqrt(mse(first, last, correct));
}

使い方としては,rmse < test.datのようにデータファイルをリダイレクトで渡すような形を考えていました.それで,メインプログラムをどのような形で書こうかな,と.最初に思いついたのは,streambuf_iteratorを使う方法だったのですが,これだと一文字づつ読み込んでしまい(100 200 300の場合,'1', '0', '0', ' ', '2', ...)期待した動きをしてくれませんでした.しばらくWebを彷徨っていたところ,stream_iteratorを利用すればうまくいく模様(100 200 300の場合,"100", "200", "300").そういう訳で,最終的なプログラム.

int main(int argc, char* argv[])
{
    if (argc < 2) exit(-1);
    int correct = std::atoi(argv[1]);
    
    std::istream_iterator<double> input(std::cin);
    std::istream_iterator<double> last;
    
    double val = rmse<double>(input, last, correct);
    std::cout << "RMSE: " << val << std::endl;
    
    return 0;
}
|<