How to extend Boost.Asio

この記事はC++ Advent Calendar 2012 12日目の記事です。

Boost.Asio、みなさんもお使いのことと存じますが、あれこのソケットなかったっけ? とかこれもAsioで使いたいなぁと思ったことはありませんか?
そんな時のためにこの記事ではBoost.AsioのProtocol, socket option, serviceの拡張を扱います。

基礎知識:
I/O service: ご存知boost::asio::io_service
I/O object: 実際にAsioを使う人が触るobject。socketやtimerなど。(例: boost::asio::tcp::socket, boost::asio::deadline_timer等)
Service: boost::asio::io_service::serviceを継承しI/O serviceに登録される。I/O objectはこのServiceをテンプレート引数に取りI/O objectに対する命令(send, recv, wait等)はServiceに渡される。(例: boost::asio::stream_sockets_service, boost::asio::deadline_timer_service等)

もしこれから説明するBoost.Asioのendpointやprotocolのイメージがつかみにくい方は

The BSD Socket API and Boost.Asio
www.boost.org/doc/html/boost_asio/overview/networking/bsd_sockets.html

をよんでいただくとソケットプログラミングの経験がある方にはaddress_v4がin_addrにendpointがsocketaddrに対応することなどがよくわかるかと思います。


・Protocolの拡張
まずはProtocolの拡張をしてみます。
ここでは例としてbasic_raw_socketにプロトコルを追加してみます。
Asioにはraw socketに対するサポートが有りますが、対応しているのはicmpだけです。
そこでtcpを扱うraw socketのプロトコルを追加してみます。
ようは

int raw_tcp_socket = socket(AF_INET, SOCK_RAW, IPPROTO_TCP);

をBoost.Asioで使える用にしてみます。

template<typename Protocol, typename Service>
class basic_socket;

を継承したbasic_stream_socket, basic_datagram_socket, basic_raw_socketのそれぞれの第二テンプレート引数は
stream_sockets_service, datagram_socket_service, raw_socket_serviceがデフォルト引数になっています。
なので第一引数のProtocolだけ指定すればいいです。

Protocolはシステムコールレベルではsocket(2)の3つの引数に相当します。
socket(2)の定義は

int socket(int domain, int type, int protocol);

です。

Asioのソケットが扱えるプロトコル

Protocol requirements:
http://www.boost.org/doc/libs/1_52_0/doc/html/boost_asio/reference/Protocol.html

を満たさなければなりません。
上記のページにはXをプロトコル、aをXのオブジェクトとした時
domainはa.family()、typeはa.type()、protocolにはa.protocol()が対応すると書いて有ります。
つまりa.family()はAF_INET、a.type()はSOCK_RAW、a.protocol()はIPPROTO_TCPを返せばいい訳です。
また、X::endpointを定義しなければなりませんがこれはAsioのboost::asio::ip::basic_endpointが使えます。
そこでこんなクラスを用意しました。

template<int Family, int FamilyV6, int Type, int Protocol>
class basic_raw_protocol
{
public:

    typedef basic_endpoint<basic_raw_protocol>   endpoint;
    typedef basic_raw_socket<basic_raw_protocol> socket;
    typedef basic_resolver<basic_raw_protocol>   resolver;

    static basic_raw_protocol v4()
    {
        return basic_raw_protocol(Protocol, Family);
    }

    static basic_raw_protocol v6()
    {
        return basic_raw_protocol(Protocol, FamilyV6);
    }

    int family() const
    {
        return family_;
    }

    int type() const
    {
        return Type;
    }

    int protocol() const
    {
        return protocol_;
    }

    friend bool operator==(const basic_raw_protocol& p1, const basic_raw_protocol& p2)
    {
        return p1.protocol_ == p2.protocol_ && p1.family_ == p2.family_;
    }

    friend bool operator!=(const basic_raw_protocol& p1, const basic_raw_protocol& p2)
    {
        return p1.protocol_ != p2.protocol_ || p1.family_ != p2.family_;
    }

private:

    explicit basic_raw_protocol(int protocol_id, int protocol_family)
        :  protocol_(protocol_id),
           family_(protocol_family)
    {
    }

