第1回 IPv6ソケットプログラミング [C++]

(2006.8.5 ページを独立。)

C/C++でのIPv6ソケットプログラミングについて。

ソケットについてはすでに掃いて捨てるほど解説サイトがあるが、IPv6に対応した、しかもポータブルな書き方を紹介しているところは見当たらなかった。

目次:

  1. サーバを作る
  2. クライアントを作る
  3. IPv6, IPv4を区別するサーバ

@ サーバを作る

まず、ソケットでIPv4あるいはIPv6クライアントからの接続を受け付けるプログラムを作ってみる。Fedora Core 5 Linuxで試した。

IPv4 onlyのときの典型的なコードは、次のようになる。※現代ではこのように書いてはいけません。

C++
[RAW]
  1. /**
  2. * TCP で listen する. IPv4 only. 実コードで使用不可!
  3. *
  4. * @param node bind() するホスト名. NULL の場合 INADDR_ANY.
  5. * @param port ポート番号. 0 の場合, ephemeral port のなかで空いているポート
  6. *
  7. * @return 正常な場合, 新しいソケット. 失敗した場合, INVALID_SOCKET.
  8. */
  9. SOCKET setupServer(const char* node, int port) noexcept(false)
  10. {
  11. // AF_INET: IPv4 only.
  12. SOCKET sock_fd = socket(AF_INET, SOCK_STREAM, 0);
  13. if ( sock_fd == INVALID_SOCKET ) {
  14. perror("socket open failed");
  15. return INVALID_SOCKET;
  16. }
  17. int reuse = 1;
  18. if ( setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, (char*) &reuse,
  19. sizeof(reuse)) < 0 ) {
  20. closesocket(sock_fd);
  21. return INVALID_SOCKET;
  22. }
  23. sockaddr_in addr; // IPv4 only. IPv4/IPv6両対応では sockaddr_storage を使え.
  24. memset(&addr, 0, sizeof(addr));
  25. addr.sin_family = AF_INET; // IPv4 only.
  26. if (node) {
  27. addr.sin_addr.s_addr = inet_addr(node); // ネットワークバイトオーダ. IPv4専用.
  28. if (addr.sin_addr.s_addr == INADDR_NONE) {
  29. closesocket(sock_fd);
  30. return INVALID_SOCKET;
  31. }
  32. }
  33. else
  34. addr.sin_addr.s_addr = htonl(INADDR_ANY);
  35. addr.sin_port = htons(port);
  36. if ( bind(sock_fd, (sockaddr*) &addr, sizeof(addr)) == -1 ) {
  37. perror("bind failed");
  38. closesocket(sock_fd);
  39. return INVALID_SOCKET;
  40. }

socket() に渡す AF_INET はIPv4を指定するものだし、bind() する自ノードアドレスを格納する sockaddr_in はIPv4 アドレスしか格納できない。inet_addr() も IPv4アドレスのみ扱える。

これではIPv6で通信はできない。ではどうするか。

とても複雑な getaddrinfo() 関数

IPv6ということで、AF_INET6sockaddr_in6を使ってみるか。それでもいいが、よりよいのは、getaddrinfo() に適切なソケットアドレス情報などを生成させ、それでソケットを生成し、bind() などを行う。

ソケットアドレス構造体が得られれば、それ以降はIPv4と大差ない。キモは getaddrinfo() にある。

getaddrinfo() のプロトタイプ宣言は次のとおり。nodename はホスト名またはIPアドレスの文字列、servname はポート番号またはサービス名、hints はヒントを与える。ヒントの制約にしたがって getaddrinfo()res にソケットアドレスのリストを生成する。

#include <sys/socket.h>
#include <netdb.h>

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

nodenameNULLを渡すと、hints.ai_flagsAI_PASSIVE を設定していればサーバ用, そうでなければクライアント用の loopback アドレスを生成する。(後述)

hints としても渡すaddrinfo構造体は、次のメンバを持つ。<netdb.h> ヘッダで定義される。

