作成日: 2003/02/18

アドレスファミリに依存しないソケットプログラミング

概要

ソケットプログラミングをする際にアドレスファミリから独立したコードを書いておくことで、 本格的に IPv6 環境に移行したときにプログラムの書き換えが不要になるうえ、 IPv4、IPv6 の両方の環境で動作するようになります。 なお、以下の説明は FreeBSD 5.0-RELEASE および gcc 3.2.1 の環境で行っています。

ソケットプログラミング

簡単に IPv4 ソケットプログラミングをおさらいします。 IPv4 ソケットプログラミングで TCP サーバおよびクライアントプログラムを書くと、例えば、以下のようになります。
以下のコードでは、サーバ(sv4.c)は引数で指定されたポートでクライアントからの接続を待ちます。クライアントから接続されると "Hello!" の文字列と改行をクライアントに送信し、接続を終了します。一方、クライアント(cl4.c)は引数で指定された IP アドレス とポートで待つサーバに接続し、サーバから送信された文字列を読み込み、標準出力に出力します。 コンパイルおよび起動は、例えば以下のようにしてください。

$ cc -o sv4 sv4.c         ← サーバのコンパイル
$ cc -o cl4 cl4.c         ← クライアントのコンパイル
$ ./sv4 5555 &            ← サーバを起動する(ポートに 5555 番を指定し、バックグラウンドで動作させる)
$ ./cl4 127.0.0.1 5555    ← クライアントを起動する(IP アドレスはローカルホスト、ポート番号 5555 番に接続する)

上記の例ではホストアドレスとしてループバックアドレス 127.0.0.1 を指定していますが、 マシンにネットワークインタフェースが存在するならば、その IP アドレスを指定してみてください。

/* sv4.c - IPv4 サーバ */
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    int port;                   /* ポート番号 */
    struct sockaddr_in addr;    /* アドレス情報を格納する */
    int svsock;                 /* サーバソケット識別子を格納する */
    int sock;                   /* クライアントと接続されたソケット識別子を格納する */
    char buf[128];
    int rc;

    /* 引数をチェックする */
    if (argc != 2) {
        fprintf(stderr, "usage: %s port\n", argv[0]);
        return -1;
    }

    /* 指定されたポート番号を得る */
    port = atoi(argv[1]);

    /* アドレス情報を設定する */
    memset(&addr, 0, sizeof(addr));

    addr.sin_family = AF_INET;              /* アドレスファミリを設定する */
    addr.sin_port = htons((u_short) port);  /* ポート番号を設定する */
    addr.sin_len = sizeof(addr);            /* アドレス情報の長さを設定する */

    /* ソケットを作成する */
    svsock = socket(AF_INET, SOCK_STREAM, 0);
    if (svsock < 0) {
        perror("socket()");
        return -1;
    }

    /* アドレスを割り当てる */
    rc = bind(svsock, (struct sockaddr *) &addr, sizeof(addr));
    if (rc < 0) {
        perror("bind()");
        close(svsock);
        return -1;
    }

    /* 接続要求の受け入れを開始する */
    rc = listen(svsock, 5);
    if (rc < 0) {
        perror("listen()");
        close(svsock);
        return -1;
    }

    for (;;) {
        /* クライアントからの接続要求を受け入れる */
        sock = accept(svsock, NULL, NULL);
        if (sock < 0) {
            perror("accept()");
            continue;
        }

        /* クライアントに文字列を送信する */
        strcpy(buf, "Hello!\n");
        write(sock, buf, strlen(buf));
        close(sock);
    }

    return 0;
}
/* cl4.c - IPv4 クライアント */
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    char *ipsaddr;              /* IPアドレス */
    int port;                   /* ポート番号 */
    in_addr_t ipnaddr;          /* IPアドレスの数値表現を格納する */
    struct sockaddr_in addr;    /* アドレス情報を格納する */
    int sock;                   /* ソケット識別子を格納する */
    char buf[128];
    int rc;

    /* 引数をチェックする */
    if (argc != 3) {
        fprintf(stderr, "usage: %s host port\n", argv[0]);
        return -1;
    }

    /* 指定されたアドレスとポート番号を得る */
    ipsaddr = argv[1];
    port = atoi(argv[2]);

    /*
     * アドレス情報を設定する
     */
    memset(&addr, 0, sizeof(addr));
    /* 文字列表現のIPアドレスを数値に変換(※注意:ブロードキャストアドレスは扱えない) */
    ipnaddr = inet_addr(ipsaddr);
    if (ipnaddr == INADDR_NONE) {
        fprintf(stderr, "inet_addr(): invalid address %s\n", ipsaddr);
        return -1;
    }

    addr.sin_family = AF_INET;                          /* アドレスファミリを設定する */
    addr.sin_port = htons((u_short) port);              /* ポート番号を設定する */
    memcpy(&addr.sin_addr, &ipnaddr, sizeof(ipnaddr));  /* IPアドレスを設定する */

    /* ソケットを作成する */
    sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) {
        perror("socket()");
        return -1;
    }

    /* サーバに接続する */
    rc = connect(sock, (struct sockaddr *) &addr, sizeof(addr));
    if (rc < 0) {
        perror("connect()");
        close(sock);
        return -1;
    }

    /* サーバから文字列を得て表示する */
    for (;;) {
        int len;
        len = read(sock, buf, sizeof(buf));
        if (len <= 0) break;
        printf("%.*s", len, buf);
    }

    close(sock);

    return 0;
}
アドレスファミリ

