Help us understand the problem. What is going on with this article?

TCPソケットにおけるTIME_WAITとSO_REUSEADDRの関係

More than 1 year has passed since last update.

経緯

TCPクライアントのローカル側ポートを固定にした状態で一旦コネクションした後、アプリを終了させ、すぐまた起動するとコネクションできないという問題が発生。色々調べていくとTIME_WAITという状態に陥っていることが原因のようだった。サーバーサイドはSO_REUSEADDRオプションをソケットオプションとして付加させることによってその問題を回避することができるようだったが、クライアントはそのオプションを付加しても問題が発生する。それはなぜか?という疑問があった。

TIME_WAIT

以下のサイトに詳しく乗っている。

ぜんぶTIME_WAITのせいだ!

TCPの状態遷移

つまりは、アクティブクローズしたアプリケーションに対してネット上に漂うパケットが到着してしまってもちゃんと破棄できるようにするためのTCP上の機構で、TIME_WAITで一定時間待っておいて、他のソケットがそのポートを使えないようにする。OSに依存するけどWindows だとデフォルト120秒だった気がする。それぐらい待てばネット上に漂うパケットもなくなってるだろう。という経験的数値...なのかな?

SO_REUSEADDR

とはいっても上記サイトのようにポートが枯渇するのも問題なので、それの対処法としてSO_REUSEADDRがついているソケットに限り、TIME_WAITで待っているポートを再利用できるようにしてあげよう。というオプションです。(例外もあるようですが)

クライアントには無効なSO_REUSEADDR

通常、クライアントアプリケーションを作成する際に、ローカルポートが自動的に付与され、固定にすることはできませんが、無理やりbindすれば固定にすることができます。その状態でサーバーに接続し、クライアントからアクティブクローズし、再度コネクションを張ろうとすると、WSAEADDRINUSEエラーが発生します。これはSO_REUSEADDRを設定していたとしても同じです。これがなぜかわからなかった。

StackOverflowに質問してみる

それがこちらになります。

http://ja.stackoverflow.com/questions/32578/%E3%82%AF%E3%83%A9%E3%82%A4%E3%82%A2%E3%83%B3%E3%83%88%E5%81%B4%E3%81%AEso-reuseaddr%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6

この中での回答では、4つのパラメータ(ローカルアドレス、ローカルポート、リモートアドレス、リモートポート)が揃うconnect()の段階でソケットが使用されているかのチェックが行われ、それら4つのパラメータがTIME_WAITになっているパラメータと合致した場合に、エラーとなる。ということでした。

なぜサーバー側は通るのか?

「4つのパラメータが揃っていて、TIME_WAITに落ちているパラメータと合致した場合」という条件であれば、なぜサーバー側はOKなのだろうか?「サーバー側からアクティブクローズした場合にはサーバー側がTIME_WAITに落ちる」と仮定した場合、サーバー側ではaccept()の段階でクライアントから要求がきてもTIME_WAITに落ちているのでaccept()がエラーになるか、クライアントのコネクション要求が拒否されるはずであるが、そうはならない。これはSO_REUSEADDRをつけていようといまいとコネクションは成功する。謎。

じゃぁ、SO_REUSEADDRってなんだ

どういうときにSO_REUSEADDRが有用に働くのか。それは以下のサイトが詳しい。

Using SO_REUSEADDR and SO_EXCLUSIVEADDRUSE

色々小難しく書いてあるわけだが、最終的に実験してわかったことは、bind()されてclosesocket()がまだ呼ばれていない状態。つまり、ソケットが使用中の場合に、新たにソケットを作って、同じローカルアドレスにbind()しにいったときにエラーにならずにバインドできるようになるってのがSO_REUSEADDRオプションの特徴らしい。上記で言っていたコネクションは一旦closesocket()でクローズを行っているので、SO_REUSEADDRは関係がなかった。

結論

クライアント側はconnect()のタイミングで4つのパラメータが指定されて且つTIME_WAITにマッチするのでWSAEADDRINUSEエラーが発生してしまう。これはOSサイドの決まり事のようなものと思って納得するしかないのかな。

