1. はじめに
この記事は,以下の記事の続きになります.
https://qiita.com/Y-Yoshimura1997/items/752d89c1e6240cfab0ce
今回は,ソケット通信を行うプログラムに修正を加え,
通信異常が起きた際にも,無事に通信を再開できるように修正を加えていきます.
また,前回同様に,誤った情報を記載している可能性もございます.
温かく見守りつつ,ご指摘いただけますと幸いです.
2. 起きうる問題とその対策
Socket通信に関わらず,通信において一般的に言えることですが,
サーバ側orクライアント側のプログラムが落ちてしまったり,
物理的に通信経路に異常が生じるなどして,通信相手を見失う状況が起こり得ます.
上記の状況に陥った際,前回のプログラムのままでは,
- データの送信send()時に,シグナル : SIGPIPEが飛んできてプログラムが落ちる
- データの受信recv()をひたすら待ち続けてしまう
といった問題が生じます.さらに,一度接続が切れてしまった際に,
- 再接続が行われず,通信が途絶えたままになる
ということも問題となってきます.
そこで,上記の問題への対策を,以下で紹介していきます.
2.1 シグナル : SIGPIPEが飛んできて落ちる
異常の詳細
データの送信send()では,Linuxに備わるpipeという仕組みを用いて,
送信を行うプロセスへデータを渡します.
しかし,送信先が存在しない際には,pipeを通してデータを渡す相手がいないので,
シグナル:SIGPIPEが飛んで,プロセスが強制終了してしまいます.
対策
幾つかの方法があるようですが,一番単純な方法として,
SIGPIPEが飛んできても無視する処理を追加します.
signal(SIGPIPE,SIG_IGN);
もう少し丁寧な方法としては,
以下のようにSIGPIPEが飛んできた際の処理を書き替えることで,
プロセスが強制終了しないように変更します.
void SigPipeHandler(int handler)
{
std::cout << "Caught SIGPIPE!" << std::endl;
}
struct sigaction sa;
sa_.sa_handler = SigPipeHandler;
sigemptyset(&sa.sa_mask);
sa_.sa_flags = 0;
sigaction(SIGPIPE, &sa_, NULL);
2.2 recv()をひたすら待ち続け固まる
異常の詳細
オプションがデフォルト状態のままだと,recv()では,
データが受信されるまで待ち続け,処理が止まってしまいます.
対策
ソケットのオプションで,タイムアウト時間の設定を行います.
タイムアウト設定を行うことで,一定時間受信または送信が出来ない際には,
強制的に受信または送信処理を終了できます.
setsockopt(socket_, SOL_SOCKET, SO_RCVTIMEO, &timeout_, sizeof(timeout_));
2.3 通信が途絶えたままになる
異常の詳細
記載の通りです.
前回のプログラムでは,異常に対する対策もなければ,
再接続も行われないので,通信が再開されません,
対策
送信または受信に失敗した際に,
サーバ側・クライアント側の両方で,再接続を待つ処理を行います.
以下のサンプルプログラムでは,再接続処理を全力で回しています.
実際は,待ちを入れたり,周期処理として実装するのが,ベターかもしれません.
void SocketServer::Connect()
{
for (;;){
int is_listen = listen(fd_socket_, SOMAXCONN);
if (is_listen != -1) {
std::cout << "Listen" << std::endl;
} else {
std::cout << "Failed to Listen" << std::endl;
}
fd_socket_new_ = accept(fd_socket_, (struct sockaddr *)&addr_client_, &addr_len_);
if (fd_socket_new_ != -1) {
setsockopt(fd_socket_new_, SOL_SOCKET, SO_RCVTIMEO, &timeout_, sizeof(timeout_));
std::cout << "Accepted FD:" << fd_socket_new_ << std::endl;
break;
} else {
std::cout << "Failed to Accept" << std::endl;
}
}
};
void SocketClient::Connect()
{
fd_socket_ = socket(AF_INET, SOCK_STREAM, 0);
if (fd_socket_ != -1) {
std::cout << "> Create Socket FD:" << fd_socket_ << std::endl;
setsockopt(fd_socket_, SOL_SOCKET, SO_RCVTIMEO, &timeout_, sizeof(timeout_));
} else
{
std::cout << "> Failed to Create Socket" << std::endl;
}
for (;;){
int is_connect = connect(fd_socket_, (struct sockaddr *)&addr_server_, addr_len_);
if (is_connect != -1)
{
std::cout << "Connected to Server" << std::endl;
break;
} else {
std::cout << "Failed to Connect" << std::endl;
}
}
};
3 サンプルプログラム
githubにサンプルプログラムを掲載します.
3.1 サンプルプログラムの概要
ざっくり,図のようなシステムでの通信を行うことを想定したプログラムになります.
また,このプログラムは,お互い1秒ごとに文字列を送ります.
本来はサーバとクライアントで別のハードウェアを用意できればいいのですが,
今回は,Linuxのnamespaceという機能を用いて,仮想的なネットワーク環境を
用意することで,それぞれ別のIPアドレスを使用できるようにします.
3.2 サンプルプログラムの動かし方
step1. 仮想ネットワークの用意
CreateVNet.shを実行することで,仮想のサーバとクライアントを用意します.
bash CreateVNet.sh
step2. サーバ・クラアイントのプログラムを立ち上げる
各仮想ネットワーク(namespace)内でプログラムのコンパイル・実行を行います.
もちろん,サーバとクライアントの立ち上げ順序はどちらで問題ありません.
ip netns exec client bash
make all
make run
ip netns exec client bash
make all
make run
step3. サーバまたはクラアイントのプログラムを終了させる
Ctrl+cで,どちらかのプログラムを落とす.
再び,
make run
で,通信が再開することを確認する.
4. まとめ
ここまで紹介した改良により,通信に問題が生じた際にも,
通信を再開することが出来るようになりました.
しかし,さらに安定・確実な通信を行うには,もっと手を入れる必要はあるかと思います.
前回の記事から,すごく時間が経ってしまいました.
期待されていた方はいないかもですが,すいません...
今後も,組み込み・制御周辺のネタを投稿できればと思うので,
よろしくお願いします.
参考記事
- シグナルの処理について
- タイムアウト処理について
- 仮想ネットワーク:networkについて