読者です 読者をやめる 読者になる 読者になる

堕(惰)プログラマ開発記録

タイトル変えようかなとも思ってるけれど,思い浮かばない

Boost.Asioばかり触っていた人がcpp-netlibを調べてみた

C++ Boost

この記事はC++ Advent Calendar 2012 24日目の記事です.こんな記事ですけど(((
今日はイブらしいですけど,当然予定なんてありません.悔しくなんて無いしっ


さて,cpp-netlibというライブラリについて紹介するのですが,似たようなライブラリ(BoostConnectとかtwit-libraryとか…)を作ってたりしたので実装に興味があったのと,簡単な日本語エントリーが少ないよなぁってことで,これについて調べてみようと思ったのがこのエントリーを書くきっかけです.

cpp-netlibとは

まずはあまり知らない人のために軽く概要を.
cpp-netlibクロスプラットフォームなネットワーク関連のライブラリとして開発され,Boost.Asioを中心に様々なBoostのライブラリを使用することで実現しています.簡単に言うとBoost.Asioのラッパーなライブラリです.Boost.Asioについては他の方の記事を参考にするとよく分かるのでは無いでしょうか.

またcpp-netlibはBoostコミュニティに関係のある人を中心にBoost Software Licenseにとして開発が行われ,Boostにレビューのための提出をしています.ライブラリが使用している名前空間を見ても解ると思うのですが,Boost.Networkの候補とかなんとかって言われてたやつです.

  • ネットワーククライアントとサーバー
  • STLフレンドリーなメッセージのアダプタとラッパーのコレクション
  • メッセージパーサー
  • カプセル化されたメッセージに関する情報を格納する

この4つを提供している,ヘッダーオンリー使用可能なライブラリです.

準備

まずはヘッダー群をダウンロードしてきます.cpp-netlib/cpp-netlib · GitHubから0.9-devel branchをcheckoutするか,Tagsからcpp-netlib-0.9.4.zipを解凍.

#define BOOST_NETWORK_NO_LIB をincludeの前に書くことでヘッダーオンリーの使用をすることが可能です.ただ内部でBoost.Spirit.Qiを使っているのもあって,コンパイル時間がご想像の通りBoooooost!*1するのでcmakeかbjamかでライブラリとしてビルドしておくことを強くおすすめします.

以下はg++を使ってDebugビルドなライブラリを作る例です.

$ cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++
$ make

masterから持ってくると上手く行かないはずです.
あと,bjamもあまりメンテされてないみたいで編集が必要です.

簡単なHTTP通信

ではとりあえず使ってみましょう.
まずはBoostのトップページにHTTP通信でGETしてみます.

HTTPリクエスト

boost::network::http::client::request を利用してHTTPリクエストに必要なヘッダ情報を追加していきます.
operator<<を使って boost::network::header をどんどん投げつけていくことが可能です.
こんなかんじに.

boost::network::http::client::request request("http://www.boost.org/");
request
    << boost::network::header("Connection", "close")
    << boost::network::header("UserAgent", "BoostAsio");
HTTP通信

boost::network::http::client のGETやPOSTなどといったメンバ関数に作成した boost::network::http::client::request を引数として呼び出すと, boost::network::http::client::response が返されます.(通信開始)

boost::network::http::client client;
boost::network::http::client::response response = client.get(request);

boost::asio::io_service が裏に隠れてて取っ付きやすそうだけど,それが逆に欠点でも有るのかなという気もします.io_serviceは便利ですが,乱立すると手に負えなくなるので.したがってio_serviceをclientの引数として渡し,それをclientに利用してもらうことを私個人としては推奨します.*2

HTTPレスポンス

受け取ったresponseにレスポンス情報が返されます.このレスポンス情報でheadersを呼ぶとレスポンスヘッダ,bodyを呼ぶとレスポンスボディが返されます.これらを呼び出すことでそれぞれのwrapperが返されますので,それを操作することやキャストが可能です.*3
その他,以下のような関数が用意されています.これらの関数にresponseを与えた時点で通信終了を待機します.デフォルトでは非同期通信(後述)なのですが,このまま同期通信のように見せかけることもできます.

headers レスポンスヘッダ
body レスポンスボディ
status ステータスコード
status_message ステータスメッセージ
version HTTPバージョン


サンプル:

const int status_code = status(response);
const std::string status_mes = status_message(response);
const std::string http_ver = version(response);
std::cout << "Version: " << http_ver << std::endl;
std::cout << "Code: " << status_code << std::endl;
std::cout << "Message: " << status_mes  << std::endl;

typedef boost::network::headers_range<boost::network::http::client::response>::type response_headers;
boost::for_each(headers(response),
    [](const response_headers::value_type& value)->void
    {
        std::cout << value.first << ": " << value.second << std::endl;
    });
std::cout << "\n---Body---\n" << body(response) << std::endl;

型名が長い気がしたりしますが,typedefすればまあそれなりに.header系はrangeとしてそのまま使えます.

簡単なHTTP通信のまとめ

ここまでのHTTP通信を叩くだけのコードをまとめてみるとこんな感じ.

// リクエスト作成
boost::network::http::client::request request("http://www.boost.org/");
request<< boost::network::header("Connection", "close");

// GETで通信
boost::network::http::client client;
boost::network::http::client::response response = client.get(request);

//レスポンスヘッダーを書き出し
typedef boost::network::headers_range<boost::network::http::client::response>::type response_headers;
boost::for_each(
    headers(response),
    [](const response_headers::value_type& value)->void
    {
        std::cout << value.first << ": " << value.second << std::endl;
    });

//レスポンスボディの書き出し
std::cout << "\n---Body---\n" << body(response) << std::endl;
$ g++ -std=c++11 sample1.cpp -I./cpp-netlib -L./cpp-netlib/libs/network/src -lpthread -lboost_system -lboost_thread -lcppnetlib-uri -lcppnetlib-client-connections -lssl -lcrypto 


実行結果:

Accept-Ranges: bytes
Connection: close
Content-Type: text/html
Date: Wed, 19 Dec 2012 12:40:26 GMT
Server: Apache/2.0.52 (Red Hat)
Transfer-Encoding: chunked

---Body---
38b
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>                                        
  <meta name="generator" content="HTML Tidy for Windows (vers 1st November 2003), see www.w3.org" />
                                              
  <title>Boost C++ Libraries</title>
……


……….
ちょっとBoostConnect(前述の自作Boost.Asioラッパもどき)を見返したら泣けてきました.

非同期SSL通信

cpp-netlibで非同期通信するのも割と簡単で,基本的なコードは変わらないです.ということでとりあえず題材として,TwitterのUserStreamと通信してみましょうか.

Twitterのsample.jsonについて

とはいっても,cpp-netlibだけではOAuth認証するにはたりないので,妥協してBasic認証でも使えるsample.jsonを使用してみます.GET statuses/sample | Twitter Developersを一読すれば分かるのですが,ランダムなツイートを送り続けてくるAPIです.

HTTPS(SSL)通信するには

まずヘッダーのincludeの前に #define BOOST_NETWORK_ENABLE_HTTPS が必要です.defineしないと実行時に「HTTPS not supported.」とか怒られます.

あとは boost::network::http::client::request にhttps://から始まるURLを渡してあげるだけで,自動的にSSL通信を始めます.証明書等を使用する場合はclientのコンストラクタで渡してあげればOKです.

ストリーミングするには

boost::network::http::client というのは boost::network::http::basic_client にテンプレートを適用した別名であり,

#define BOOST_NETWORK_HTTP_CLIENT_DEFAULT_TAG tags::http_async_8bit_udp_resolve
……
typedef basic_client<BOOST_NETWORK_HTTP_CLIENT_DEFAULT_TAG, 1, 1> client;

となっています.

テンプレート引数1つ目のタグは 何ビットの(tcp|udp)(非同期|同期)通信 を行うことを示しており,2つ目・3つ目でHTTPバージョンを示しています.よってデフォルトでは「非同期で8ビットのUDP通信をHTTP1.1」で行うというわけです.


ということは

typedef boost::network::http::basic_client<boost::network::http::tags::http_async_8bit_tcp_resolve, 1, 1> client_type;

にするとTCPで非同期なTCP通信がHTTP1.1で動きます.ただし,clientの型を変えると,request, response etc.の型も変える必要があります.

また,clientのメンバ関数呼び出し時にハンドラーを渡すと,それが逐次呼び出せれるのでこれを利用します.渡すことのできるハンドラーは const boost::iterator_range& と const boost::system::error_code& を引数として取るものであり,今回はstructを作るのが面倒なのと,折角なのでC++11感を出したいので,ハンドラーとしてラムダを渡すことにします.

とりあえず書いてみた

とりあえず書いて動かしてみました.
やはりrequestやresponseの型に左右するので,typedefしておくのが必須ですね.

typedef boost::network::http::basic_client<boost::network::http::tags::http_async_8bit_tcp_resolve, 1, 1> client_type; 
typedef boost::network::headers_range<client_type::response>::type response_headers;
const std::string auth = base64_encode(user_id + ":" + password);

client_type::request request("https://stream.twitter.com/1.1/statuses/sample.json");
request
    << boost::network::header("Connection", "close")
    << boost::network::header("Authorization", "Basic "+auth);

client_type client;
client_type::response response = client.get(request,
    [](const boost::iterator_range<const char*>& range, const boost::system::error_code& ec)
    {
        // ハンドラーです
        if(!ec)
        {
            std::cout << range; // ただ表示するだけ
        }
    });

std::cout << "Status Code: " << status(response) << std::endl;


実行結果:

Status Code: 200
7b
{"delete":{"status":{"id":282132178014461952,"user_id":31002938,"id_str":"282132178014461952","user_id_str":"31002938"}}}

7d
{"delete":{"status":{"id":282131955724718080,"user_id":246868423,"id_str":"282131955724718080","user_id_str":"246868423"}}}

11cd
{"created_at":"Fri Dec 21 14:35:48 +0000 2012","id":282132266094821378,"id_str":"282132266094821378","text
……
改良・発展

このままではずっとデータが流れ続けるだけで終了しません.
先ほどclientのコンストラクタでio_serviceを渡すことができる話をしましたが,今度はそれを利用して流れ続けるデータを途中で停止させてみます.

具体的な方法としてはハンドラーにカウンターを仕込み,そのカウンターが一定数を超えた時にio_serviceをstopするという方法がわかりやすいでしょう.以下がそれを実装したコードとその実行結果です.

client_type client(io_service);
client_type::response response = client.get(request,
    [&io_service](const boost::iterator_range<const char*>& range, const boost::system::error_code& ec)
    {
        static int counter = 1;
        if(!ec)
        {
            std::cout << range;
            ++counter;
        }
        if(counter == 10) io_service.stop();
    });

io_service.run();
std::cout << "\n\nStatus Code: " << status(response) << std::endl;


実行結果:

7d
{"delete":{"status":{"id":211388026851565568,"user_id":396940109,"id_str":"211388026851565568","user_id_str":"396940109"}}}

……

6f
{"delete":{"status":{"id":20985348166,"user_id":150563721,"id_str":"20985348166","user_id_str":"150563721"}}}



Status Code: 200


これである程度流れたところ(10回ハンドラーが呼ばれたところ)で通信がストップし,終了するようになりました.但しハンドラーはチャンクごとに呼ばれる仕様では無いようです..

HTTPサーバー

さて,ここまでクライアント側としての使い方について説明してきましたが,これからはサーバーとしての使い方について説明していきます.とは言っても疲れたあまり真新しいものが見つからなかったので,軽く説明して流しますが……

呼び出し

サーバーを立てて呼び出す部分はこんなコードになります.

struct response_maker;
typedef boost::network::http::server<response_maker> server_type;

response_maker handler;
server_type server("192.168.1.16", "50000", handler);
server.run();

これによってhttp://192.168.1.16:50000/に接続できるようになるわけです.

リクエストハンドラー

response_makerはリクエストを受信するたびに実行されるハンドラーであり,

  • void operator() (const server_type::request&, server_type::response&)
  • void log(const char*)

を持っていることを要求されます.というわけで,それを実現する簡単なresponse_makerはこんな感じになります.

struct response_maker{
    void operator() (const server_type::request& request, server_type::response& response)
    {
        std::ostringstream data;
        data << "Your Address is " << (server_type::string_type)source(request) << "<br />\n";
        data << "Request Path is " << (server_type::string_type)destination(request) << "\n";
        response = server_type::response::stock_reply(server_type::response::ok, data.str());
    }
    void log(const char*){} // Nothing
};

レスポンス解析と同様,リクエストもラッパーを返す関数が用意されています.

source 相手のアドレス
destination リクエストされたパス
headers リクエストヘッダ
body リクエストボディ


ちなみにこのコードを実行して,例えば192.168.1.68のPCから http://192.168.1.16:50000/hoge に接続すると,

Your IP Address is 192.168.1.68
Request Path is /hoge

と表示されます.

まとめ

  • Boost.Asioをラップしたcpp-netlib
  • 事前にビルドせずにヘッダーのみでの使用をすることも可能
  • リクエストやレスポンスを直感的に書ける
  • コードにハンドラーが大量に存在し,読みにくくなるのを防げる
  • HTTPヘッダーのパースも行ってくれるため,解りにくいと評判のBoost.Spirit.Qiを触らなくて良い

終わりに

英語で用意されているドキュメントを無理やりまとめてみた感じになってしまいました.今回の記事はここで終わりなのですが,ちょこちょこと本文で触れていたBoostConnectやらとtwit-libraryとやらをまとめたいなという野望のようなものが残ってたりします.というか,寧ろそっちを記事にするべきだったのでは……


なんか記事としてまとめてみたので気がついたのですが,こうやってまとめると頭の中の内容がすっきりしますね.やっぱり大したネタじゃなくても日数を重ねて更新していくことが大切なのでしょうか…….


さて,明日は25日.@hotwatermorningさんです.
規格書を読もう - How to disappear completely


<12/25 追記>
cpp-netlibの実装を読んでみたりより深く知りたい時は,同じC++AdC2012のこちらの記事なんかを頭に入れてから読むと分かりやすいと思いますよー.
How to extend Boost.Asio - devm33の備忘録
C++ AdventCalendar 2012 9日目 「Boost.AsioでGraceful Restart」 - にゃははー
Boost.Asioによる非同期関数呼び出しと、非同期ノンブロッキングFuture - NIES - 白線オール

*1:言いたかっただけです

*2:そうするとなぜかclang++でコンパイル失敗,g++では成功する

*3:詳しくはcpp-netlib/boost/network/message/wrappers/のコードを.