struct addrinfo {
    int              ai_flags;
    int              ai_family;
    int              ai_socktype;
    int              ai_protocol;
    socklen_t        ai_addrlen;
    struct sockaddr* ai_addr;
    char*            ai_canonname;
    struct addrinfo* ai_next;
};

いずれのフィールドもそれぞれ 0にすると、制約を課さずに、任意の組み合わせを得られる。ただし、あくまでも hint であって, 値を設定したからといってその通りになるとは限らない (後述 AI_V4MAPPED を見よ)。

ai_socktype, ai_protocol は、socket() に与える予定の値とする。

ai_family については, サーバ側では, 通常は AF_INET6 を設定する。IPv6アドレスは IPv4/IPv6両方からの接続を受け付けられる。クライアント側では AF_UNSPEC が通常。後のセクションで、サーバ側で ai_familyAF_UNSPEC を設定して、IPv4/IPv6二つのソケットアドレスを得る方法も解説する。

Note.

(2012.11) Windows XP または Windows Server 2003では, AF_INET6を指定すると, IPv6クライアントからしか接続できなくなってしまう。これらでも動かしたいときは, ソケットを2つ作らないといけない。

Windows Vista以降は問題ない。Dual-Stack Sockets for IPv6 Winsock Applications (Windows)

ai_flagsにいろいろな値を設定して、getaddrinfo()の挙動を制御できる。場合分けが複雑すぎる。

AI_PASSIVE
nodename = NULL のときの挙動。偽なら loopback アドレスを生成し、真なら INADDR_ANY (0.0.0.0) または IN6ADDR_ANY_INIT (::) を生成する。サーバ側では, any アドレスに bind() するために, 真にすること。
AI_CANONNAME
ホストの正式名を要求する。
AI_NUMERICHOST
nodename は数値形式のネットワークアドレスに限る。ホスト名の名前解決は行わない。
AI_NUMERICSERV
servname はポート番号の文字列に限る。サービス名の解決は行わない. ※Windows ではこの定数はない。
AI_V4MAPPED
hints.ai_familyAF_INET6でかつ nodename の IPv6アドレスが見つからなかった場合の挙動。AI_V4MAPPED が真のときは IPv4-mapped IPv6 アドレスを返す。偽のときは, hints.ai_family を無視して, IPv4 アドレス (AF_INET) が得られる. 特に前者は, ソケットアドレスは IPv6 にも関わらず, 通信は IPv4 で行われることに注意。

hints.ai_familyAF_INET6 でない場合、AI_V4MAPPED は無視される。

AI_ALL
AI_V4MAPPED フラグとともに指定された場合、getaddrinfo() はIPv4, IPv6アドレスの両方を返す。AI_V4MAPPEDフラグなしに指定した場合、AI_ALLフラグは無視される。このフラグを使うことはない.
AI_ADDRCONFIG
ローカルシステムがIPv4アドレスを持つ場合に限りIPv4アドレスを取得し、ローカルシステムがIPv6アドレスを持つ場合に限りIPv6アドレスを取得する。

glibc は hints.ai_family = AF_INET6 の場合, ai_flagsAI_V4MAPPED | AI_ADDRCONFIG を与えるのを推奨するが, IPv4-mapped IPv6 というのがそもそも不味いアイディアなので、よろしくない。

getaddrinfo() 内部から DNS で名前解決するので、この関数はブロックすることにも注意。

ソケットアドレスの生成〜listen()

では、作っていこう。

まず、ヘッダファイルの取り込み。Windows Winsock2 は UNIX とかなり異なる。適宜、差異を吸収するよう定義する. UNIX では file descriptor の型は int だが、Windows では UINT_PTR 型で符号なしになっている。

C++
[RAW]
  1. #include <string.h>
  2. #include <errno.h>
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include <assert.h>
  6. #include <stdint.h>
  7. #ifndef _WIN32
  8. #include <unistd.h>
  9. #define closesocket close
  10. typedef int SOCKET; // Windows: UINT_PTR 型
  11. #define WINAPI
  12. #define INVALID_SOCKET -1
  13. typedef uint32_t DWORD;
  14. #else
  15. #define STRICT
  16. #define WIN32_LEAN_AND_MEAN
  17. #include <winsock2.h>
  18. #endif
  19. #include "debug_print.h"

