IPv6ソケットプログラミング

(2006.8.5 ページを独立。)

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

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

技術的に言えば、IPv6しか喋れないソフトウェア(クライアント)は、IPv4のサービス(サーバ)を利用できない。IPv6のサーバはIPv4しか喋れないクライアントからの接続を受けることができる。(IPv4-mapped address)

まず社会全体のサーバがIPv6に対応し、なおかつIPv4が廃れるまでIPv4アドレスも持つ必要がある。「すべての」クライアントがIPv6に対応するまでIPv4アドレスを捨てるわけにはいかない。

結局、「すべての」ソフトウェアがIPv4/IPv6の両方に対応するまでIPv4アドレスの需要が減らない。かなり無駄、不効率、不経済。本当になんでIPv6をIPv4と互換性を持たせるようにしなかったんだろう。

目次:

  1. サーバを作る
  2. クライアントを作る
  3. IPv6, IPv4を区別するサーバ
  4. 非同期 (non-blocking) I/OでIPv4/IPv6両対応

@ サーバを作る

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

IPv4 onlyのときの典型的なコードは、次のようになる。

C
[POPUP]
  1. int sockfd = socket(AF_INET, SOCK_STREAM, 0);
  2. sockaddr_in addr;
  3. memset( &addr, 0, sizeof(addr));
  4. addr.sin_family = AF_INET;
  5. addr.sin_addr.s_addr = htonl(INADDR_ANY);
  6. addr.sin_port = htons(port);
  7. bind(sockfd, (sockaddr*) &addr, sizeof(addr));

AF_INETはIPv4を指定するものだし、sockaddr_inはIPv4のアドレスしか格納できない。これではIPv6で通信はできない。ではどうするか。

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

IPv6ということで、AF_INET6とsockaddr_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);

nodenameにNULLを渡すと、INADDR_ANY (0.0.0.0), IN6ADDR_ANY_INIT (::) IPアドレスでソケットを生成できる。サーバでは、通常はNULLでいいだろう。

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を設定すると、制約を課さずに、任意の組み合わせを得られる。

ai_family, ai_socktype, ai_protocolは、socket()に与える値とする。ここではai_familyにはAF_INET6を設定する。後のセクションで、ai_familyにAF_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
ソケットアドレスをbind()する場合に指定。
AI_CANONNAME
ホストの正式名を要求する。
AI_NUMERICHOST
nodenameは数値形式のネットワークアドレスに限る。ホスト名の名前解決は行わない。
AI_NUMERICSERV
servnameはポート番号の文字列に限る。サービス名の解決は行わない。
AI_V4MAPPED
ai_familyがAF_INET6でIPv6アドレスが見つからなかった場合は、IPv4-mapped IPv6アドレスを返す。ai_familyがAF_INET6でない場合、AI_V4MAPPEDは無視される。
AI_ALL
AI_V4MAPPEDフラグとともに指定された場合、getaddrinfo()はIPv4, IPv6アドレスの両方を返す。AI_V4MAPPEDフラグなしに指定した場合、AI_ALLフラグは無視される。
AI_ADDRCONFIG
ローカルシステムがIPv4アドレスを持つ場合に限りIPv4アドレスを取得し、ローカルシステムがIPv6アドレスを持つ場合に限りIPv6アドレスを取得する。