上で紹介したコードは IPv4 しか扱えませんし、IPv4 アドレスファミリに依存したコードになっています。例えば、

  • sockaddr_in は IPv4 アドレスしか扱うことができません。
  • socket(2) の引数などに使われている AF_INET は IPv4 のアドレスファミリです。
  • in_addr_t は IPv4 のアドレス値のみしか表現できません。
  • inet_addr(3) は IPv4 のアドレス表現しか変換することができません。

では、上記の点に留意すれば IPv6 に対応したプログラムを書くことができるのでしょうか。
IPv6 ではアドレスファミリに AF_INET6 を用います。また、sockaddr_in の代わりに sockaddr_in6 を使います。 確かに、AF_INET を AF_INET6 に置き換え、sockaddr_in を sockaddr_in6 に置き換え、 文字列表現の IP アドレスを数値に変換する箇所を IPv6 に対応させるようプログラミングすれば、 一応は IPv6 に対応できそうです。 しかし、それでは IPv6 でしか動作しないプログラムになってしまうでしょう。 現在のところ、IPv6 のみサポートすれば良い環境というのはあまり存在せず、 IPv4 と IPv6 混在の環境の方が多いのではないでしょうか。 これらアドレスファミリに依存したプログラミングを行う限り、プログラムは IPv4用 と IPv6用の2つを用意しなければならなくなってしまいます。 逆にアドレスファミリから独立したプログラミングが行えるならば、 プログラムは IPv4、IPv6 のどちらにも対応するようになり、プログラムも 1つで済むことになります。

アドレスファミリから独立するための API

IPv4、IPv6 の両方の環境で動作するプログラムを書くために、主役となる2つの API があります。
ひとつは、ノード名、サービス名をアドレス情報に変換するための getaddrinfo(3) です。この API は gethostbyname(3) および getservbyname(3) と似たような機能を持っており、アドレスファミリから独立した方法で アドレス情報を取得することができます。
もうひとつは、アドレス情報をノード名、サービス名に変換するための getnameinfo(3) です。 ちょうど getaddrinfo(3) と逆の処理を受け持ちます。この API は gethostbyaddr(3) および getservbyport(3) と似た機能を持ち、やはり、アドレスファミリから独立した方法でノード名、サービス名を取得することができます。
以下に 2つの API の簡単な説明を示しますが、詳細は man(1) などで確認してください。

int getaddrinfo(const char *nodename, const char *servname, const struct addrinfo *hints, struct addrinfo **res);

