ネットワーク¶
libuvのネットワーク処理は、BSDソケットインターフェイスを直接使う場合と大きくは変わりません。簡単になる部分があり、すべてノンブロッキングですが、概念は同じです。さらにlibuvは、BSDソケット構造体によるソケットの設定、DNS検索、各種ソケットパラメータの調整といった、面倒で反復的な低レベルの作業を抽象化するユーティリティ関数を提供します。
uv_tcp_tとuv_udp_t構造体は、ネットワークI/Oに使います。
注記
この章のコードサンプルは、特定のlibuv APIを示すためのものです。良質なコードの例ではありません。メモリリークがあり、接続を常に適切に閉じるわけでもありません。
TCP¶
TCPはコネクション指向のストリームプロトコルなので、libuvのストリーム基盤に基づいています。
サーバー¶
サーバーソケットでは、次の手順を実行します。
uv_tcp_initでTCPハンドルを初期化します。uv_tcp_bindでバインドします。ハンドルに対して
uv_listenを呼び出し、クライアントが新しい接続を確立するたびにコールバックが呼び出されるようにします。uv_acceptを使って接続を受け入れます。ストリーム操作でクライアントと通信します。
単純なエコーサーバーを示します。
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回ずつ報告されるため、同じ名前が複数回報告されることに注意してください。
https://beej.us/guide/bgnet/html/#broadcast-packetshello-world
libuvは、libuvのスレッドプール内で、システムのgetaddrinfoを使います。libuv v0.8.0以前では、代替として、c-aresも含まれていましたが、v0.9.0で削除されています。