TCPを使う(サーバ、SO_REUSEADDR)
前述したTCPサーバ例では、サーバを終了した直後にもう一度サーバを起動しようとすると、bindがエラーで終了することがあります。 ここでは、その問題を回避するためにSO_REUSEADDRを有効にする方法を説明したいと思います。
TIME_WAIT
TCPサーバのプログラムを書いていて、TCPサーバを終了して直後にもう一度起動したときに、 bindが「Address already in use」というようなエラーで失敗してしまったとこは無いでしょうか? 「あれ?もうTCPのサーバプロセスは終了しているのに。何故、bind出来ないのだろう?」と思いつつ、 しばらく時間がたってからもう一度実行すると問題なくbindが成功したりします。 この問題はTCP自体の仕組み(仕様)によって引き起こされています。(winsockの問題ではなく、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 <winsock2.h>
int
main()
{
WSADATA wsaData;
SOCKET sock0;
struct sockaddr_in addr;
struct sockaddr_in client;
int len;
SOCKET sock;
BOOL yes = 1;
WSAStartup(MAKEWORD(2,0), &wsaData);
sock0 = socket(AF_INET, SOCK_STREAM, 0);
addr.sin_family = AF_INET;
addr.sin_port = htons(12345);
addr.sin_addr.S_un.S_addr = INADDR_ANY;
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);
send(sock, "HELLO", 5, 0);
closesocket(sock);
}
closesocket(sock0);
WSACleanup();
return 0;
}
ここでの改造内容はsetsockopt()部分だけです。 SO_REUSEADDRが無いとサーバが作れないわけではありませんが、知っておくと便利です。