libuv 网络编程实战:TCP、UDP、DNS 解析与网卡信息获取 📅 发布时间:2026/9/20 23:07:21 👁 浏览次数: libuv 网络编程实战TCP、UDP、DNS 解析与网卡信息获取【免费下载链接】libuvCross-platform asynchronous I/O项目地址: https://gitcode.com/gh_mirrors/li/libuvlibuv 是一套跨平台异步 I/O 库其网络编程能力建立在 BSD socket 概念之上与直接使用 socket 接口相比所有操作均是非阻塞的并额外提供了地址转换、DNS 查询、socket 参数调节等工具函数把繁琐的底层细节抽象掉。本文以 libuv 官方指南的网络章节为骨架结合本仓库的示例代码docs/code与头文件定义include/uv.h完整演示如何用uv_tcp_t、uv_udp_t写出 TCP 回显服务器/客户端、UDP 广播收发、异步 DNS 解析并枚举系统网络接口。说明本文的示例代码主要用于演示 libuv API 的用法并非高质量工程代码——它们会泄漏内存、也不总是正确关闭连接请勿直接用于生产环境。TCP基于流Stream的面向连接通信TCP 是面向连接的流式协议因此它在 libuv 中直接建立在流stream基础设施之上。核心句柄是uv_tcp_t它同时服务于服务器端与客户端。服务器init → bind → listen → accept服务器端 socket 的建立遵循四个步骤用uv_tcp_init初始化 TCP 句柄用uv_tcp_bind绑定地址与端口调用uv_listen注册回调每当客户端发起新连接时回调被触发在回调中用uv_accept接受连接之后通过[流操作]与客户端通信读/写。下面的监听 socket 初始化代码来自 docs/code/tcp-echo-server/main.cuv_tcp_t server; uv_tcp_init(loop, server); uv_ip4_addr(0.0.0.0, DEFAULT_PORT, addr); uv_tcp_bind(server, (const struct sockaddr*)addr, 0); int r uv_listen((uv_stream_t*) server, DEFAULT_BACKLOG, on_new_connection); if (r) { fprintf(stderr, Listen error %s\n, uv_strerror(r)); return 1; } return uv_run(loop, UV_RUN_DEFAULT);其中uv_ip4_addr(0.0.0.0, DEFAULT_PORT, addr)把人类可读的 IP 端口转换为 BSD socket API 需要的struct sockaddr_in这是最常用的工具函数定义于 include/uv.h。反向转换从sockaddr_in得到字符串 IP用uv_ip4_nameIPv6 对应地有uv_ip6_addr/uv_ip6_name两个变体。大部分搭建类函数init、bind是同步的因为它们属于 CPU 密集型的本地操作。真正回到 libuv 回调风格的是uv_listen其第二个参数是 backlog 队列长度即内核中排队等待 accept 的连接最大数目示例中取DEFAULT_BACKLOG 128。当客户端发起连接时on_new_connection回调被调用见 docs/code/tcp-echo-server/main.cvoid on_new_connection(uv_stream_t *server, int status) { if (status 0) { fprintf(stderr, New connection error %s\n, uv_strerror(status)); return; } uv_tcp_t *client (uv_tcp_t*) malloc(sizeof(uv_tcp_t)); uv_tcp_init(loop, client); if (uv_accept(server, (uv_stream_t*) client) 0) { uv_read_start((uv_stream_t*) client, alloc_buffer, echo_read); } else { uv_close((uv_handle_t*) client, on_close); } }要点回调必须在每个新连接上新建一个客户端句柄并通过uv_accept与服务器句柄关联uv_accept返回 0 表示成功此时即可调用uv_read_start建立对该流的读兴趣若不想接受该连接如业务上拒绝可以直接在uv_listen回调里对客户端句柄调用uv_close关闭后续的读回调uv_read_start的 alloc/read 回调与写请求uv_write与通用流示例完全一致区别只在句柄类型。回显逻辑见echo_read/echo_write注意写请求需要自行管理uv_write_t及其缓冲区的生命周期示例中用write_req_t把请求与uv_buf_t打包在一起。客户端一步 uv_tcp_connect与服务器端的 bind/listen/accept 流程相对客户端只需调用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_tcp_connect与回调签名见 include/uv.h。回调接收的uv_connect_t结构体含有一个.handle成员指向发起连接的 socket随后即可对它执行uv_read_start等流操作。完整的客户端示例连接irc.libera.chat见下文 DNS 一节。UDP无连接、不可靠的数据报通信UDP 提供无连接、不可靠的网络通信因此 libuv 不为其提供流接口而是通过uv_udp_t句柄负责接收和uv_udp_send_t请求负责发送及相关函数实现非阻塞 UDP 支持。虽然底层模型不同但读/写的 API 风格与普通流读非常接近。本仓库的 docs/code/udp-dhcp/main.c 演示了从 DHCP 服务器获取 IP 地址的第一阶段——发送 DHCP Discover 广播包。注意udp-dhcp示例需要以root身份运行因为它使用了低于 1024 的知名端口。发送与接收 socket 的搭建uv_udp_init(loop, recv_socket); struct sockaddr_in recv_addr; uv_ip4_addr(0.0.0.0, 68, recv_addr); uv_udp_bind(recv_socket, (const struct sockaddr *)recv_addr, UV_UDP_REUSEADDR); uv_udp_recv_start(recv_socket, alloc_buffer, on_read); uv_udp_init(loop, send_socket); struct sockaddr_in broadcast_addr; uv_ip4_addr(0.0.0.0, 0, broadcast_addr); uv_udp_bind(send_socket, (const struct sockaddr *)broadcast_addr, 0); uv_udp_set_broadcast(send_socket, 1); uv_udp_send_t send_req; uv_buf_t discover_msg make_discover_msg(); struct sockaddr_in send_addr; uv_ip4_addr(255.255.255.255, 67, send_addr); uv_udp_send(send_req, send_socket, discover_msg, 1, (const struct sockaddr *)send_addr, on_send);代码中几个地址语义需要分清0.0.0.0表示绑定到所有网卡接口255.255.255.255是广播地址意味着数据报会发送到子网内的所有接口端口0表示由操作系统随机分配一个端口。接收 socket 绑定在 68 端口DHCP 客户端端口并对所有接口监听用于读回任何 DHCP 服务器的响应绑定参数UV_UDP_REUSEADDR值见 include/uv.h让本程序能与机器上其他系统 DHCP 客户端共享同一端口。发送 socket 则用uv_udp_send向 67 端口DHCP 服务器端口发送广播报文。广播标志是必须设置的如果不调用uv_udp_set_broadcast(send_socket, 1)发送广播时会得到EACCES错误。与流 I/O 一样读/写回调都会在出错时收到小于 0 的 status 码。示例中具体发送的 DHCP 报文内容BOOTREQUEST、XID、CHADDR、magic cookie、DHCPDISCOVER 选项等与本书主题无关可自行阅读 docs/code/udp-dhcp/main.c 的make_discover_msg。读回调数据报来源与截断标志由于 UDP socket 不与特定对端建立连接读回调比流读回调多一个发送者地址参数void on_read(uv_udp_t *req, ssize_t nread, const uv_buf_t *buf, const struct sockaddr *addr, unsigned flags) { if (nread 0) { fprintf(stderr, Read error %s\n, uv_err_name(nread)); uv_close((uv_handle_t*) req, NULL); free(buf-base); return; } char sender[17] { 0 }; uv_ip4_name((const struct sockaddr_in*) addr, sender, 16); fprintf(stderr, Recv from %s\n, sender); // ... 解析 DHCP 响应 ... free(buf-base); uv_udp_recv_stop(req); }读回调各参数的含义nread读取到的字节数为 0 表示没有更多数据可读addr数据报的发送者地址若为NULL表示当前没有可读内容回调此时不应做任何处理若不为NULL则表示收到来自该主机的一个空数据报flags当 alloc 回调提供的缓冲区不足以容纳全部数据时该参数为UV_UDP_PARTIAL此时放不下的数据会被操作系统直接丢弃这正是 UDP 不可靠的体现。该标志定义于 include/uv.h。UDP 选项uv_udp_t支持调节多项 socket 参数全部声明于 include/uv.h生存时间TTL用uv_udp_set_ttl(uv_udp_t* handle, int ttl)修改 socket 上发出的数据报的 TTL。仅限 IPv6 栈IPv6 socket 默认可以同时承载 IPv4 与 IPv6 通信双栈模式。若希望把 socket 限制为只处理 IPv6在uv_udp_bind时传入UV_UDP_IPV6ONLY标志即可对应enum uv_udp_flags中的UV_UDP_IPV6ONLY 1。多播Multicastsocket 可以订阅/退订多播组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退出。此外多播数据包的本地回环默认是开启的可用uv_udp_set_multicast_loop关闭多播数据包的 TTL 用uv_udp_set_multicast_ttl修改若接口上存在多个地址可用uv_udp_set_multicast_interface指定发往多播组的源接口地址需要携带源地址过滤时还有uv_udp_set_source_membershipSSM 模型。异步 DNS 解析uv_getaddrinfo 与 uv_getnameinfolibuv 提供自己的getaddrinfo替代实现实现异步 DNS 解析在回调中即可对解析出的地址执行正常的 socket 操作无需阻塞事件循环。解析并连接以 irc.libera.chat 为例docs/code/dns/main.c 演示了完整的解析 → 连接 → 读取流程struct addrinfo hints; hints.ai_family PF_INET; hints.ai_socktype SOCK_STREAM; hints.ai_protocol IPPROTO_TCP; hints.ai_flags 0; uv_getaddrinfo_t resolver; fprintf(stderr, irc.libera.chat is... ); int r uv_getaddrinfo(loop, resolver, on_resolved, irc.libera.chat, 6667, hints); if (r) { fprintf(stderr, getaddrinfo call error %s\n, uv_err_name(r)); return 1; } return uv_run(loop, UV_RUN_DEFAULT);使用要点函数签名见 include/uv.h若uv_getaddrinfo返回非零值说明参数设置阶段就出了问题回调将完全不会被调用hostname、servname与hints的结构语义与 POSIXgetaddrinfo(3)一致ai_family、ai_socktype、ai_protocol、ai_flags等所有参数字符串与 hints在uv_getaddrinfo返回后即可安全释放libuv 内部已做拷贝若回调传NULL函数会同步执行解析解析失败时错误码通过回调的status返回负值可用uv_err_name打印。在回调中连接并释放结果解析回调里可以从struct addrinfo链表中任选一个地址发起连接同时演示了客户端uv_tcp_connect的完整用法docs/code/dns/main.cvoid on_resolved(uv_getaddrinfo_t *resolver, int status, struct addrinfo *res) { if (status 0) { fprintf(stderr, getaddrinfo callback error %s\n, uv_err_name(status)); return; } char addr[17] {\0}; uv_ip4_name((struct sockaddr_in*) res-ai_addr, addr, 16); fprintf(stderr, %s\n, addr); uv_connect_t *connect_req (uv_connect_t*) malloc(sizeof(uv_connect_t)); uv_tcp_t *socket (uv_tcp_t*) malloc(sizeof(uv_tcp_t)); uv_tcp_init(loop, socket); uv_tcp_connect(connect_req, socket, (const struct sockaddr*) res-ai_addr, on_connect); uv_freeaddrinfo(res); }必须调用uv_freeaddrinfo(res)释放 libuv 内部 malloc 的地址链表否则会泄漏内存。连接成功后on_connect通过req-handle拿到 socket即可uv_read_start开始读数据。底层实现线程池中的系统 getaddrinfo从源码看libuv 的异步解析是把系统调用搬到线程池里执行src/unix/getaddrinfo.c 中当传入回调时调用uv__work_submit(loop, req-work_req, UV__WORK_SLOW_IO, uv__getaddrinfo_work, uv__getaddrinfo_done)即通过 libuv 线程池异步执行系统getaddrinfo完成后把结果投递回事件循环在uv__getaddrinfo_done中调用用户回调。v0.9.0 之前的旧版本曾内置 c-ares 作为备选解析器但已被移除如今统一使用系统getaddrinfov0.8.0 及更早版本同时携带 c-ares 备选方案。反向解析uv_getnameinfolibuv 还提供与解析相反的操作uv_getnameinfo声明见 include/uv.h给定struct sockaddr*地址在回调中返回主机名与端口服务名适用于日志记录、反向 DNS 查询等场景。枚举网络接口uv_interface_addresses服务启动时往往需要绑定到本机的具体 IP 地址libuv 用uv_interface_addresses提供系统网络接口信息。docs/code/interfaces/main.c 打印全部接口详情char buf[512]; uv_interface_address_t *info; int count, i; uv_interface_addresses(info, count); i count; printf(Number of interfaces: %d\n, count); while (i--) { uv_interface_address_t interface_a info[i]; printf(Name: %s\n, interface_a.name); printf(Internal? %s\n, interface_a.is_internal ? Yes : No); if (interface_a.address.address4.sin_family AF_INET) { uv_ip4_name(interface_a.address.address4, buf, sizeof(buf)); printf(IPv4 address: %s\n, buf); } else if (interface_a.address.address4.sin_family AF_INET6) { uv_ip6_name(interface_a.address.address6, buf, sizeof(buf)); printf(IPv6 address: %s\n, buf); } printf(\n); } uv_free_interface_addresses(info, count);字段说明结构体定义见 include/uv.hname接口名如eth0、lophys_addr[6]MAC 物理地址is_internal是否为回环loopback等内部接口address联合体存放struct sockaddr_inIPv4或struct sockaddr_in6IPv6按sin_family区分netmask对应的子网掩码同样按地址族联合存放。两个注意点若一个物理接口上配了多个 IPv4/IPv6 地址该接口的name会出现多次每次对应其中一个地址使用完毕后必须调用uv_free_interface_addresses(info, count)释放由 libuv 分配的数组内存对应声明见 include/uv.h。小结libuv 的网络编程保持了与 BSD socket 一致的心智模型但把非阻塞、地址转换、DNS 解析等细节全部封装进事件循环TCP 走 init/bind/listen/accept服务端或一步uv_tcp_connect客户端的流模型UDP 通过uv_udp_tuv_udp_send_t收发数据报并支持 TTL、IPv6-only、多播等 socket 选项uv_getaddrinfo/uv_getnameinfo把系统 DNS 解析搬到线程池实现异步化uv_interface_addresses则让服务在启动时就能感知本机网卡与地址。结合 docs/code 下tcp-echo-server、udp-dhcp、dns、interfaces四个完整示例以及 include/uv.h 中的 API 声明即可快速搭建自己的跨平台异步网络程序。更多进阶用法句柄/请求模型、缓冲区与流操作、事件循环机制可继续阅读 docs/src/guide 下的其他章节。【免费下载链接】libuvCross-platform asynchronous I/O项目地址: https://gitcode.com/gh_mirrors/li/libuv创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考