John Torjoの本Boost.Asio C ++ Network Programmingの翻訳を続けています。
内容:
- 第1章:Boost.Asioの使用開始
- 第2章:Boost.Asioの基本
- 第3章:エコーサーバー/クライアント
- 第4章:クライアントとサーバー
- 第5章:同期と非同期
- 第6章:Boost.Asio-その他の機能
- 第7章:Boost.Asio-追加トピック
この章では、小さなクライアント/サーバーアプリケーションを実装します。これは、おそらく最も単純なクライアント/サーバーアプリケーションです。 このアプリケーションはエコーサーバーであり、クライアントに書き込んだ内容をクライアントに返し、クライアントの接続を閉じます。 サーバーは、任意の数のクライアントと連携できます。 新しいクライアントが接続すると、メッセージを送信します。 サーバーはメッセージ全体を受信し、返信します。 その後、彼は接続を閉じます。
したがって、各エコークライアントはサーバーに接続し、メッセージを送信し、サーバーが応答したものを読み取り、これが送信したメッセージと同じであることを確認して、サーバーとの通信を終了します。
最初に同期アプリケーションを実装し、次に非同期アプリケーションを実装して、簡単に比較できるようにします。
ここではすべてのコードが表示されるわけではありませんが、記事の最後にあるリンクですべてのコードを見ることができます。
TCPエコーサーバー/クライアント
TCPの場合、各メッセージが文字「\ n」で終わるという追加の利点があります。 同期サーバー/クライアントエコーの作成は非常に簡単です。
同期クライアント、同期サーバー、非同期クライアント、非同期サーバーなどのプログラムの例を示します。
TCP同期クライアント
自明でないほとんどの例では、通常、クライアントコードはサーバーよりもはるかに単純です(サーバーは複数のクライアントを処理する必要があるため)。
次の例は、ルールの例外です。
ip::tcp::endpoint ep( ip::address::from_string("127.0.0.1"), 8001); size_t read_complete(char * buf, const error_code & err, size_t bytes) { if ( err) return 0; bool found = std::find(buf, buf + bytes, '\n') < buf + bytes; // we read one-by-one until we get to enter, no buffering return found ? 0 : 1; } void sync_echo(std::string msg) { msg += "\n"; ip::tcp::socket sock(service); sock.connect(ep); sock.write_some(buffer(msg)); char buf[1024]; int bytes = read(sock, buffer(buf), boost::bind(read_complete,buf,_1,_2)); std::string copy(buf, bytes - 1); msg = msg.substr(0, msg.size() - 1); std::cout << "server echoed our " << msg << ": "<< (copy == msg ? "OK" : "FAIL") << std::endl; sock.close(); } int main(int argc, char* argv[]) { char* messages[] = { "John says hi", "so does James", "Lucy just got home", "Boost.Asio is Fun!", 0 }; boost::thread_group threads; for ( char ** message = messages; *message; ++message) { threads.create_thread( boost::bind(sync_echo, *message)); boost::this_thread::sleep( boost::posix_time::millisec(100)); } threads.join_all(); }
sync_echo
関数に注意して
sync_echo
。 これには、サーバーに接続するためのすべてのロジックが含まれており、サーバーにメッセージを送信して、戻り応答を待ちます。
文字 '\ n'までのメッセージ全体を受信するため、読み取りにfree
read()
関数を使用していることに気付きました。
sock.read_some()
関数は十分ではありません。なぜなら、それは利用可能なものだけを読むからであり、メッセージ全体をまったく読むわけではないからです。
read()関数の3番目の引数は最終ハンドラーです。 メッセージが完全に読み取られた場合、0を返します。 それ以外の場合、最大バッファサイズが返され、次のステップで読み取ることができます(
read
完了するまで)。 この例では、必要以上に誤って読みたくないため、常に1が返されます。
main()
いくつかのスレッドを作成します。 クライアントが送信するメッセージごとに1つのスレッドを作成し、完了するまで待機します。 プログラムを実行すると、次の出力が表示されます。
server echoed our John says hi: OK server echoed our so does James: OK server echoed our Lucy just got home: OK server echoed our Boost.Asio is Fun!: OK
同期クライアントを扱っているため、
service.run()
を呼び出す必要がないことに注意してください。
TCP同期サーバー
次のコードフラグメントに示すように、同期エコーサーバーの作成は非常に簡単です。
io_service service; size_t read_complete(char * buff, const error_code & err, size_t bytes) { if ( err) return 0; bool found = std::find(buff, buff + bytes, '\n') < buff + bytes; // we read one-by-one until we get to enter, no buffering return found ? 0 : 1; } void handle_connections() { ip::tcp::acceptor acceptor(service, ip::tcp::endpoint(ip::tcp::v4(),8001)); char buff[1024]; while ( true) { ip::tcp::socket sock(service); acceptor.accept(sock); int bytes = read(sock, buffer(buff), boost::bind(read_complete,buff,_1,_2)); std::string msg(buff, bytes); sock.write_some(buffer(msg)); sock.close(); } } int main(int argc, char* argv[]) { handle_connections(); }
すべてのサーバーロジックは、
handle_connections()
囲まれてい
handle_connections()
。 シングルスレッドであるため、新しいクライアントを受け入れ、彼が送信したメッセージを読み取り、それを送り返し、次のクライアントを待ちます。 たとえば、2つのクライアントが同時に接続する場合、サーバーが最初のクライアントにサービスを提供している間、2番目のクライアントは待機する必要があります。
同期して作業しているため、
service.run()
を呼び出す必要はありません。
TCP非同期クライアント
非同期で作業を開始するとすぐに、コードはもう少し複雑になります。 2番目の章に示すように、
connection
クラスをモデル化し
connection
。
このセクションの次のコードスニペットを見ると、各非同期操作が新しい非同期操作を開始し、service.run()を操作中に維持していることがわかります。
まず、主な機能:
#define MEM_FN(x) boost::bind(&self_type::x, shared_from_this()) #define MEM_FN1(x,y) boost::bind(&self_type::x, shared_from_this(),y) #define MEM_FN2(x,y,z) boost::bind(&self_type::x, shared_from_this(),y,z) class talk_to_svr : public boost::enable_shared_from_this<talk_to_svr>,boost::noncopyable { typedef talk_to_svr self_type; talk_to_svr(const std::string & message) : sock_(service), started_(true),message_(message) {} void start(ip::tcp::endpoint ep) { sock_.async_connect(ep, MEM_FN1(on_connect,_1)); } public: typedef boost::system::error_code error_code; typedef boost::shared_ptr<talk_to_svr> ptr; static ptr start(ip::tcp::endpoint ep, const std::string & message) { ptr new_(new talk_to_svr(message)); new_->start(ep); return new_; } void stop() { if ( !started_) return; started_ = false; sock_.close(); } bool started() { return started_; } ... private: ip::tcp::socket sock_; enum { max_msg = 1024 }; char read_buffer_[max_msg]; char write_buffer_[max_msg]; bool started_; std::string message_; };
talk_to_svr
は常に共有ポインターを使用するため、
talk_to_svr
インスタンスには非同期操作が存在しますが、このインスタンスは存続します。 スタックに
talk_to_svr
インスタンスを作成するなどのエラーを回避するために、コンストラクターをプライベートにし、コピーコンストラクターを禁止しました(
boost::noncopyable
から継承)。
start(), stop()
、
started()
ような基本的な関数があり、それらの名前が示すとおりに機能します。 接続を作成するには、単に
talk_to_svr::start(endpoint, message)
呼び出し
talk_to_svr::start(endpoint, message)
。 また、読み取りおよび書き込み用のバッファー(
read_buffer_
および
write_buffer_
)もあります。
前に説明したように、次の行は大きく異なります。
// equivalent to "sock_.async_connect(ep, MEM_FN1(on_connect,_1));" sock_.async_connect(ep, boost::bind(&talk_to_svr::on_connect,shared_ptr_from_this(),_1)); sock_.async_connect(ep, boost::bind(&talk_to_svr::on_connect,this,_1));
最初のケースでは、最終ハンドラー
async_connect
を正しく作成します。これは、最終ハンドラーを呼び出すまで
talk_to_server
インスタンスへの共有ポインターを保存します。
後者の場合、誤って最終ハンドラーを作成します。
talk_to_server
インスタンスが
talk_to_server
、すでに削除されている可能性があります!
ソケットの読み取りと書き込みには、次のコードフラグメントを使用します。
void do_read() { async_read(sock_, buffer(read_buffer_), MEM_FN2(read_complete,_1,_2), MEM_FN2(on_read,_1,_2)); } void do_write(const std::string & msg) { if ( !started() ) return; std::copy(msg.begin(), msg.end(), write_buffer_); sock_.async_write_some( buffer(write_buffer_, msg.size()), MEM_FN2(on_write,_1,_2)); } size_t read_complete(const boost::system::error_code & err, size_t bytes) { // similar to the one shown in TCP Synchronous Client }
do_read()
関数は、まずサーバーからメッセージを確実に読み取り、その後に
on_read()
呼び出されるようにします。
do_write()
関数は、最初にメッセージをバッファーにコピーし(msgが時間の経過とともに範囲外になって
on_write()
する可能性があります)、実際の記録後に
on_write()
呼び出しが発生することを確認します。
そして、クラスのコアロジックを含む最も重要な関数:
void on_connect(const error_code & err) { if ( !err) do_write(message_ + "\n"); else stop(); } void on_read(const error_code & err, size_t bytes) { if ( !err) { std::string copy(read_buffer_, bytes - 1); std::cout << "server echoed our " << message_ << ": "<< (copy == message_ ? "OK" : "FAIL") << std::endl; } stop(); } void on_write(const error_code & err, size_t bytes) { do_read(); }
その後、サーバーに接続してメッセージ
do_write()
を送信します。 書き込み操作が完了すると、
on_write()
が呼び出され、
do_read()
関数が開始されます。
do_read()
完了すると、
do_read()
が呼び出されます。ここでは、サーバーからのメッセージが送信したものと同じであることを確認して終了します。
これをさらに面白くするためだけに、サーバーに3つのメッセージを送信します。
int main(int argc, char* argv[]) { ip::tcp::endpoint ep( ip::address::from_string("127.0.0.1"), 8001); char* messages[] = { "John says hi", "so does James", "Lucy got home", 0 }; for ( char ** message = messages; *message; ++message) { talk_to_svr::start( ep, *message); boost::this_thread::sleep( boost::posix_time::millisec(100)); } service.run(); }
前のコードフラグメントでは、次のコードが生成されます。
server echoed our John says hi: OK server echoed our so does James: OK server echoed our Lucy just got home: OK
TCP非同期サーバー
以下に示すように、主な機能は非同期クライアントの機能に非常に似ています。
class talk_to_client : public boost::enable_shared_from_this<talk_to_client>, boost::noncopyable { typedef talk_to_client self_type; talk_to_client() : sock_(service), started_(false) {} public: typedef boost::system::error_code error_code; typedef boost::shared_ptr<talk_to_client> ptr; void start() { started_ = true; do_read(); } static ptr new_() { ptr new_(new talk_to_client); return new_; } void stop() { if ( !started_) return; started_ = false; vsock_.close(); } ip::tcp::socket & sock() { return sock_;} ... private: ip::tcp::socket sock_; enum { max_msg = 1024 }; char read_buffer_[max_msg]; char write_buffer_[max_msg]; bool started_; };
これは非常に単純なエコーサーバーであり、
is_started()
関数は必要ありません。 各クライアントについて、送信したメッセージを読み取り、同じメッセージを送り返し、接続を閉じます。
関数
do_read(), do_write()
、および
read_complete()
、非同期TCPクライアントとまったく同じです。
クラスのメインロジックは、
on_read()
および
on_write()
関数にあります。
void on_read(const error_code & err, size_t bytes) { if ( !err) { std::string msg(read_buffer_, bytes); do_write(msg + "\n"); } stop(); } void on_write(const error_code & err, size_t bytes) { do_read(); }
クライアントとの連携は次のとおりです。
ip::tcp::acceptor acceptor(service, ip::tcp::endpoint(ip::tcp::v4(), 8001)); void handle_accept(talk_to_client::ptr client, const error_code & err) { client->start(); talk_to_client::ptr new_client = talk_to_client::new_(); acceptor.async_accept(new_client->sock(), boost::bind(handle_accept,new_client,_1)); } int main(int argc, char* argv[]) { talk_to_client::ptr client = talk_to_client::new_(); acceptor.async_accept(client->sock(), boost::bind(handle_accept,client,_1)); service.run(); }
クライアントがサーバーに接続する
handle_accep
、
handle_accep
tが
handle_accep
、このクライアントから非同期的に読み取りを開始し、非同期的に新しいクライアントを待機します。
UDPエコーサーバー/クライアント
UDPではすべてのメッセージが受信者に届くわけではないため、メッセージが完全に到着したという保証はありません。 UDPで作業するため、受信するすべてのメッセージは、ソケットを閉じずに(サーバー側で)出力するだけです。
UDP同期エコークライアント
UDPエコークライアントは、TCPエコークライアントよりも少し単純です。
ip::udp::endpoint ep( ip::address::from_string("127.0.0.1"), 8001); void sync_echo(std::string msg) { ip::udp::socket sock(service, ip::udp::endpoint(ip::udp::v4(), 0) ); sock.send_to(buffer(msg), ep); char buff[1024]; ip::udp::endpoint sender_ep; int bytes = sock.receive_from(buffer(buff), sender_ep); std::string copy(buff, bytes); std::cout << "server echoed our " << msg << ": "<< (copy == msg ? "OK" : "FAIL") << std::endl; sock.close(); } int main(int argc, char* argv[]) { char* messages[] = { "John says hi", "so does James", "Lucy got home", 0 }; boost::thread_group threads; for ( char ** message = messages; *message; ++message) { threads.create_thread( boost::bind(sync_echo, *message)); boost::this_thread::sleep( boost::posix_time::millisec(100)); } threads.join_all(); }
すべてのロジックは
synch_echo()
関数にあります。 サーバーへの接続、メッセージの送信、サーバーからの応答メッセージの受信、接続の終了。
UDP同期エコーサーバー
UDPエコーサーバーは、作成できる最も単純なサーバーです。
io_service service; void handle_connections() { char buff[1024]; ip::udp::socket sock(service, ip::udp::endpoint(ip::udp::v4(), 8001)); while ( true) { ip::udp::endpoint sender_ep; int bytes = sock.receive_from(buffer(buff), sender_ep); std::string msg(buff, bytes); sock.send_to(buffer(msg), sender_ep); } } int main(int argc, char* argv[]) { handle_connections(); }
ここではすべてが非常にシンプルで、それ自体が語っています。
演習として、非同期UDPサーバーとクライアントの記述は読者に任せましょう。
まとめ
いくつかのアプリケーションを作成し、最終的にBoost.Asioを開始しました。 これらのアプリケーションは、このライブラリを使い始めるのに非常に適しています。
次の章では、より複雑なクライアント/サーバーアプリケーションを作成し、メモリリークやデッドロックなどのエラーを回避する方法を学習します。
どうもありがとうございました!
この記事のリソース: リンク