「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_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を開始しました。 これらのアプリケーションは、このライブラリを使い始めるのに非常に適しています。

次の章では、より複雑なクライアント/サーバーアプリケーションを作成し、メモリリークやデッドロックなどのエラーを回避する方法を学習します。



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



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



All Articles