<IN> nodename: ノード名を渡します。例えば、"192.168.1.155" や "super-www-sv" などを渡せます。
<IN> servname: サービス名を渡します。例えば、"21" や "ftp" などを渡せます。
<IN> hints: 取得するアドレス情報の種類などを指定します。
<OUT> res: 取得されたアドレス情報へのポインタが返されます。ポインタが不要になったら、freeaddrinfo(3) で解放しなければなりません。
<RETURN> 成功時は 0 です。失敗時は gai_strerror(3) に返り値を渡してエラー文字列を取得できます。


int getnameinfo(const struct sockaddr *sa, socklen_t salen, char *host, size_t hostlen, char *serv, size_t servlen, int flags);

<IN> sa: アドレス情報へのポインタを渡します。
<IN> salen: アドレス情報の長さを渡します。
<OUT> host: 取得されたノード名を格納するバッファを渡します。
<IN> hostlen: host のバッファ長を渡します。
<OUT> serv: 取得されたサービス名を格納するバッファを渡します。
<IN> servlen: serv のバッファ長を渡します。
<IN> flags: ノード名、サービス名の取得方法などを指定します。
<RETURN> 成功時は 0 です。失敗時は gai_strerror(3) に返り値を渡してエラー文字列を取得できます。


以下のようなプログラムを使って、実際に上記 API を使用してみると、その動作がよくわかります。

/* tes.c */
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
    char *nodename;                 /* ノード名 */
    char *servname;                 /* サービス名 */
    struct addrinfo hints;          /* getaddrinfo() への指示用 */
    struct addrinfo *res = NULL;    /* getaddrinfo() からの返り値を受ける */
    struct addrinfo *adrinf;

    /* getnameinfo() からの返り値を受け取るバッファ */
    char hbuf[NI_MAXHOST];          /* ノード名 */
    char sbuf[NI_MAXSERV];          /* サービス名 */

    int rc;

    if (argc != 3) {
        fprintf(stderr, "usage %s nodename servname\n", argv[0]);
        return -1;
    }

    nodename = argv[1];
    servname = argv[2];

    /* まずアドレス情報を得る */
    memset(&hints, 0, sizeof(hints));
    hints.ai_socktype = SOCK_STREAM;
    rc = getaddrinfo(nodename, servname, &hints, &res);
    if (rc != 0) {
        fprintf(stderr, "getaddrinfo(): %s\n", gai_strerror(rc));
        return -1;
    }

    /* 得られたアドレス情報すべてに対して処理を行う */
    for (adrinf = res; adrinf != NULL; adrinf = adrinf->ai_next) {

        /* 得られたアドレス情報を文字列表現に戻す */
        rc = getnameinfo((struct sockaddr *) adrinf->ai_addr, adrinf->ai_addrlen,
                    hbuf, sizeof(hbuf),
                    sbuf, sizeof(sbuf),
                    NI_NUMERICHOST | NI_NUMERICSERV);   /* アドレス形式を得る */
                    /* 0); */   /* できる限り FQDN を得る */
        if (rc != 0) {
            fprintf(stderr, "getnameinfo(): %s\n", gai_strerror(rc));
            continue;
        }
        printf("---------------\n");
        printf("hbuf:[%s]\n", hbuf);
        printf("sbuf:[%s]\n", sbuf);
    }

    /* アドレス情報を解放する */
    if (res != NULL) freeaddrinfo(res);

    return 0;
}

コンパイルおよび起動は、例えば以下のようにしてください(上記コードを tes.c として保存したものとします)。

$ cc -o tes tes.c         ← コンパイル
$ ./tes localhost 21      ← 動かしてみる

ノード名に IP アドレス(例えば 127.0.0.1)や、ホスト名(例えば localhost)を渡してみてください。 また、サービス名にポート番号(例えば 21)や、サービス名(例えば ftp)を渡してみてください。 IPアドレス、ホスト名、ポート番号、サービス名、すべて変わりなくアドレス情報を取得でき、また、元に戻せるのが 分かると思います。また、IPv4 アドレスだけでなく IPv6 アドレスも扱えるのが分かると思います。
例えば、私のマシンについているネットワークインタフェースにはリンクローカルアドレスとして fe80::202:b3ff:fe20:fc49%fxp0 が割り当てられていますが、このアドレスを使って、