    int protocol_;
    int family_;
};

追記: IPv6用のファミリーを指定し忘れていたのを修正
これをtypedefして

typedef basic_raw_protocol<AF_INET, AF_INET6, SOCK_RAW, IPPROTO_TCP> raw_tcp;

などとしておきます。これでbasic_raw_socketにTCPを扱うraw socketが追加出来ました。
使い方はboost::asio::ip::icmpと全く同じです。
ちなみにlibarexという自分用の小さいライブラリを作っており、その中でLinuxのpacket socketを使用できるよう拡張した時
basic_raw_protocolの第三引数のProtocolにホストバイトオーダからネットワークバイトオーダ変換する必要がありました。
そんな時は

constexpr std::uint16_t htons(std::uint16_t s)
{
    return (s >> 8) | (s << 8);
}

みたいな関数を作っておけばネットワークバイトオーダでテンプレート引数に渡せます。


・ソケットオプション
もう基本的にType Requirementsを満たせば何でもできます。
ソケットのソケットオプションを取得するには

Gettable socket option requirements
www.boost.org/doc/html/boost_asio/reference/GettableSocketOption.html

ソケットに渡すソケットオプションは

Settable socket option requirements
http://www.boost.org/doc/html/boost_asio/reference/SettableSocketOption.html

を満たせば良いわけです。それぞれ例のごとくPOSIXの関数であるgetsockopt(2), setsockopt(2)

int getsockopt(int socket, int level, int option_name,
       void *restrict option_value, socklen_t *restrict option_len);
int setsockopt(int socket, int level, int option_name,
       const void *option_value, socklen_t option_len);

に渡せるよう、level(), name(), data(), size()メンバー関数を定義すれば良いです。
注意点としてGettableの場合data()メンバー関数は(void *)に変換可能なポインター
Settableではdata()メンバー関数は(void const *)に変換可能なポインターを返さねばなりません、これだけです。
試しにこんなクラスを用意しました。

template<int Level, int Name, typename ValueType = int>
class basic_option {
public:

    basic_option() = default;

    basic_option(ValueType const& value) : optval_(value)
    {
    }

    template<typename Protocol>
    int level(Protocol const& p) const
    {
        return Level;
    }

    template<typename Protocol>
    int name(Protocol const& p) const
    {
        return Name;
    }

    template<typename Protocol>
    void *data(Protocol const &p)
    {
        // Should return a pointer that is convertible to void*
        return reinterpret_cast<void *>(&optval_);
    }

    template<typename Protocol>
    void const *data(Protocol const& p) const
    {
        // Should return a pointer that is convertible to void*
        return reinterpret_cast<void const *>(&optval_);
    }

    template<typename Protocol>
    size_t size(Protocol const& p) const
    {
        // size() returns the size of *data()
        return sizeof(optval_);
    }

    template <typename Protocol>
    void resize(Protocol const& p, std::size_t s)
    {
        if (s != sizeof(optval_)) {
            std::length_error ex("basic_option resize error");
            boost::throw_exception(ex);
        }
    }

    void set_value(ValueType const& val)
    {
        optval_ = val;
    }

    ValueType const& get_value() const
    {
        return optval_;
    }

private:
    ValueType optval_;
};

使い方としてSO_RCVTIMEOオプションを扱ってみます。
こんな感じで書けます。

#include <iostream>
#include <boost/asio.hpp>
#include <boost/assert.hpp>

namespace asio = boost::asio;

void print_opt(struct timeval const& tv)
{
    std::cout << "socket option: sec=" << tv.tv_sec
        << " usec=" << tv.tv_usec << std::endl;
}

int main(int argc, char const* argv[])
{
    asio::io_service io;
    asio::ip::tcp::socket socket(io, asio::ip::tcp::v4());
    typedef basic_option<
        SOL_SOCKET,
        SO_RCVTIMEO,
        struct timeval
    > recvtimeo;

    recvtimeo getopt;
    socket.get_option(getopt);
    print_opt(getopt.get_value());

    struct timeval recvtv = {5, 0};
    recvtimeo opt(recvtv);
    socket.set_option(opt);

    recvtimeo afteropt;
    socket.get_option(afteropt);
    print_opt(afteropt.get_value());

    BOOST_ASSERT(opt.get_value().tv_sec == afteropt.get_value().tv_sec);
    BOOST_ASSERT(opt.get_value().tv_usec == afteropt.get_value().tv_usec);

    return 0;
}