実際のコードは、次のようになる。C言語で書いている。C++コンパイラでもコンパイルできる。ヘッダファイルの取り込み、デバッグ用表示関数は省略。

 11| #define BACKLOG 5
 12| #define PORT "12345"
 13| 
 14| // 戻り値 <0   エラー
 15| //        >=0  listenしているソケット
 16| int tcp_listen(const char* service) { // サービス名 or ポート番号(の文字列)
 17|     int err;
 18|     struct addrinfo hints;
 19|     struct addrinfo* res = NULL;
 20|     struct addrinfo* ai;
 21|     int sockfd;
 22| 
 23|     memset(&hints, 0, sizeof(hints));
 24|     hints.ai_family = AF_INET6;    // AF_INET6は、IPv4/IPv6両方を受け付ける。
 25|     hints.ai_socktype = SOCK_STREAM;
 26|     hints.ai_flags = AI_PASSIVE;   // bind()する場合は指定する。
 27| 
 28|     // node = NULLのとき、INADDR_ANY, IN6ADDR_ANY_INIT に相当。
 29|     err = getaddrinfo(NULL, service, &hints, &res);
 30|     if (err != 0) {
 31|         printf("getaddrinfo(): %s\n", gai_strerror(err));
 32|         return -1;
 33|     }
 34| 
 35|     ai = res;
 36|     sock_print("create socket", ai->ai_family, ai->ai_socktype);
 37|     sockfd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
 38|     if (sockfd < 0)
 39|         return -1;

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

 41|     int on = 1;
 42|     if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
 43|         return -1;
 44|     else
 45|         printf("set SO_REUSEADDR\n");
 46| 
 47|     if (bind(sockfd, ai->ai_addr, ai->ai_addrlen) < 0)
 48|         return -1;
 49| 
 50|     if (listen(sockfd, BACKLOG) < 0)
 51|         return -1;
 52|     else
 53|         sockaddr_print("listen succeeded", ai->ai_addr, ai->ai_addrlen);
 54| 
 55|     freeaddrinfo(res);
 56|     return sockfd;
 57| }

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

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

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

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

 59| void test_server() {
 60|     int sockfd;
 61| 
 62|     sockfd = tcp_listen(PORT);
 63|     if (sockfd < 0) {
 64|         perror("server");
 65|         exit(1);
 66|     }
 67| 
 68|     printf("wait...\n");
 69| 
 70|     while (1) {
 71|         int cs;
 72|         struct sockaddr_storage sa;  // sockaddr_in 型ではない。
 73|         socklen_t len = sizeof(sa);  // クライアントの情報を得る場合
 74|         cs = accept(sockfd, (struct sockaddr*) &sa, &len);
 75|         if (cs < 0) {
 76|             if (errno == EINTR)
 77|                 continue;
 78|             perror("accept");
 79|             exit(1);
 80|         }
 81| 
 82|         printf("accepted.\n");
 83|         sockaddr_print("peer", (struct sockaddr*) &sa, len);
そうしたら、forkして、親プロセスはクライアントからの接続を待ちつつ、子プロセスで通信を行う。
 85|         if (fork() == 0) {
 86|             // 子プロセス
 87|             char ch;
 88|             close(sockfd);
 89| 
 90|             read(cs, &ch, 1);
 91|             ch++;
 92|             write(cs, &ch, 1);
 93| 
 94|             close(cs);
 95|             exit(0);
 96|         }
 97|         close(cs);
 98|     }
 99| }
100| 
101| int main() {
102|     test_server();
103|     return 0;
104| }

@ クライアントを作る

(2006.6.29の日記を加筆。)

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

IPv6でもIPv4と同様にホスト名とポート番号の組み合わせでサーバに接続する。

サーバがIPv4にしか対応していない場合、IPv6にしか対応していない場合があるので、IPv4のみで接続するときに比べてコードがいくぶん長くなる。

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

基本的なアイディアは、ai_family を AF_UNSPEC にして getaddrinfo() 関数を呼び出し、両方の候補(IPv4, IPv6)を得る。getaddrinfo()は、ヒントのai_familyがAF_UNSPECで、指定したホスト名がIPv4アドレス、IPv6アドレスの両方を持つ場合に限り、二つのソケットアドレスを返す。そうでないときは一つだけ返す。