getaddrinfo() を使う実際のコードは、次のようになる [C++]. AF_INET6 は (名前と異なり) IPv4/IPv6 両方を受け付ける。

C++
[RAW]
  1. constexpr int BACKLOG = 5;
  2. constexpr int PORT = 12345;
  3. /**
  4. * @param node bind() するホスト名. NULL の場合, INADDR_ANY or IN6ADDR_ANY_INIT.
  5. * @param port ポート番号. 0 の場合、空き番号を利用.
  6. * @return INVALID_SOCKET エラー
  7. * >=0 listenしているソケット
  8. */
  9. SOCKET tcp_listen(const char* node, int port)
  10. {
  11. if ( port < 0 )
  12. return INVALID_SOCKET;
  13. int err;
  14. struct addrinfo hints;
  15. struct addrinfo* res = NULL;
  16. struct addrinfo* ai;
  17. SOCKET sockfd;
  18. memset(&hints, 0, sizeof(hints));
  19. if (!node)
  20. hints.ai_family = AF_INET6; // AF_INET6は、IPv4/IPv6両方を受け付ける。
  21. hints.ai_socktype = SOCK_STREAM;
  22. // AI_PASSIVE をセットせずに, node = NULL の場合は, loopbackアドレス
  23. // (INADDR_LOOPBACK or IN6ADDR_LOOPBACK_INIT).
  24. // AI_PASSIVE をセットして, node = NULLのときは, INADDR_ANY, IN6ADDR_ANY_INIT.
  25. #ifdef _WIN32
  26. hints.ai_flags = AI_PASSIVE;
  27. #else
  28. hints.ai_flags = AI_PASSIVE | AI_NUMERICSERV;
  29. #endif
  30. // (node, service) の両方を nullptr にすると getaddrinfo() が失敗するが,
  31. // このようにすれば, ephemeral port から取れる.
  32. char service[11];
  33. sprintf(service, "%d", port);
  34. err = getaddrinfo(node, service, &hints, &res);
  35. if (err != 0) {
  36. fprintf(stderr, "getaddrinfo() failed: %s\n", gai_strerror(err));
  37. return INVALID_SOCKET;
  38. }
  39. ai = res;
  40. sock_print("create socket", ai->ai_family, ai->ai_socktype);
  41. sockfd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
  42. if ( sockfd == INVALID_SOCKET ) {
  43. freeaddrinfo(res);
  44. return INVALID_SOCKET;
  45. }

エラー発生時は errno ではない。

ソケットアドレスを得たら、あとはIPv4のときと大差ない。socket() でソケットを生成し、bind()でIPアドレスと結びつけ、listen()でlistenキューを生成する。freeaddrinfo()でソケットアドレスのリストを解放するのを忘れずに。

C++
[RAW]
  1. int on = 1;
  2. if ( setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (const char*) &on,
  3. sizeof(on)) < 0 ) {
  4. closesocket(sockfd);
  5. freeaddrinfo(res);
  6. return INVALID_SOCKET;
  7. }
  8. printf("set SO_REUSEADDR\n");
  9. if (bind(sockfd, ai->ai_addr, ai->ai_addrlen) < 0) {
  10. closesocket(sockfd);
  11. freeaddrinfo(res);
  12. return INVALID_SOCKET;
  13. }
  14. // もういらん.
  15. freeaddrinfo(res);
  16. res = NULL;
  17. // 表示する. port = 0 の場合は [::]:0 になってしまう。取得しなおす
  18. sockaddr_storage addr; // sockaddr_in は IPv4 only.
  19. socklen_t socklen = sizeof(addr);
  20. if ( getsockname(sockfd, (sockaddr*) &addr, &socklen) == -1 ) {
  21. perror("get port failed");
  22. closesocket(sockfd);
  23. return INVALID_SOCKET;
  24. }
  25. sockaddr_print("listen socket", (sockaddr*) &addr, socklen);
  26. if (listen(sockfd, BACKLOG) < 0) {
  27. closesocket(sockfd);
  28. return INVALID_SOCKET;
  29. }
  30. printf("Listen succeeded\n");
  31. return sockfd;
  32. }

