John Torjoの本Boost.Asio C ++ Network Programmingの翻訳を続けています。
内容:
- 第1章:Boost.Asioの使用開始
- 第2章:Boost.Asioの基本
- 第3章:エコーサーバー/クライアント
- 第4章:クライアントとサーバー
- 第5章:同期と非同期
- 第6章:Boost.Asio-その他の機能
- 第7章:Boost.Asio-追加トピック
この章では、Boost.Asioのあまり知られていない機能のいくつかを見ていきます。 stdストリームとstreambufオブジェクトは、使用するのが少し難しい場合がありますが、自分でわかるように、それらには利点があります。 最後に、Boost.Asioへのかなり遅い追加が表示されます。コルーチンを使用すると、非同期コードを作成できますが、読みやすい(同期のように) これは非常に素晴らしい機能です。
stdストリームおよびstd入力/出力バッファー
このセクションで記述されていることを理解するには、STLストリームやSTL streambufなどのオブジェクトに精通している必要があります。
Boost.Asioには、I / Oを操作するための2種類のバッファーがあります。
- boost :: asio :: buffer()
- ブースト:: asio :: streambuf
この本を通して、基本的に次のようなものを見ました。
size_t read_complete(boost::system::error_code, size_t bytes){ ... } char buff[1024]; read(sock, buffer(buff), read_complete); write(sock, buffer("echo\n"));
通常、これで十分です。 ただし、より柔軟性が必要な場合は、
streambuf
を使用できます。 以下は、
streambuf
オブジェクトでできる最も単純で最悪なことです。
streambuf buf; read(sock, buf);
この読み取りは、
streambuf
オブジェクトが
streambuf
になるまで続きます
streambuf
オブジェクトは、より多くのスペースを収容するためにそれ自体を再配布できるため、基本的に、接続が閉じられるまで読み取りが行われます。
read_until
関数を使用して、最後の文字まで読み取ることができます。
streambuf buf; read_until(sock, buf, "\n");
ここで、読み取りは文字「\ n」まで進み、読み取られたものがバッファに追加され、読み取り機能が終了します。
streambuf
オブジェクトに何かを書き込むには、次のようなことを行います。
streambuf buf; std::ostream out(&buf); out << "echo" << std::endl; write(sock, buf);
とても簡単です。STLストリームを作成し、構築中にstreambufオブジェクトをそこに配置し、送信したいメッセージを
write
から、
write
関数を使用してバッファーの内容を送信する必要があります。
Boost.AsioおよびSTLストリーム
C Boost.Asioは、STLストリームとネットワークを統合して素晴らしい仕事をしました。 つまり、STLをすでに広く使用している場合は、オーバーロードされた演算子>>および<<を含む多くのクラスが既にあるはずです。 ソケットへの読み書きは、公園を散歩するよりも魅力的です。
次のコードスニペットがあるとします。
struct person { std::string first_name, last_name; int age; }; std::ostream& operator<<(std::ostream & out, const person & p) { return out << p.first_name << " " << p.last_name << " " << p.age; } std::istream& operator>>(std::istream & in, person & p) { return in >> p.first_name >> p.last_name >> p.age; }
ネットワークを介した人間のデータの転送は、以下に示すように簡単です。
streambuf buf; std::ostream out(&buf); person p; // ... initialize p out << p << std::endl; write(sock, buf);
反対側でも同じように簡単に読むことができます。
read_until(sock, buf, "\n"); std::istream in(&buf); person p; in >> p;
streambuf
オブジェクトともちろん対応する
std::ostream
を書き込みまたは
std::istream
を読み取りに使用することの本当に良い面は、通常と見なされるコードを書くことになります。
- ネットワークを介して送信されるものを作成する場合、複数のデータが存在する可能性が非常に高くなります。 したがって、最後に、データをバッファに追加します。 このデータが文字列でない場合は、まず文字列に変換する必要があります。 これはすべて、<<演算子を使用するとデフォルトで発生します。
- メッセージを読むとき、反対側でも同じことが起こります。 それを解析する必要があります。つまり、一度に1つのデータを読み取ります。データが文字列でない場合は、変換する必要があります。 これはすべて、読み取り時に>>演算子を使用するとデフォルトで発生します。
最後に、次のコードを使用して、コンソールに
streambuf
オブジェクトの内容をダンプする非常にクールなトリックが知られています。
streambuf buf; ... std::cout << &buf << std::endl; // dumps all content to the console
同様に、コンテンツを文字列に変換するには、次のコードスニペットを使用します。
std::string to_string(streambuf &buf) { std::ostringstream out; out << &buf; return out.str(); }
クラスstreambuf
前述したように、
streambuf
std::streambuf.
から派生してい
std::streambuf.
std :: streambufのように、コピーコンストラクターはありません。
さらに、次のようないくつかの追加機能があります。
-
streambuf ([max_size,] [allocator])
:この関数は、 streambufオブジェクトを作成します。 必要に応じて、オプションでメモリの割り当て/解放に使用される最大バッファサイズとアロケータを設定できます。 -
prepare(n)
:この関数は、n
文字の連続シーケンスを配置するために使用されるサブバッファーを返します。 読み取りまたは書き込みに使用できます。 この関数の結果は、streambuf
オブジェクトで機能する関数だけでなく、読み取り/書き込みを行うBoost.Asioの独立した関数で使用できます。 -
data()
:この関数は、バッファー全体を連続した文字シーケンスとして返し、書き込みに使用されます。 この関数の結果は、streambuf
オブジェクトで機能する関数だけでなく、記録するBoost.Asioの任意の独立した関数で使用できます。 -
consume(n)
:この関数では、データは(読み取り操作から)入力シーケンスから削除されます。 -
commit(n)
:この関数では、データは出力シーケンス(書き込み操作)から削除され、入力シーケンス(読み取り操作)に追加されます。 -
size()
:この関数は、streambuf
オブジェクト全体のサイズを文字数で返します。 -
max_size()
:この関数は、streambuf
オブジェクトにstreambuf
ことができる最大文字数を返します。
最後の2つの機能を除いて、残りはそれほど簡単に理解できません。 まず、ほとんどの場合、
streambuf
インスタンスを引数として送信し、以下に示すように、独立した関数を読み書きします。
read_until(sock, buf, "\n"); // reads into buf write(sock, buf); // writes from buf
前のフラグメントに示したように、バッファ全体を独立した関数に送信する場合、関数はまずバッファのサイズを増やすかどうかを確認し、入力ポインタと出力ポインタを探す必要があります。 つまり、読み取るデータがあれば、それを読み取ることができます。
例:
read_until(sock, buf, '\n'); std::cout << &buf << std::endl;
前のスニペットは、ソケットから読み取った内容をリセットします。 次の例では、何かをダンプしません。
read(sock, buf.prepare(16), transfer_exactly(16) ); std::cout << &buf << std::endl;
バイトは読み取られますが、ポインターは移動しません。 以下に示すように、自分で移動する必要があります。
read(sock, buf.prepare(16), transfer_exactly(16) ); buf.commit(16); std::cout << &buf << std::endl;
同様に、
streambuf
オブジェクトに書き込みたい場合、および独立した書き込み関数を使用する場合は、次のコードフラグメントを使用します。
streambuf buf; std::ostream out(&buf); out << "hi there" << std::endl; write(sock, buf);
次のコードは、
hi there
3回
hi there
を送信します。
streambuf buf; std::ostream out(&buf); out << "hi there" << std::endl; for ( int i = 0; i < 3; ++i) write(sock, buf.data());
これは、バッファが破壊されることはなく、データはそこに残るためです。 データを破棄する場合は、これがどのように実装されているかを確認してください。
streambuf buf; std::ostream out(&buf); out << "hi there" << std::endl; write(sock, buf.data()); buf.consume(9);
結論として、
streambuf
インスタンス全体を処理することを選択する必要があります。 微調整する場合は、前の機能を使用します。
読み取りと書き込みに同じstreambufインスタンスを使用できる場合でも、読み取り用と書き込み用の2つの別々のインスタンスをお勧めします。 それはより簡単に、より明確に知覚され、多くの起こりうる間違いを避けます。
streambuf
オブジェクトを操作する独立した関数
次のリストは、
streambuf
オブジェクトを操作するBoost.Asioの独立した関数を示しています。
-
read (sock, buf [, completion_function])
:この関数は、ソケットからstreambuf
オブジェクトに読み取ります。 最後の機能はオプションです。 そうである場合は、読み取り操作が成功するたびに呼び出され、操作が完了したかどうかをBoost.Asioに通知します(完了していない場合は読み取りを続行します)。 そのシグネチャは次のとおりですsize_t completion(const boost::system::error_code & err, size_t bytes_transfered)
完了すると、関数は0を返します。これは、読み取り操作が完全に完了したかどうかを意味します。 ゼロ以外の値を返す場合、これはread_some
ストリーム関数の次の呼び出しの最大バイト数が返されたことを意味します。 -
read_at(radom_stream, offset, buf [, completion_function])
:この関数はランダムストリームから読み取ります。 これはソケットには適用されないことに注意してください(ランダムフローの概念をモデル化していないため)。 -
read_until(sock, buf, char | string | regex | match_condition)
:この関数は、この条件が満たされている間に読み取ります。 特定の文字を読み取る必要があるか、一部の行または正規表現が読み取り行の1つと一致するか、match_condition
関数から関数を終了する必要があることが通知されます。match_condition
関数のシグネチャは次のとおりですmatch_conditionis pair<iterator,bool>match(iterator begin, iterator end)
; ここで、メインのイテレータはbuffers_iterator <streambuf::const_buffers_type>
です。 一致するものが見つかった場合、ペアは戻ります(passed-end-of-match
がtrue
設定されtrue
)。一致するものが見つからない場合、他のペアは戻ります(false
設定されbegin
false
)。 -
write(sock, buf [, completion_function])
:この関数は、すべてのコンテンツをstreambuf
オブジェクトに書き込みます。 終了関数はオプションであり、その動作は終了read()
関数に似ています:書き込み操作が完了すると0を返し、次のwrite_some
ストリーム関数の呼び出し中に書き込まれるバイト数が示されるとゼロ以外の値をwrite_some
。 -
write_at(random_stream,offset, buf [, completion_function])
:この関数はランダムストリームに書き込みます。 この場合も、ソケットには適用されません。 -
async_read(sock, buf [, competion_function], handler)
:この非同期doubleのread()
関数。 プロセッサのシグネチャは次のとおりですvoid handler(const boost::system::error_code, size_t bytes)
。 -
async_read_at(radom_stream, offset, buf [, completion_function] ,handler)
:これはread_at()
関数の非同期ダブルです。 -
async_read_until (sock, buf, char | string | regex | match_condition, handler)
:これはread_until()
関数の非同期ダブルです。 -
async_write(sock, buf [, completion_function] , handler)
:これはwrite()
関数の非同期ダブルです。 -
async_write_at(random_stream,offset, buf [, completion_function], handler)
:これはwrite_at()
関数の非同期doubleです。
母音まで読みたいとしましょう:
streambuf buf; bool is_vowel(char c) { return c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u'; } size_t read_complete(boost::system::error_code, size_t bytes) { const char * begin = buffer_cast<const char*>( buf.data()); if ( bytes == 0) return 1; while ( bytes > 0) { if ( is_vowel(*begin++)) return 0; else --bytes; } return 1; } ... read(sock, buf, read_complete);
たとえば、正規表現を使用する場合、これは非常に簡単です。
read_until(sock, buf, boost::regex("^[aeiou]+") );
または、例を少し変更して、
match_condition
関数を機能させることができます。
streambuf buf; bool is_vowel(char c) { return c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u'; } typedef buffers_iterator<streambuf::const_buffers_type> iterator; std::pair<iterator,bool> match_vowel(iterator b, iterator e) { while ( b != e) { if ( is_vowel(*b++)) return std::make_pair(b, true); } return std::make_pair(e, false); } ... size_t bytes = read_until(sock, buf, match_vowel);
コルーチン
2009年から2010年頃のBoost.Asioの作成者は、非同期アプリケーションの作成をさらに容易にするコルーチンの非常にクールなアイデアを実装しました。
これにより、非同期アプリケーションを簡単に記述でき、アプリケーションが順番に記述されているかのように、制御フローを簡単にたどることができます。
最初のケースでは、通常のアプローチが表示されます。 コルーチンを使用すると、2番目のケースにできるだけ近くなります。
簡単に言えば、コルーチンを使用すると、複数のエントリポイントを使用して、関数内の特定の場所で実行を一時停止および再開できます。
コルーチンを使用する場合は、2つのヘッダーファイルを含める必要があります。これらのファイルは、
boost/libs/asio/example/http/server4: yield.hpp
および
boost/libs/asio/example/http/server4: yield.hpp
のみ見つけることができます。 Boost.Asioでは、2つのマクロと1つのクラスが定義されています。
-
coroutine
:このクラスは、あなたの派生物またはコルーチンを実装するために使用するconnection
クラスです。 -
reenter(entry)
:これはコルーチン本体です。 入力引数は、たとえば、関数全体の内部でブロックとして使用されるサブルーチンへのポインターです。 - yieldコード:コルーチンの一部として命令を実行します。
よりよく理解するために、いくつかの例を検討してください。 第4章からアプリケーションを再実装します。これは、システムに入り、応答し、他のどのクライアントがログに記録されているかを通知する単純なクライアントです。
メインコードは次のようになります。
class talk_to_svr : public boost::enable_shared_from_this<talk_to_svr> , public coroutine, boost::noncopyable { ... void step(const error_code & err = error_code(), size_t bytes = 0) { reenter(this) { for (;;) { yield async_write(sock_, write_buffer_, MEM_FN2(step,_1,_2) ); yield async_read_until( sock_, read_buffer_,"\n", MEM_ FN2(step,_1,_2)); yield service.post( MEM_FN(on_answer_from_server)); } } } };
最初に変更されたのは、
connect(), on_connect(), on_read(),do_read(), on_write(), do_write()
など
connect(), on_connect(), on_read(),do_read(), on_write(), do_write()
多数のメンバー関数が消失した
step()
。
関数の本体は
reenter(this) { for (;;) { }}
ます。
reenter(this)
を最後に実行したコードと考えることができるため、次のコードを呼び出すことができます。
reenter
ブロック内では、いくつかの進行中の呼び出しを確認できます。 関数が最初に起動されると、
async_write
関数が
async_write
、2回目は
async_read_until
関数が
async_read_until
、3回目は
service.post
関数、4回目は
async_write
れます。
for(;;) {}.
インスタンスを決して忘れないでください
for(;;) {}.
次のコードを見てみましょう。
void step(const error_code & err = error_code(), size_t bytes = 0) { reenter(this) { yield async_write(sock_, write_buffer_, MEM_FN2(step,_1,_2) ); yield async_read_until( sock_, read_buffer_, "\n",MEM_FN2(step,_1,_2)); yield service.post( MEM_FN(on_answer_from_server)); } }
前のコードフラグメントを3回使用した場合、関数を入力して
service.post
を実行します。 4回目は、
service.post
を渡し、何もしません。 同じことが、5回目と次のすべての場合に発生します。
class talk_to_svr : public boost::enable_shared_from_this<talk_to_svr> , public coroutine, boost::noncopyable { talk_to_svr(const std::string & username) : ... {} void start(ip::tcp::endpoint ep) { sock_.async_connect(ep, MEM_FN2(step,_1,0) ); } static ptr start(ip::tcp::endpoint ep, const std::string & username) { ptr new_(new talk_to_svr(username)); new_->start(ep); return new_; } void step(const error_code & err = error_code(), size_t bytes = 0) { reenter(this) { for (;;) { if ( !started_) { started_ = true; std::ostream out(&write_buf_); out << "login " << username_ << "\n"; } yield async_write(sock_, write_buf_, MEM_FN2(step,_1,_2) ); yield async_read_until( sock_,read_buf_,"\n", MEM_FN2(step,_1,_2)); yield service.post( MEM_FN(on_answer_from_server)); } } } void on_answer_from_server() { std::istream in(&read_buf_); std::string word; in >> word; if ( word == "login") on_login(); else if ( word == "ping") on_ping(); else if ( word == "clients") on_clients(); read_buf_.consume( read_buf_.size()); if (write_buf_.size() > 0) service.post( MEM_FN2(step,error_code(),0)); } ... private: ip::tcp::socket sock_; streambuf read_buf_, write_buf_; bool started_; std::string username_; deadline_timer timer_; };
接続を開始すると、サーバーに非同期で接続する
start()
関数が呼び出されます。 接続が確立されると、初めて
step()
入ります。 これは、ユーザー名でメッセージを送信するときです。
この後、
async_write
を使用してから
async_write
を使用してメッセージを処理します(
on_answer_from_server
)。
on_answer_from_server
関数
on_answer_from_server
は
on_answer_from_server
着信メッセージを処理します。 最初の単語を読み、対応する関数に送信し、メッセージの残りを無視します(いずれにしても):
class talk_to_svr : ... { ... void on_login() { do_ask_clients(); } void on_ping() { std::istream in(&read_buf_); std::string answer; in >> answer; if ( answer == "client_list_changed") do_ask_clients(); else postpone_ping(); } void on_clients() { std::ostringstream clients; clients << &read_buf_; std::cout << username_ << ", new client list:" << clients. str(); postpone_ping(); } void do_ping() { std::ostream out(&write_buf_); out << "ping\n"; service.post( MEM_FN2(step,error_code(),0)); } void postpone_ping() { timer_.expires_from_now(boost::posix_time::millisec(rand() % 7000)); timer_.async_wait( MEM_FN(do_ping)); } void do_ask_clients() { std::ostream out(&write_buf_); out << "ask_clients\n"; } };
ランダムな瞬間にサーバーとの接続を確認する必要があるため、この例はもう少し複雑です。 これを行うには、初めて顧客リストを正常に要求した後、ping操作を延期します。 次に、サーバーからの各pingに対して、別のping操作を延期します。
これをすべて実行するには、次のコードスニペットを使用します。
int main(int argc, char* argv[]) { ip::tcp::endpoint ep( ip::address::from_string("127.0.0.1"), 8001); talk_to_svr::start(ep, "John"); service.run(); }
コルーチンを使用して、コードを15行に減らし、さらに読みやすくしました。 ここでは、コルーチンのトピックについてはほとんど触れませんでした。 この問題の詳細については、このページにアクセスしてください 。
まとめ
Boost.AsioがSTLストリームとstreambufオブジェクトをどのように簡単に使用できるかを見ました。 また、コルーチンによってコードがよりコンパクトになり、理解が容易になることも確認しました。
次の章では、Asio vs. Boost.Asio、プログレッシブデバッグ、SSL、およびその他のプラットフォーム固有の機能などのトピックを扱います。
この記事のリソース: リンク
ご清聴ありがとうございました、すぐに会いましょう!