TCPを使う(サーバ、SO_REUSEADDR)

前述したTCPサーバ例では、サーバを終了した直後にもう一度サーバを起動しようとすると、bindがエラーで終了することがあります。 ここでは、その問題を回避するためにSO_REUSEADDRを有効にする方法を説明します。

TIME_WAIT

TCPサーバのプログラムを書いていて、TCPサーバを終了して直後にもう一度起動したときに、 bindが「Address already in use」というようなエラーで失敗してしまったとこは無いでしょうか? 「あれ?もうTCPのサーバプロセスは終了しているのに。何故、bind出来ないのだろう?」と思いつつ、 しばらく時間がたってからもう一度実行すると問題なくbindが成功したりします。 この問題はTCP自体の仕組み(仕様)によって引き起こされています。(Mac OS Xソケットの問題ではなく、TCPの仕様です)。 具体的にはTIME_WAIT状態という状態がbindをfailさせています。

サーバがTCPセッションを受け付けcloseすると、TIME_WAIT状態になります。 ここで注意しないといけないのは、TCPサーバでTIME_WAITが発生する場合と発生しない場合があることです。 TCPサーバ側でcloseを先に実行するとTIME_WAIT状態が発生しますが、TCPクライアント側で先にcloseを実行すると、サーバ側ではTIME_WAITは発生しません。 さらに具体的に書くと、サーバではなくクライアントが先にFINを送信すればTCPサーバ側はTIME_WAIT状態に入りません。

TIME_WAIT状態は、「netstat -na」コマンドをコマンドプロンプトで実行すると確認できます。 「netstat -na」では、TCPセッションが確立している状態は、ESTABLISHEDとして表示されます。 TCPセッションが終了した直後に「netstat -na」を実行すると、TIME_WAITという状態であるのがわかります。

TIME_WAIT状態は、同一ポートを別プロセスが利用するのを防ぐためにTCP規格で規定されています。 TIME_WAIT状態のポートと同一のポートをbindしようとすると、bindは失敗してしまいます。 ただ、もう終わってしまったプロセスで使っていたポート番号をすぐに再利用できないと困るので、TIME_WAIT状態で残っているTCPセッションがあってもbindできる方法があります。 その方法がSO_REUSEADDRを有効にすることです。

サンプルコード

setsockopt()を利用してソケットに対してSO_REUSEADDRを設定すると、TIME_WAIT状態のポートが存在していてもbindができるようになります。

以下に改造したTCPサーバを示します。


#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

int
main()
{
 int sock0;
 struct sockaddr_in addr;
 struct sockaddr_in client;
 socklen_t len;
 int sock;
 int yes = 1;


 sock0 = socket(AF_INET, SOCK_STREAM, 0);

 addr.sin_family = AF_INET;
 addr.sin_port = htons(12345);
 addr.sin_addr.s_addr = INADDR_ANY;
 addr.sin_len = sizeof(addr);


 setsockopt(sock0,
   SOL_SOCKET, SO_REUSEADDR, (const char *)&yes, sizeof(yes));

 bind(sock0, (struct sockaddr *)&addr, sizeof(addr));

 listen(sock0, 5);

 while (1) {
   len = sizeof(client);
   sock = accept(sock0, (struct sockaddr *)&client, &len);
   write(sock, "HELLO", 5);

   close(sock);
 }

 close(sock0);

 return 0;
}

ここでの改造内容はsetsockopt()部分だけです。 SO_REUSEADDRが無いとサーバが作れないわけではありませんが、知っておくと便利です。