ネットワーク

libuvのネットワーク処理は、BSDソケットインターフェイスを直接使う場合と大きくは変わりません。簡単になる部分があり、すべてノンブロッキングですが、概念は同じです。さらにlibuvは、BSDソケット構造体によるソケットの設定、DNS検索、各種ソケットパラメータの調整といった、面倒で反復的な低レベルの作業を抽象化するユーティリティ関数を提供します。

uv_tcp_tとuv_udp_t構造体は、ネットワークI/Oに使います。

注記

この章のコードサンプルは、特定のlibuv APIを示すためのものです。良質なコードの例ではありません。メモリリークがあり、接続を常に適切に閉じるわけでもありません。

TCP

TCPはコネクション指向のストリームプロトコルなので、libuvのストリーム基盤に基づいています。

サーバー

サーバーソケットでは、次の手順を実行します。

  1. uv_tcp_initでTCPハンドルを初期化します。

  2. uv_tcp_bindでバインドします。

  3. ハンドルに対してuv_listenを呼び出し、クライアントが新しい接続を確立するたびにコールバックが呼び出されるようにします。

  4. uv_acceptを使って接続を受け入れます。

  5. ストリーム操作でクライアントと通信します。

単純なエコーサーバーを示します。

tcp-echo-server/main.c - リスニングソケット

 1        uv_close((uv_handle_t*) client, on_close);
 2    }
 3}
 4
 5int main() {
 6    loop = uv_default_loop();
 7
 8    uv_tcp_t server;
 9    uv_tcp_init(loop, &server);
10
11    uv_ip4_addr("0.0.0.0", DEFAULT_PORT, &addr);
12
13    uv_tcp_bind(&server, (const struct sockaddr*)&addr, 0);
14    int r = uv_listen((uv_stream_t*) &server, DEFAULT_BACKLOG, on_new_connection);
15    if (r) {
16        fprintf(stderr, "Listen error %s\n", uv_strerror(r));
17        return 1;
18    }
19    return uv_run(loop, UV_RUN_DEFAULT);
20}

ユーティリティ関数、uv_ip4_addrは、人が読めるIPアドレスとポートの組を、BSDソケットAPIが必要とするsockaddr_in構造体に変換するために使っています。逆方向の変換には、uv_ip4_nameを使えます。

注記

ip4関数に対応するuv_ip6_*関数もあります。

設定関数の多くはCPU処理が中心なので、同期的に実行されます。uv_listenで、libuvのコールバック形式に戻ります。第2引数はバックログキュー、すなわち待機中の接続の最大数です。

クライアントが接続を開始したとき、コールバックはクライアントソケット用のハンドルを設定し、uv_acceptでそのハンドルを関連付ける必要があります。この例では、このストリームからの読み取りも監視対象にします。