接続を受け付け、通信する

クライアントからの接続を受ける場合、一番簡単なのは、accept() でブロックしてしまう。accept() は接続を受け付けたら新しいソケットを生成してそれを返す。

ソケットアドレス構造体は、IPv4、IPv6で大きさが違う。sockaddr_storage は、システムでサポートされているソケットアドレス構造体のどれよりも大きいことが確かなので、ソケットアドレス構造体を格納するときはこれを使う。

POSIXでは sockaddr_in, sockaddr_in6, sockaddr_un だけが定められている。Linuxでは、このほかに sockaddr_ax25, sockaddr_ipx などもある。

C++
[RAW]
  1. void test_server()
  2. {
  3. SOCKET listen_sockfd;
  4. listen_sockfd = tcp_listen(NULL, PORT); // [::]:12345
  5. if (listen_sockfd == INVALID_SOCKET) {
  6. perror("server");
  7. exit(1);
  8. }
  9. printf("wait...\n");
  10. while (1) {
  11. SOCKET cs;
  12. // クライアントの情報を得る場合
  13. struct sockaddr_storage client_sa; // sockaddr_in 型ではない。
  14. socklen_t len = sizeof(client_sa);
  15. cs = accept(listen_sockfd, (struct sockaddr*) &client_sa, &len);
  16. if ( cs == INVALID_SOCKET ) {
  17. if (errno == EINTR)
  18. continue;
  19. perror("accept");
  20. exit(1);
  21. }
  22. // accept した相手先を表示.
  23. printf("accepted.\n");
  24. sockaddr_print("peer", (struct sockaddr*) &client_sa, len);

クライアントとの通信は並列化する。UNIX と Windows で書き方が変わる。

UNIX では、簡単に fork() して、親プロセスはクライアントからの接続を待ちつつ、子プロセスで通信を行う。Windows には fork() がないので、新しいスレッドを作ってやる。いずれの場合も、同時接続数が多くなるとリソースが必要になる。そのような場合は、非同期通信などにする。(別サンプル)

C++
[RAW]
  1. #ifdef _WIN32
  2. // Windows: マルチスレッド
  3. MyArgData* arg = (MyArgData*) HeapAlloc(GetProcessHeap(),
  4. HEAP_ZERO_MEMORY,
  5. sizeof(MyArgData) );
  6. arg->sockfd = cs;
  7. HANDLE h = CreateThread(
  8. NULL, // lpThreadAttributes
  9. 0, // dwStackSize. 0 is default size.
  10. handler, // lpStartAddress
  11. arg, // lpParameter
  12. 0, // dwCreationFlags
  13. NULL); // lpThreadId
  14. if ( h == NULL ) {
  15. printf("CreateThread failed: %lu\n", GetLastError() );
  16. exit(1);
  17. }
  18. #else
  19. // UNIX: 簡単に fork する
  20. if (fork() == 0) {
  21. // 子プロセス
  22. closesocket(listen_sockfd);
  23. MyArgData* arg = (MyArgData*) malloc(sizeof(MyArgData));
  24. arg->sockfd = cs;
  25. handler(arg);
  26. exit(0);
  27. }
  28. // 親プロセス
  29. closesocket(cs);
  30. #endif
  31. }
  32. }

ハンドラで実際の通信を行う。Windows でも動かす場合, read(), write() ではなく, recv(), send() を用いる。

C++
[RAW]
  1. struct MyArgData {
  2. SOCKET sockfd;
  3. int hoge;
  4. };
  5. // Windows: スレッド, UNIX: 子プロセス
  6. DWORD WINAPI handler(void* arg)
  7. {
  8. MyArgData* myArg = (MyArgData*) arg;
  9. // 1バイトだけやり取りする
  10. unsigned char ch;
  11. recv( myArg->sockfd, (char*) &ch, 1, 0);
  12. ch++;
  13. send( myArg->sockfd, (char*) &ch, 1, 0);
  14. closesocket(myArg->sockfd);
  15. #ifdef _WIN32
  16. HeapFree(GetProcessHeap(), 0, arg);
  17. #else
  18. free(arg);
  19. #endif
  20. return 0;
  21. }

