简介ithewei_libhv.zip 为 libhv 跨平台网络通信库的完整源码包面向物联网通信与底层网络开发人员可用于学习事件驱动模型、HTTP/WebSocket 协议实现以及高并发服务搭建。压缩包共 376 个文件大小约 1.2MB其中 115 个 h 头文件与 162 个 C/C 源文件构成核心库主体32 个 md 文档提供使用说明与设计笔记另有 cmake、bazel、conf 等构建配置以及 shell 脚本、Python 辅助脚本便于在 Linux/Windows 等环境下编译集成。针对物联网项目场景包内含若干常用协议处理与网络底层组件涵盖 JSON/HTTP 解析、跨平台事件轮询、可靠 UDP 传输及安全通信扩展可参考其实现完成自定义通信模块。目前已有 47 人学习下载适合需要从源码层面理解网络库内部机制或进行二次开发的中高级嵌入式及后台开发者。1. 从 ithewei_libhv.zip 说起一个能顶三件套的高性能网络库打开 ithewei_libhv.zip 这个压缩包其实是在打开一个作者名加项目名的组合ithewei 是作者在 GitHub 上的用户名libhv 是他维护的跨平台高性能网络库。用一句话概括它解决什么问题当你在 C/C 项目里需要 HTTP 服务、WebSocket、MQTT 或 TCP/UDP 长连接又不想把 libevent、openssl、http-parser 这些第三方库一个个拼起来libhv 直接把一套可用的协议栈和事件循环封装好了。比起手写 epoll 状态机libhv 把网络层最难的部分作为黑匣子处理好对外留出干净的 API。这个压缩包适合两类人一类是服务器后端工程师想快速在 CentOS、Ubuntu 或 Windows 上起一个高并发服务另一类是客户端 C 开发需要给自己的 SDK 内置一个长连接通道。下面的内容会从事件模型讲起再到编译、最小工程、HTTP 服务与压测验证把拿到 zip 包之后该走的路径完整走一遍。2. 先看明白再动手libhv 的事件模型、线程模型与最小示例2.1 一个以 hloop 为中心的分层架构libhv 的核心不是 HTTP而是一个叫 hloop 的事件循环。在 Linux 上它封装 epollmacOS 上封装 kqueueWindows 上封装 IOCP对外统一提供hloop_t、hio_t、htimer_t这套 C API。你可以把它理解成一个把网络事件、定时器、信号都揉在一起处理的“事件中心”。HTTP、WebSocket、MQTT 这些模块全部构建在 hloop 之上而不是像很多项目那样自己再写一套 event loop。选择 libhv 而不是 libevent有几个很现实的原因。libevent 对 HTTP 协议的支持比较弱客户端和服务端都要自己拼装报文WebSocket 和 MQTT 更是要另找库libhv 把这三块协议栈都补齐了你不需要再手写 HTTP 状态机。另一个原因是构建简单CMake 一把过要带 OpenSSL 也只是一个编译选项的事。相比 ASIO 那种模板层层嵌套的写法libhv 的 C 封装直观很多回调函数里直接操作HttpRequest和HttpResponse对象新人上手成本低。如果你之前习惯用 socket epoll 手写网络层会发现 libhv 的分层非常眼熟。最底层是hloop_t事件循环负责收集读写事件、定时器超时中间层是hio_t一个hio_t对应一个 socket 连接或监听套接字再往上是hv::HttpServer、hv::HttpClient、hv::WebSocketClient这些 C 封装。核心点在于网络传输和协议解析被拆开了你写业务只需要关心回调里收到的请求和要返回的响应。2.2 线程模型与 Worker 数的选型逻辑每个 hloop 只能在一个线程里运行hloop_run是阻塞的会一直转圈直到 loop 被退出。这就是很多人一开始没意识到的约束libhv 的“高并发”不是在一个线程里用异步回调硬扛而是靠多个 loop 分散到多个线程。HTTP server 启动时创建 N 个 worker 线程每个线程各自跑一个hloop_t网络事件被分发到不同线程多核 CPU 才真正被用起来。worker 数量怎么定我一般按场景分两种情况。如果业务是纯 IO 转发、请求处理里没有重计算worker 设成 CPU 核数或者核数加一就够起太多线程只会增加锁竞争和缓存 miss如果 handler 里有 CPU 密集任务比如频繁做 JSON 解析、加解密、图像处理不能只堆 worker而要把这部分计算挪到独立业务线程池里去跑否则事件循环线程被堵住连接就会大量积压。另一个常见误区是跨线程操作 socket。一个hio_t属于它所在 loop 的那个线程你在别的线程里直接调用写接口轻则数据错乱重则段错误。正确做法是用hloop_post把任务投递到目标 loop 所在线程去执行。等你后面跑压测、调参数时很多莫名其妙的问题都是从这里冒出来的所以这个模型最好提前记牢。2.3 第一个最小 hloop 定时器程序验证 API 是否跑通在正式写 HTTP 服务前我建议先跑一个最小程序验证 libhv 的链接和事件循环本身没问题。下面这段代码只创建一个 loop 和一个定时器每秒触发一次回调// min_loop.cpp #include hv/hloop.h #include cstdio // 定时器回调在 hloop 所在线程执行 static void on_timer(htimer_t* timer) { printf(timer fire\n); } int main() { // 创建事件循环参数传 0 表示由 libhv 自动绑定当前线程 hloop_t* loop hloop_new(0); // 1000ms 后首次触发之后每 1000ms 重复一次 hloop_timer_add(loop, 1000, 1000, on_timer, NULL); // 进入事件循环阻塞运行 hloop_run(loop); hloop_free(loop); return 0; }这段代码最关键的是hloop_timer_add的参数顺序第二个参数是首次触发延迟第三个参数是重复周期单位都是毫秒最后一个参数是 userdata可以传任意指针在回调里通过timer-userdata取出来这里用不到就传 NULL。hloop_run会一直阻塞直到某处调用hloop_stop或进程收到退出信号。编译命令我先给一版临时的完整的 CMake 链接方式放到下一章。这里假设你已经用 CMake 构建过 libhv头文件在源码的include目录库文件在build/lib或build/Release目录g -stdc11 min_loop.cpp -I libhv/include -L libhv/build/lib -lhv -o min_loop ./min_loop程序跑起来后每秒打印一行timer fire说明事件循环和定时器模块都正常。这一步看起来简单但能提前暴露一个最常见的问题头文件和库版本不匹配或者没有链接 pthread。3. 把压缩包变成可运行库CMake 编译与三种安装姿势3.1 在 Linux/macOS 上用 CMake 构建与安装拿到 ithewei_libhv.zip 以后第一个动作不是急着翻源码而是先解压、构建、跑通示例。用 CMake 的标准流程就行unzip ithewei_libhv.zip -d libhv cd libhv mkdir -p build cmake -S . -B build -DCMAKE_BUILD_TYPERelease cmake --build build -j4 sudo cmake --install build逐条解释一下。第一行的unzip把压缩包解压到libhv目录第二行进入源码目录第三行新建独立的build目录所有编译产物都放这里不会污染源码第四行里的-S指定源码根目录-B指定构建目录-DCMAKE_BUILD_TYPERelease表示开优化编译库的体积和性能差别很大发布服务时不要用 Debug 版第五行的-j4是并行编译参数数值按机器核数调整低配虚拟机上用-j2更稳否则 gcc 内存占用可能把机器卡死。sudo cmake --install build会把头文件装到/usr/local/include/hv库装到/usr/local/lib。如果你不想污染系统目录也可以跳过安装步骤直接在项目 CMakeLists 里指向源码路径后面第 3.3 节会讲这个做法。装完后可以顺手验证一下文件是否齐全ls /usr/local/include/hv ls /usr/local/lib | grep hv看到HttpServer.h、HttpClient.h等头文件以及libhv.so或libhv.a就说明安装成功了。如果机器上还没有 cmakeUbuntu 用apt install cmakeCentOS 用yum install cmakemacOS 用brew install cmake这一步不需要任何特殊配置。3.2 在 Windows 上用 CMake 生成 VS 工程Windows 上不建议直接用 Visual Studio 新建空工程去包含 libhv 源码又慢又容易把平台宏搞乱。更常见的做法是直接用 CMake 生成 VS 工程并编译静态库或 DLLunzip ithewei_libhv.zip -d libhv cd libhv mkdir build cmake -S . -B build -A x64 -DBUILD_SHARED_LIBSON cmake --build build --config Release这里的-A x64指定生成 64 位工程配合现代 Windows 默认的 64 位程序使用BUILD_SHARED_LIBSON让 CMake 产出 DLL 而不是一坨静态库这样后续调试和部署都灵活一些。编译完成后二进制输出在build\bin\Release目录里面是hv.dll和对应的导入库hv.lib。Windows 上最常踩的坑是运行时找不到 DLL。你自己写的 exe 编译链接时能找到hv.lib但运行时会到 exe 目录和系统 PATH 里找hv.dll。最简单的处理是把你程序生成的 exe 复制到build\bin\Release或者把hv.dll复制到 exe 同目录。还有一种做法是在 PATH 环境变量里临时加上build\bin\Release不过只对当前终端窗口有效换台机器部署又是一轮问题。提示CMake 版本低于 3.16 时-S -B这种写法可能不被识别可以改成进入 build 目录后执行cmake ..的旧式写法效果完全相同。3.3 用 CMake 链接自己的工程而不是裸写 g 命令命令行直接-I-L链接只适合临时验证维护性很差。实际项目里我一般会把 libhv 当成第三方依赖用一个独立 CMakeLists 管理。先把最小模板放在项目根目录cmake_minimum_required(VERSION 3.10) project(libhv_demo CXX) set(CMAKE_CXX_STANDARD 11) # LIBHV_SRC_DIR 是解压后的 libhv 源码路径 find_path(HV_INCLUDE_DIR hv/HttpServer.h HINTS /usr/local/include ${LIBHV_SRC_DIR}/include) find_library(HV_LIBRARY hv HINTS /usr/local/lib ${LIBHV_SRC_DIR}/build) add_executable(http_demo main.cpp) target_include_directories(http_demo PRIVATE ${HV_INCLUDE_DIR}) target_link_libraries(http_demo PRIVATE ${HV_LIBRARY})find_path和find_library是 CMake 提供的查找命令HINTS 里写的是候选路径。查找头文件时看hv/HttpServer.h是否存在查找库时找libhv.so、libhv.a或hv.lib。这套机制会自动区分你用的是系统安装版本还是源码构建版本避免“头文件是一套、库是另一套”的错位。链接阶段还有一个容易忽略的点libhv 依赖系统线程库Linux 上要额外写target_link_libraries(http_demo PRIVATE ${HV_LIBRARY} pthread)。有些版本的头文件自带-lpthread的 pragma但 CMake 的纯函数查找路径下不一定有效手工加 pthread 最稳。下一章的 HTTP server 代码直接放到main.cpp里用这个 CMakeLists 一编译就是完整服务。4. 用 libhv 搭一个可用的 HTTP 服务路由、静态文件和连接参数4.1 路由注册与请求分发一个 60 行的 HTTP serverHTTP 服务端是 libhv 使用频率最高的模块代码量比手写状态机少一个数量级。下面这个例子包含最基础的路由注册和静态文件兜底可以直接放进上一章的main.cpp#include hv/HttpServer.h #include fstream #include sstream #include string // 读取静态文件内容路径里出现 .. 直接拒绝 static std::string read_file(const std::string root, const std::string path) { if (path.find(..) ! std::string::npos) return ; std::ifstream ifs(root path, std::ios::binary); if (!ifs) return ; std::stringstream ss; ss ifs.rdbuf(); return ss.str(); } int main() { HttpServer server; server.setPort(8080); server.setThreadNum(4); // 精确路由GET /hello 返回 pong server.on(/hello, [](HttpRequest* req, HttpResponse* resp) { resp-body hello, libhv; }); // 兜底路由未匹配的路径都进这里按 URL 拼文件路径 server.on(/, [](HttpRequest* req, HttpResponse* resp) { std::string path req-path; if (path /) path /index.html; std::string body read_file(./html, path); if (body.empty()) { resp-status_code 404; resp-body not found; return; } resp-body body; }); server.run(); return 0; }setPort指定监听端口setThreadNum指定 worker 线程数。server.on的第一个参数是路径规则第二个参数是请求处理回调回调里修改resp-body就会作为响应体发回客户端。注意server.on(/)在 libhv 里通常作为兜底处理所有未命中的路径适合做静态文件服务但要放在具体路由后面注册否则会先于/hello匹配。这个 handler 是同步执行在 worker 线程里的。如果read_file读取的文件很大或者磁盘繁忙这个 worker 会被拖住后面的请求只能排队。生产环境我会把文件读取放到独立线程池或者用 sendfile 之类手段而不是在回调里同步做 IO。另外resp-body赋值后不会自动推断 Content-Type浏览器访问时会出现文件能下载但显示不正常的现象需要自己维护一份后缀到 MIME 类型的映射表。4.2 静态文件服务path 拼接与目录穿越问题上一节的read_file只挡住了字面意义的..但 URL 里的%2e%2e解码后也是..这个检查会被绕过。真实场景下静态文件服务是安全事故高发区我一般会至少做两层处理先把 URL 路径做一次百分号解码再用std::filesystem::weakly_canonical把最终路径规范化最后确认规范化后的路径仍然在 root 目录内。#include filesystem // 安全检查full 必须是 root 的子路径 static bool is_under_root(const std::string root, const std::string full) { std::error_code ec; auto root_canon std::filesystem::weakly_canonical(root, ec); auto full_canon std::filesystem::weakly_canonical(full, ec); if (ec) return false; return full_canon.string().rfind(root_canon.string(), 0) 0; }这个函数把root和拼接后的文件路径都转成绝对路径再检查前缀关系。直接用rfind看前缀有一个边界问题/html和/html2也会匹配所以比较时要把 root 末尾处理成带斜杠的形式或者比较后确认下一个字符是路径分隔符。这类代码在网上能搜到很多版本核心逻辑就是“规范化后再校验而不是黑名单过滤”。4.3 与连接寿命相关的两个参数Keep-Alive 与 idle_timeoutHTTP 服务的吞吐量往往不取决于协议解析而取决于连接管理和线程调度。下面是三个需要重点关注的参数参数作用常见取值worker_threads创建几个事件循环线程CPU 核数或核数 1idle_timeout空闲连接多久被回收60s 到 120smax_connections最大并发连接数取决于 ulimit 和内存worker_threads在前面已经提过纯 IO 场景核数足够。idle_timeout是 Keep-Alive 连接的空闲阈值调太短长连接频繁被回收客户端下次请求又要走一遍 TCP 握手调太长大量半死不活的连接占着文件描述符。max_connections在 Linux 上受ulimit -n限制压测前先执行ulimit -n 1024000把 fd 上限抬高不然线程和连接都没满系统就开始拒绝新连接。这三个参数需要配合压测结果一起调整不要第一次上线就追求极限值。后面第 6 章会给一个可复现的验证方法用 libhv 的 HttpClient 分别测长连接和短连接的 QPS 差异根据结果反向调整参数。如果你发现关闭 Keep-Alive 后 QPS 掉了近一半说明握手开销占大头这时候idle_timeout就该调大一点如果开了 Keep-Alive 后 QPS 上不去问题可能出在 worker 线程数不够或者回调里有阻塞。5. libhv 避坑清单链接、线程、SSL 与端口冲突的五个翻车现场5.1 动态库链接失败找不到 -lhv 或 hv/HttpServer.h现象编译自己的程序时报fatal error: hv/HttpServer.h: No such file or directory或者链接阶段报undefined reference to hv::HttpServer::run()。原因头文件找到了但库没链接上或者头文件和库来自两个不同的构建版本。Windows 上最常见的是动态库的 DLL 和头文件都存在但运行时系统找不到 DLL。解决不要用裸命令硬拼回到第 3.3 节的 CMake 模板用find_path和find_library固定头文件与库的路径。Linux 还经常遇到/usr/local/lib不在默认搜索路径的发行版链接没问题但运行报cannot open shared object file此时执行export LD_LIBRARY_PATH/usr/local/lib:$LD_LIBRARY_PATH再运行。Windows 上把hv.dll复制到 exe 同目录这一步几乎能解决所有运行时报错。5.2 回调里做阻塞操作单核 CPU 被打满现象服务刚启动时一切正常压测几分钟后延迟从几毫秒涨到几百毫秒但 CPU 使用率只占了一个核其他核闲着。原因handler 回调里做了数据库查询、文件读取或sleep这类阻塞操作。事件循环线程被堵住后续所有连接事件都在排队不是并发变差是线程根本没空去轮询。解决把耗时操作从回调里拆出去放到独立业务线程池。完成后如果要往 socket 写数据不要在线程池里直接操作hio_t而要用hloop_post把写回动作投递到连接所属的 loop 线程。另一个快速缓解办法是给HttpServer增加 worker 线程数但这只能分担连接不能解决单连接回调阻塞的根因。5.3 跨线程调用 hloop 导致数据竞争现象程序偶发崩溃压测时线程数一高就出问题单线程、低并发下完全复现不了gdb 看到的栈信息每次都不一样。原因多个线程同时操作同一个hloop_t内部的数据结构比如定时器链表和事件队列。hloop 内部只保证它在所属线程里被访问是安全的跨线程直接读写没有任何保护。解决记住一条铁律——每个 hloop 只在创建它的线程里运行。需要从业务线程往 loop 线程塞任务时统一走hloop_post(loop, event_cb, userdata)接口由 libhv 内部完成队列投递和唤醒。自己做一把锁去保护 hloop 的 socket 列表不是不行但很容易破坏 epoll 的事件语义属于给自己挖坑。5.4 开 SSL 后握手失败多半是构建选项没开现象客户端请求 https 地址时报handshake failed或certificate verify failed但 HTTP 协议访问完全正常。原因libhv 编译时没有启用 OpenSSL 支持或者证书文件路径不对。后者在容器化部署时非常常见证书挂载路径和配置里的相对路径不一致进程启动时不会报错等到握手才发现读不到证书。解决重新用 CMake 构建时打开 SSL 选项Linux 上通常写成-DHV_WITH_OPENSSLON具体开关名以源码根目录的 CMakeLists 里查到的为准。证书文件建议用绝对路径传入请求配置。开发环境可以临时关闭证书校验跑通流程生产环境一定不要关这是网络安全红线。5.5 端口被占用与 TIME_WAIT 导致的启动失败现象服务启动时报bind failed或Address already in use换一个端口就正常了。原因上一个进程没退出或者前一个压测进程挂了但 socket 还处于 TIME_WAIT 状态。频繁重启服务的开发阶段TIME_WAIT 累积最明显。解决先排查再动手。Linux 用lsof -i:8080或ss -lntpWindows 用netstat -ano | findstr 8080看到残留进程就按 PID 杀掉。关于 SO_REUSEADDR不同版本 libhv 是否默认开启不一致源码里搜REUSEADDR关键字可以直接确认没开的话在监听前设置对应 socket 选项。6. 进阶验证用 20 行 HttpClient 小程序对比 Keep-Alive 的 QPS看完上面的参数和避坑下一步是验证这些策略到底有没有用。相比于依赖外部压测工具我更喜欢用 libhv 自带的 HttpClient 写一个五分钟就能跑完的对比程序专门测长连接和短连接的差距// bench_keepalive.cpp #include hv/HttpClient.h #include chrono #include cstdio #include string int main(int argc, char** argv) { // 传 ka 代表开启 keep-alive不传代表每次新建连接 bool keepalive (argc 1 std::string(argv[1]) ka); const int total 5000; hv::HttpClient cli; auto start std::chrono::steady_clock::now(); for (int i 0; i total; i) { HttpRequest req; req.url http://127.0.0.1:8080/hello; if (!keepalive) { req.headers[Connection] close; } HttpResponse resp cli.syncSend(req); if (resp.status_code ! 200) { printf(unexpected status: %d\n, resp.status_code); return 1; } } double seconds std::chrono::durationdouble( std::chrono::steady_clock::now() - start).count(); printf(qps%.0f, avg%.2f ms\n, total / seconds, seconds * 1000.0 / total); return 0; }用Connection: close头强制关闭 Keep-Alive和不传这个头的长连接模式做对比。我实际跑过的项目里短连接模式 QPS 明显偏低握手次数越多差距越大。如果这个差距在你这台机器上不明显先检查服务端是否真的接受了 Keep-Alive响应头里没有Connection: keep-alive说明服务端的连接复用配置有问题需要回看idle_timeout参数。这个程序还能用来验证 worker 线程数的选型。分别用-j1、-j4、-j8编译出三个服务端同一个压测客户端跑一遍记录 QPS 和平均延迟。当 worker 从 4 加到 8QPS 没有明显上涨时瓶颈就转移到别处了可能是回调里的 CPU 计算也可能是文件描述符上限再去调参数就是有的放矢。我第一次做这种验证时压测数据一直很难看折腾了半天才发现客户端默认把连接关了Keep-Alive 实际没生效。后面养成的习惯是先看响应头再调参数最后才怀疑工具。这个顺序帮我少走了很多弯路希望也帮到你。本文还有配套的精品资源点击获取