$ ./tes fe80::202:b3ff:fe20:fc49%fxp0 21

などとすると、

---------------
hbuf:[fe80::202:b3ff:fe20:fc49%fxp0]
sbuf:[21]

と表示され、正しく変換が行われているのが分かります。
また、上記コードのなかで getnameinfo(3) の第7引数を 0 にすると、できるかぎり ホスト名、サービス名を返すようになります。例えば、

$ ./tes ::1 21

に対しては、

---------------
hbuf:[localhost]
sbuf:[ftp]

のように表示されるようになります。もし、ホスト名やサービス名が得られない場合でも、対応する数値形式が 取得されます。

アドレス情報を格納する構造体

さきほど紹介したコードをみれば自明のことかもしれませんが、上記コードのなかではアドレス情報を格納するために、 struct addrinfo を用いています。今まで IPv4 で用いていた struct sockaddr_in や、IPv6 のための struct sockaddr_in6 を使う代わりに、struct addrinfo を用いることでアドレスファミリに依存せずに アドレス情報を保持することができるようになります。
struct addrinfo のメンバをみてみましょう(以下は getaddrinfo(3) のマニュアルから引用しました)。

struct addrinfo {
    int     ai_flags;     /* AI_PASSIVE, AI_CANONNAME, AI_NUMERICHOST */
    int     ai_family;    /* PF_xxx */
    int     ai_socktype;  /* SOCK_xxx */
    int     ai_protocol;  /* 0 or IPPROTO_xxx for IPv4 and IPv6 */
    size_t  ai_addrlen;   /* length of ai_addr */
    char   *ai_canonname; /* canonical name for nodename */
    struct sockaddr  *ai_addr; /* binary address */
    struct addrinfo  *ai_next; /* next structure in linked list */
};

ai_family というメンバがあります。getaddrinfo(3) でアドレス情報を取得すると、 ai_family には取得したアドレス情報のアドレスファミリが設定されます。そのほかにも、ai_socktype には ソケットタイプ、ai_protocol にはプロトコル、ai_addrlen にはアドレス情報の長さが設定されます。
今までのプログラミングでは socket(2)bind(2) を呼び出す際に、 AF_INET や IPPROTO_TCP といった定数をハードコーディングしていました。また、アドレス情報の長さも sizeof(struct sockaddr_in) のようにして、直接指定していることが多かったと思います。
しかし、struct addrinfo のメンバにはこれらの API に必要な値が保持されているため、ハードコーディングする必要は なくなります。socket(2) や bind(2) には、getaddrinfo(3) で得た struct addrinfo のメンバの値をそのまま渡すだけで良くなりました。 例えば、以下のようになります。

    struct addrinfo *adrinf;
    int sock, rc;
    /* アドレス情報を得る */
    rc = getaddrinfo(..., &adrinf);
        ...
    /* ソケットを作成する */
    sock = socket(adrinf->ai_family, adrinf->ai_socktype, adrinf->ai_protocol);
        ...
    /* ソケットにアドレス情報を関連付ける */
    rc = bind(sock, adrinf->ai_addr, adrinf->ai_addrlen);
        ...

最初に紹介した TCP サーバ(sv4.c)の例と比べてみてください。最初の例では socket(2) の引数には AF_INET がハードコーディングされていましたし、bind(2) の引数にも sockaddr_in 型の変数のサイズを sizeof() で直接渡していました。 しかし、上記の方法ならば、定数 AF_INET を書く必要もありませんし、アドレス長も sizeof() で取る必要はありません。 struct addrinfo のメンバの値をそのまま渡すだけで済んでいます。

アドレス情報を格納する構造体(その2)