getaddrinfo()は (AI_NUMERICHOSTを指定しない限り) ホスト名の名前解決も行う。

 17| // サーバに接続する。
 18| int connect_to_server(
 19|     const char* hostname,  // IPv4 or IPv6ホスト名
 20|     const char* service)   // ポート番号(の文字列)
 21| {
 22|     int sockfd;
 23|     int err;
 24|     struct addrinfo hints;
 25|     struct addrinfo* res = NULL;
 26|     struct addrinfo* ai;
 27| 
 28|     memset(&hints, 0, sizeof(hints));
 29|     hints.ai_family = AF_UNSPEC;     // IPv4/IPv6両対応
 30|     hints.ai_socktype = SOCK_STREAM;
 31|     // serviceはポート番号でなければならない。AI_NUMERICSERV を指定しなければ、
 32|     // 'pop'などでもよい。
 33|     hints.ai_flags = AI_NUMERICSERV;  
 34| 
 35|     err = getaddrinfo(hostname, service, &hints, &res);
 36|     if (err != 0) {
 37|         printf("getaddrinfo(): %s\n", gai_strerror(err));
 38|         return -1;
 39|     }

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

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

 41|     for (ai = res; ai; ai = ai->ai_next) {
 42|         sockaddr_print("connect...", ai->ai_addr, ai->ai_addrlen);
 43|         sockfd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
 44|         if (sockfd < 0)
 45|             return -1;
 46|         if (connect(sockfd, ai->ai_addr, ai->ai_addrlen) < 0) {
 47|             close(sockfd);
 48|             sockfd = -1;
 49|             continue;
 50|         }
 51|         // ok
 52|         sockaddr_print("connected", ai->ai_addr, ai->ai_addrlen);
 53|         break;
 54|     }
 55|     freeaddrinfo(res);
 56|     return sockfd;
 57| }

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

 59| int main() {
 60|     int sockfd;
 61|     char ch;
 62| 
 63|     sockfd = connect_to_server(HOSTNAME, PORT);
 64|     if (sockfd < 0) {
 65|         perror("client");
 66|         return 1;
 67|     }
 68| 
 69|     ch = 'A';
 70|     write(sockfd, &ch, 1);
 71|     read(sockfd, &ch, 1);
 72|     printf("char from server = '%c'\n", ch);
 73|     
 74|     close(sockfd);
 75|     return 0;
 76| }

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

(2006.6.29の日記を加筆。)

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

