资讯动态

libhv UdpServer 类全解析:基于事件循环的 C++ UDP 服务端开发指南

发布时间:2026/9/28 3:28:31 来源:尧图企业网站定制
网络后端通信【免费下载链接】libhv 比libevent/libuv/asio更易用的网络库。A c/c network library for developing TCP/UDP/SSL/HTTP/WebSocket/MQTT/Redis client/server.项目地址https://gitcode.com/gh_mirrors/li/libhv点击查看免费下载本篇技术指南以 docs/cn/UdpServer.md 为骨架系统讲解 libhv 中hv::UdpServer类的完整 API、线程模型、消息回调与 KCP 扩展并结合 evpp/UdpServer.h、evpp/UdpServer_test.cpp 等源码与测试用例做底层原理剖析。读完本文你将掌握用 libhv 快速搭建可运行的 UDP 服务端如 echo 服务器、数据采集服务、广播服务并理解其事件循环 单通道设计的本质。一、UdpServer 类概览面向对象的 UDP 服务端封装libhv 在底层 C 事件循环hloop_t/hio_t之上提供了 C 封装层 evpphv::UdpServer就是其中的 UDP 服务端类。它把创建 socket → 注册读写回调 → 启动事件循环 → 收发数据这一整套流程收敛为一个简洁的类接口。原文档给出了类的核心声明完整继承如下class UdpServer { // 返回所在的事件循环 const EventLoopPtr loop(); // 创建套接字 int createsocket(int port, const char* host 0.0.0.0); // 关闭套接字 void closesocket(); // 开始运行 void start(bool wait_threads_started true); // 停止运行 void stop(bool wait_threads_stopped true); // 发送 int sendto(const void* data, int size, struct sockaddr* peeraddr NULL); int sendto(Buffer* buf, struct sockaddr* peeraddr NULL); int sendto(const std::string str, struct sockaddr* peeraddr NULL); // 设置KCP void setKcp(kcp_setting_t* setting); // 消息回调 std::functionvoid(const TSocketChannelPtr, Buffer*) onMessage; // 写完成回调 std::functionvoid(const TSocketChannelPtr, Buffer*) onWriteComplete; };从源码看UdpServer实际是模板UdpServerTmplSocketChannel的类型别名见 evpp/UdpServer.h模板参数TSocketChannel决定了通道类型默认即SocketChannel因此回调签名中的TSocketChannelPtr就是std::shared_ptrSocketChannel。类继承自EventLoopThread与UdpServerEventLoopTmpl前者提供事件循环线程后者提供所有 UDP 收发逻辑——这也是下文线程安全特性的来源。二、快速上手5 分钟跑通 UDP echo 服务端原文档末尾指明测试代码见 evpp/UdpServer_test.cpp该测试文件头部注释给出了完整的构建与联调命令build make evpp server bin/UdpServer_test 1234 client bin/UdpClient_test 1234先执行make evpp编译然后在一个终端启动服务端另一个终端启动配套的 UDP 客户端evpp/UdpClient_test.cpp两者通过端口 1234 通信。整个服务端核心逻辑只有几步#include iostream #include UdpServer.h using namespace hv; int main(int argc, char* argv[]) { if (argc 2) { printf(Usage: %s port\n, argv[0]); return -10; } int port atoi(argv[1]); UdpServer srv; int bindfd srv.createsocket(port); // 1. 绑定端口创建 UDP socket if (bindfd 0) { return -20; } printf(server bind on port %d, bindfd%d ...\n, port, bindfd); srv.onMessage [](const SocketChannelPtr channel, Buffer* buf) { // echo把收到的数据原样写回 printf( %.*s\n, (int)buf-size(), (char*)buf-data()); channel-write(buf); }; srv.start(); // 2. 启动事件循环线程 // 3. 主线程可继续处理标准输入动态控制服务 std::string str; while (std::getline(std::cin, str)) { if (str close) { srv.closesocket(); } else if (str start) { srv.start(); } else if (str stop) { srv.stop(); break; } else { srv.sendto(str); // 4. 主线程直接发送 } } return 0; }这个例子展示了 evpp 式 UDP 服务端的典型形态创建 → 注册回调 → 启动 → 收发。值得注意start()之后主线程并没有阻塞因为事件循环运行在独立的线程中主线程既可以交互式控制服务状态也可以直接调用sendto主动向对端发数据。如果只想验证 UDP 原生收发仓库还提供了纯 C 版的极简 echo 服务端 examples/udp_echo_server.c构建方式为make examples运行bin/udp_echo_server 1234用nc -u 127.0.0.1 1234即可测试。三、API 逐项解析生命周期、收发与回调3.1 创建与关闭createsocket / closesocketint createsocket(int port, const char* host 0.0.0.0); void closesocket();createsocket以默认值0.0.0.0绑定全部网卡传入指定 IP 则可只监听该地址返回值是绑定后的 socket 文件描述符bindfd大于等于 0 表示成功小于 0 表示失败。closesocket是线程安全的内部调用channel-close(true)走异步关闭路径见 evpp/UdpServer.h可在任意线程调用。从实现看createsocket底层调用hloop_create_udp_server(loop_-loop(), host, port)evpp/UdpServer.h该函数在 event/hloop.c 中进一步映射为hio_create_socket(loop, host, port, HIO_TYPE_UDP, HIO_SERVER_SIDE)——即创建一个 UDP 类型的服务端 IO 对象并封装进SocketChannel。createsocket未执行bind时是 UDP 默认行为libhv 内部已按HIO_SERVER_SIDE语义完成地址绑定。3.2 启动与停止start / stopvoid start(bool wait_threads_started true); void stop(bool wait_threads_stopped true);两个方法的布尔参数都默认true含义分别是等待事件循环线程启动完成后再返回与等待线程真正停止后再返回。UdpServer继承自EventLoopThread因此其start/stop语义与线程生命周期绑定evpp/UdpServer.h首次调用start()时会通过EventLoopThread::start创建一个独立的事件循环线程并在线程内执行startRecv()此后再次调用start()比如测试程序里的交互命令则只需在线程内恢复接收即可。stop(true)会先异步关闭 socket再停止事件循环线程。析构函数中同样会调用stop(true)保证资源回收。由is_loop_owner标记可以看出若构造函数未传入外部EventLoopPtr则该线程由本对象创建并拥有stop()时才真正终止线程若传入了外部循环则只负责关闭 socket不接管循环的生命周期。这是 libhv evpp 层循环可内建、可外挂的设计体现——UdpServer 既可以自起线程独立运行也可以像插件一样挂载到你自己的EventLoop中与其他 IO 共用一个循环。3.3 发送三种重载的 sendtoint sendto(const void* data, int size, struct sockaddr* peeraddr NULL); int sendto(Buffer* buf, struct sockaddr* peeraddr NULL); int sendto(const std::string str, struct sockaddr* peeraddr NULL);三种重载分别接收裸指针 长度、Buffer 对象和std::string返回值为发送字节数或负的错误码。可选的peeraddr参数用于指定目标地址——不传时使用创建 socket 时设定的默认对端。源码中evpp/UdpServer.h三个重载最终都汇聚到同一路径int sendto(const void* data, int size, struct sockaddr* peeraddr NULL) { if (channel NULL) return -1; std::lock_guardstd::mutex locker(sendto_mutex); // 多线程发送互斥 if (peeraddr) hio_set_peeraddr(channel-io(), peeraddr, SOCKADDR_LEN(peeraddr)); return channel-write(data, size); }两个关键实现事实线程安全sendto内部用sendto_mutex保护因此可以从任意线程包括事件循环线程之外的主线程安全调用这正是测试程序能在主线程std::getline循环里直接srv.sendto(str)的原因。Buffer即HBufBuffer是base/hbuf.h中HBuf的别名evpp/Buffer.h提供data()、size()等接口在回调中可直接读取或透传。3.4 回调onMessage 与 onWriteCompletestd::functionvoid(const TSocketChannelPtr, Buffer*) onMessage; std::functionvoid(const TSocketChannelPtr, Buffer*) onWriteComplete;onMessage收到一条 UDP 数据报时触发参数为发起会话的SocketChannelPtr和携带数据的Buffer*。对 UDP 而言每个对端 IP 与端口组合在内部被抽象为一个逻辑通道因此通过channel可以区分数据来源也能直接channel-write(buf)回包echo 示例即如此。onWriteComplete写完成回调。头文件注释特别提醒应通过Channel::isWriteComplete()判断数据是否全部写出writeBufsize() 0而不是把回调本身当作全部写完的信号见 evpp/UdpServer.h 与 evpp/Channel.h。回调的注册发生在startRecv()中evpp/UdpServer.h即启动事件循环时把onMessage/onWriteComplete分别挂到channel-onread/channel-onwrite再通过channel-startRead()hio_read_start真正开始接收数据。四、线程模型事件循环、Channel 与 Buffer 如何协作UdpServer 的整体线程与数据流结构可以概括为主线程 (用户代码) 事件循环线程 ──────────────── ──────────────── srv.start() ──► EventLoopThread::start ──► EventLoop::run srv.sendto() ──(加锁)──► channel-write ──► hio_write 异步写出 ◄── hio 读事件 ──► onread ──► onMessage(channel, buf)事件循环loop()返回EventLoopPtr即std::shared_ptrEventLoop。它是 IO 事件分发的核心UdpServer 所有收发都调度在它上面。Channel 抽象SocketChannel包装底层hio_t对外暴露write、read*、close、setReadBuf、超时设置setReadTimeout/setWriteTimeout等方法evpp/Channel.h。UDP 场景下onconnect等 TCP 专有成员不会被使用。Buffer 抽象BufferHBuf在底层 IO 回调中由Channel::on_read构造直接把hio读到的(data, readbytes)包装成Buffer传给用户回调evpp/Channel.h用户拿到的是直接指向接收缓冲区的数据无需额外拷贝即可处理。五、KCP 扩展setKcp 与 kcp_setting_t 参数速查原文档中的setKcp(kcp_setting_t* setting)是 libhv 的一大特色——UDP 之上可选叠加KCP 可靠传输协议仓库内嵌实现位于 event/kcp让 UDP 服务端获得类似 TCP 的可靠性同时保留 UDP 的灵活性与低延迟。setKcp仅在编译期开启WITH_KCP宏时可用源码用#if WITH_KCP包裹见 evpp/UdpServer.h。启用方式参考 examples/udp_echo_server.c 的注释./configure --with-kcp make clean make。传入NULL时释放已有设置传入非空kcp_setting_t*时做值拷贝之后可安全复用局部变量。kcp_setting_t的完整字段与默认值定义在 event/hloop.h其中 C 构造器默认值如下C 代码需自行 memset 或使用下方初始化函数字段默认值说明conv0x11223344会话号通信双方需一致nodelay00普通模式1快速模式interval40ms内部状态刷新间隔越小越灵敏fastresend0快速重传触发条件连续丢包数nocwnd01关闭拥塞窗口降低延迟sndwnd/rcvwnd0发送/接收窗口0 为默认mtu1400最大传输单元update_interval10msKCP 更新间隔同一头文件还提供了四组开箱即用的初始化函数event/hloop.h可直接按需选择kcp_setting_init_with_normal_mode普通模式interval40kcp_setting_init_with_fast_mode快速模式interval30, fastresend2, nocwnd1kcp_setting_init_with_fast2_mode更激进nodelay1, interval20kcp_setting_init_with_fast3_mode低延迟取向nodelay1, interval10典型的用法是kcp_setting_t st; kcp_setting_init_with_fast3_mode(st); srv.setKcp(st);之后在onMessage中通过channel收发上层代码几乎无感知因为hio_set_kcp会把 IO 类型切换为HIO_TYPE_KCP见 event/kcp/hkcp.c。如需强制关闭某个对端的 KCP 会话C 示例中展示了hio_close_rudp(io, hio_peeraddr(io))的用法。六、与底层 C API 的对应关系一条调用链看懂本质UdpServer 并非黑盒它本质上是对 C 层 hloop/hio API 的薄封装。梳理整条调用链可以帮你更好地定位问题、阅读底层日志UdpServer 成员底层实现位置createsocket(port, host)hloop_create_udp_server(loop, host, port)→hio_create_socket(loop, host, port, HIO_TYPE_UDP, HIO_SERVER_SIDE)evpp/UdpServer.h、event/hloop.csendto(...)channel-write(...)→hio_write(io, data, size)evpp/UdpServer.honMessage触发hio读事件 →Channel::on_read→ 用户onMessageevpp/Channel.hstart()EventLoopThread::start→ 线程内EventLoop::runstartRecv()→hio_read_startevpp/UdpServer.hsetKcphio_set_kcp(io, setting)IO 类型切为HIO_TYPE_KCPevent/kcp/hkcp.cC 层的对照示例即 examples/udp_echo_server.c用hloop_new建循环、hloop_create_udp_server建 UDP 服务端 IO、hio_setcb_read注册读回调、hio_read开始接收、hloop_run运行循环。它演示了同一能力在 C 与 C 两条路径上的等价实现也说明了 evpp 只是把回调 循环的组织方式对象化了底层 IO 模型完全一致。七、进阶实战广播、多目标回包与超时控制基于上述 API 与源码可以组合出一些常见的 UDP 服务端实战模式7.1 对指定地址发送peeraddr 参数sendto的第三参支持struct sockaddr*例如配合sockaddr_u构造目标地址后定向发送实现一个服务端、多个目标地址的分发逻辑。不传peeraddr时则发往默认对端。7.2 UDP 广播客户端侧支持广播发送UdpClient::createsocket在检测到 remote_host 以.255结尾时会自动调用udp_broadcast(sockfd, 1)开启广播选项见 evpp/UdpClient.h。服务端默认绑定0.0.0.0即可接收广播包这是局域网内设备发现类服务的常见做法。7.3 每通道独立处理onMessage的channel参数按对端区分可结合Channel的上下文机制newContext/getContext/deleteContext见 evpp/Channel.h为每个客户端附加独立的状态如会话 ID、计数器、密钥实现有状态的 UDP 服务。7.4 心跳与超时虽然 UDP 无连接但 libhv 的SocketChannel仍提供setHeartbeat(interval_ms, fn)、setReadTimeout、setWriteTimeout等能力evpp/Channel.h可用于在应用层实现逻辑连接的保活与超时清理——这在 KCP 模式下尤其实用。八、实践注意事项createsocket返回值判错测试代码中bindfd 0即退出端口被占用或参数非法时务必检查返回值。start()的线程语义wait_threads_started true时start()会阻塞直到循环线程就绪若传入外部EventLoop构造注意对象不拥有该循环的生命周期。写完成判断onWriteComplete回调不代表数据全部落盘/发出用channel-isWriteComplete()判断writeBufsize() 0。KCP 需要编译开关setKcp只有在./configure --with-kcp构建下才可用否则WITH_KCP未定义、该方法不存在。sendto返回值返回负值表示失败如 socket 未打开返回 -1发送前可用channel-isOpened()前置检查。调试入口libhv 的日志宏hloge会在createsocket失败、恢复监听失败等路径打印详细错误如createsocket %s:%d return %d排查问题时配合 base/hlog.h 的日志级别配置使用。至此围绕 docs/cn/UdpServer.md 定义的UdpServer类本文从接口声明、源码实现、测试验证到底层调用链、KCP 扩展与实战模式做了完整展开。对照 evpp/UdpServer_test.cpp 和 evpp/UdpClient_test.cpp 动手编译运行一遍你将快速建立起对 libhv UDP 服务端开发的全貌认知。赞分享网络后端通信【免费下载链接】libhv 比libevent/libuv/asio更易用的网络库。A c/c network library for developing TCP/UDP/SSL/HTTP/WebSocket/MQTT/Redis client/server.项目地址https://gitcode.com/gh_mirrors/li/libhv点击查看免费下载相关推荐OpenChamber Session Goal基于 OpenCode 的服务端自续目标控制循环解析OpenChamber Session Goal基于 OpenCode 的服务端自续目标控制循环解析 导读 Session Goal 是 OpenChambeAI Agent人工智能代码智能体交互助手MCP Python SDK 服务端分页实战基于游标的 list_* 分页与客户端循环全解析MCP Python SDK 服务端分页实战基于游标的 list_ 分页与客户端循环全解析 大多数 MCP 服务器根本用不到分页 MCPServer 默认会人工智能MCP 服务MCP ClientsLightdash 服务端埋点系统基于 RudderStack 的类型安全事件追踪实战Lightdash 服务端埋点系统基于 RudderStack 的类型安全事件追踪实战 本文以 Lightdash 后端 analytics 模块为核心讲解后端前端数据分析数据可视化人工智能AI Agent上一篇libkperf完全指南打造高性能PMU采集库的终极实践下一篇UMDK革命性内存语义通信框架 - 颠覆传统网络架构的终极指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