サーバー側はaccept()のタイミングで4つのパラメータが確定するはずだがなぜかコネクションが確立する。こちらは要調査が必要。

ソースコード

誰かの助けになるかもしれないのでソースコードを載せておく。

client.c
#include <stdio.h>
#include <winsock2.h>

int communicate(char *deststr, int dstport, int srcport) {
    struct sockaddr_in client;
    struct sockaddr_in server;
    SOCKET sock;
    int ret;
    BOOL yes = 1;
    char inbuf[2048];

    sock = socket(AF_INET, SOCK_STREAM, 0);

    // SO_REUSEADDR を ON にする
    ret = setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char *)&yes, sizeof(yes));
    if(ret) {
        printf("setsockopt(SO_REUSEADDR) error : %d\n", WSAGetLastError());
        closesocket(sock);
        return ret;
    }

    // ret = setsockopt(sock, SOL_SOCKET, SO_LINGER, (const char *)&yes, sizeof(yes));
    // if(ret) {
    //  printf("setsockopt(SO_LINGER) error : %d\n", WSAGetLastError());
    //  closesocket(sock);
    //  return ret;
    // }

    memset(&server, 0, sizeof(server));
    memset(&client, 0, sizeof(client));

    // サーバー側のアドレスとポートを指定する
    server.sin_family = AF_INET;
    server.sin_port = htons(dstport);
    server.sin_addr.S_un.S_addr = inet_addr(deststr);

    // クライアント側のポートを固定にする
    client.sin_family = AF_INET;
    client.sin_port = htons(srcport);
    client.sin_addr.S_un.S_addr = inet_addr(deststr);

    // ソケットをバインドする。ここではエラーは起こらない。
    ret = bind(sock, (struct sockaddr *)&client, sizeof(client));
    if (ret) {
        closesocket(sock);
        printf("bind error : %X\n", ret);
        return ret;
    }

    ret = connect(sock, (struct sockaddr *)&server, sizeof(server));
    if (ret) {
        printf("connect error : %d\n", WSAGetLastError());
        closesocket(sock);
        return ret;
    } else {
        // サーバーのアクティブクローズ時のウェイト用
        // memset(inbuf, 0, sizeof(inbuf));
        // recv(sock, inbuf, sizeof(inbuf), 0);
        printf("returned communicate()\n");
        closesocket(sock);
    }

    return ret;
}

// 指定のアドレス、ポートに接続し、接続できたらすぐさま切断し、次のコネクションを張る
int main(int argc, char *argv[]) {
    WSADATA wsaData;

    WSAStartup(MAKEWORD(2,0), &wsaData);

    while(1) {
        int ret = communicate("127.0.0.1", 12345, 23456);
        if(ret) {
            break;
        }
    };

    WSACleanup();

    return 0;
}
server.c
#include <stdio.h>
#include <winsock2.h>

int main()
{
    WSADATA wsaData;
    SOCKET sock;
    SOCKET sock0;

    struct sockaddr_in addr;
    struct sockaddr_in client;

    char inbuf[2048];

    int len;
    int n;

    if (WSAStartup(MAKEWORD(2 ,0), &wsaData) == SOCKET_ERROR) {
        return -1;
    }

    sock0 = socket(AF_INET, SOCK_STREAM, 0);
    if (sock0 < 0) {
        return 1;
    }

    addr.sin_family = AF_INET;
    addr.sin_port = htons(12345);
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if (bind(sock0, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
        return 1;
    }

    if (listen(sock0, 5) != 0) {
        return 1;
    }

    while (1) {
        len = sizeof(client);
        printf("wait for accept.\n");
        sock = accept(sock0, (struct sockaddr *)&client, &len);
        if (sock < 0) {
            perror("accept");
            break;
        }
        printf("accept!!\n");

        // クライアントアクティブクローズ時のウェイト用
        memset(inbuf, 0, sizeof(inbuf));
        recv(sock, inbuf, sizeof(inbuf), 0);

        closesocket(sock);
    }

    closesocket(sock0);
    WSACleanup();

    return 0;
}
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away