C言語 「もしも」に備えてこそプロのコード
さて今回は。
エラー処理です。
ここまで書き進めてきたプログラム「エコーサーバ」はたった30行だけのプログラムであるにも関わらず、7つもの関数を呼び出しています。正確には、実ステップ数で24ラインあるので、7ライン/24ライン=約30%が関数呼び出しになります。
そして。
関数の呼び出しというものには失敗が付きまとう・・・
特に今回のような汎用的なライブラリは、引数が間違っているというだけでも失敗します。エラーリターンしてくるのです。関数の呼び出しに失敗していないかどうかについては、必ず確認しなければなりません。
関数の呼び出しを1つずつ見ていきましょう。
socket のエラー
以前にもリンクした次のURLに「戻り値」という項目があります。
そして、そこには次のように記載されています。
成功した場合、新しいソケットのファイルディスクリプターを返す。
エラーが発生した場合は -1 を返し、 errno を適切に設定する。
すなわち、戻り値が「-1」であればエラー、それ以外であれば成功ということになります。
「エコーサーバ」は関数「socket」を固定の引数で呼び出しているのでエラーで終了するというケースはあまりないのかもしれません。ただ、「socket」でエラーになった場合、後に続くすべての関数呼び出しが失敗します。「socket」で失敗したら、ここでとどめておくべきでしょう。
s_fd = socket(AF_INET, SOCK_STREAM, 0);
if (s_fd == -1) {
exit(EXIT_FAILURE);
}bind のエラー
「bind」のエラーも確認しておきましょう。
成功した場合にはゼロが返される。
エラー時には -1 が返され、 errno が適切に設定される。
こちらも「-1」でエラーです。
int bind_err;
bind_err = bind(s_fd, (struct sockaddr *)&s_addr, sizeof(s_addr));
if (bind_err == -1) {
exit(EXIT_FAILURE);
}listen のエラー
「listen」のエラーも確認しておきます。
成功した場合にはゼロが返される。
エラー時には -1 が返され、 errno が適切に設定される。
int listen_err;
listen_err = listen(s_fd, LISTEN_BACKLOG);
if (listen_err == -1) {
exit(EXIT_FAILURE);
}accept のエラー
「accept」のエラーも確認しておきます。
「戻り値」の項が英語のままになっていたので翻訳します。
成功した場合、受け入れられたソケットのファイルディスクリプタ(0以上の整数)を返す。
エラー時には -1 が返され、 errno が適切に設定される。addrlen は変更されない。
c_fd = accept(s_fd, (struct sockaddr *)&c_addr, &len);
if (c_fd == -1) {
exit(EXIT_FAILURE);
}read のエラー
「read」のエラーも確認しておきます。
成功した場合、読み込んだバイト数を返す (0 はファイルの終りを意味する)。
エラーの場合は、-1 が返され、 errno が適切に設定される。
n = read(c_fd, buf, sizeof(buf));
if (n == -1) {
exit(EXIT_FAILURE);
}write のエラー
「write」のエラーも確認しておきます。
成功すると、書き込まれたバイト数が返される。
エラーの場合、 -1 が返り、 errno にエラーの原因を示す値が設定される。
int write_err;
write_err = write(c_fd, buf, n);
if (write_err == -1) {
exit(EXIT_FAILURE);
}close のエラー
「close」のエラーも確認しておきます。
close() は成功した場合は 0 を返す。
エラーが発生した場合は -1 を返して、 errno を適切に設定する。
これも他と同じですが、中断しなければならない後続の処理がありません。また、次のリソース解放にも関連しますが、「close」という処理はエラー処理でも実行しなければなりません。
エラー処理で実行した「close」がさらにエラーになったら?
そういうものまでをもエラー処理していると、エラー処理中にエラーが発生してさらにエラー処理をしてまたエラーが発生して・・・というように、エラー処理の無限実行に陥りかねません。このため、「close」などの後片付けでは、エラーは確認しないことが少なくありません。
今回の場合も、エラー処理はしないものとします。
エラー処理1 リソースの解放
ここまでのエラー処理をまとめると次のようになります。
// ソケット作成:IPv4, ストリーム(TCP)
s_fd = socket(AF_INET, SOCK_STREAM, 0);
if (s_fd == -1) {
exit(EXIT_FAILURE);
}
// サーバー情報設定
s_addr.sin_family = AF_INET;
s_addr.sin_addr.s_addr = INADDR_ANY;
s_addr.sin_port = htons(SERVER_PORT);
// 紐付け
bind_err = bind(s_fd, (struct sockaddr *)&s_addr, sizeof(s_addr));
if (bind_err == -1) {
exit(EXIT_FAILURE);
}
// 待ち受け準備
listen_err = listen(s_fd, LISTEN_BACKLOG);
if (listen_err == -1) {
exit(EXIT_FAILURE);
}
// 接続受付
c_fd = accept(s_fd, (struct sockaddr *)&c_addr, &len);
if (c_fd == -1) {
exit(EXIT_FAILURE);
}
// 読み込む
n = read(c_fd, buf, sizeof(buf));
if (n == -1) {
exit(EXIT_FAILURE);
}
// 読み込んだデータをそのまま送る
write_err = write(c_fd, buf, n);
if (write_err == -1) {
exit(EXIT_FAILURE);
}
// 終了
close(c_fd);
close(s_fd);
return EXIT_SUCCESS;ですが、このプログラムには重要な問題があります。
解放できていないリソースがあるのです。