・I/O objectとServiceの拡張
ここでは超手抜きのタイマーを作ります。(もはや某所の翻訳)
必要なもの:
boost::asio::basic_io_objectを継承するI/Oオブジェクト。(easy_timer)
boost::asio::io_service::serviceを継承するService。(basic_timer_service)
Serviceの実装(implementation)クラス (timer_impl)
順番に説明していきます。

まず、下に示すeasy_timerを見て下さい。

#include <boost/asio.hpp>

template <typename Service>
class easy_timer : public boost::asio::basic_io_object<Service>
{
public:
    explicit easy_timer(boost::asio::io_service& io_service)
        : boost::asio::basic_io_object<Service>(io_service)
    {
    }

    void wait(std::size_t seconds)
    {
        return this->get_service().wait(this->get_implementation(), seconds);
    }

    template <typename Handler>
    void async_wait(std::size_t seconds, Handler handler)
    {
        this->get_service().async_wait(this->get_implementation(), seconds, handler);
    }
};

このクラスは手抜タイマーのI/Oオブジェクトです。実際にAsioを使う人が触るobjectです。
basic_io_object継承することにより、このI/Oオブジェクトがインスタンス化された時Serviceは自動的にI/O serviceに登録されます。
テンプレート引数のServiceへはbasic_io_objectのget_service()でアクセスできます。処理は基本的にServiceに丸投げします。
その時、一緒に実装(this->get_implementation())を取得し渡します。
(get_service(), get_implementation()の代わりにservice, implementatioデータメンバーも有りますがどちらも現在ではdeprecatedです。)

要点は:
・Serviceの登録とServiceの実装の生成は親クラスのbasic_io_objectがする。
・メンバー関数(wait, async_wait等)は基本的にServiceに実装(this->get_implementation())を渡して丸投げ。
です。

次はServiceことbasic_timer_serviceです。

#include <boost/asio.hpp>
#include <boost/thread.hpp>
#include <boost/bind.hpp>
#include <boost/scoped_ptr.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/weak_ptr.hpp>
#include <boost/system/error_code.hpp>

template <typename TimerImplementation = timer_impl>
class basic_timer_service : public boost::asio::io_service::service {
public:
    static boost::asio::io_service::id id;

    explicit basic_timer_service(boost::asio::io_service& io_service)
        : boost::asio::io_service::service(io_service),
          async_work_(new boost::asio::io_service::work(async_io_service_)),
          async_thread_(boost::bind(&boost::asio::io_service::run, &async_io_service_))
    {
    }

    ~basic_timer_service()
    {
        async_work_.reset();
        async_io_service_.stop();
        async_thread_.join();
    }

    typedef boost::shared_ptr<TimerImplementation> implementation_type;

    void construct(implementation_type& impl)
    {
        impl.reset(new TimerImplementation());
    }

    void destroy(implementation_type& impl)
    {
        impl->destroy();
        impl.reset();
    }

    void wait(implementation_type& impl, std::size_t seconds)
    {
        boost::system::error_code ec;
        impl->wait(seconds, ec);
        boost::asio::detail::throw_error(ec);
    }

    template <typename Handler>
    class wait_operation
    {
    public:
        wait_operation(implementation_type& impl, boost::asio::io_service& io_service, std::size_t seconds, Handler handler)
            : impl_(impl),
              io_service_(io_service),
              work_(io_service),
              seconds_(seconds),
              handler_(handler)
        {
        }

        void operator()() const
        {
            implementation_type impl = impl_.lock();
            if (impl) {
                boost::system::error_code ec;
                impl->wait(seconds_, ec);
                this->io_service_.post(boost::asio::detail::bind_handler(handler_, ec));
            }
            else {
                this->io_service_.post(boost::asio::detail::bind_handler(handler_, boost::asio::error::operation_aborted));
            }
        }

