記事ディレクトリ
- 1 はじめに
- 2. クライアントの設計
- 3. サーバー設計
- 4. 効果試験
- 5. 発生した問題
-
-
- 5.1. サーバーで発生した問題
- 5.2. 数字 12 や文字列などの通常のメッセージを送信する 構造プロトコルなどを送信する場合に protobuf を使用する理由
- 5.3. エラーが発生しました! エラー コード: 10054 メッセージ: リモート ホストは既存の接続を強制的に閉じました。[システム:10054]
- 5.4、std::shared_ptr<std::thread> t = std::make_shared<std::thread>() および () 関数の違いと使用法を追加
- 5.5. void Server::Session(std::shared_ptr<<boost::asio::ip::tcp::socket>>ソケット, uint32_t user_id) for ループに read_some を記述する必要がある理由
-
- 6. std::make_shared と std::shared_ptr
- 7. まとめ: 同期読み取りと書き込みの長所と短所
1 はじめに
先ほど、同期的に読み書きするboost::asioのAPI関数を紹介しました。今度は、前のAPI を直列に接続して、実行中のクライアントとサーバーを作成します。クライアントとサーバーは、ブロッキング同期読み取りおよび書き込みを使用して通信を完了します。
2. クライアントの設計
クライアント設計の基本的な考え方は、サーバー ピアのIP とポートに基づいてエンドポイントを作成し、このエンドポイントに接続するためのソケットを作成します。その後、データの読み取りと同期の方法でデータを送受信できるようになります。書き込み。
- エンドポイント(IP+ポート)を作成します。
- ソケットを作成します。
- ソケット接続エンドポイント。
- データを送信または受信します。
client.h:
#pragma once
#ifndef __CLIENT_H_2023_8_16__
#define __CLIENT_H_2023_8_16__
#include<iostream>
#include<boost/asio.hpp>
#include<string>
#define Ip "127.0.0.1"
#define Port 9273
#define Buffer 1024
class Client {
public:
Client();
bool StartConnect();
private:
std::string ip_;
uint16_t port_;
};
#endif
クライアント.cpp:
#include"client.h"
Client::Client() {
ip_ = Ip;
port_ = Port;
}
bool Client::StartConnect() {
try {
//Step1: create endpoint
boost::asio::ip::tcp::endpoint ep(boost::asio::ip::address::from_string(ip_), port_);
//Step2: create socket
boost::asio::io_context context;
boost::asio::ip::tcp::socket socket(context, ep.protocol());
//Step3: socket connect endpoint
boost::system::error_code error = boost::asio::error::host_not_found;
socket.connect(ep, error);
if (error) {
std::cout << "connect failed,error code is: " << error.value() << " .error message is:" << error.message() << std::endl;
return false;
}
else {
std::cout << "connect successed!" << std::endl;
}
while (true) {
//Step4: send message
std::cout << "Enter message:";
char req[Buffer];
std::cin.getline(req, Buffer);
size_t req_length = strlen(req);
socket.send(boost::asio::buffer(req, req_length));
//Step5: receive message
char ack[Buffer];
size_t ack_length = socket.receive(boost::asio::buffer(ack, req_length));
std::cout << "receive message: " << ack << std::endl;
}
}
catch (boost::system::system_error& e) {
std::cout << "Error occured!Error code: " << e.code().value() << ". Message: " << e.what() << std::endl;
return e.code().value();
}
return true;
}
- このコードはクライアント用の C++ プログラムであり、以前に定義された Client クラスを使用して接続を確立し、サーバーと通信します。コードの動作を 1 行ずつ説明します。
-
#include "client.h":このファイルでこのクラスを使用するには、クライアント クラスのヘッダー ファイルをインクルードします。
-
Client::Client():これはクライアント クラスのコンストラクターであり、ip_ および port_ メンバー変数を初期化します。
-
bool Client::StartConnect():サーバーへの接続を開始し、通信を行うために使用されるメンバー関数です。
-
try:考えられる例外をキャッチするための例外処理ブロックを開始します。
-
boost::asio::ip::tcp::endpoint ep(boost::asio::ip::address::from_string(ip_), port_);: TCP エンドポイントを作成し、サーバーの IP アドレスとポート番号を指定します。接続します。
-
boost::asio::io_context context;:非同期 I/O 操作の管理に使用される I/O コンテキスト オブジェクトを作成します。
-
boost::asio::ip::tcp::socketsocket(context, ep.protocol());:以前に作成した I/O コンテキストと指定されたプロトコルを使用して TCP ソケットを作成します。
-
boost::system::error_code error = boost::asio::error::host_not_found;:エラー コード オブジェクトを作成し、接続の初期状態として見つからないホストのエラー コードに初期化します。
-
socket.connect(ep, error);:サーバーへの接続を試行し、接続が失敗した場合は、接続エラーの情報を反映するようにエラー コードが更新されます。
-
if (error):エラー コードを確認します。0 でない場合は、接続が失敗したことを意味します。
-
std::cout << "接続に失敗しました。エラー コードは次のとおりです: " << error.value() << " .エラー メッセージは次のとおりです:" << error.message() << std::endl;: 出力接続失敗エラーコードとエラーメッセージ。
-
else:接続が成功した場合は、このブランチに入ります。
-
while (true):メッセージを継続的に送受信するための無限ループ。
-
std::cout << "Enter message:";:ユーザーにメッセージの入力を求めます。
-
char req[Buffer];:ユーザーが入力したメッセージを格納するための文字配列を作成します。
-
std::cin.getline(req, Buffer);:ユーザー入力からメッセージの行を読み取ります。
-
size_t req_length = strlen(req);:ユーザー入力メッセージの長さを取得します。
-
socket.send(boost::asio::buffer(req, req_length));:ユーザーが入力したメッセージをサーバーに送信します。
-
char ack[Buffer];:サーバーから返されたメッセージを受信するための文字配列を作成します。
-
size_t ack_length =ソケット.receive(boost::asio::buffer(ack, req_length));:サーバーから返されたメッセージを受信します。
-
std::cout << "receive message: " << ack << std::endl;:受信したメッセージを出力します。
-
catch (boost::system::system_error& e):例外をキャッチします。例外が発生した場合は、このブランチに入ります。
-
std::cout << "エラーが発生しました。エラー コード: " << e.code().value() << "。メッセージ: " << e.what() << std::endl;: 例外情報を出力します、エラー コードとエラー メッセージが含まれます。
-
return e.code().value();:例外のエラー コードを返します。
-
return true;:例外がない場合は、通信が成功したことを示す true を返します。
-
要約すると、このコードはクライアント オブジェクトを作成し、サーバーに接続して、ユーザーがメッセージを入力してサーバーに送信し、サーバーから返されたメッセージを受信して表示できるようにする単純なループを実装します。同時に、接続中や通信中に発生する可能性のある異常事態にも対応できます。
メイン.cpp:
#include"client.h"
int main() {
Client client;
if (client.StartConnect()) {
;
}
return 0;
}
-
このコードは、前に作成したクライアント クラスを使用する main 関数です。それぞれの部分が何をするのか説明しましょう。
-
#include "client.h":この行にはクライアント クラスのヘッダー ファイルが含まれており、このクラスを main 関数で使用できるようになります。
-
int main():これはプログラムのメイン関数であり、プログラムのエントリ ポイントです。すべてのコードはここから実行されます。
-
Client client;:この行では、前に定義した Client クラスのコンストラクターを使用して、client というクライアント オブジェクトを作成します。
-
if (client.StartConnect()) { … }:この行は条件文を開始します。client.StartConnect() が呼び出され、サーバーとの接続を確立して通信を実行しようとします。接続が成功し、通信が正常である場合、StartConnect() 関数は true を返し、条件が満たされた分岐に入ります。
-
;:これは何も行わない空のステートメントです。コード内で実行される実際のアクションはないようなので、空のステートメントで表されます。
-
return 0;:この行は main 関数の最後の行で、main 関数の終了後にプログラムが正常に終了することを示すステータス コード 0 を返すようにプログラムに指示します。
-
要約すると、このコードはクライアント オブジェクトを作成し、そのStartConnect()関数を呼び出してサーバーに接続して通信します。その後、プログラムはステータス コード 0 で正常に終了します。接続または通信に問題がある場合は、必要に応じてエラー処理コードを追加できます。
3. サーバー設計
3.1、セッション機能
サーバーに対するクライアント要求を処理するセッション関数を作成し、クライアント接続を取得するたびにこの関数を呼び出します。セッション関数では、エコーモードで読み書きを行います、いわゆるエコーとは応答処理(リクエストとレスポンス)のことです。
void Server::Session(std::shared_ptr<boost::asio::ip::tcp::socket> socket,uint32_t user_id) {
try {
for (;;) {
char ack[Buffer];
memset(ack, '\0', Buffer);
boost::system::error_code error;
size_t length = socket->read_some(boost::asio::buffer(ack, Buffer), error);
if (error == boost::asio::error::eof) {
std::cout << "the usred_id "<<user_id<<"connect close by peer!" << std::endl;
socket->close();
break;
}
else if (error) {
throw boost::system::system_error(error);
}
else {
if (socket->is_open()) {
std::cout << "the usre_id " << user_id << " ip " << socket->remote_endpoint().address();
std::cout << " send message: " << ack << std::endl;
socket->send(boost::asio::buffer(ack, length));
}
}
}
}
catch (boost::system::system_error& e) {
std::cout << "Error occured ! Error code : " << e.code().value() << " .Message: " << e.what() << std::endl;
}
}
3.2、StartListen関数
StartListen 関数は、サーバーIP とデータを受信するポートに基づいてサーバーアクセプターを作成し、ソケットを使用して新しい接続を受信し、このソケットのセッションを作成します。
bool Server::StartListen(boost::asio::io_context& context) {
//create endpoint
boost::asio::ip::tcp::endpoint ep(boost::asio::ip::tcp::v4(), port_);
//create acceptor
boost::asio::ip::tcp::acceptor accept(context, ep);
//acceptor bind endport
//accept.bind(ep);
//acceptor listen
/*accept.listen(30);*/
std::cout << "start listen:" << std::endl;
for (;;) {
std::shared_ptr<boost::asio::ip::tcp::socket> socket(new boost::asio::ip::tcp::socket(context));
accept.accept(*socket);
user_id_ = user_id_ + 1;
std::cout << "the user_id "<<user_id_<<" client connect,the ip:" << socket->remote_endpoint().address() << std::endl;
//auto t = std::make_shared<std::thread>([&]() {
// this->Session(socket);
// });
auto t = std::make_shared<std::thread>([this, socket]() {
Session(socket,user_id_);
});
thread_set_.insert(t);
}
return true;
}
スレッドを作成してセッション関数を呼び出すと、ソケットの読み取りおよび書き込み用に独立したスレッドを割り当てることができ、ソケットの読み取りおよび書き込みによってアクセプターがブロックされないようにすることができます。
3.全体のデザイン
サーバー.h:
#pragma once
#ifndef __SERVER_H_2023_8_16__
#define __SERVER_H_2023_8_16__
#include<iostream>
#include<boost/asio.hpp>
#include<string>
#include<set>
#define Port 9273
#define Buffer 1024
#define SIZE 30
class Server {
public:
Server();
bool StartListen(boost::asio::io_context& context);
void Session(std::shared_ptr<boost::asio::ip::tcp::socket> socket,uint32_t user_id);
std::set<std::shared_ptr<std::thread>>& GetSet() {
return thread_set_;
}
private:
uint16_t port_;
uint32_t user_id_;
std::set<std::shared_ptr<std::thread>> thread_set_;
};
#endif
サーバー.cp:
#include"server.h"
Server::Server() {
port_ = Port;
user_id_ = 0;
thread_set_.clear();
}
void Server::Session(std::shared_ptr<boost::asio::ip::tcp::socket> socket,uint32_t user_id) {
try {
for (;;) {
char ack[Buffer];
memset(ack, '\0', Buffer);
boost::system::error_code error;
size_t length = socket->read_some(boost::asio::buffer(ack, Buffer), error);
if (error == boost::asio::error::eof) {
std::cout << "the usred_id "<<user_id<<"connect close by peer!" << std::endl;
socket->close();
break;
}
else if (error) {
throw boost::system::system_error(error);
}
else {
if (socket->is_open()) {
std::cout << "the usre_id " << user_id << " ip " << socket->remote_endpoint().address();
std::cout << " send message: " << ack << std::endl;
socket->send(boost::asio::buffer(ack, length));
}
}
}
}
catch (boost::system::system_error& e) {
std::cout << "Error occured ! Error code : " << e.code().value() << " .Message: " << e.what() << std::endl;
}
}
bool Server::StartListen(boost::asio::io_context& context) {
//create endpoint
boost::asio::ip::tcp::endpoint ep(boost::asio::ip::tcp::v4(), port_);
//create acceptor
boost::asio::ip::tcp::acceptor accept(context, ep);
//acceptor bind endport
//accept.bind(ep);
//acceptor listen
/*accept.listen(30);*/
std::cout << "start listen:" << std::endl;
for (;;) {
std::shared_ptr<boost::asio::ip::tcp::socket> socket(new boost::asio::ip::tcp::socket(context));
accept.accept(*socket);
user_id_ = user_id_ + 1;
std::cout << "the user_id "<<user_id_<<" client connect,the ip:" << socket->remote_endpoint().address() << std::endl;
//auto t = std::make_shared<std::thread>([&]() {
// this->Session(socket);
// });
auto t = std::make_shared<std::thread>([this, socket]() {
Session(socket,user_id_);
});
thread_set_.insert(t);
}
return true;
}
メイン.cpp:
#include"server.h"
int main() {
try {
boost::asio::io_context context;
Server server;
server.StartListen(context);
for (auto& t : server.GetSet()) {
t->join();
}
}
catch (std::exception& e) {
std::cerr << "Exception " << e.what() << "\n";
}
return 0;
}
ピアが接続するたびに、サーバーはacceptコールバック関数をトリガーしてセッションを作成します。セッションの読み取りおよび書き込みイベント トリガーとサーバーの受け入れトリガーに関しては、基礎となるasioの多重化モデルがイベントの準備ができたと判断した後にコールバックされます。現在、それはシングルスレッド モードであるため、それらはすべてメインスレッドでトリガーされます。
さらに、サーバーが終了しないのは、サーバー内にループがあるためではなく、 iocontextのrun関数を呼び出すためです。この関数はasio の最下層によって提供され、ループ内で準備完了イベントをディスパッチします。
4. 効果試験
5. 発生した問題
5.1. サーバーで発生した問題
5.1.1. 表示なしでバインドバインディングおよびリッスン監視関数を呼び出す
初期のブースト アクセプタをポートにバインドする方法と、後のブーストを最適化する方法があり、アクセプタの初期化時にポートを直接指定することでバインドと監視を実現できます。
StartListen 関数:
bool Server::StartListen(boost::asio::io_context& context) {
//create endpoint
boost::asio::ip::tcp::endpoint ep(boost::asio::ip::tcp::v4(), port_);
//create acceptor
boost::asio::ip::tcp::acceptor accept(context, ep);
//acceptor bind endport
//accept.bind(ep);
//acceptor listen
/*accept.listen(30);*/
std::cout << "start listen:" << std::endl;
for (;;) {
std::shared_ptr<boost::asio::ip::tcp::socket> socket(new boost::asio::ip::tcp::socket(context));
accept.accept(*socket);
user_id_ = user_id_ + 1;
std::cout << "the user_id "<<user_id_<<" client connect,the ip:" << socket->remote_endpoint().address() << std::endl;
//auto t = std::make_shared<std::thread>([&]() {
// this->Session(socket);
// });
auto t = std::make_shared<std::thread>([this, socket]() {
Session(socket,user_id_);
});
thread_set_.insert(t);
}
return true;
}
-
bool Server::StartListen(boost::asio::io_context& context):これはServerクラスのメンバー関数であり、サーバーの待機プロセスを開始するために使用されます。
-
boost::asio::ip::tcp::endpoint ep(boost::asio::ip::tcp::v4(), port_);: IPv4アドレスと指定されたポート番号を使用してTCPエンドポイントを作成します。
-
boost::asio::ip::tcp::acceptor accept(context, ep);:以前に作成したI/O コンテキストとエンドポイントを使用してTCPレシーバーを作成します。
-
std::cout << "start listen:" << std::endl;:リスニングを開始するメッセージを出力します。
-
for (;; ) { ... }:無限ループ。クライアントが接続してセッションを処理するのを継続的に待機するために使用されます。
-
std::shared_ptr<boost::asio::ip::tcp::socket>ソケット(new boost::asio::ip::tcp::socket(context));: tcp::socketを指すスマートを作成するクライアントとの接続を処理するためのポインタ。
-
* accept.accept( socket);:クライアント接続を待って受け入れ、以前に作成したソケットオブジェクトに接続ソケットを割り当てます。
-
user_id_ = user_id_ + 1;:さまざまな接続を識別するためにユーザー ID を追加します。
-
std::cout << "the user_id "<<user_id_<<" client connect,the ip:" <<ソケット->remote_endpoint().address() << std::endl;: クライアント接続のメッセージを出力します, クライアントのユーザー ID と IP アドレスを含めます。
-
auto t = std::make_shared <std::thread> ( [this,ソケット] { … });:クライアントセッションを処理するためのスレッドを作成します。スレッド内で、ラムダ式を介してSession関数を呼び出し、現在のソケットとユーザーIDを渡します。
-
thread_set_.insert(t); :作成されたスレッドをスレッド セットに追加し、メイン スレッドが終了する前にスレッドが完了するのを待ちます。
-
return true ;: trueを返し、監視プロセスが正常に開始されたことを示します。
-
accept.bind(ep)およびaccept.listen(30)に関する注意:
-
accept.bind(ep) : 上記のコードでは、このメソッドは呼び出されません。これは、acceptオブジェクトが作成時にエンドポイントepを渡しているため、明示的なバインディングは必要ありません。バインドとは、ソケットを特定の IP アドレスとポートにバインドすることを意味しますが、この場合、バインドはレシーバーの作成時にすでに行われています。
-
accept.listen(30):繰り返しますが、上記のコードでは、このメソッドは呼び出されません。listen()メソッドはソケットを待機状態にするために使用され、パラメータはキューに入れられた接続の最大数を示します。ただし、このコードでは、accept()メソッドを呼び出すと、ソケットが自動的に待機状態になり、クライアントの接続を待機するため、明示的に listen() メソッドを呼び出す必要はありません。
-
5.1.2. エラーが発生しました! エラー コード: 10009 メッセージ: 指定されたファイル ハンドルが無効です。[システム:10009]
start listen:
have client connect,the ip:127.0.0.1
Error occured!Error code : 10009 .Message: 提供的文件句柄无效。 [system:10009]
-
コードには、クライアントの接続後に「指定されたファイル ハンドルが無効です」エラーを引き起こす問題がまだいくつかあります。お客様の環境ではコードを直接実行できないため、考えられる原因と解決策をいくつか示します。
-
リソースの競合:複数のスレッドが同時にソケット オブジェクトにアクセスするため、リソースの競合やソケットの状態の不一致が発生する可能性があります。ミューテックスなどのメカニズムを使用して、ソケットの読み取りおよび書き込み時に適切な同期を確保します。
-
ソケットのライフサイクル:ソケットの使用が終了したら、ソケットが適切に閉じられていることを確認します。コードをチェックして、ソケットの使用が終了したときに各スレッドがソケットを閉じることを確認します。あるスレッドでソケットを閉じて、別のスレッドで引き続き使用しないでください。
-
ハンドルの再利用:ソケットが複数回使用または再利用されていないことを確認してください。すでに閉じられているソケットを使用しようとすると、「無効なファイル ハンドルが指定されました」エラーが発生する場合があります。
-
スレッドの同期:スレッドの実行を完了する前に、他のスレッドが完了するまで待機するようにしてください。main 関数で t->join() を使用して、すべてのスレッドの実行が完了するのを待ちます。
-
その他のエラー条件: 10009 エラーには、無効なソケットの使用、ソケットが閉じられているがまだ使用されているなど、いくつかの状況が考えられます。詳細については、エラー コードのコンテキストを詳しく調べる必要がある場合があります。
-
まとめると、この問題は、マルチスレッド環境でソケットのライフサイクルと状態を適切に管理することが原因である可能性があります。コードを注意深く確認して、ソケットが各スレッドで適切に使用および閉じられていること、および競合状態を回避するために適切な同期メカニズムが使用されていることを確認してください。問題が解決しない場合は、問題を切り分けるために各スレッドのコードをさらに詳しく調べる必要がある場合があります。
auto t = std::make_shared<std::thread>([this, socket]() {
Session(socket);
});
为啥 auto t = std::make_shared<std::thread>([&]() {
this->Session(socket);
});
传引用不行
コードでは[&]を使用して参照を渡しますが、接続の処理に非同期スレッドを使用しているため、バックグラウンド スレッドの実行時に参照の内容が無効になり、無効なリソースにアクセスする可能性があります。これがエラーの原因である可能性があります。
正しい方法は、ラムダ関数でパラメーター ソケットをキャプチャし、それを (参照ではなく) 値で渡すことです。これにより、スレッドの実行時にソケット オブジェクトが依然として有効であることが保証されます。これが、コードの最初の例の動作です。
auto t = std::make_shared<std::thread>([this, socket]() {
Session(socket);
});
-
この問題は、スレッド間の競合状態に関連している可能性があります。C++ では、マルチスレッド環境でシェア変数にアクセスする場合、あるスレッドがリソースを変更している間に別のスレッドがそのリソースにアクセスし、未定義の動作が発生するという競合状態が発生しないようにする必要があります。
-
上記 2 つの書き方では、ダングリング参照の問題が発生する可能性があります。これは、Lambda 関数内で外部変数 (ソケット) が参照されているためですが、Lambda 関数の実行時に外部変数のライフサイクルが終了し、無効なリソースにアクセスしてしまう可能性があります。
-
最初の書き方では次のようになります。
- ソケットをキャプチャすると、ソケットオブジェクトがLambda関数にコピーされるため、無効化の問題は発生しません。
auto t = std::make_shared<std::thread>([this, socket]() {
Session(socket);
});
- 2 番目の書き方では次のようになります。
- 参照をキャプチャすると、ソケットオブジェクトの参照がLambda関数に渡されます。ただし、バックグラウンド スレッドの実行中に、メイン スレッドがソケットオブジェクトを終了または破棄し、無効なリソースにアクセスする可能性があります。
これらの問題を回避するために、マルチスレッド プログラミングでは一般に、スレッドが外部リソースにアクセスするときに、スレッドの実行中に外部リソースのライフ サイクルが終了しないようにすることが推奨されます。このような問題は、適切な同期メカニズム、ライフサイクル管理、およびダングリング参照の回避によって解決できます。
5.2. 数字 12 や文字列などの通常のメッセージを送信する 構造プロトコルなどを送信する場合に protobuf を使用する理由
-
ネットワーク通信では、データ送信では、データ形式、シリアル化と逆シリアル化、ネットワークバイトオーダーなどの複数の要素を考慮する必要があります。一般的なメッセージと単純なデータ型 (整数や文字列など) のみを送信する必要がある場合は、元のデータ形式を直接使用して送信できます。ただし、複雑なデータ構造、オブジェクト、クラス、ネストされたデータなどを転送する必要がある場合は、シリアル化プロトコルを使用する方が便利、安全、効率的です。
-
プロトコル バッファー (protobuf) は、さまざまなプラットフォーム上で構造化データのシリアル化および逆シリアル化を行うためにGoogleによって開発された一般的なシリアル化ライブラリです。protobuf は、構造化データをバイナリ形式にシリアル化するメカニズムを提供します。これにより、異なるシステム間で転送および解析できるようになります。次のような利点があります。
-
クロスプラットフォームと言語のサポート: プロトコル バッファーは、 C++、Java、Python、C#などの複数のプログラミング言語をサポートし、異なるプラットフォーム上のアプリケーションが簡単にデータを交換できるようにします。
-
効率的なシリアル化と逆シリアル化: プロトコル バッファーのシリアル化と逆シリアル化のプロセスは効率的で、生成されるバイナリ データは小さく、送信効率は高くなります。
-
バージョンの互換性:データ構造が変更された場合、プロトコル バッファーは下位互換性と上位互換性のメカニズムを提供し、プロトコルの進化とアップグレードを容易にします。
-
強力な型のサポート: プロトコル バッファーは、明確に定義されたメッセージ構造を使用して、エンコードおよびデコード時にユーザーが特定のメッセージ形式に従うことを強制し、一部のエラーを回避します。
-
複雑なデータ構造を転送する必要がある場合、特にプラットフォームや言語間でデータを交換する必要がある場合は、プロトコル バッファーを使用するのが良い選択です。明確なメッセージ定義構文、効率的なバイナリのシリアル化と逆シリアル化、および複数の言語のサポートを提供します。
5.2.1. 文字列または数値メッセージをクラスまたはより複雑なオブジェクトに変更する
#include"server.h"
Server::Server() {
port_ = Port;
user_id_ = 0;
thread_set_.clear();
}
void Server::Session(std::shared_ptr<boost::asio::ip::tcp::socket> socket,uint32_t user_id) {
try {
for (;;) {
char ack[Buffer];
memset(ack, '\0', Buffer);
boost::system::error_code error;
size_t length = socket->read_some(boost::asio::buffer(ack, Buffer), error);
if (error == boost::asio::error::eof) {
std::cout << "the usred_id "<<user_id<<"connect close by peer!" << std::endl;
socket->close();
break;
}
else if (error) {
throw boost::system::system_error(error);
}
else {
if (socket->is_open()) {
std::cout << "the usre_id " << user_id << " ip " << socket->remote_endpoint().address();
std::cout << " send message: " << ack << std::endl;
socket->send(boost::asio::buffer(ack, length));
}
}
}
}
catch (boost::system::system_error& e) {
std::cout << "Error occured ! Error code : " << e.code().value() << " .Message: " << e.what() << std::endl;
}
}
bool Server::StartListen(boost::asio::io_context& context) {
boost::asio::ip::tcp::endpoint ep(boost::asio::ip::tcp::v4(), port_);
boost::asio::ip::tcp::acceptor accept(context, ep);
std::cout << "start listen:" << std::endl;
for (;;) {
std::shared_ptr<boost::asio::ip::tcp::socket> socket(new boost::asio::ip::tcp::socket(context));
accept.accept(*socket);
user_id_ = user_id_ + 1;
std::cout << "the user_id "<<user_id_<<" client connect,the ip:" << socket->remote_endpoint().address() << std::endl;
//auto t = std::make_shared<std::thread>([&]() {
// this->Session(socket);
// });
auto t = std::make_shared<std::thread>([this, socket]() {
Session(socket,user_id_);
});
thread_set_.insert(t);
}
return true;
}
- 構造体またはクラスのインスタンスを送信するには、**プロトコル バッファー (protobuf)** などのシリアル化ライブラリを使用して、構造体またはクラスをバイト ストリームにシリアル化し、ネットワーク経由で送信する必要があります。構造体またはクラス インスタンスの送信をサポートするようにコードを変更する方法は次のとおりです。
- 構造体またはクラスを定義する:まず、送信する構造体またはクラスを定義する必要があります。例としてサンプル構造体を見てみましょう。
struct Message {
int id;
std::string content;
};
- プロトコル バッファーを使用する:データを送受信するときは、シリアル化と逆シリアル化にプロトコル バッファーを使用します。まず、メッセージの構造を記述する.protoファイルを定義します。
syntax = "proto3";
message Message {
int32 id = 1;
string content = 2;
}
- 次に、プロトコル バッファーコンパイラーを使用してC++コード を生成します。
- セッション関数を変更する: Server::Session関数を変更して、構造化メッセージのシリアル化と逆シリアル化をサポートします。
#include "message.pb.h" // Generated header from Protocol Buffers compiler
// ...
void Server::Session(socket_ptr socket) {
try {
for (;;) {
Message received_message;
char buffer[Buffer];
memset(buffer, '\0', Buffer);
boost::system::error_code error;
size_t length = socket->read_some(boost::asio::buffer(buffer, Buffer), error);
if (error == boost::asio::error::eof) {
// 客户端连接关闭
std::cout << "connect close by peer!" << std::endl;
break;
}
else if (error) {
// 发生了其他错误
throw boost::system::system_error(error);
}
else {
// 成功读取length个字节
received_message.ParseFromArray(buffer, static_cast<int>(length));
std::cout << "Received message from: " << socket->remote_endpoint().address() << std::endl;
std::cout << "ID: " << received_message.id() << std::endl;
std::cout << "Content: " << received_message.content() << std::endl;
// 做出响应
// ...
// 将消息序列化并发送回客户端
std::string serialized_message;
received_message.SerializeToString(&serialized_message);
socket->send(boost::asio::buffer(serialized_message.c_str(), serialized_message.size()));
}
}
}
catch (boost::system::system_error& e) {
std::cout << "Error occured! Error code : " << e.code().value() << " .Message: " << e.what() << std::endl;
}
}
このようにして、サーバーは受信したシリアル化されたメッセージをMessage構造体に解析し、メッセージの受信後に対応するシリアル化されたメッセージをクライアントに送り返します。
上記のサンプル コードは、プロトコル バッファーを使用してメッセージ構造を定義し、対応する C++ コードを生成していることを前提としていることに注意してください。ヘッダー ファイルへの正しいパスを必ず含め、実際の構造とメッセージ形式に基づいて適切に変更してください。
5.3. エラーが発生しました! エラー コード: 10054 メッセージ: リモート ホストは既存の接続を強制的に閉じました。[システム:10054]
エラー コード 10054 「リモート ホストが既存の接続を強制的に閉じました」。通常は、リモート ホスト (クライアント) がサーバーとの接続を閉じたことが原因です。これは、クライアントが接続を積極的に閉じているか、ネットワーク上の予期せぬ問題により接続が予期せず中断されたことが原因である可能性があります。
コードでは、クライアントが接続を閉じるときに、Session 関数で boost::asio::error::eof エラーをキャッチし、ソケットを閉じようとして、ループから抜け出します。ロジックのこの部分は正しいので、サーバー側で接続が閉じられ、適切に処理されるはずです。
ただし、エラー コード 10054 は、ネットワークの問題、タイムアウト、オペレーティング システムの構成など、いくつかの要因によって発生する可能性があります。接続の終了を処理するコード内のロジックが正しいと確信できる場合は、問題は別の場所にある可能性があります。
-
考えられる解決策とデバッグ方法をいくつか示します。
-
ネットワーク接続を確認する: ネットワーク接続が安定しており、パケット損失やその他の問題がないことを確認します。
-
クライアントを確認する: 特定のクライアントでのみ問題が発生する場合は、クライアントのネットワーク構成と状態をチェックして、異常がないことを確認します。
-
ファイアウォールとセキュリティ ソフトウェアを確認する: ファイアウォールまたはセキュリティ ソフトウェアがネットワーク接続を妨害している可能性があります。それらが接続をブロックしていないことを確認してください。
-
タイムアウト設定を確認する: サーバーにタイムアウトが設定されている場合は、それが妥当であり、接続が途中で閉じられていないことを確認してください。
-
サーバー側のリソースを確認する: サーバー側の接続が多すぎると、リソースが枯渇する可能性があります。接続を処理するのに十分なリソースがサーバーにあることを確認してください。
-
例外のキャッチ: 例外をキャッチするときは、問題が何であるかをよりよく理解するために、より詳細な情報を出力してみてください。エラー コードとエラー メッセージを出力して、問題のトラブルシューティングを改善できます。
-
ログ記録とデバッグ: ログ記録ツールとデバッグ ツールを使用してネットワーク接続と対話を監視し、接続が閉じられた理由をより詳細に把握します。
-
最終的に、エラー コード10054には複数の原因が考えられ、包括的な調査とトラブルシューティングが必要になります。問題がまだ存在する場合は、トラブルシューティングのためにネットワーク構成、サーバー側のリソース、接続タイムアウト設定などをさらに考慮する必要がある場合があります。
5.4、std::shared_ptrstd::thread t = std::make_sharedstd::thread() および () 関数の違いと使用法を追加
auto t = std::make_shared<std::thread>();
-
このコード スニペットはstd::threadオブジェクトを作成しようとしますが、実行する関数を指定していないため、実際には新しいスレッドは作成されません。
-
std::make_sharedを使用する場合は、通常、 std::shared_ptrなどのスマート ポインターを作成するために使用されます。このコンテキストでは、std::make_sharedはstd::threadオブジェクトを作成し、スマート ポインターを返しますが、構築されるオブジェクトの型と構築パラメーターを指定する必要があります。
-
std::threadの場合、スレッドの作成時に実行が開始されるように、実行する関数を引数として指定する必要があります。実行する関数が指定されていない場合、作成されたstd::threadオブジェクトには有効な作業タスクはありませんが、std::thread オブジェクトは作成されます。
-
auto t = std::make_shared<std::thread>(); は、 t という名前のstd::shared_ptr <std::thread> オブジェクトを作成しますが、 std::make_sharedにパラメータを渡さないため、スレッドに対して実行する関数。
通常、スレッドを作成するには、スレッド内で実行される呼び出し可能な関数または関数オブジェクト (関数ポインター、ラムダ関数、クラス メンバー関数、通常の関数など) を指定する必要があります。ただし、このコード スニペットでは、そのような呼び出し可能オブジェクトが提供されていないため、このスレッドには実際には有効な作業タスクがありません。このようにして作成されたスレッド オブジェクトはアイドル状態であり、実際の作業内容はありません。
std::shared_ptr<std::thread> t = std::make_shared<std::thread>( [this, ソケット] { Session(socket, user_id_); });
-
各クライアント セッション(セッション)は、 std::threadによって作成された個別のスレッドで実行されます。これは、各クライアント セッションが相互にブロックされることなく、別個のスレッドで処理されることを意味します。
-
クライアントが接続してメッセージを送信すると、Session関数が実行され、その中のループがクライアントのソケットからデータ (メッセージ) を継続的に読み取ろうとします。読み取るデータがない場合、read_some関数は読み取るデータができるまでブロックされます。ただし、各クライアントのセッションは別のスレッドで実行されるため、1 つのクライアントによるブロックが他のクライアントのセッションに影響を与えることはありません。
-
そのため、1 つのクライアントが閉じずにメッセージを送信し続けても、他のクライアントのセッションはブロックされません。各セッションは個別のスレッドで実行され、相互に影響を受けません。1 つのクライアントのセッションがデータを待機している間、他のクライアントのセッションは引き続き実行できます。
-
各クライアントのセッションは独立したスレッドで実行されますが、スレッド間に競合状態やスレッドの安全性の問題が依然として存在する可能性があることに注意してください。マルチスレッド環境では、潜在的な問題を回避するために共有リソースを慎重に扱う必要があります。
-
Session 関数は無限ループ内に配置されていますが、コードは異なるスレッドで異なる Session 関数を呼び出しています。新しいクライアントが接続するたびに、新しいスレッドが作成され、Session 関数が呼び出され、このスレッドでループが実行されます。したがって、各セッション関数には無限ループがありますが、これらのループは互いに独立した別のスレッドで実行されます。
-
これが、異なるクライアントのセッションが相互にブロックしない理由です。各クライアントのセッションは異なるスレッドで独立して実行されるため、あるクライアントのセッションがデータを待機している間、他のクライアントのセッションに影響を与えることはありません。Session 関数は StartListen 関数のループ内で呼び出されますが、各 Session 関数は別のスレッドで実行されるため、それらの実行は互いに並行して行われるため、互いにブロックされることはありません。
-
このコードでは、C++11 のラムダ式を使用して新しいスレッドを作成します。ラムダ式の内容は新しいスレッドで実行されます。ここで、[this,socket] はラムダ式の形式であり、現在のオブジェクト (this) とソケット変数をキャプチャし、新しいスレッドのコードに渡すことを意味します。
-
新しいスレッドでは、Session 関数が呼び出され、クライアントのセッション ロジックが実行されます。各クライアント接続は独立したスレッドでセッション機能を実行するため、異なるクライアント間のセッションは互いにブロックすることなく並行して処理できます。
-
要約すると、コードは、異なるクライアントのセッションを同時に処理する複数のスレッドを作成することによって、複数のクライアント接続を同時に処理する機能を実現します。この同時処理により、サーバーのパフォーマンスと応答性が向上します。
5.5. void Server::Session(std::shared_ptr<boost::asio::ip::tcp::socket>ソケット, uint32_t user_id) for ループに read_some を記述する必要がある理由
-
read_some は、データが到着するまで待機するか、読み取るデータがない場合にエラーが発生するまで待機するブロック関数です。このコードでは、read_some 関数が for ループ内で呼び出されていますが、この関数はブロックされ、読み取るデータがBuffer バッファーにあるまで待機します。クライアントがメッセージを送信すると、read_some が戻ってデータを読み取り、次のサイクルに入ります。
-
ループ内であっても、read_some関数はデータの到着を待機している間スレッド全体をブロックせず、現在呼び出しているスレッドのみをブロックし、他のスレッドが実行を継続できるようにします。これにより、各クライアント接続のブロッキング待機が他の接続の処理に影響を与えないため、サーバーは複数のクライアント接続を同時に処理できるようになります。
-
したがって、read_some はループ内で呼び出されますが、ループ全体をブロックするのではなく、読み取るデータがないときにブロックして待機し、データが到着するまで戻りません。これにより、サーバーは複数のクライアントからメッセージを継続的に受信できるようになります。
-
ループ内に記述されていない場合、クライアントは現時点でメッセージを 1 回しか送信できません。
-
はい、read_some をループの外側に置いた場合、メッセージはクライアント接続ごとに 1 回だけ受信され、処理されます。サーバーがクライアントからメッセージを受信すると、バッファー内に読み取り可能なデータがあるため、read_some はブロックされなくなります。ただし、read_some のブロックが解除されると、ループ内に新しいデータの到着を待機するコードがなくなるため、サーバーはクライアントからメッセージを受信し続けなくなります。
-
メッセージを継続的に受信する機能を実装したい場合は、メッセージを読み取るロジック全体をループに入れることができます。このようにして、サーバーは各ループでクライアントからの新しいメッセージを待機して受信します。コード内で for (; ; )) { … } 部分のコメントを解除すると、クライアントが切断されるかエラーが発生するまでサーバーがループでメッセージを受信し続けることができます。
-
これは、コード内でread_some関数がループの外で呼び出されるためです。クライアントがメッセージを送信し、サーバーがメッセージを正常に読み取ると、バッファー内に読み取り可能なデータが存在するため、read_someはブロックされなくなります。ループの外側には新しいメッセージの到着を待つロジックがないため、サーバーは後続のメッセージの読み取りと処理を続行しません。
-
クライアントがメッセージを複数回送信でき、サーバーがこれらのメッセージを継続的に受信して処理できることを実現するには、ループ内に read_some を配置する必要があります。これにより、サーバーはループの反復ごとにクライアントによって送信されたメッセージの読み取りを試みることができます。常時コミュニケーションを実現します。これにより、サーバーは、クライアントが接続を閉じるまで、接続上で複数のメッセージの受信と処理を続けることができます。
-
6. std::make_shared と std::shared_ptr
shared_ptr<string> p1 = make_shared<string>(10, '9');
shared_ptr<string> p2 = make_shared<string>("hello");
shared_ptr<string> p3 = make_shared<string>();
スマート ポインターはC++11で導入されており、指定された型のstd::shared_ptrを返すことができるテンプレート関数std::make_sharedもあります。
// make_shared example
#include <iostream>
#include <memory>
int main () {
std::shared_ptr<int> foo = std::make_shared<int> (10);
// same as:
std::shared_ptr<int> foo2 (new int(10));
auto bar = std::make_shared<int> (20);
auto baz = std::make_shared<std::pair<int,int>> (30,40);
std::cout << "*foo: " << *foo << '\n';
std::cout << "*bar: " << *bar << '\n';
std::cout << "*baz: " << baz->first << ' ' << baz->second << '\n';
return 0;
}
std::make_shared は、スマート ポインター (std::shared_ptr) によって管理されるオブジェクトを作成するための C++ 標準ライブラリの関数テンプレートです。その役割は、オブジェクトの作成とスマート ポインターの管理を組み合わせて、オブジェクトのライフ サイクルをより安全かつ便利に管理することです。
-
具体的には、std::make_sharedの機能と意味は次のとおりです。
-
オブジェクトの作成と管理を簡素化する:スマート ポインターを作成するときに、 std::shared_ptr コンストラクターを直接使用して作成する場合は、スマート ポインター オブジェクトと管理対象オブジェクトに同時にメモリを割り当てる必要があります。また、 std::make_shared(args...) は、スマート ポインター オブジェクトと管理対象オブジェクトに一度にメモリを割り当てることができるため、より効率的かつ簡潔になります。
-
メモリ割り当ての数を減らす: std::make_shared は、スマート ポインター オブジェクトと管理対象オブジェクトに必要なメモリを一度にメモリに割り当てます。これにより、メモリ割り当ての数が減り、パフォーマンスが向上し、メモリの断片化が軽減されます。
-
リソース リークの回避: std::make_shared は、オブジェクトのライフ サイクルを自動的に管理するスマート ポインターを使用し、オブジェクトが不要になったときにオブジェクトが適切に破棄されるようにし、リソース リークを回避します。
-
#include <memory>
int main() {
// 创建智能指针并初始化为一个 int 对象
std::shared_ptr<int> num_ptr = std::make_shared<int>(42);
// 创建智能指针并初始化为一个动态分配的数组
std::shared_ptr<int[]> array_ptr = std::make_shared<int[]>(10);
return 0;
}
要約すると、std::make_shared は、スマート ポインターによって管理されるオブジェクトを作成および管理するための推奨される方法です。これにより、コードが簡素化されるだけでなく、パフォーマンスとリソース管理も向上します。
6.1、shared_ptrオブジェクトの作成方法
- 通常、 std::shared_ptrを初期化するには 2 つの方法があります。
- ① 独自のコンストラクターを介して。
- ②std::make_shared経由。
6.1.2. これら 2 つの方法の異なる特徴は何ですか
shared_ptrは非侵入的です。つまり、カウンタの値はshared_ptrに格納されません。shared_ptrがメモリの生のポインタによって作成されるとき、カウンタの値は実際にはヒープ上の別の場所に存在します(ネイティブメモリ:これを指します)。このメモリを指す他のshared_ptrはありません)、このカウンタはそれに応じて生成され、このカウンタ構造のメモリは常に存在します。すべてのshared_ptrとweak_ptrが破棄されるまで、今回はより賢明です。すべてのshared_ptrが破棄されたとき、このメモリ部分は解放されましたが、weak_ptr がまだ存在する可能性があります。つまり、カウンタの破壊は、メモリ オブジェクトが破壊されてからずっと後に発生する可能性があります。
class Object
{
private:
int value;
public:
Object(int x = 0):value(x) {
}
~Object() {
}
void Print() const {
cout << value << endl; }
};
int main()
{
std::shared_ptr<Object> op1(new Object(10)); //①
std::shared_ptr<Object> op2 = std::make_shared<Object>(10); //②
return 0;
}
6.1.3. これら 2 つの作成方法の違いは何ですか
-
最初の方法を使用する場合、 op1 には 3 つのメンバーop1._Ptr、op1._Rep、op1._mDがあり、op1._PtrポインターはObjectオブジェクトを指し、 op1._Rep は参照カウント構造体を指し、参照カウント構造体にも 3 つのメンバーがあります。_Ptr、_Uses、_Weaks、_Ptr はObjectオブジェクトを指します。 _Usesと **_Weaksは両方とも 1 です。実際、ヒープ領域は2 回構築されます。1 つは Object** オブジェクトの構築で、もう 1 つは参照の構築です。カウント構造
-
2 番目の方法を使用すると、ヒープ領域は1 回だけ構築され、参照カウント構造体のサイズと Object オブジェクトのサイズが計算され、一度に大きな領域が解放されます。_PtrポインタはObjectを指します。オブジェクト、_Usesおよび_Weaks の値は 1
6.1.4. std::make_shared の 3 つの利点
-
①ヒープ領域のオープンを一度だけ行うことで、ヒープ領域のオープンと解放の回数を削減します。
- make_ptrを使用する最大の利点は、単一メモリの割り当ての数を減らすことです。後で説明する悪影響がそれほど重要ではない場合、これが make_shared を使用するほぼ唯一の理由です。もう 1 つの利点は、大規模メモリの局所性を高めることができることです。
キャッシュ(キャッシュ局所性) : make_sharedを使用すると、カウンターのメモリとネイティブ メモリがヒープ上に並べられます。このようにして、これら 2 つのメモリにアクセスするすべての操作で、他のソリューションと比較してキャッシュ ミスが半分に減ります。したがって、キャッシュミスが正しい場合、それが問題となる場合は、make_sharedについて実際に検討する必要があります。
- make_ptrを使用する最大の利点は、単一メモリの割り当ての数を減らすことです。後で説明する悪影響がそれほど重要ではない場合、これが make_shared を使用するほぼ唯一の理由です。もう 1 つの利点は、大規模メモリの局所性を高めることができることです。
-
②ヒット率を向上させるために、オブジェクトと参照カウント構造を同じ空間に置きます。
- 空間局所性により、オブジェクトにアクセスした後、オブジェクトの前後のメモリブロックにアクセスすることになるため、Cache ブロックで素早くヒットすることができ、オブジェクトと参照カウント構造がそれぞれ隣接しているため、ヒット率が非常に高くなります。その他。
- キャッシュ導入の理論的基礎は、時間的局所性と空間的局所性を含むプログラムの局所性の原理、つまり、CPUが最近アクセスしたデータは短時間(時間)にCPUによってアクセスされ、アクセスされたデータに近いデータCPUによるアクセスは短時間で行われますが、アクセス(空間)も必要となるため、直前にアクセスしたデータをCacheにキャッシュしておけば、次回アクセスしたときに、キャッシュから直接フェッチされるため、速度が桁違いに向上しますCPUがアクセスするデータはキャッシュ内にあります キャッシュはヒット (Hit) と呼ばれ、それ以外の場合はミス (Miss) と呼ばれます。
実行順序と例外の安全性も考慮すべき問題です。
struct Object
{
int i;
};
void doSomething(double d,std::shared_ptr<Object> pt)
double couldThrowException();
int main()
{
doSomething(couldThrowException(),std::shared_ptr<Object> (new Object(10));
return 0;
}
上記のコードを分析すると、dosomething関数が呼び出される前に少なくとも 3 つのことが行われます。
- ① オブジェクトを構築してメモリを割り当てます。
- ②shared_ptrを構築します。
- ③couldThrowException()。
C++17 では、関数パラメータの構築順序を識別するより厳密な方法が導入されていますが、その前に、上記 3 つの実行順序は次のようになっている必要があります。
- ①new Object()。
- ② CouldThrowException() 関数を呼び出します。
- ③shared_ptrを構築し、手順1で確保したメモリを管理します。
上記の問題は、ステップ 2 で例外がスローされると、ステップ 3 は決して発生しないため、ステップ 1 で作成されたメモリを管理するスマート ポインタが存在しないことです。メモリ リークは発生しますが、スマート ポインタはそれが無害であると示します。まだ時間がありません この世界をのぞいてみましょう。
これが、途中で何が起こるかわからないため、できる限りstd::make_sharedを使用してステップ 1 とステップ 3 を近づける理由です。
- ③ 呼び出しの順序が不確かな場合でも、オブジェクトを管理できる場合があります。
- doSomething(couldThrowException(),std::make_shared (10)); を使用して構築する場合、オブジェクトと参照カウント構造体は構築中に一緒に構築されますが、後で例外がスローされた場合でも、オブジェクトも破棄されます。 。
6.1.5. make_shared を使用するデメリット
make_sharedを使用する場合、最も可能性の高い問題は、make_shared関数がターゲットの型コンストラクターまたは構築メソッドを呼び出すことができなければならないということですが、現時点では、make_shared をクラスのフレンドとして設定するだけでは十分ではない可能性があります。 type は、 make_shared関数ではなく、補助関数が呼び出されます。
もう 1 つの問題は、ターゲット メモリのライフ サイクルです (ターゲット オブジェクトのライフ サイクルについては話していません)。上で述べたように、たとえshared_ptr によって管理されているターゲットが解放されても、shared_ptr のカウンタは最後まで存在し続けます。このときmake_shared関数を使用すると、ターゲットメモリを指すweak_ptrが破壊されます。
ここで問題が発生します。プログラムは、管理対象オブジェクトが占有するメモリとカウンタ全体が占有するヒープ メモリを自動的に管理します。つまり、管理対象オブジェクトが破壊されても領域はまだ存在し、メモリが使用できなくなる可能性があります。返された - すべてのweak_ptrがクリアされて、カウンタによって占有されていたメモリが返されるのを待ちます。オブジェクトが少し大きい場合、かなりの量のメモリがしばらくの間無意味にロックされていることを意味します。影付きの領域は
、weak_ptrのカウンタが0になるのを待っているshared_ptrが管理するオブジェクトが、上の薄オレンジ色の領域(カウンタのメモリ)とともに解放されます。
7. まとめ: 同期読み取りと書き込みの長所と短所
- 同期読み取りおよび書き込みの欠点は、読み取りと書き込みがブロックされることです。クライアントがサーバーにデータを送信しない場合、サーバーの読み取り操作がブロックされ、サーバーがブロック待機状態になります。
- 新しいスレッドを開いて、新しく生成された接続の読み取りと書き込みを処理できますが、プロセスによって開かれるスレッドの数は制限されており、約 2048 スレッドです。Linux 環境では、プロセスによって開かれるスレッドの数を無制限に増やすことができます。ただし、スレッドが多すぎると、切り替えによって消費されるタイム スライスが増加します。
- サーバーとクライアントは応答モードであり、実際のシーンは全二重通信モードであり、送信と受信は独立して分離される必要があります。
- サーバーとクライアントはスティッキー パケットの処理を考慮していません。
結論から言えば、サーバーとクライアントの問題ですが、上記の問題を解決するために、次回の記事で主に上記の解決策を非同期読み書きによる改善を中心に改良を加えていきたいと思います。
もちろん、同期読み取りおよび書き込みの方法にも利点があり、たとえば、クライアントの接続数が少なく、サーバーの同時実行性が高くない場合には、同期読み取りおよび書き込みの方法を使用できます。同期読み取りと書き込みを使用すると、コーディングの難しさを簡素化できます。