IP アドレスの変更をチェックする

WER 記事の最中ですが、諸事情により、急きょ、割り込み別ネタです。

Windows で IPv4 アドレスがあった場合に、変更されたことを確認する方法を紹介します。
IP Helper API の NotifyAddrChange() という API を使います。

なんとも便利な API があったりするもんです。
例えば、サービスプログラムの起動方法を “自動” にしていて OS  起動時に動き出した場合を考えてみます。
OS 起動時、割り当てられた IP アドレスが実際に有効になるのは、かなり遅いタイミングになるかと思います。
なぜかというと、まず、Windows ファイアウォールが動いてからでないと、IP アドレスの割り当ては行われません。
では、Windows ファイアウォール はいつ動き出すのかというと、WMI(Windows Management Instrumentation)
サービスが動いた後です。
では、WMI はいつ動き出すのかというと…きりがないので、この辺にしておきます。

OS 起動時、まだ、IP アドレスが有効でない場合には、localhost 127.0.0.1 が割り当てられています。
このアドレスをプログラムが使っても意味ありませんので、通常は、IP アドレスが有効になるまで”待つ”ことになるはずです。
こんなシナリオで使えそうな方法を考えてみます。上記 NotifyAddrChange() のページにあるサンプルコードは一度きりの
実行なので、これを”待つ”状態にします。


#define TIMEOUT (5 * 60 * 1000)

DWORD WaitNetworkEnabled()

    OVERLAPPED overlap;
    DWORD ret = NO_ERROR;
    HANDLE hand = NULL;
    DWORD nNumberOfBytes;
    DWORD dwLastError;

    // Winsockイベントハンドルを作成する
    overlap.hEvent = WSACreateEvent();
    if (overlap.hEvent == NULL) 
        return WSAGetLastError();   // イベントが作成できない

     // IPアドレスの変更があるまで、ループする
    while (TRUE)
    {
        WSAResetEvent(overlap.hEvent);
        if (NO_ERROR != NotifyAddrChange(&hand, &overlap))
        {
            dwLastError = WSAGetLastError();
            if (dwLastError != WSA_IO_PENDING)
            {
                CloseHandle(overlap.hEvent);
                return dwLastError;
            }
        }
        // localhost かどうかの判定条件が必要
        if (WAIT_OBJECT_0 != WaitForSingleObject(overlap.hEvent, TIMEOUT)) break; 
    }

    return ret;
}

これで、IP アドレスの変更を待つことができるわけですが、このループを抜けるための条件である localhost アドレスから
変更されたという条件を入れる必要があります。
これには、GetAdaptersAddresses() という IP Helper API を使って、ローカルコンピューターのアダプターと関連する
IP アドレスを取得します。

このリファレンスにもサンプルが載っていますが、ちょっと長いので、最低限の要件だけ抜き出してみます。

#define  GAA_FLAGS (GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_DNS_SERVER |\
                    GAA_FLAG_SKIP_FRIENDLY_NAME | GAA_FLAG_SKIP_MULTICAST |\
                    GAA_FLAG_SKIP_UNICAST)

#define WORKING_BUFFER_SIZE 15000

#define MALLOC(x) HeapAlloc(GetProcessHeap(), 0, (x))
#define FREE(x) HeapFree(GetProcessHeap(), 0, (x))

static DWORD IsInterfaceUp(void)
{
    IP_ADAPTER_ADDRESSES  *pAdapters = NULL, *pipa;
    DWORD nBufferLength = WORKING_BUFFER_SIZE;
    DWORD ret;   

    do
    {
        if (pAdapters != NULL) 
            FREE(pAdapters); 
        pAdapters = (IP_ADAPTER_ADDRESSES*)MALLOC(nBufferLength);
        if (pAdapters == NULL)
            return GetLastError();
         ret = GetAdaptersAddresses(AF_UNSPEC, GAA_FLAGS, NULL, pAdapters, &nBufferLength);
    }
    while (ret == ERROR_BUFFER_OVERFLOW);

    if (ret != ERROR_SUCCESS)
    {
        FREE(pAdapters);
        return ret;
    }

    for (pipa = pAdapters; pipa != NULL; pipa = pipa->Next)
    {
        if (pipa->IfType == IF_TYPE_SOFTWARE_LOOPBACK) continue;
        if (pipa->IfType == IF_TYPE_TUNNEL) continue;
        if (pipa->OperStatus == IfOperStatusUp) break;
    }
    FREE(pAdapters);
    pAdapters = NULL;

    if (pipa != NULL)
        return ERROR_SUCCESS;
     
    return ERROR_NO_DATA;
}

これを使って、IPアドレスが localhost アドレスから変更されたという条件に組み込みます。
先の WaitNetworkEnabled() の while 文を以下のように変更します。

// IPアドレスの変更があるまで、ループする
while (TRUE)
{
    WSAResetEvent(overlap.hEvent);
・・・・・
    ret = IsInterfaceUp();
    if (ret != ERROR_NO_DATA) break;
    if (WAIT_OBJECT_0 != WaitForSingleObject(overlap.hEvent, TIMEOUT)) break;
    GetOverlappedResult(hand, &overlap, &nNumberOfBytes, TRUE);
}

あとは、while 文を抜けた後、最後に後始末をやっておきます。

CancelIo(hand);
GetOverlappedResult(hand, &overlap, &nNumberOfBytes, TRUE);

これで完成しました。簡単に使うときには、

int _tmain(int argc, _TCHAR* argv[])
{
    printf("return = %d\n", WaitNetworkEnabled());
    return 0;
}

でいけます。(ちなみに、通常時に動かしても何も起こりません)
上記をビルドして、LAN ケーブルを抜いた状態で、起動すると、待ち状態になって、
LAN ケーブルを繋ぐとリターン(return = 0 で終了)してきます。

サービスプログラムを実装する際、こんな機能をスレッドに埋め込んでおいて、
OS 起動時のサービス自動起動で役立てるといいかもしれません。
例えば、サービスでソケットの listen ポートを開く際、localhost のままで動いても意味ないですから。

なお、IPv6 も同じような方法で実装することができると思います。

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中

%d人のブロガーが「いいね」をつけました。