「Boost.Asio C ++ネットワークプログラミング。」 第3章:エコーサーバー/クライアント

みなさんこんにちは!
John Torjoの本Boost.Asio C ++ Network Programmingの翻訳を続けています。

内容:


この章では、小さなクライアント/サーバーアプリケーションを実装します。これは、おそらく最も単純なクライアント/サーバーアプリケーションです。 このアプリケーションはエコーサーバーであり、クライアントに書き込んだ内容をクライアントに返し、クライアントの接続を閉じます。 サーバーは、任意の数のクライアントと連携できます。 新しいクライアントが接続すると、メッセージを送信します。 サーバーはメッセージ全体を受信し、返信します。 その後、彼は接続を閉じます。
したがって、各エコークライアントはサーバーに接続し、メッセージを送信し、サーバーが応答したものを読み取り、これが送信したメッセージと同じであることを確認して、サーバーとの通信を終了します。
最初に同期アプリケーションを実装し、次に非同期アプリケーションを実装して、簡単に比較できるようにします。



ここではすべてのコードが表示されるわけではありませんが、記事の最後にあるリンクですべてのコードを見ることができます。


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_accephandle_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を開始しました。 これらのアプリケーションは、このライブラリを使い始めるのに非常に適しています。
次の章では、より複雑なクライアント/サーバーアプリケーションを作成し、メモリリークやデッドロックなどのエラーを回避する方法を学習します。

どうもありがとうございました!

この記事のリソース: リンク

Source: https://habr.com/ru/post/J195386/


All Articles