最後に main() を書いて、完成!

C++
[RAW]
  1. int main()
  2. {
  3. #ifdef _WIN32
  4. // Initialize Winsock2
  5. WSADATA wsaData;
  6. int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
  7. if (iResult != 0) {
  8. printf("WSAStartup failed: %d\n", iResult);
  9. return 1;
  10. }
  11. #endif
  12. test_server();
  13. return 0;
  14. }

@ クライアントを作る

(2006.6.29の日記を加筆。)

クライアントは、サーバに接続する部分をIPv4/IPv6両対応にする。いったん接続すれば、IPv4, IPv6の違いを意識する必要はない。

IPv6でもIPv4と同様に、ホスト名とポート番号の組み合わせでサーバに接続する。しかし、サーバがIPv4にしか対応していない、IPv6にしか対応していない、どちらもありうるので、IPv4のみで接続するときに比べてコードがいくぶん長くなる。

getaddrinfo() 関数は、サーバを制作するときと同様、ヒントにしたがって接続先のソケットアドレスの選択肢を返す。クライアント側ではホスト名も与える。

基本的なアイディアは、ai_familyAF_UNSPEC にして getaddrinfo() 関数を呼び出し、いずれかまたは両方の候補 (IPv4, IPv6) を得る。getaddrinfo() は, AI_NUMERICHOST を指定しない限り, DNS などでホスト名の名前解決も行う (ブロックする). ヒントの ai_familyAF_UNSPEC のとき, 指定したホストがIPv4アドレス、IPv6アドレスの両方を持つ場合は, 二つのソケットアドレスを返す。そうでないときは一つだけ返す。

connect_to_server.cpp

C++
[RAW]
  1. /**
  2. * サーバに接続する。Block する.
  3. * @return エラーのとき INVALID_SOCKET
  4. */
  5. SOCKET connect_to_server(
  6. const char* hostname, // IPv4 or IPv6ホスト名
  7. int port ) // ポート番号
  8. {
  9. assert( hostname );
  10. if ( port <= 0 || port > 65535 )
  11. return INVALID_SOCKET;
  12. SOCKET sockfd = INVALID_SOCKET;
  13. int err;
  14. struct addrinfo hints;
  15. struct addrinfo* res = NULL;
  16. struct addrinfo* ai;
  17. memset(&hints, 0, sizeof(hints));
  18. hints.ai_family = AF_UNSPEC; // IPv4/IPv6両対応
  19. hints.ai_socktype = SOCK_STREAM;
  20. // AI_NUMERICSERV を指定しなければ、service は 'pop'などでもよい。
  21. hints.ai_flags = AI_NUMERICSERV;
  22. char service[11];
  23. sprintf(service, "%d", port);
  24. err = getaddrinfo(hostname, service, &hints, &res);
  25. if (err != 0) {
  26. printf("getaddrinfo() failed: %s\n", gai_strerror(err));
  27. return INVALID_SOCKET;
  28. }

接続アドレスに対して順に接続できるかどうか試していく。

getaddrinfo() がIPv4、IPv6アドレスのどちらを先に返すかは分からないので、このコードではIPv4/IPv6の両方を受け付けるサーバに対してどちらで接続するかは決められない。

C++
[RAW]
  1. // どれか一つが成功するまで試みる.
  2. for (ai = res; ai; ai = ai->ai_next) {
  3. sockaddr_print("connect...", ai->ai_addr, ai->ai_addrlen);
  4. RETRY:
  5. sockfd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
  6. if ( sockfd == INVALID_SOCKET ) {
  7. freeaddrinfo(res);
  8. return INVALID_SOCKET;
  9. }
  10. // 実際に connect しないと正解か分からない.
  11. if ( connect(sockfd, ai->ai_addr, ai->ai_addrlen) < 0 ) {
  12. if (errno == EINTR) { // シグナル割り込み
  13. // FreeBSD では, open からやり直さないと EALREADY エラー.
  14. closesocket(sockfd);
  15. goto RETRY;
  16. }
  17. closesocket(sockfd);
  18. sockfd = INVALID_SOCKET;
  19. continue;
  20. }
  21. // OK. Blocking 版は一つの成功でOK.
  22. sockaddr_print("connected", ai->ai_addr, ai->ai_addrlen);
  23. break;
  24. }
  25. freeaddrinfo(res);
  26. return sockfd;
  27. }