getaddrinfo()に与えるホスト名をNULL, ヒントのai_familyをAF_UNSPECにすると、複数のソケットアドレスを得られる。これらを順にbind()し、listen()していく。

 16| // 戻り値 <0  エラー
 17| //        0   listenできたソケットがない
 18| //        >=1 listenできたソケットの数
 19| int tcp_listen(
 20|     const char* service,  // サービス名 or ポート番号(の文字列)
 21|     int* sockfd,          // (out) ソケットfdの配列
 22|     int fd_size)
 23| {
 24|     int err;
 25|     struct addrinfo hints;
 26|     struct addrinfo* res = NULL;
 27|     struct addrinfo* ai;
 28|     int socksize = 0;
 29| 
 30|     assert(sockfd);
 31| 
 32|     memset(&hints, 0, sizeof(hints));
 33|     hints.ai_family = AF_UNSPEC;   // IPv4/IPv6専用ソケットを作る。
 34|     hints.ai_socktype = SOCK_STREAM;
 35|     hints.ai_flags = AI_PASSIVE;   // acceptするためにbind()する場合は指定する。
 36| 
 37|     // node = NULLのとき、INADDR_ANY, IN6ADDR_ANY_INIT に相当。
 38|     err = getaddrinfo(NULL, service, &hints, &res);
 39|     if (err != 0) {
 40|         printf("getaddrinfo(): %s\n", gai_strerror(err));
 41|         return -1;
 42|     }
 43| 
 44|     for (ai = res; ai; ai = ai->ai_next) {
 45|         sock_print("create socket", ai->ai_family, ai->ai_socktype);
 46| 
 47|         *sockfd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
 48|         if (*sockfd < 0)
 49|             return -1;

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

 51|         // IPv6ソケットはIPv6からの接続だけ受け付ける。
 52|         if (ai->ai_family == AF_INET6) {
 53|             int on = 1;
 54|             if (setsockopt(*sockfd, IPPROTO_IPV6, IPV6_V6ONLY, &on, sizeof(on)) < 0) 
 55|                 return -1;
 56|             else
 57|                 printf("set IPV6_V6ONLY\n");
 58|         }

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

 60|         if (ai->ai_family == AF_INET || ai->ai_family == AF_INET6) {
 61|             int on = 1;
 62|             if (setsockopt(*sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
 63|                 return -1;
 64|             else
 65|                 printf("set SO_REUSEADDR\n");
 66|         }
 67|         if (bind(*sockfd, ai->ai_addr, ai->ai_addrlen) < 0)
 68|             return -1;
 69| 
 70|         if (listen(*sockfd, BACKLOG) < 0)
 71|             return -1;
 72|         else
 73|             sockaddr_print("listen succeeded", ai->ai_addr, ai->ai_addrlen);
 74| 
 75|         sockfd++;
 76|         socksize++;
 77|         if (socksize >= fd_size)
 78|             break;
 79|     }
 80|     freeaddrinfo(res);
 81|     return socksize;
 82| }

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

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

 84| void test_server() {
 85|     int sockfd[FD_SETSIZE];
 86|     int socknum, smax;
 87|     fd_set rfd, rfd_init;
 88|     int i;
 89| 
 90|     socknum = tcp_listen(PORT, sockfd, sizeof(sockfd) / sizeof(int));
 91|     if (socknum < 0) {
 92|         perror("server");
 93|         exit(1);
 94|     }
 95|     else if (socknum == 0) {
 96|         printf("can't listen socket.\n");
 97|         exit(1);
 98|     }
 99| 
100|     FD_ZERO(&rfd_init);
101|     smax = -1;
102|     for (i = 0; i < socknum; i++) {
103|         FD_SET(sockfd[i], &rfd_init);
104|         if (sockfd[i] > smax) smax = sockfd[i];
105|     }
106| 
107|     printf("wait...\n");
108| 
109|     while (1) {
110|         rfd = rfd_init;
111|         int m = select(smax + 1, &rfd, NULL, NULL, NULL);
112|         if (m < 0) {
113|             if (errno == EINTR)
114|                 continue;
115|             perror("select");
116|             exit(1);
117|         }
118| 
119|         for (i = 0; i < socknum; i++) {
120|             if (FD_ISSET(sockfd[i], &rfd)) {
121|                 int cs;
122|                 struct sockaddr_storage sa;  // sockaddr_in 型ではない。
123|                 socklen_t len = sizeof(sa);  // クライアントの情報を得る場合
124|                 cs = accept(sockfd[i], (struct sockaddr*) &sa, &len);
125|                 if (cs < 0) {
126|                     perror("accept");
127|                     exit(1);
128|                 }
129| 
130|                 printf("accepted.\n");
131|                 sockaddr_print("peer", (struct sockaddr*) &sa, len);

@ 非同期 (non-blocking) I/OでIPv4/IPv6両対応

Note.

(2006.8.5 追記)

下記のコードでは、connect(), read() を非同期にしている。しかし、getaddrinfo() もホスト名の解決を行うために時間が掛かる。この考慮が足りない。

スレッドを生成し、getaddrinfo(), ブロッキングモードのconnect() を呼び出してサーバに接続するようにすべき。Windowsでも、同様の方法が推奨されている。(WSAAsyncGetHostByName の頁を参照。) もっとシンプルにできるだろう。

(以下、2000.10.7, 2000.10.8の日記を加筆。)

ソケット関係の関数は、connect(), accept(), read() と、どれもこれもブロックする。ネットワーク越しのデータのやり取りは本質的に非同期なので、平行して入力がある場合、いちいちブロックするようにプログラムを書いてはいけない。

例えば、GUIプログラムの場合は、ソケットをブロックしていたら、マウスのクリック、画面の再描画などを受け付けられず、フリーズしてしまう。

ではどうするか。

ここでは、フレームワークが用意したコールバックの仕組みを使ってみる。どのようなフレームワークでも、ソケットに限らずさまざまな入力を捌く機構が用意されているので、それを使う。そうすれば、ソケットの入力を待ちながら画面を更新したりできるだろう。

Fedora Core 5 と gtk+ 2.8.20 という環境で試してみた。全体的にやっつけで作っているので、実用にしようと思うと、まだ考えるべきところが多いので注意。

非同期I/O

UNIXでは、ソケットに限らず、IOをブロックしないようにも設定できる。open()またはfcntl()でO_NONBLOCKフラグを設定すると、非同期 (non-blocking) モードになる。ソケットに限ると、fcntl()関数のF_SETFLコマンドでO_NONBLOCKフラグを設定できる。

non-blockingモードでは、read(), write(), send(), recv()関数は、ブロックせずに、すぐに返ってくるようになる。戻り値とエラーコードで状況を調べる必要がある。

connect()関数もすぐに返ってくる。EINPROGRESSエラーコードを確認する。

accept()も同様。listenキューが空でも、すぐに返ってる。EAGAINまたはEWOULDBLOCKエラー(この二つは同じ値かもしれないし、異なるかもしれない。Linuxでは同じ。)が発生する。

ヘルパクラス(参照コンテナ)

C++のコンテナは基本的に値を格納するようにできている。オブジェクトのコピーは難しいところがあるので、ごく簡単に、参照を所有するコンテナを作る。

 14| // 参照を所有するコンテナ
 15| template <typename Ty>
 16| class ptr_vector: public std::vector<Ty> {
 17| public:
 18|     typedef std::vector<Ty> super;
 19|     typedef typename super::iterator iterator;
 20| 
 21|     ptr_vector() { }
 22|     virtual ~ptr_vector() { clear(); }
 23|     void clear() {
 24|         for (iterator i = super::begin(); i != super::end(); i++)
 25|             delete *i;
 26|         super::clear();
 27|     }
 28|     iterator erase(iterator p) {
 29|         delete *p; return super::erase(p);
 30|     }
 31|     void detach(Ty ptr) {
 32|         for (iterator i = super::begin(); i != super::end(); i++) {
 33|             if (ptr == *i) {
 34|                 super::erase(i);
 35|                 break;
 36|             }
 37|         }
 38|     }
 39| };

ソケットとコールバック関数

非同期ソケットでは、connect()がすぐに返ってきてしまう。サーバがIPv4対応かIPv6対応か分からない状況では、ソケットを2本使う必要がある。両方でconnectを試みて、接続できたほうを生かす。

接続できたかどうか判明したら、そのソケットは書き込み可能になる。接続できたら、こんどは先方からデータが届いたときに読み込み可能になる。

gtk+では、g_io_add_watch_full()で、ソケットが読み込み可能、書き込み可能になったときにコールバックされる関数を登録できる。

ということを踏まえて、ソケットクラスを作る。ソケット一つにつきこのクラスのインスタンスを一つ用意する。後述するConnectionオブジェクトがSocketConnectionインスタンスを所有する。

 93| // ソケットにつき一つ作る
 94| class SocketConnection {
 95|     // sockaddr_inはIPv4専用。sockaddr_in6はIPv6専用。
 96|     // sockaddr_storageは、システムでサポートされるすべてのプロトコルのsockaddr
 97|     // を格納できるほど大きい。<sys/socket.h>で定義。
 98|     sockaddr_storage addr;
 99|     socklen_t addrlen;
100|     guint io_watch; // GSourceの生成前は0
101| public:
102|     int sockfd;
103|     Connection* conn;
104| 
105|     SocketConnection(Connection* conn_): conn(conn_), sockfd(-1), io_watch(0) {}
106| 
107|     int connect(int sockfd_, const sockaddr* addr_, socklen_t addrlen_) {
108|         assert(sockfd_ >= 0);
109|         sockfd = sockfd_; addrlen = addrlen_;
110|         memcpy(&addr, addr_, addrlen);
111|         return ::connect(sockfd, addr_, addrlen);
112|     }
113|     
114|     int reconnect() {
115|         assert(sockfd >= 0);
116|         sockaddr_print("re-connect...", (sockaddr*) &addr, addrlen);
117|         return ::connect(sockfd, (sockaddr*) &addr, addrlen);
118|     }
119| 
120|     virtual ~SocketConnection() {
121|         detach_listener();
122|         if (sockfd >= 0) {
123|             close(sockfd);
124|             sockfd = -1;
125|         }
126|     }

コールバック関数を登録するメソッドを用意する。gdk_input_add() があったが、代替関数なしにdeprecated になっているので困る。gtk+はこういうところが多くて面倒。ドキュメントも不足している。

128|     // gdk_input_add()が deprecated (非推奨) なので、似たようなメソッドを作る。
129|     void attach_listener(int condition, GIOFunc proc) {
130|         GIOChannel* channel = g_io_channel_unix_new(sockfd);
131|         io_watch = g_io_add_watch_full(channel, G_PRIORITY_DEFAULT,
132|                                        (GIOCondition) condition, proc, this,
133|                                        io_destroy);
134|         assert(io_watch);
135|         // g_io_add_watch()でもrefされるので、ここでは解放する
136|         g_io_channel_unref(channel);
137|     }
138| 
139|     void detach_listener() {
140|         if (io_watch) {
141|             g_source_remove(io_watch);
142|             io_watch = 0;
143|         }
144|     }
145|     
146| private:
147|     static void io_destroy(void* data) {
148|         SocketConnection* self = static_cast<SocketConnection*>(data);
149|         self->io_watch = 0;
150|     }
151| };

状態機械

IPv4/IPv6の両方に対応するために、サーバへの接続は、次のような手順を踏む。

  1. IPv4ソケット、IPv6ソケットでサーバへ接続するようconnect()する。
  2. どちらかが接続できれば、それ以外のソケットをすべて閉じる。
  3. すべてのソケットがエラーになれば終了。
  4. 接続できたソケットで先方と通信する。

そんな感じになるよう、サーバに接続するクラスを書いてみる。

まずはSocketConnectionオブジェクトを扱うメソッド。

153| class Connection {
154|     int state;
155|     guint time_handle;
156|     typedef ptr_vector<SocketConnection*> Sockets;
157|     Sockets sockets;
158| public:
159|     Connection(): state(0) { }
160|     
161|     void add_socket(SocketConnection* sc) {
162|         sockets.push_back(sc);
163|     }
164| 
165|     void remove_socket(SocketConnection* sc) {
166|         for (Sockets::iterator i = sockets.begin(); i != sockets.end(); i++) {
167|             if (*i == sc) {
168|                 sockets.erase(i);
169|                 break;
170|             }
171|         }
172|     }
173| 
174|     void clear_sockets() { sockets.clear(); }
175| 
176|     int sockets_size() const { return sockets.size(); }

状態を管理する。0が初期状態、1が(複数のソケットが)接続待ち、2が接続できて通信中。

178|     void set_state(int new_val) {
179|         printf("%s: %d -> %d\n", __func__, state, new_val);
180|         if (state == new_val)
181|             return;
182| 
183|         switch (state) {
184|         case 0:
185|             switch (new_val) {
186|             case 1:
187|                 status_text = "接続中";
188|                 time_handle = g_timeout_add(int(0.3 * 1000), on_time, NULL);
189|                 for (Sockets::iterator i = sockets.begin(); i != sockets.end(); i++)
190|                     (*i)->attach_listener(G_IO_OUT | G_IO_ERR, sock_proc);
191|                 break;
192|             case 2:
193|                 time_handle = g_timeout_add(int(0.3 * 1000), on_time, NULL);
194|                 set_state_2();
195|                 break;
196|             default: assert(0);
197|             }
198|             break;
199|         case 1:
200|             switch (new_val) {
201|             case 0:
202|                 clear_sockets();
203|                 g_source_remove(time_handle); time_handle = 0;
204|                 break;
205|             case 2:
206|                 set_state_2();
207|                 break;
208|             default: assert(0);
209|             }
210|             break;
211|         case 2:
212|             switch (new_val) {
213|             case 0:
214|                 clear_sockets();
215|                 g_source_remove(time_handle); time_handle = 0;
216|                 break;
217|             default: assert(0);
218|             }
219|             break;
220|         default: assert(0);
221|         }
222| 
223|         state = new_val;
224|     }
225|     
226|     int get_state() const { return state; }
227|     
228| private:
229|     void set_state_2() {
230|         status_text = "読み込み中";
231|         // コールバックの条件を変更する。
232|         assert(sockets.size() == 1);
233|         SocketConnection* sock = sockets.front();
234|         sock->attach_listener(G_IO_IN | G_IO_HUP | G_IO_ERR, &sock_proc);
235| 
236|         // サーバにデータを送信。
237|         write(sock->sockfd, "AAA", 3);
238|     }

set_state_2()のなかでサーバにデータを送信するコードを書いているあたり、やっつけ感が高い。真のリスナを登録させたりするのがいいだろうと思う。

ウィンドウに表示するためのコールバック関数。

240|     static gint on_time(void* data) {
241|         status_text += ".";
242|         gtk_statusbar_pop(GTK_STATUSBAR(status_bar), 1);
243|         gtk_statusbar_push(GTK_STATUSBAR(status_bar), 1, status_text.c_str());
244|         return 1;  // not remove
245|     }

次は、ソケットのコールバック関数。ここでもサーバから受信するコードを直接書いている。ダサい。

247|     static gboolean sock_proc(GIOChannel* source, GIOCondition condition, gpointer data);
248|     static void connect_error(SocketConnection* sock);
249| };
250| 
251| void Connection::connect_error(SocketConnection* sock) {
252|     Connection* conn = sock->conn;
253|     conn->remove_socket(sock);
254|     if (conn->sockets_size() <= 0) {
255|         error_dialog("接続に失敗しました。");
256|         conn->set_state(0);
257|         reset_start_button();
258|     }
259| }
260| 
261| gboolean Connection::sock_proc(GIOChannel* source, GIOCondition condition,
262|                                gpointer data) {
263|     print_condition(__func__, condition);
264| 
265|     SocketConnection* sock = static_cast<SocketConnection*>(data);
266|     switch (sock->conn->get_state()) {
267|     case 1: // connect待ち
268|         if (condition & G_IO_ERR) {
269|             connect_error(sock);
270|         }
271| 
272|         if (condition & G_IO_OUT) {
273|             if (sock->reconnect() < 0) {
274|                 perror("connect() error");
275|                 connect_error(sock);
276|             }
277|             else {
278|                 // 接続できたソケット以外閉じる。
279|                 Connection* conn = sock->conn;
280|                 conn->sockets.detach(sock);
281|                 conn->clear_sockets();
282|                 conn->add_socket(sock);
283|                 conn->set_state(2);
284|             }
285|         }
286|         return FALSE; // not remove
287|     case 2:
288|         if ((condition & G_IO_HUP) || (condition & G_IO_ERR)) {
289|             sock->conn->set_state(0);
290|             reset_start_button();
291|             return FALSE; // remove
292|         }
293| 
294|         if (condition & G_IO_IN) {
295|             char buf[1000];
296|             int bytes_read = read(sock->sockfd, buf, sizeof(buf));
297|             if (bytes_read > 0) {
298|                 GtkTextBuffer* buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(text_view));
299|                 assert(buffer);
300|                 gtk_text_buffer_insert_at_cursor(buffer, buf, bytes_read);
301|             }
302|             else if (bytes_read == 0) {
303|                 sock->conn->set_state(0);
304|                 reset_start_button();
305|                 return FALSE;
306|             }
307|             else { // < 0
308|                 assert(0);
309|             }
310|         }
311|         return TRUE;
312|     default:
313|         assert(0);
314|     }
315|     return TRUE;
316| }

ここでは非同期でやっているが、GUIプログラムの場合は、同期型(ブロッキングモード)でも同じようなコードになるだろう。

ブロッキングモードのときは、read()でブロックしないように注意が必要。read()は次のいずれかで返る。non-blockingモードでも同じ。

  • 引数として与えたバッファが埋まった
  • 書き込み側のwrite()の切れ目
  • 書き込み側でソケットが閉じられた

バッファのバイト長より読めた長さが短くても、ソケットが閉じられたとは限らない。同様にread()の戻り値がバッファのバイト長と同じでも,まだデータがあるとは限らない。

read()の戻り値がたまたまバッファのバイト長と同じで、まだデータがあると思ってさらにread()するとブロックしてしまう。

一回だけread()したら読み込みのルーチンを終了し,メインループに戻すようにする。メインループのselect()でデータがあるかどうかを検査し、また読み込みルーチンを呼び出してもらう。

non-blockingモードの設定、サーバへの接続

ソケットを非同期にするにはfcntl()を使う。F_GETFLコマンドで現在のフラグを読み出し、F_SETFLで設定する。

connect() はブロックされず、EINPROGRESSエラーを発生させる。それを確認して、メインループでselect() を回すようにする。

接続に成功すると書き込み可能状態になるので,そのようにコールバック関数を登録する。上記、set_state()のなかにコードがある。

書き込み可能になったのが分かったら,同じソケットに対してもう一度connect() する。これでエラーが返れば接続に失敗している。

接続に成功したら,読み込み可能状態でコールバックしてもらうように登録し直す。あとはコールバックされたらread()すればよい。

318| Connection* connect_to_server_nonblock(const char* hostname, const char* port) {
319|     struct addrinfo hints;
320|     memset(&hints, 0, sizeof(hints));
321|     hints.ai_family = AF_UNSPEC;
322|     hints.ai_socktype = SOCK_STREAM;
323| 
324|     struct addrinfo* res = NULL;
325|     int err = getaddrinfo(hostname, port, &hints, &res);
326|     if (err != 0) {
327|         printf("getaddrinfo(): %s\n", gai_strerror(err));
328|         error_dialog("ホストまたはポートが不正です。");
329|         return NULL;
330|     }
331|     Connection* conn = new Connection();
332|     int state_val = 0;
333|     for (addrinfo* ai = res; ai; ai = ai->ai_next) {
334|         int sockfd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
335|         assert(sockfd >= 0);
336| 
337|         sockaddr_print("connect...", ai->ai_addr, ai->ai_addrlen);
338| 
339|         // 非ブロックモードに設定
340|         int flags = fcntl(sockfd, F_GETFL, 0);
341|         printf("orig flags = %d\n", flags);
342|         fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
343| 
344|         SocketConnection* sc = new SocketConnection(conn);
345|         int r = sc->connect(sockfd, ai->ai_addr, ai->ai_addrlen); // ブロックしない
346|         printf("connect() return: %d\n", r);
347|         if (r < 0) {
348|             if (errno == EINPROGRESS) {
349|                 conn->add_socket(sc); // サーバのacceptを待つ
350|                 state_val = 1;
351|             }
352|             else {
353|                 perror("connect");
354|                 delete sc;
355|                 continue;
356|             }
357|         }
358|         else {
359|             // この場で接続できたものがあった場合
360|             conn->clear_sockets();
361|             conn->add_socket(sc);
362|             state_val = 2;
363|             break;
364|         }
365|     }
366|     freeaddrinfo(res);
367| 
368|     printf("state_val = %d\n", state_val);
369|     if (state_val == 0) {
370|         delete conn;
371|         return NULL;
372|     }
373|     else {
374|         conn->set_state(state_val); // コールバックの待ち方を設定
375|         return conn;
376|     }
377| }

あとは、開始ボタンのハンドラ。

379| // 開始ボタンのクリック
380| void start(const char* hostname, const char* port) {
381|     assert(hostname);
382|     assert(port);
383| 
384|     Connection* r = connect_to_server_nonblock(hostname, port);
385|     if (!r) 
386|         reset_start_button();
387|     else
388|         conn_list.push_back(r);
389| }