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

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のように扱うためのクラス