この図のピンク枠で exitした場合に、「s_fd」や「c_fd」を「close」せずにプログラムを終了しています。この場合、「s_fd」や「c_fd」のリソースが解放されずに残っているかもしれません。
途中でプログラムを中断するや関数の途中で return する場合に、常に警戒しなければならない問題です。
では、どうすればいいでしょうか。
単純にやるとこうなります。

この図の赤枠のように、 exit する前に それぞれのディスクリプタを close する処理を実行します。
次のような感じになります。

これで、「s_fs」も「c_fd」も、ちゃんと close されることになります。
エラー処理2 エラーを出力する
ここまでのエラー処理では、エラーが発生した時点でプログラムを中断するだけで、どこでエラーになったのかわかりません。
そもそもエラーになったことさえわかりません。
このエラーに関する情報をどこにどのような形で出力するのかも難しい設計です。ですが、今回はコマンドラインプログラムですので、標準エラー出力に出力しましょう。
標準エラー出力は標準出力と見た目は同じですが、シェルスクリプトなどを記載する場合には、標準出力と標準エラー出力を分けて扱うことができます。このため、エラー出力は標準エラー出力に出力する方がいいでしょう。
どの関数も、「errno」にエラー番号を設定するようですので、次のように出力しましょう。
fprintf(stderr, "socket の呼び出しでエラーが発生しました: %d\n", errno);「fprintf」というと、「ファイルに出力するprintf」というイメージがあるかもしれませんが、ディスクリプタに「stderr」を指定すると「標準エラー出力」に出力されます。
これでエラーが発生した場合に、どこでエラーが発生したのか、そしてその時のエラー番号を知ることができます。
エラー処理3 エラーの内容を出力する
先ほどは「エラー番号」を出力しましたが、エラー番号を出力されても何が起こったのかすぐにわかりません。
$ ./echo_server
bind の呼び出しでエラーが発生しました: 98
$「errno」の「98」って、何だろう。
ということになってしまいます。
上記にリンクしたページでも、あいにく errno のシンボルは記載されていますが数値までは記載されていません。
ですが、「errno」についてはもう少しわかりやすくする工夫があります。
strerror
1つ目は「strerror」を使用する方法です。
文字通り「errno」を「エラーメッセージの文字列」に変換してくれる関数です。引数に「errno」を指定すれば、そのエラー番号に対応する文字列のポインタを返してくれます。
例えば、こんな風に書いてみます。
fprintf(stderr, "socket の呼び出しでエラーが発生しました: %d %s\n", errno, strerror(errno));すると例えば、エラーはこんな風に表示されます。
$ ./echo_server
bind の呼び出しでエラーが発生しました: 98 Address already in use
$ これは「echo_server」を2回同時に実行させたときのエラーです。
すでに「IPアドレス=0.0.0.0、ポート番号=8080」でbindしている状態で、再度新たに「IPアドレス=0.0.0.0、ポート番号=8080」をbindしようとしてエラーになりました。
OSは、最初に実行した「echo_server」に「IPアドレス=0.0.0.0、ポート番号=8080」を結び付けています。その後にさらに別のプログラムから「IPアドレス=0.0.0.0、ポート番号=8080」を結び付けてくれと言われても困るわけです。ネットワークから、任意のIPアドレスのポート番号8080でSYNを受信したり、データを受信したりしたときに、どちらのプログラムに通知していいのかわからなくなります。
このために「Address already in use」という「そのアドレスは既に使われています」というエラーメッセージが出力されました。
perror
もう一つは「perror」を使用する方法です。
先ほど紹介した「fprintf」のコードは次のようになります。
perror("socket の呼び出しでエラーが発生しました");perror は、指定した文字列の後に続けて次のように表示してくれます。
bind の呼び出しでエラーが発生しました: Address already in useプログラムの実装が少しすっきりします。
strerror と perror とどちらがいい?
strerror と perror。
どちらを選択するのかは難しいですね。
「perror」は、実装がとてもすっきりします。
すっきりするというのは大切なことで、間違いにくく保守メンテナンスも容易になります。
一方で、「strerror」を使って「fprintf」で出力する場合は、拡張性があります。出力したい情報を追加しやすいわけです。
エラー処理というのは意外に難しく、実際にエラーに遭遇した時には「あの情報もほしかった」「この情報もほしかった」ということは少なくありません。発生してしまったエラーについては無力ですが、今後同じようなエラーが発生した場合に対して対策することはできます。そのときにはやはり標準エラー出力に出力する情報を追加することになるでしょう。
「perror」でも情報を追加することはできます。
ですが、一旦「fprintf」によってローカルバッファにメッセージを構築しておいてそのメッセージを「perror」に引き渡すということになります。
例えば、 perror でも errno を表示したいとしたらどうでしょうか。
こんな風になります。
fprintf(errmsg, "socket の呼び出しでエラーが発生しました: Error number %d", errno);
perror(errmsg);これでしたら、「fprintf(stderr, ・・・)」で全て表示してしまった方が、1回ですみますね。
選択はその都度において検討する必要がありますが、ここでは「strerror」を使用します。
エラー処理 完成版
これらをプログラムすると次のようになります。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <errno.h>
#include <string.h>
#define SERVER_PORT 8080
#define RECEIVE_BUF_SIZE 2048
#define LISTEN_BACKLOG 5
int main(void) {
int s_fd, c_fd;
int bind_err;
int listen_err;
int write_err;
struct sockaddr_in s_addr, c_addr;
socklen_t len = sizeof(c_addr);
char buf[RECEIVE_BUF_SIZE];
int n;
// ソケット作成:IPv4, ストリーム(TCP)
s_fd = socket(AF_INET, SOCK_STREAM, 0);
if (s_fd == -1) {
fprintf(stderr, "socket の呼び出しでエラーが発生しました: Error number %d. %s.\n", errno, strerror(errno));
exit(EXIT_FAILURE);
}
// サーバー情報設定
s_addr.sin_family = AF_INET;
s_addr.sin_addr.s_addr = INADDR_ANY;
s_addr.sin_port = htons(SERVER_PORT);
// 紐付け
bind_err = bind(s_fd, (struct sockaddr *)&s_addr, sizeof(s_addr));
if (bind_err == -1) {
fprintf(stderr, "bind の呼び出しでエラーが発生しました: Error number %d. %s.\n", errno, strerror(errno));
close(s_fd);
exit(EXIT_FAILURE);
}
// 待ち受け準備
listen_err = listen(s_fd, LISTEN_BACKLOG);
if (listen_err == -1) {
fprintf(stderr, "listen の呼び出しでエラーが発生しました: Error number %d. %s.\n", errno, strerror(errno));
close(s_fd);
exit(EXIT_FAILURE);
}
// 接続受付
c_fd = accept(s_fd, (struct sockaddr *)&c_addr, &len);
if (c_fd == -1) {
fprintf(stderr, "accept の呼び出しでエラーが発生しました: Error number %d. %s.\n", errno, strerror(errno));
close(s_fd);
exit(EXIT_FAILURE);
}
// 読み込む
n = read(c_fd, buf, sizeof(buf));
if (n == -1) {
fprintf(stderr, "read の呼び出しでエラーが発生しました: Error number %d. %s.\n", errno, strerror(errno));
close(c_fd);
close(s_fd);
exit(EXIT_FAILURE);
}
// 読み込んだデータをそのまま送る
write_err = write(c_fd, buf, n);
if (write_err == -1) {
fprintf(stderr, "write の呼び出しでエラーが発生しました: Error number %d. %s.\n", errno, strerror(errno));
close(c_fd);
close(s_fd);
exit(EXIT_FAILURE);
}
// 終了
close(c_fd);
close(s_fd);
return EXIT_SUCCESS;
}エラー処理というのは意外に煩雑です。このプログラムでは、元のプログラムよりエラー処理の方が長いのではないかと思われるほどです。
さらには、どのエラー処理もとても似ています。毎回これを書くのは大変です。そして同じ処理というのは、間違いが混入しやすくもあります。
次のステップではこれらのエラー処理をもっとスッキリさせてみましょう!