main() では、簡単に、1バイト送信し1バイト受信して表示するだけ。以上!

sock_client.cpp

C++
[RAW]
  1. int main()
  2. {
  3. SOCKET sockfd;
  4. char ch;
  5. #ifdef _WIN32
  6. // Initialize Winsock
  7. WSADATA wsaData;
  8. int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
  9. if (iResult != 0) {
  10. printf("WSAStartup failed: %d\n", iResult);
  11. return 1;
  12. }
  13. #endif
  14. sockfd = connect_to_server(HOSTNAME, PORT);
  15. if ( sockfd == INVALID_SOCKET ) {
  16. perror("client");
  17. return 1;
  18. }
  19. // 送受信
  20. ch = 'A';
  21. send(sockfd, &ch, 1, 0);
  22. recv(sockfd, &ch, 1, 0);
  23. printf("char from server = '%c'\n", ch);
  24. closesocket(sockfd);
  25. return 0;
  26. }

@ IPv6, IPv4を区別するサーバ

(2006.6.29の日記を加筆。)

今度は、IPv6のときのみ違うサービスが提供できるよう、IPv6専用ソケット、IPv4専用ソケットを作ってみよう。

getaddrinfo() に与えるホスト名を NULL, ヒントの ai_familyAF_UNSPEC にすると、:: (IPv6) と 0.0.0.0 (IPv4) の二つのソケットアドレスを得られる。これらを順に bind() し、両方を listen() する。

server2.cpp

C++
[RAW]
  1. /**
  2. * IPv4専用とIPv6専用の2つのソケットで待ち受ける. listen まで行う.
  3. * @param node bind() するホスト名. NULL の場合, INADDR_ANY or IN6ADDR_ANY_INIT.
  4. * @param port ポート番号. 0 の場合, 空き番号を利用.
  5. * @return -1 エラー発生
  6. * 0 listenできたソケットがない
  7. * >=1 listenできたソケットの数
  8. */
  9. int tcp_listen_v6separate(
  10. const char* node,
  11. int port,
  12. SOCKET* sockfd, // (out) ソケットfdの配列
  13. int fd_size)
  14. {
  15. assert(sockfd);
  16. if (port < 0)
  17. return -1;
  18. if (fd_size <= 0)
  19. return -1;
  20. int err;
  21. struct addrinfo hints;
  22. struct addrinfo* res = NULL;
  23. struct addrinfo* ai;
  24. int socksize = 0;
  25. memset(&hints, 0, sizeof(hints));
  26. hints.ai_family = AF_UNSPEC; // IPv4/IPv6専用ソケットを作る。
  27. hints.ai_socktype = SOCK_STREAM;
  28. hints.ai_flags = AI_PASSIVE | AI_NUMERICSERV; // acceptするためにbind()する場合は指定する。
  29. char service[11];
  30. sprintf(service, "%d", port);
  31. // node = NULLのとき、INADDR_ANY, IN6ADDR_ANY_INIT に相当。
  32. err = getaddrinfo(node, service, &hints, &res);
  33. if (err != 0) {
  34. fprintf(stderr, "getaddrinfo() failed: %s\n", gai_strerror(err));
  35. return -1;
  36. }
  37. // 順に試す.
  38. for (ai = res; ai; ai = ai->ai_next) {
  39. sock_print("create socket", ai->ai_family, ai->ai_socktype);
  40. *sockfd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
  41. if ( *sockfd == INVALID_SOCKET )
  42. goto err;
  43. socksize++;

IPv6ソケットに対して IPV6_V6ONLY ソケットオプションを設定するのがポイント。このオプションを設定すると、このソケットはIPv6でのみ通信できる。IPv4-mapped (IPv6) アドレスも, アドレスは IPv6にも関わらず IPv4通信なので, 弾く。RFC 3493 Basic Socket Interface Extensions for IPv6 (Obsoletes RFC 2553) で提案され、POSIXにもある。

C++
[RAW]
  1. // IPv6ソケットはIPv6からの接続だけ受け付ける。
  2. if (ai->ai_family == AF_INET6) {
  3. int on = 1;
  4. if ( setsockopt(*sockfd, IPPROTO_IPV6, IPV6_V6ONLY, (char*) &on,
  5. sizeof(on)) < 0 ) {
  6. goto err;
  7. }
  8. printf("set IPV6_V6ONLY\n");
  9. }
  10. if (ai->ai_family == AF_INET || ai->ai_family == AF_INET6) {
  11. int on = 1;
  12. if (setsockopt(*sockfd, SOL_SOCKET, SO_REUSEADDR, (char*) &on,
  13. sizeof(on)) < 0) {
  14. goto err;
  15. }
  16. printf("set SO_REUSEADDR\n");
  17. }

あとは二つのソケットをそれぞれのソケットアドレスに bind, listen すればいい。

C++
[RAW]
  1. if (bind(*sockfd, ai->ai_addr, ai->ai_addrlen) < 0)
  2. goto err;
  3. if (listen(*sockfd, BACKLOG) < 0)
  4. goto err;
  5. sockaddr_print("listen succeeded", ai->ai_addr, ai->ai_addrlen);
  6. sockfd++;
  7. if (socksize >= fd_size)
  8. break;
  9. }
  10. freeaddrinfo(res);
  11. return socksize;
  12. err:
  13. for (int i = 0; i < socksize; i++)
  14. closesocket(sockfd[i]);
  15. freeaddrinfo(res);
  16. return -1;
  17. }