上で紹介した struct addrinfo と別に、もうひとつ構造体を紹介しておく必要があります。その構造体とは、 struct sockaddr_storage です。この構造体は、accept(2)recvfrom(2) を呼び出した際に、 リモートホストのアドレス情報を受け取るために使用できます。この構造体はどんな種類のアドレス情報に対しても十分に 大きいことが保障されていますので、安心してアドレス情報を受け取ることができます。 struct sockaddr_storage で受け取ったアドレス情報は struct addrinfo と同じように getnameinfo(3) に渡すことで、 ノード名、サービス名を得ることができます。例えば、以下のようになります。

    struct sockaddr_storage peer; /* クライアントのアドレス情報を格納する */
    socklen_t len;                /* クライアントのアドレス情報の長さを格納する */
    int svsock;
    int sock;
    char hbuf[NI_MAXHOST];  /* ノード名 */
    char sbuf[NI_MAXSERV];  /* サービス名 */
    int rc;

    /* サーバソケットをセットアップする */
    svsock = socket(...);
        ...

    /*
     * クライアントからのコネクト要求を受け入れると共に、
     * クライアントのアドレス情報を取得する。
     */
    len = sizeof(peer);
    sock = accept(svsock, (struct sockaddr *) &peer, &len);
    if (sock < 0) {
        perror("accept()");
        ...
    }

    /* クライアントのノード名とサービス名を得る */
    rc = getnameinfo((struct sockaddr *) &peer, len,
            hbuf, sizeof(hbuf),
            sbuf, sizeof(sbuf),
            0);
       ...
サンプルソース

後は紹介した API を駆使してプログラミングするだけです。ソースの解説をしても退屈でしょうから、 サンプルソースの提示のみにとどめておきます。

sv.c (4.63 KB)

サーバプログラムです。クライアントの接続に対し、 "Hello" の文字列とともに、クライアントのIPアドレス、ポート番号を返します。

cl.c (2.13 KB)

クライアントプログラムです。サーバに接続して文字列を受け取ります。

なお、上記ソースは文字コードを EUC で保存してあります。Windows 環境で閲覧するときはご留意ください。 ちなみに、コンパイルおよび起動した例を以下に示しておきます。

$ cc -o sv sv.c                                        ・・・(1)
$ cc -o cl cl.c                                        ・・・(2)
$ ./sv 5555 &                                          ・・・(3)
[::]:5555
[0.0.0.0]:5555
$ ./cl fe80::202:b3ff:fe20:fc49%fxp0 5555              ・・・(4)
[fe80::202:b3ff:fe20:fc49%fxp0]:5555
Hello [fe80::202:b3ff:fe20:fc49%fxp0]:49154
$ ./cl localhost 5555                                  ・・・(5)
[::1]:5555
Hello [::1]:49155
[127.0.0.1]:5555
Hello [127.0.0.1]:49156
$ ./cl 127.0.0.1 5555                                  ・・・(6)
[127.0.0.1]:5555
Hello [127.0.0.1]:49157

(1)、(2) サーバ、クライアントをそれぞれコンパイルしています。
(3) サーバをポート 5555 番を指定し、バックグラウンド動作させています。
(4) クライアントを私のマシンのリンクローカルアドレスの 5555 番ポートに接続させています。
(5) クライアントを localhost の 5555 番ポートに接続させています。
(6) クライアントを 127.0.0.1 の IPv4 ループバックアドレスに接続させています。
よくみてください。(5) で IPv4、IPv6 の両方のアドレスに対し接続しているのがわかるでしょうか。 ループバックアドレスに対してなので、あんまり嬉しくは感じないかもしれませんが、少なくともサーバ、クライアントともに IPv4、IPv6 の両方に対して動作しているのがわかります。

参考文献

IPv6 ネットワークプログラミング 萩野 純一郎 著 小山 彩子 訳
株式会社 アスキー ISBN4-7561-4236-2
この本を読むまで IPv4、IPv6 の両方で動作するプログラムを書くなどということは思いもしませんでした。 目から鱗が落ちました。内容も非常にわかりやすく、IPv4、IPv6 混在環境における問題点などを指摘されているほか、 コードのサンプルも豊富です。また、コーディングに際し留意する点なども細かに書かれており、大変、勉強になりました。 お勧めできる1冊です。