4.网络

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 
 5 int 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回调风格的地方。第二个参数是待定队列——排队连接的最大长度。

当客户端发起连接时,需要使用回调来为客户端套接字建立句柄,并使用uv_accept关联该句柄。在这种情况下,我们还建立了从这个流中阅读的兴趣。

tcp-echo-server/main.c - 接受客户端连接

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

其余的函数集与流示例非常相似,可以在代码中找到。只要记得在不需要套接字时调用uv_close。如果你不愿意接受这个连接,这甚至可以在uv_listen回调中完成。

客户端

当您在服务器上绑定/侦听/接受时,在客户端只需调用uv_tcp_connect即可。uv_tcp_connect使用uv_listen的相同uv_connect_cb风格回调。试一试:

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

udp-dhcp/main.c - 设置并发送udp包

 1 uv_loop_t *loop;
 2 uv_udp_t send_socket;
 3 uv_udp_t recv_socket;
 4 
 5 int main() {
 6     loop = uv_default_loop();
 7 
 8     uv_udp_init(loop, &recv_socket);
 9     struct sockaddr_in recv_addr;
10     uv_ip4_addr("0.0.0.0", 68, &recv_addr);
11     uv_udp_bind(&recv_socket, (const struct sockaddr *)&recv_addr, UV_UDP_REUSEADDR);
12     uv_udp_recv_start(&recv_socket, alloc_buffer, on_read);
13 
14     uv_udp_init(loop, &send_socket);
15     struct sockaddr_in broadcast_addr;
16     uv_ip4_addr("0.0.0.0", 0, &broadcast_addr);
17     uv_udp_bind(&send_socket, (const struct sockaddr *)&broadcast_addr, 0);
18     uv_udp_set_broadcast(&send_socket, 1);
19 
20     uv_udp_send_t send_req;
21     uv_buf_t discover_msg = make_discover_msg();
22 
23     struct sockaddr_in send_addr;
24     uv_ip4_addr("255.255.255.255", 67, &send_addr);
25     uv_udp_send(&send_req, &send_socket, &discover_msg, 1, (const struct sockaddr *)&send_addr, on_send);
26 
27     return uv_run(loop, UV_RUN_DEFAULT);
28 }
  • 注意:IP地址0.0.0.0用于绑定所有接口。IP地址255.255.255.255是一个广播地址,意味着数据包将被发送到子网内的所有接口。端口0为操作系统随机分配的端口。

首先,我们将接收套接字设置为绑定端口68 (DHCP客户端)上的所有接口,并开始读取。这将从应答的任何DHCP服务器读取响应。我们使用UV_UDP_REUSEADDR标志来与在同一端口上运行在这台计算机上的任何其他系统DHCP客户端友好相处。然后,我们设置一个类似的发送套接字,并使用uv_udp_send在端口67 (DHCP服务器)上发送广播消息。

必须设置广播标志,否则将得到EACCES错误1。发送的确切信息与本书无关,如果你感兴趣,可以研究代码。通常,如果发生错误,读和写回调将收到< 0的状态码。

因为UDP套接字没有连接到一个特定的对等体,读回调收到一个额外的参数关于数据包的发送者。

如果没有更多的数据要读取,则Nread可能为零。如果addr是NULL,它表示没有什么可读的(回调不应该做任何事情),如果不是NULL,它表示从主机addr接收到一个空的数据报。如果分配器提供的缓冲区不够大,不能容纳数据,则flags参数可以是UV_UDP_PARTIAL。在这种情况下,操作系统将丢弃不适合的数据(这是UDP为您!)

udp-dhcp/main.c - 读取包

 1 void 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选项

Time-to-live(存活时间)

通过uv_udp_set_ttl可以改变socket上发送的数据包的生存时间。

只设置IPv6协议

IPv6 socket既可以用于IPv4通信,也可以用于IPv6通信。如果你想限制套接字仅为IPv6,传递UV_UDP_IPV6ONLY标志给uv_udp_bind 

多播

套接字可以使用以下方法订阅多播组:

其中成员为UV_JOIN_GROUP或UV_LEAVE_GROUP。

本指南很好地解释了多播的概念。

缺省情况下,启用组播本地环回功能。使用uv_udp_set_multicast_loop来关闭它。

组播报文的生存时间可以通过uv_udp_set_multicast_ttl来改变。

查询DNS

libuv提供异步DNS解析。为此,它提供了自己的getaddrinfo替换4。在回调中,您可以对检索到的地址执行正常的套接字操作。让我们连接到Freenode来看看DNS解析的示例。

dns/main.c

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

如果uv_getaddrinfo返回非零,在设置中出现了错误,你的回调根本不会被调用。所有参数都可以在uv_getaddrinfo返回后立即释放。主机名、服务器名和提示结构在getaddrinfo手册页中有文档说明。回调可以是NULL,在这种情况下,函数将同步运行。

在解析器回调中,你可以从结构addrinfo(s)的链表中选择任何IP。这也演示了uv_tcp_connect。在回调中调用uv_freeaddrinfo是必要的。

dns/main.c

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

Libuv还提供了相反的uv_getnameinfo。

网络接口

通过libuv可以通过uv_interface_addresses获取系统的网络接口信息。这个简单的程序只是打印出所有接口细节,以便您了解可用的字段。这对于允许服务在启动时绑定到IP地址很有用。

interfaces/main.c

 1 #include <stdio.h>
 2 #include <uv.h>
 3 
 4 int 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 = info[i];
15 
16         printf("Name: %s\n", interface.name);
17         printf("Internal? %s\n", interface.is_internal ? "Yes" : "No");
18         
19         if (interface.address.address4.sin_family == AF_INET) {
20             uv_ip4_name(&interface.address.address4, buf, sizeof(buf));
21             printf("IPv4 address: %s\n", buf);
22         }
23         else if (interface.address.address4.sin_family == AF_INET6) {
24             uv_ip6_name(&interface.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 }

对于loopback接口,Is_internal为true。需要注意的是,如果一个物理接口有多个IPv4/IPv6地址,则该名称会被报告多次,每个地址只报告一次。

posted @ 2021-05-11 10:01  风吹大风车  阅读(307)  评论(0)    收藏  举报