ソケットをlistenしたらクライアントからの接続を受け付けられるが、ソケットがいくつもあるので、単に accept() でブロックするわけにはいかない。ブロックしてしまうと指定したソケット以外のソケットで受け付けられない。

select() でブロックし、どれかに接続があれば、そのソケットを accept() する。あとは先ほどのサーバと同じ。

C++
[RAW]
  1. void test_server()
  2. {
  3. SOCKET sockfds[FD_SETSIZE];
  4. int socknum;
  5. SOCKET smax;
  6. fd_set rfd, rfd_init;
  7. int i;
  8. socknum = tcp_listen_v6separate(NULL, PORT, sockfds,
  9. sizeof(sockfds) / sizeof(SOCKET) );
  10. if (socknum < 0) {
  11. perror("server");
  12. exit(1);
  13. }
  14. else if (socknum == 0) {
  15. printf("can't listen socket.\n");
  16. exit(1);
  17. }
  18. FD_ZERO(&rfd_init);
  19. smax = -1;
  20. for (i = 0; i < socknum; i++) {
  21. FD_SET(sockfds[i], &rfd_init);
  22. smax = max(smax, sockfds[i]);
  23. }
  24. printf("wait...\n");
  25. while (1) {
  26. rfd = rfd_init;
  27. // 第一引数は個数ではなく (値の最大値 + 1)
  28. int m = select(smax + 1, &rfd, NULL, NULL, NULL);
  29. if (m < 0) {
  30. if (errno == EINTR)
  31. continue;
  32. perror("select");
  33. exit(1);
  34. }
  35. // それぞれのソケットを調べる
  36. for (i = 0; i < socknum; i++) {
  37. if (!FD_ISSET(sockfds[i], &rfd))
  38. continue;
  39. struct sockaddr_storage sa; // sockaddr_in 型ではない。
  40. socklen_t len = sizeof(sa); // クライアントの情報を得る場合
  41. SOCKET cs = accept(sockfds[i], (struct sockaddr*) &sa, &len);
  42. if ( cs == INVALID_SOCKET ) {
  43. perror("accept");
  44. exit(1);
  45. }
  46. printf("accepted.\n");
  47. sockaddr_print("peer", (struct sockaddr*) &sa, len);