    private:
        boost::weak_ptr<TimerImplementation> impl_;
        boost::asio::io_service &io_service_;
        boost::asio::io_service::work work_;
        std::size_t seconds_;
        Handler handler_;
    };

    template <typename Handler>
    void async_wait(implementation_type& impl, std::size_t seconds, Handler handler)
    {
        this->async_io_service_.post(wait_operation<Handler>(impl, this->get_io_service(), seconds, handler));
    }

private:
    void shutdown_service()
    {
    }

    boost::asio::io_service async_io_service_;
    boost::scoped_ptr<boost::asio::io_service::work> async_work_;
    boost::thread async_thread_;
};

template <typename TimerImplementation>
boost::asio::io_service::id basic_timer_service<TimerImplementation>::id;

Boost.Asioに揃えるためにServiceは以下のような事を満たさねばなりません。
・boost::asio::io_service::serviceを継承し、コンストラクタがboost::asio::io_service::serviceのコンストラクタに渡されたI/O servieへの参照を受け取ること。
・いかなるServiceも一意に識別するためにpublicでstaticなboost::asio::io_service::idを持つ。
・implementation_type(TimerImplementationのshared_ptr)を受け取るconstruct()とdestruct()というメンバー関数を持つ。
・shutdown_service()メンバー関数を持つ。この関数はprivateにでき、多くの場合何もしない関数である。

async_wait()はwait()の非同期版です。
非同期処理を実現するためにこの例ではboost::threadを使っています。
boost::threadで実行するのはService用に用意されたio_serviceであるasync_io_service_のboost::asio::io_service::run()です。
run()関数はServiceのコンストラクタで実行する非同期処理がない状態で実行されるのでboost::asio::io_service::workですぐにreturnするのを防ぎます。
async_io_service_にpost()するのはoperator()をオーバーロードしたwait_operation関数オブジェクトです。
operator()()ではimplementationがdestruct()されているかもしれないのでTimerImplementationのshared_ptrであるimpl引数からweak_ptrを作ってlock()で有効であるか確認してから
同期版wait()と同じようにimplementationのwait()を呼び出しその後、handler_を呼び出します。

最後にimplementationことtimer_implです。

#include <boost/system/error_code.hpp>
#include <cstddef>
#include <unistd.h>

class timer_impl
{
public:
    timer_impl()
    {
    }

    ~timer_impl()
    {
    }

    void destroy()
    {
    }

    void wait(std::size_t seconds, boost::system::error_code& ec)
    {
        int ret = sleep(seconds);
        if (ret == 0)
            ec = boost::system::error_code();
        else
            ec = boost::asio::error::operation_aborted;
    }
};

はい、もう解説はいらないでしょう。ただ呼び出しスレッドをsleep(3)するだけです。(超簡易なのでお許しを)

以上です。こんなふうに使えます。

#include <boost/asio.hpp>
#include <iostream>
#include <chrono>
#include <functional>
//
// timer_impl, basic_timer_service, easy_timerをincludeする
//

// 今回作ったタイマー
typedef easy_timer<basic_timer_service<> > etimer;

// Asioのタイマー
typedef boost::asio::basic_waitable_timer<std::chrono::steady_clock> ctimer;

void wait_handler(boost::system::error_code const& ec, int sec)
{
    std::cout << sec << " sec" << std::endl;
}

int main()
{
    boost::asio::io_service io_service;

    etimer timer(io_service);
    timer.async_wait(5, std::bind(&wait_handler, std::placeholders::_1, 5));

    ctimer timer2(io_service, std::chrono::seconds(3));
    timer2.async_wait(std::bind(&wait_handler, std::placeholders::_1, 3));

    std::cout << "[*] Main: start io_service.run()" << std::endl;
    io_service.run();
    std::cout << "[*] Main: finished io_service.run()" << std::endl;
    return 0;
}

3秒後に"3 sec", 5秒後に"5 sec"と表示されて終わるはずです。

以上でひと通り荒いですが、終了です。

もし、もうちょっとコード読みたいなぁって方はgithub
GitHub - pfpacket/libarex: Asio Rawsocket EXtension
こんなもの上げてるので良かったらどうぞ。

次はid:lnseabさんです。