4.网络
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 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地址,则该名称会被报告多次,每个地址只报告一次。

浙公网安备 33010602011771号