tcp-echo-server/main.c - クライアントの受け入れ

 1
 2    free(buf->base);
 3}
 4
 5void on_new_connection(uv_stream_t *server, int status) {
 6    if (status < 0) {
 7        fprintf(stderr, "New connection error %s\n", uv_strerror(status));
 8        // error!
 9        return;
10    }
11
12    uv_tcp_t *client = (uv_tcp_t*) malloc(sizeof(uv_tcp_t));
13    uv_tcp_init(loop, client);
14    if (uv_accept(server, (uv_stream_t*) client) == 0) {
15        uv_read_start((uv_stream_t*) client, alloc_buffer, echo_read);
16    }

残りの関数はストリームの例と非常によく似ており、コードで確認できます。ソケットが不要になったら、uv_closeを呼び出すことを忘れないでください。接続を受け入れるつもりがなければ、uv_listenのコールバック内でも呼び出せます。

クライアント

サーバーではbind/listen/acceptを行いますが、クライアント側では単にuv_tcp_connectを呼び出すだけです。uv_connect_cb形式で、uv_listenと同様のコールバックを、uv_tcp_connectでも使います。試してみましょう。

uv_tcp_t* socket = (uv_tcp_t*)malloc(sizeof(uv_tcp_t));
uv_tcp_init(loop, socket);

uv_connect_t* connect = (uv_connect_t*)malloc(sizeof(uv_connect_t));

struct sockaddr_in dest;
uv_ip4_addr("127.0.0.1", 80, &dest);

uv_tcp_connect(connect, socket, (const struct sockaddr*)&dest, on_connect);

ここで、on_connectは接続が確立した後に呼び出されます。コールバックは、uv_connect_t構造体を受け取ります。この構造体には、ソケットを指す.handleメンバーがあります。

UDP

User Datagram Protocolは、コネクションレスで、信頼性を保証しないネットワーク通信を提供します。そのため、libuvはストリームを提供しません。代わりに、受信用のuv_udp_tハンドルと、送信用のuv_udp_send_t要求、および関連関数によって、ノンブロッキングUDPをサポートします。ただし、実際の読み書きAPIは、通常のストリームの読み取りと非常によく似ています。UDPの使い方を見るため、この例では、DHCPサーバーからIPアドレスを取得する最初の段階、DHCP Discoverを示します。

注記

udp-dhcpは、rootとして実行する必要があります。1024未満のウェルノウンポート番号を使うためです。

udp-dhcp/main.c - UDPパケットの設定と送信

 1
 2uv_loop_t *loop;
 3uv_udp_t send_socket;
 4uv_udp_t recv_socket;
 5
 6
 7int main() {
 8    loop = uv_default_loop();
 9
10    uv_udp_init(loop, &recv_socket);
11    struct sockaddr_in recv_addr;
12    uv_ip4_addr("0.0.0.0", 68, &recv_addr);
13    uv_udp_bind(&recv_socket, (const struct sockaddr *)&recv_addr, UV_UDP_REUSEADDR);
14    uv_udp_recv_start(&recv_socket, alloc_buffer, on_read);
15
16    uv_udp_init(loop, &send_socket);
17    struct sockaddr_in broadcast_addr;
18    uv_ip4_addr("0.0.0.0", 0, &broadcast_addr);
19    uv_udp_bind(&send_socket, (const struct sockaddr *)&broadcast_addr, 0);
20    uv_udp_set_broadcast(&send_socket, 1);
21
22    uv_udp_send_t send_req;
23    uv_buf_t discover_msg = make_discover_msg();
24
25    struct sockaddr_in send_addr;
26    uv_ip4_addr("255.255.255.255", 67, &send_addr);
27    uv_udp_send(&send_req, &send_socket, &discover_msg, 1, (const struct sockaddr *)&send_addr, on_send);
28
29    return uv_run(loop, UV_RUN_DEFAULT);
30}

注記

IPアドレス0.0.0.0は、すべてのインターフェイスにバインドするために使います。IPアドレス255.255.255.255はブロードキャストアドレスで、サブネット上のすべてのインターフェイスにパケットを送ることを意味します。ポート0は、OSがランダムにポートを割り当てることを意味します。

まず、受信用ソケットを、すべてのインターフェイスのポート68(DHCPクライアント)にバインドし、読み取りを開始します。これにより、応答するDHCPサーバーからの応答を読み取ります。このコンピュータで同じポートを使っている他のシステムDHCPクライアントと共存するため、UV_UDP_REUSEADDRフラグを使います。次に、同様の送信用ソケットを設定し、uv_udp_sendを使って、ブロードキャストメッセージをポート67(DHCPサーバー)へ送ります。

ブロードキャストフラグの設定は、必須です。設定しないと、EACCESエラーになります[1]。送信するメッセージの具体的な内容は本書の主題ではないため、興味があればコードを調べてください。通常どおり、問題が発生した場合、読み取り・書き込みのコールバックは0未満のステータスコードを受け取ります。

UDPソケットは特定の相手に接続されていないため、読み取りコールバックは、パケットの送信者に関する追加パラメータを受け取ります。

nreadは、読み取るデータがもうない場合に0になることがあります。addrがNULLなら、読み取るものがないことを示します(コールバックは何もすべきではありません)。NULLでなければ、addrにあるホストから空のデータグラムを受信したことを示します。flagsパラメータは、アロケータが提供したバッファがデータを収容するのに十分な大きさでない場合、UV_UDP_PARTIALになることがあります。この場合、OSは収まらなかったデータを破棄します(これがUDPというものです!)。

udp-dhcp/main.c - パケットの読み取り

 1void on_read(uv_udp_t *req, ssize_t nread, const uv_buf_t *buf, const struct sockaddr *addr, unsigned flags) {
 2    if (nread < 0) {
 3        fprintf(stderr, "Read error %s\n", uv_err_name(nread));
 4        uv_close((uv_handle_t*) req, NULL);
 5        free(buf->base);
 6        return;
 7    }
 8
 9    char sender[17] = { 0 };
10    uv_ip4_name((const struct sockaddr_in*) addr, sender, 16);
11    fprintf(stderr, "Recv from %s\n", sender);
12
13    // ... DHCP specific code
14    unsigned int *as_integer = (unsigned int*)buf->base;
15    unsigned int ipbin = ntohl(as_integer[4]);
16    unsigned char ip[4] = {0};
17    int i;
18    for (i = 0; i < 4; i++)
19        ip[i] = (ipbin >> i*8) & 0xff;
20    fprintf(stderr, "Offered IP %d.%d.%d.%d\n", ip[3], ip[2], ip[1], ip[0]);
21
22    free(buf->base);
23    uv_udp_recv_stop(req);
24}

UDPオプション

生存時間

ソケットから送信するパケットのTTLは、uv_udp_set_ttlで変更できます。

IPv6スタックのみ

IPv6ソケットは、IPv4とIPv6の両方の通信に使えます。ソケットをIPv6のみに制限したい場合は、UV_UDP_IPV6ONLYフラグを、uv_udp_bindに渡します。

マルチキャスト

ソケットは、次の関数でマルチキャストグループへの参加・離脱ができます。

int uv_udp_set_membership(uv_udp_t* handle, const char* multicast_addr, const char* interface_addr, uv_membership membership);

ここで、membershipは、UV_JOIN_GROUPまたはUV_LEAVE_GROUPです。

マルチキャストの概念は、このガイドでわかりやすく説明されています。

マルチキャストパケットのローカルループバックは、デフォルトで有効です[2]。uv_udp_set_multicast_loopで無効にできます。

マルチキャストパケットの生存時間は、uv_udp_set_multicast_ttlで変更できます。

DNS問い合わせ

libuvは非同期のDNS名前解決を提供します。そのために、独自のgetaddrinfo代替関数を提供しています[3]。コールバックでは、取得したアドレスに対して通常のソケット操作を行えます。DNS名前解決の例として、Libera.chatに接続してみましょう。

dns/main.c

 1
 2int main() {
 3    loop = uv_default_loop();
 4
 5    struct addrinfo hints;
 6    hints.ai_family = PF_INET;
 7    hints.ai_socktype = SOCK_STREAM;
 8    hints.ai_protocol = IPPROTO_TCP;
 9    hints.ai_flags = 0;
10
11    uv_getaddrinfo_t resolver;
12    fprintf(stderr, "irc.libera.chat is... ");
13    int r = uv_getaddrinfo(loop, &resolver, on_resolved, "irc.libera.chat", "6667", &hints);
14
15    if (r) {
16        fprintf(stderr, "getaddrinfo call error %s\n", uv_err_name(r));
17        return 1;
18    }
19    return uv_run(loop, UV_RUN_DEFAULT);
20}

uv_getaddrinfoが0以外を返した場合は、設定で問題が発生しており、コールバックは一切呼び出されません。すべての引数は、uv_getaddrinfoから戻った直後に解放できます。hostname、servname、hintsの各構造体については、getaddrinfoのmanページで説明されています。コールバックは、NULLにもでき、その場合、関数は同期的に実行されます。

リゾルバのコールバックでは、struct addrinfo(s)の連結リストから任意のIPを選べます。この例は、uv_tcp_connectの実演にもなっています。コールバック内で、uv_freeaddrinfoを呼び出す必要があります。

dns/main.c

 1
 2void on_resolved(uv_getaddrinfo_t *resolver, int status, struct addrinfo *res) {
 3    if (status < 0) {
 4        fprintf(stderr, "getaddrinfo callback error %s\n", uv_err_name(status));
 5        return;
 6    }
 7
 8    char addr[17] = {'\0'};
 9    uv_ip4_name((struct sockaddr_in*) res->ai_addr, addr, 16);
10    fprintf(stderr, "%s\n", addr);
11
12    uv_connect_t *connect_req = (uv_connect_t*) malloc(sizeof(uv_connect_t));
13    uv_tcp_t *socket = (uv_tcp_t*) malloc(sizeof(uv_tcp_t));
14    uv_tcp_init(loop, socket);
15
16    uv_tcp_connect(connect_req, socket, (const struct sockaddr*) res->ai_addr, on_connect);
17
18    uv_freeaddrinfo(res);
19}

libuvは、逆引き用のuv_getnameinfoも提供します。

ネットワークインターフェイス

システムのネットワークインターフェイス情報は、libuvのuv_interface_addressesで取得できます。この単純なプログラムは、すべてのインターフェイスの詳細を表示するだけなので、利用できるフィールドを把握できます。サービスが起動時にIPアドレスへバインドできるようにする場合に有用です。

interfaces/main.c

 1#include <stdio.h>
 2#include <uv.h>
 3
 4int main() {
 5    char buf[512];
 6    uv_interface_address_t *info;
 7    int count, i;
 8
 9    uv_interface_addresses(&info, &count);
10    i = count;
11
12    printf("Number of interfaces: %d\n", count);
13    while (i--) {
14        uv_interface_address_t interface_a = info[i];
15
16        printf("Name: %s\n", interface_a.name);
17        printf("Internal? %s\n", interface_a.is_internal ? "Yes" : "No");
18        
19        if (interface_a.address.address4.sin_family == AF_INET) {
20            uv_ip4_name(&interface_a.address.address4, buf, sizeof(buf));
21            printf("IPv4 address: %s\n", buf);
22        }
23        else if (interface_a.address.address4.sin_family == AF_INET6) {
24            uv_ip6_name(&interface_a.address.address6, buf, sizeof(buf));
25            printf("IPv6 address: %s\n", buf);
26        }
27
28        printf("\n");
29    }
30
31    uv_free_interface_addresses(info, count);
32    return 0;
33}

is_internalは、ループバックインターフェイスではtrueです。物理インターフェイスに複数のIPv4/IPv6アドレスがある場合、各アドレスが1回ずつ報告されるため、同じ名前が複数回報告されることに注意してください。