### Cn/Channel
通道类
```
/* Detailed source-code truncated for AI context efficiency. */
```
---
### Cn/EventLoop
事件循环类
```
/* Detailed source-code truncated for AI context efficiency. */
```
测试代码见:
- [evpp/EventLoop_test.cpp](../../evpp/EventLoop_test.cpp)
- [evpp/EventLoopThread_test.cpp](../../evpp/EventLoopThread_test.cpp)
- [evpp/EventLoopThreadPool_test.cpp](../../evpp/EventLoopThreadPool_test.cpp)
---
### Cn/Hbase
一些基础函数
```
/* Detailed source-code truncated for AI context efficiency. */
```
单元测试代码见 [unittest/hbase_test.c](../../unittest/hbase_test.c)
---
### Cn/Hdns
# hdns: 异步DNS解析
`hdns` 是 `libhv` 内置的**异步 DNS 解析器**,完全运行在事件循环(hloop)之内。
## 为什么需要它
`libhv` 是一个事件循环库,但此前域名解析走的是**阻塞的** `getaddrinfo()`(`base/hsocket.c` 的 `ResolveAddr`)。在事件循环里同步解析 DNS 会**卡住整个 loop**——期间所有其它连接的读写、定时器都会停摆,网络抖动或 DNS 不可达时甚至会阻塞数秒。
`hdns` 参考 `libevent` 的 `evdns` 思路,**自己实现了一套原生的、基于非阻塞 UDP 的 DNS 解析器**:构造 DNS 报文、通过 hloop 的非阻塞 UDP 收发、解析响应、处理超时/重试,全程不阻塞事件循环,也**不引入任何第三方依赖**(不同于 libuv 的线程池方案,也不同于接入 c-ares)。
> 注意:`hdns` 与 `protocol/dns.*` 相互独立。后者是早期的同步 demo(仅供学习),`hdns` 自带一套完整、独立的 DNS 报文实现。
`hdns` 属于 `event` 核心模块,**默认编入**,无需额外编译开关。对外头文件:`event/hdns.h`。
## 特性
- 异步 A(IPv4)/ AAAA(IPv6)查询,纯事件驱动。
- 自动读取系统 nameserver:
- Unix/Linux/macOS:解析 `/etc/resolv.conf`;
- Windows:`GetAdaptersAddresses()`(链接 `iphlpapi`);
- 任意平台若拿不到 nameserver,统一回退到 `8.8.8.8`。
- 加载 `/etc/hosts`(Windows 为 `%SystemRoot%\System32\drivers\etc\hosts`),查询前先查表,`localhost` 等本地映射行为与系统一致。
- 数字 IP 快速路径(字面量 IPv4/IPv6 不发起查询)。
- 每次查询支持超时、重试、多 nameserver 轮转。
- DNS 名字压缩解析。
- **尊重 TTL 的进程内缓存**(正向缓存 + 负向缓存)。
- 可取消的查询句柄。
- 结果直接以 `sockaddr_u` 返回,拿到即可用于 connect。
> 第一版**暂不包含**:search domains / ndots、TCP fallback(TC 位截断重查)、nameserver 健康探测、SRV/TXT/MX 等裸记录查询。这些留作后续增强。
## 接口
```
/* Detailed source-code truncated for AI context efficiency. */
```
> C 层句柄的生命周期契约与 `htimer_t` 一致:句柄在查询完成(回调触发)或被 `hdns_cancel` 后失效,之后不得再使用。**如果需要免疫 use-after-free 的句柄,使用 C++ 层 `EventLoop::resolveDns()` / `cancelDns()`**——它维护 `DnsID → hdns_t` 映射(对标 `TimerID`),失效的 `DnsID` 在 `cancelDns` 里查不到即安全 no-op。
### 状态码
```c
#define HDNS_STATUS_OK 0 // 成功
#define HDNS_STATUS_TIMEOUT (-1) // 所有尝试均超时
#define HDNS_STATUS_NXDOMAIN (-2) // 无此域名 / 无地址记录
#define HDNS_STATUS_SERVFAIL (-3) // 服务器失败 / 响应异常
#define HDNS_STATUS_BADNAME (-4) // 非法域名
#define HDNS_STATUS_NONAMESERVER (-5) // 无可用 nameserver
#define HDNS_STATUS_NOMEM (-6) // 内存不足
#define HDNS_STATUS_CANCELLED (-7) // 已取消(不会回调)
#define HDNS_STATUS_ERROR (-8) // 其它错误
```
## 回调时序保证
`hdns_resolve` **绝不会在调用内部同步触发回调**。即使是数字 IP、`/etc/hosts` 命中、缓存命中这类“立即有结果”的情况,完成也会被投递到**下一次 loop 迭代**再回调。因此调用方总能先拿到有效句柄,随后可以确定性地保存或取消它,不会出现回调重入或 use-after-free。
## 示例
```c
#include "hloop.h"
#include "hdns.h"
#include "hsocket.h"
static int pending = 0;
static void on_resolved(hdns_t* query, const hdns_result_t* result, void* userdata) {
hloop_t* loop = (hloop_t*)userdata;
(void)query;
if (result->status == HDNS_STATUS_OK) {
printf("%s =>\n", result->host);
for (int i = 0; i < result->naddrs; ++i) {
char ip[SOCKADDR_STRLEN] = {0};
sockaddr_ip((sockaddr_u*)&result->addrs[i], ip, sizeof(ip));
printf(" %s\n", ip);
}
} else {
printf("%s => 解析失败, status=%d\n", result->host, result->status);
}
if (--pending == 0) hloop_stop(loop);
}
int main() {
hloop_t* loop = hloop_new(0);
const char* hosts[] = { "localhost", "www.example.com", "github.com" };
pending = 3;
for (int i = 0; i < 3; ++i) {
hdns_resolve(loop, hosts[i], on_resolved, loop);
}
hloop_run(loop);
hloop_free(&loop);
return 0;
}
```
完整示例见 `examples/host.c`,性能对比见 `unittest/hdns_benchmark.c`。
## C++ 层:EventLoop::resolveDns
C++ 客户端不直接用 C 层裸指针句柄,而是通过 `EventLoop` 提供的 id 化接口(对标 `setTimer`/`killTimer` 的 `TimerID`):
```cpp
// 返回免疫 use-after-free 的 DnsID(0 = 失败);cb 在 loop 线程回调
DnsID resolveDns(const char* host, DnsCallback cb, const hdns_setting_t* opt = NULL);
void cancelDns(DnsID id); // 线程安全;失效 id 自动 no-op
// DnsCallback: void(int status, int naddrs, const sockaddr_u* addrs)
```
`EventLoop` 内部维护 `DnsID → hdns_t*` 映射:查询完成时在完成回调里擦除映射项,`cancelDns` 按 id 查表(查不到即安全 no-op)。因此上层只持有一个 `DnsID` 整数,**即使底层 `hdns_t` 已被释放,拿旧 id 去 cancel 也不会 UAF**——与 `TimerID` 完全同构。
## 与 connect 路径的集成
`hdns` 已接入 C++ 客户端的 connect 路径。**异步解析逻辑统一沉淀在基类 `TcpClientEventLoopTmpl`(`evpp/TcpClient.h`)里**,并通过上面的 `EventLoop::resolveDns` 使用 `DnsID` 句柄,因此所有继承自 `TcpClientTmpl` 的客户端(`TcpClient`、`WebSocketClient`,以及未来任何 `XXXClient`)都**自动获得**异步 DNS,无需各自编写胶水代码。
### TcpClientTmpl 派生类(TcpClient / WebSocketClient / ...)
- 目标是**数字 IP**(或 Unix Domain Socket)时,`createsocket` 走原有同步快速路径立即建 socket,行为不变;
- 目标是**域名**时,`createsocket` 只记录 host/port 并**延迟解析**(不阻塞);`startConnect()`(首次连接与每次重连都会调用)先用 `EventLoop::resolveDns` 异步解析,拿到地址后再在 `startConnectWithAddr()` 建 socket、connect。整个过程**不阻塞事件循环**。旧实现在 loop 线程里调用阻塞的 `getaddrinfo`,会卡住整个 loop。
- 每次连接/重连都会重新解析,自动应对 DNS 变化;
- 对象只持有 `DnsID`;析构或主动 `closesocket()` 时 `cancelDns` 即可,**无悬垂指针风险**(销毁客户端时查询仍在途也安全,由 `unittest/tcpclient_dns_test` 覆盖)。
- 解析失败(含首次连接就失败、此时还没有 channel)时,会用一个 NULL-io 的 channel(`isConnected()` 为 false、各方法对空 io 已做防护)走 `onConnection(断开)` 通知,**保证用户总能收到失败回调**(而不是静默丢失);若配置了重连,则继续按重连策略重试。由 `unittest/tcpclient_dns_test` 覆盖。
- 用户可在 `onConnection` 里用 `channel->error()` 区分失败原因:DNS 解析失败返回 `ERR_DNS_RESOLVE`(见 `herr.h`),连接失败则返回底层 IO 错误(如 `ETIMEDOUT`、连接被拒等)。`Channel` 新增了 `setError()`/`error()`——`error()` 优先返回上层显式设置的错误码,否则回退到 `hio_error(io)`,且对空 io 安全。
> 新增客户端零成本:只要继承 `TcpClientTmpl` 并用 `createsocket(port, host)` + `start()`,就自动拥有异步 DNS,不需要实现任何 `onDnsResolved` 之类的回调。
### AsyncHttpClient
`AsyncHttpClient` 不继承 `TcpClientTmpl`(自带连接池等逻辑),单独接入,但同样使用 `EventLoop::resolveDns`:
- 请求 URL 里是**数字 IP**(或 Unix Domain Socket)时,走原有同步快速路径;
- 请求 URL 里是**域名**时,`doTask` 会先用 `EventLoop::resolveDns` 异步解析,回调里再建立连接、发送请求——同样**不阻塞 loop**。旧实现直接在 loop 线程里对域名做阻塞 `getaddrinfo`。
- 查询生命周期由 `EventLoop` 的 `DnsID` 映射管理,随 loop 拆除统一回收,无需客户端手工跟踪。
> 说明:同步的 `HttpClient` / `requests` 请求路径运行在调用方线程(并非在 loop 内),阻塞解析可接受,故该路径维持不变。
## 性能说明
`unittest/hdns_benchmark.c` 对比“顺序阻塞 `getaddrinfo`” 与 “单 loop 并发 `hdns`” 解析同一批域名:由于异步解析把所有查询一次性发出、由事件循环并发多路复用,总耗时约等于**一次最慢的解析**,而不是所有解析之和;更关键的是解析期间**事件循环始终不被阻塞**,其它 IO / 定时器可以继续运行。
> 注意:单次“命中缓存”的孤立解析,系统 `getaddrinfo`(下游有 `systemd-resolved` 等本地缓存)可能更快;`hdns` 通过尊重 TTL 的进程内缓存来弥补——命中缓存时是进程内查表(微秒级),且同样不阻塞 loop。
---
### Cn/Hlog
日志
```
/* Detailed source-code truncated for AI context efficiency. */
```
测试代码见 [examples/hloop_test.c](../../examples/hloop_test.c)
---
### Cn/Hloop
事件循环和IO多路复用机制介绍
事件循环是`libevent、libev、libuv、libhv`这类网络库里最核心的概念,即在事件循环里处理IO读写事件、定时器事件、自定义事件等各种事件;
IO多路复用即在一个IO线程监听多个fd,如最早期的`select`、后来的`poll`,`linux的epoll`、`windows的iocp`、`bsd的kqueue`、`solaris的port`等,都属于IO多路复用机制。
非阻塞NIO搭配IO多路复用机制就是高并发的钥匙。
`libhv`下的`event`模块正是封装了多种平台的IO多路复用机制,提供了统一的事件接口,是`libhv`的核心模块。
`hloop.h`: 事件循环模块对外头文件。
```
/* Detailed source-code truncated for AI context efficiency. */
```
示例代码:
- 事件循环: [examples/hloop_test.c](../../examples/hloop_test.c)
- 定时器: [examples/htimer_test.c](../../examples/htimer_test.c)
- TCP回显服务: [examples/tcp_echo_server.c](../../examples/tcp_echo_server.c)
- TCP聊天服务: [examples/tcp_chat_server.c](../../examples/tcp_chat_server.c)
- TCP代理服务: [examples/tcp_proxy_server.c](../../examples/tcp_proxy_server.c)
- TCP客户端: [examples/tcp_client_test.c](../../examples/tcp_client_test.c)
- UDP回显服务: [examples/udp_echo_server.c](../../examples/udp_echo_server.c)
- UDP代理服务: [examples/udp_proxy_server.c](../../examples/udp_proxy_server.c)
- 网络客户端: [examples/nc](../../examples/nc.c)
- SOCKS5代理服务: [examples/socks5_proxy_server.c](../../examples/socks5_proxy_server.c)
- HTTP服务: [examples/tinyhttpd.c](../../examples/tinyhttpd.c)
- HTTP代理服务: [examples/tinyproxyd.c](../../examples/tinyproxyd.c)
- jsonRPC示例: [examples/jsonrpc](../../examples/jsonrpc)
- protobufRPC示例: [examples/protorpc](../../examples/protorpc)
多进程/多线程模式示例代码:
- 多accept进程模式: [examples/multi-thread/multi-acceptor-processes.c](../../examples/multi-thread/multi-acceptor-processes.c)
- 多accept线程模式: [examples/multi-thread/multi-acceptor-threads.c](../../examples/multi-thread/multi-acceptor-threads.c)
- 一个accept线程+多worker线程: [examples/multi-thread/one-acceptor-multi-workers.c](../../examples/multi-thread/one-acceptor-multi-workers.c)
---
### Cn/HttpClient
HTTP 客户端类
```
/* Detailed source-code truncated for AI context efficiency. */
```
测试代码见 [examples/http_client_test.cpp](../../examples/http_client_test.cpp)
---
### Cn/HttpContext
```
/* Detailed source-code truncated for AI context efficiency. */
```
---
### Cn/HttpLuaHandler
# Http Lua Handler
`HttpScriptHandler` 允许 `HttpService` 调用脚本里的 `handle(ctx)` 方法处理 HTTP 请求。当前支持 `.lua` 脚本,适合把少量业务逻辑从 C++ 编译周期里解耦出来:修改脚本后无需重新编译服务,下一次请求会自动加载新脚本。
该功能是可选模块,默认不编译。
## 编译
需要 Lua 5.3 或更新版本的开发库。
Makefile:
```bash
make libhv WITH_LUA=yes
make http_server_test WITH_LUA=yes
make unittest WITH_LUA=yes
```
如果系统没有 `pkg-config`,或者 Lua 安装在自定义路径,可以显式指定:
```bash
make libhv WITH_LUA=yes \
LUA_CFLAGS="-I/usr/local/include/lua" \
LUA_LIBS="-L/usr/local/lib -llua"
```
CMake:
```bash
cmake -S . -B build -DWITH_LUA=ON -DBUILD_EXAMPLES=ON -DBUILD_UNITTEST=ON
cmake --build build
```
## 基本用法
C++:
```cpp
#include "HttpServer.h"
#include "HttpScriptHandler.h"
using namespace hv;
int main() {
HttpService router;
router.GET("/hello", HttpScriptHandler("scripts/hello.lua"));
HttpServer server;
server.port = 8080;
server.service = &router;
server.run();
return 0;
}
```
Lua:
```lua
function handle(ctx)
local id = ctx:query("id", "")
return ctx:json({
ok = true,
id = id,
path = ctx:path()
})
end
```
如果需要明确指定 Lua 引擎,也可以直接使用 `HttpLuaHandler("scripts/hello.lua")`。推荐用户代码优先使用 `HttpScriptHandler`,这样后续增加 JS/Python 等脚本引擎时不用改路由注册代码。
## 目录映射
`HttpService::Script(path, script_dir)` 可以把 URL 前缀映射到脚本目录,内部同样使用 `HttpScriptHandler`:
```cpp
router.Script("/script/", "scripts");
```
访问 `/script/user?id=42` 时会调用 `scripts/user.lua`。访问 `/script/` 时会调用 `scripts/index.lua`。当前目录映射只自动补 `.lua` 后缀。
目录映射默认支持 `GET`、`POST`、`PUT`、`DELETE`、`PATCH`。路径中包含 `..` 路径段时返回 `403`。
## ctx API
首版只暴露 HTTP handler 必需的最小 API:
```lua
ctx:method() -- GET/POST/...
ctx:path() -- URL path
ctx:param(name, default) -- query/restful params
ctx:query(name, default) -- alias of param
ctx:header(name, default)
ctx:body()
ctx:status(code)
ctx:set_header(name, value)
ctx:text(str)
ctx:json(table)
```
`handle(ctx)` 可以直接调用 `ctx:text` / `ctx:json` 返回,也可以返回字符串、数字状态码或 Lua table:
```lua
function handle(ctx)
ctx:status(201)
ctx:set_header("X-From", "lua")
return ctx:text("created")
end
```
## hv API
脚本运行在所属 IO 线程的 **协程** 里,因此可以用 **同步写法** 调用异步能力:调用会挂起当前请求的协程、把控制权交还事件循环,结果就绪后在同一线程恢复,全程不阻塞 loop。
所有脚本可用能力都挂在统一的全局 `hv` 表下:
```lua
hv.version() -- libhv 版本串, 如 "1.3.4"
hv.log(...) -- 日志 (INFO), 等价 hv.logi
hv.logd(...) / hv.logi(...) / hv.logw(...) / hv.loge(...) -- debug/info/warn/error
hv.json.encode(tbl) -- table -> json string
hv.json.decode(str) -- json string -> table
hv.setTimeout(ms, fn) -- 定时器 (返回句柄)
hv.setInterval(ms, fn)
hv.clearTimer(handle)
hv.sleep(ms) -- 协程同步 sleep: 挂起当前协程 ms 毫秒, 不阻塞 loop
hv.resolveDns(host) -- 协程同步 DNS 解析: 返回 { ip, ... } 或 nil, err
hv.run() / hv.stop() -- 运行/停止当前线程的 event loop (独立脚本用; HTTP handler 内不需要)
-- TCP/UDP (协程同步, event 层, 仅当前 loop)
local conn, err = hv.connect(host, port [, timeout_ms]) -- TCP 客户端
hv.tcpServer(host, port, function(conn) ... end) -- 每连接一个协程
local sock = hv.udpClient(host, port)
hv.udpServer(host, port, function(sock, data, peer) ... end)
-- conn: conn:read() / conn:readline() / conn:readuntil(d) / conn:readbytes(n)
-- conn:setUnpack(opts) / conn:write(s) / conn:close() / conn:fd() / conn:peeraddr()
-- sock: sock:sendto(s) / sock:recvfrom() -> data,peer / sock:close()
```
`conn:setUnpack(opts)` 让后续 `conn:read()` 每次返回一个完整的包(参考 libhv `hio_set_unpack`):
```lua
conn:setUnpack({
mode = "length_field", -- none|fixed|delimiter|length_field
body_offset = 5, length_field_offset = 1,
length_field_bytes = 4, length_field_coding = "be", -- be|le|varint|asn1
-- delimiter 模式: delimiter = "\r\n" ; fixed 模式: fixed_length = 16
})
```
示例(handler 内部“同步”写法,实际异步,loop 不阻塞):
```lua
function handle(ctx)
local addrs, err = hv.resolveDns("example.com")
if err then
ctx:status(502)
return ctx:json({ ok = false, error = err })
end
return ctx:json({ ok = true, addrs = addrs })
end
```
多个请求会在同一 IO 线程上并发交错执行:某个请求在 `hv.resolveDns` / `hv.sleep` 处挂起时,同线程的其它请求会继续推进。注意协作式调度的语义——跨越挂起点不要对全局状态做原子性假设。
> `hv.setTimeout` / `hv.resolveDns` 对应 `event/` 层能力,`hv.log` / `hv.version` 对应 `base/` 层,`hv.json` 对应 `cpputil/`;实现分别在 `lua/hvlua_event.c`、`lua/hvlua_base.c`、`lua/hvlua_json.cpp`。TCP/HTTP client、Redis 等高层 client 绑定后续按 `WITH_LUA` / `WITH_REDIS` 等开关编入。
## 热更新
`HttpScriptHandler` 当前会把 `.lua` 文件转给 `HttpLuaHandler`。`HttpLuaHandler` 会记录脚本文件的 `mtime`。每次请求前,如果文件被修改,会重新加载脚本。
重新加载失败时:
- 如果已有旧版本脚本,继续使用旧版本。
- 如果首次加载失败,返回 `500`。
这能避免线上脚本语法错误直接打断已有服务。
## 示例
```bash
make http_server_test WITH_LUA=yes
bin/http_server_test 8080
curl "http://127.0.0.1:8080/lua/hello?id=42"
curl "http://127.0.0.1:8080/script/hello?id=42"
```
---
### Cn/HttpMessage
```
/* Detailed source-code truncated for AI context efficiency. */
```
---
### Cn/HttpServer
HTTP 服务端类
```
/* Detailed source-code truncated for AI context efficiency. */
```
测试代码见 [examples/http_server_test.cpp](../../examples/http_server_test.cpp)
---
### Cn/Lua
# Lua Binding
libhv 提供一套 Lua 绑定,让 Lua 脚本直接驱动 libhv 的事件循环与异步网络能力。核心特点是**用同步写法表达异步 IO**:脚本里 `local data = conn:read()`、`local resp = hv.http.get(url)`、`local v = r:get(k)` 读起来都是阻塞式直觉,但底层通过协程 `yield → 异步回调 → resume`,事件循环全程不阻塞(与 OpenResty 同模型)。
该功能是可选模块,默认不编译。
## 编译
需要 Lua 5.3 或更新版本的开发库。
Makefile:
```bash
./configure --with-lua --with-http --with-redis --with-mqtt
make libhv
make hvlua # 独立 Lua 运行时
make unittest # 编译 lua 相关单测
```
CMake:
```bash
cmake -S . -B build -DWITH_LUA=ON -DWITH_HTTP=ON -DWITH_REDIS=ON -DWITH_MQTT=ON
cmake --build build
```
分层说明:`hloop.*` 定时器、`hv.tcpClient/tcpServer/udpClient/udpServer`、`hv.resolveDns`、`hv.json`、`hv.log` 只依赖纯 C 的 event 层,`WITH_LUA` 即可用;`hv.http` / `hv.ws` 需要 `WITH_HTTP`,`hv.redis` 需要 `WITH_REDIS`,`hv.mqtt` 需要 `WITH_MQTT`。未开启对应模块时,Lua 里 `hv.http` / `hv.redis` / `hv.mqtt` 为 `nil`。
## 运行脚本
独立运行时 `hvlua`:
```bash
bin/hvlua examples/lua/timer.lua
bin/hvlua examples/lua/tcp_client.lua 127.0.0.1 10514
```
`examples/lua/` 下有 timer / sleep / dns / tcp / udp / http / ws / redis / mqtt 各场景的示例脚本。
## 核心模型
- **每个 loop 线程一个 lua_State**,存放在 `hloop_t` 上,loop 销毁时关闭。
- **协程同步写法**:每个任务(一段脚本 / 每个 HTTP 请求 / 每个接入连接)跑在独立协程里;可挂起的绑定(`conn:read`、`hv.sleep`、`hv.http.get`、`r:get` 等)内部 `yield`,异步结果回来后在**同一 loop 线程**上 `resume`,脚本从挂起点继续。
- **无锁**:单 loop 线程 + 协作式协程,任意时刻只有一个协程在执行,其余停在各自 yield 点。注意跨 yield 点的全局状态可能被其它任务穿插修改(与 OpenResty 同模型)。
- **协作式调度**:纯 CPU 死循环会阻塞该 loop 线程。
## 返回值约定(统一)
- 成功:返回 Lua 原生值(string / integer / boolean / table)。
- 无结果(Redis nil 回复、DNS 无记录):返回 `nil`。
- 失败:返回 `nil, "错误消息"`(第二个返回值为错误串),脚本用 `local v, err = ...; if err then ... end` 处理。
---
## hv 通用工具
```lua
hv.version() -- libhv 版本串,如 "1.3.4"
hv.log(...) -- 以 tab 连接参数,info 级日志(logi 的别名)
hv.logd(...) / hv.logi(...) / hv.logw(...) / hv.loge(...) -- debug/info/warn/error
hv.json.encode(tbl) -- table -> json 字符串
hv.json.decode(str) -- json 字符串 -> table
```
## 定时器与协程 sleep
```lua
local id = hv.setTimeout(1000, function() print("once") end) -- 返回句柄
local id2 = hv.setInterval(500, function() print("tick") end)
hv.clearTimer(id)
hv.sleep(1000) -- 协程同步:挂起当前协程 1000ms,loop 不阻塞
hv.run() -- 兼容保留;loop 由宿主自动驱动,无需调用
hv.stop() -- 停止当前 loop
```
## hv.resolveDns(协程同步)
```lua
local addrs, err = hv.resolveDns("example.com")
-- addrs: { "93.184.216.34", ... } ; 失败: nil, err
```
## TCP / UDP(event 层,协程同步)
命名对齐 C++ 类 `hv::TcpClient` / `TcpServer` / `UdpClient` / `UdpServer`;`hv.connect` 是 `hv.tcpClient` 的别名,`hv.listen` 是 `hv.tcpServer` 的别名。
### TCP 客户端
```lua
local conn, err = hv.tcpClient(host, port [, timeout_ms]) -- 别名 hv.connect;协程同步,连上或失败
conn:write("hello") -- 非阻塞,进写队列,即发即走
local data, err = conn:read() -- 协程同步:挂起直到有数据;对端关闭返回 nil,"closed"
conn:close()
conn:fd() -- fd 或 -1
conn:peeraddr() -- "ip:port"
```
拆包读(文本协议 / 二进制协议):
```lua
local line = conn:readline() -- 读到 '\n'(含)
local data = conn:readuntil("\n") -- 读到单字节分隔符(含)
local buf = conn:readbytes(16) -- 读满 16 字节
-- 设置一次后,conn:read() 每次返回一个完整的包(二进制协议最常用)
conn:setUnpack({
mode = "length_field", -- none | fixed | delimiter | length_field
package_max_length = 1 << 21,
-- length_field 模式:
body_offset = 5, length_field_offset = 1,
length_field_bytes = 4, length_field_coding = "be", -- be | le | varint | asn1
length_adjustment = 0,
-- delimiter 模式: delimiter = "\r\n"
-- fixed 模式: fixed_length = 16
})
```
### TCP 服务端
`on_conn(conn)` 在每个新连接的独立协程里被调用,因此可在里面用同步写法。
```lua
hv.tcpServer("0.0.0.0", 8080, function(conn) -- 别名 hv.listen
while true do
local data, err = conn:read()
if err then break end -- 连接关闭
conn:write(data) -- echo
end
end)
```
### UDP
UDP 无连接,`sock` 复用 conn 对象。
```lua
local sock = hv.udpClient("127.0.0.1", 8080)
sock:sendto("ping")
local data, peer = sock:recvfrom() -- 协程同步,返回数据 + 对端地址
hv.udpServer("0.0.0.0", 8080, function(sock, data, peer)
sock:sendto("pong")
end)
```
## hv.http(协程同步,需 WITH_HTTP)
```lua
local resp, err = hv.http.get("http://127.0.0.1:8080/ping")
-- resp: { status = 200, body = "...", headers = { ... } }
local resp2 = hv.http.post("http://.../echo", "body", { ["Content-Type"] = "text/plain" })
local resp3 = hv.http.put("http://.../item", "body")
local resp4 = hv.http.delete("http://.../item")
local resp5 = hv.http.request("GET", url [, body [, headers]])
```
## hv.ws(WebSocket,协程同步,需 WITH_HTTP)
WebSocket 是消息驱动的,收到的消息缓存在收件箱,`ws:recv()` 挂起直到有一条消息。连接断开时 `recv()`/`send()` 返回 `(nil, err)`:开启了自动重连时 `err="reconnecting"`(临时断开,底层正在重连),否则 `err="closed"`(终止)。
```lua
-- 第二参数为可选 opts 表
local ws, err = hv.ws.connect("ws://127.0.0.1:8888/path", {
headers = { ["X-Token"] = "..." }, -- 可选:握手请求头
ping_interval = 10000, -- 可选:心跳 ping 间隔 ms
reconnect = { -- 可选:给了就开自动重连
min_delay = 1000, -- ms
max_delay = 10000, -- ms
delay_policy = 2, -- 0 固定 / 1 线性 / 2 指数退避
max_retry = 0, -- 最大重试次数,0 = 无限
},
})
ws:send("hello") -- 文本帧
ws:send(payload, "binary") -- 二进制帧
local msg, err = ws:recv() -- 协程同步:挂起直到收到一条消息
if err == "reconnecting" then ... end -- 临时断开,正在重连
ws:close() -- 显式关闭(终止,禁用重连)
```
> 开启重连后:重连成功会自动恢复,后续 `recv()`/`send()` 继续可用;断开期间 `send()` 会返回 `(nil,"reconnecting")` 而非静默丢弃,`recv()` 同样返回 `(nil,"reconnecting")` 让脚本自行决定继续等待还是退出。`ws:close()` 是显式终止,会禁用重连。
## hv.redis(协程同步,需 WITH_REDIS)
```lua
local r = hv.redis.new({ host = "127.0.0.1", port = 6379, auth = "", db = 0, timeout = 3000 })
r:set("k", "v") -- 语法糖
local v = r:get("k") -- bulk -> string / nil
local n = r:incr("c") -- integer
r:del("k") / r:decr("c") / r:expire("k", 60) / r:exists("k")
-- 任意命令:变参或数组表两种形态
local pong = r:command("PING")
local ret = r:command({ "HSET", "u:1", "name", "tom" })
```
回复到 Lua 值的映射:string -> string,integer -> integer,nil 回复 -> nil,array -> 表(1 起始,其中嵌套的 nil 元素用 `false` 占位),error 回复 -> `nil, "错误消息"`。
> `hv.redis` 用 `new` 而非 `connect`:它是纯构造,连接是懒发起 + 断线重连,命令可在未连接时排队;这与 `hv.ws.connect` / `hv.mqtt.connect` 挂起到握手完成才返回的语义不同。
## hv.mqtt(协程同步,需 WITH_MQTT)
MQTT 是消息驱动的,`m:recv()` 挂起直到 broker 推来一条 PUBLISH。
```lua
local m, err = hv.mqtt.connect({
host = "127.0.0.1", port = 1883,
id = "client-1", username = "", password = "",
keepalive = 60, clean_session = true, ssl = false,
reconnect = { -- 可选:给了就开自动重连
min_delay = 1000, max_delay = 10000, delay_policy = 2, max_retry = 0,
},
}) -- 协程同步:挂起到 CONNACK 或失败
m:subscribe("topic", 1) -- 返回 mid
m:publish("topic", "payload", 1, 0) -- topic, payload, qos, retain -> mid
m:unsubscribe("topic")
local msg, err = m:recv() -- { topic =, payload =, qos = }
if err == "reconnecting" then ... end -- 临时断开,正在重连;err="closed" 为终止
m:disconnect() -- 显式断开(终止,禁用重连)
```
> 与 hv.ws 一致:开启重连后断线是临时的,`recv()` 在断线期间返回 `(nil,"reconnecting")`,重连成功后自动恢复;`m:disconnect()` 是显式终止,禁用重连。
## HTTP Lua Handler
在 HTTP 服务端里用 Lua 脚本处理请求(`handle(ctx)`),请求在 IO 线程的协程里执行,脚本内可用上述同步写法调用异步 client。详见 [HttpLuaHandler.md](HttpLuaHandler.md)。
---
### Cn/README
## c接口
- [hloop: 事件循环](hloop.md)
- [hdns: 异步DNS解析](hdns.md)
- [hbase: 基础函数](hbase.md)
- [hlog: 日志](hlog.md)
## lua接口
- [Lua Binding: hv.* Lua 绑定](lua.md)
## c++接口
- [class EventLoop: 事件循环类](EventLoop.md)
- [class Channel: 通道类](Channel.md)
- [class TcpServer: TCP服务端类](TcpServer.md)
- [class TcpClient: TCP客户端类](TcpClient.md)
- [class UdpServer: UDP服务端类](UdpServer.md)
- [class UdpClient: UDP客户端类](UdpClient.md)
- [class HttpServer: HTTP服务端类](HttpServer.md)
- [Http Lua Handler: HTTP Lua脚本处理器](HttpLuaHandler.md)
- [class HttpClient: HTTP客户端类](HttpClient.md)
- [class WebSocketServer: WebSocket服务端类](WebSocketServer.md)
- [class WebSocketClient: WebSocket客户端类](WebSocketClient.md)
- [class RedisClient: Redis客户端类](RedisClient.md)
## rpc
- [hrpc: TLV + protobuf RPC 框架](rpc.md)
---
### Cn/RedisClient
Redis 客户端类
libhv 的 Redis C++ 模块位于仓库根目录 `redis/`,采用 `.h + .cpp` 分离结构。第一版只支持单机 Redis 与 RESP2 协议,提供同步 / 异步命令、typed helpers、pipeline、transaction 和 pub/sub。
Redis 模块默认不编译,需要显式开启:
```shell
./configure --with-redis && make libhv
# 或
cmake -S . -B build -DWITH_EVPP=ON -DWITH_REDIS=ON && cmake --build build
```
## 结果模型
命令的结果严格区分三类语义:客户端错误、Redis 服务端错误回复、nil / 空结果。
```c++
// RESP2 回复类型
enum RedisReplyType {
REDIS_REPLY_NIL, // nil
REDIS_REPLY_STRING, // 字符串 (SimpleString / BulkString)
REDIS_REPLY_ERROR, // 服务端错误回复
REDIS_REPLY_INTEGER, // 整数
REDIS_REPLY_ARRAY, // 数组
};
// 统一底层回复对象
struct RedisReply {
RedisReplyType type;
std::string str; // 字符串 / 错误信息
int64_t integer; // 整数
std::vector elements; // 数组元素
bool isNil() const;
bool isError() const;
bool isArray() const;
bool isString() const;
const std::string& error() const; // 错误信息
const std::string& asString() const;
int64_t asInt() const;
const std::vector& asArray() const;
};
// 一次命令调用的统一结果
struct RedisResult {
int code; // 0 表示客户端侧成功
RedisReply reply; // 客户端侧成功时保存 Redis 回复
bool ok() const; // code == 0 且 reply 不是错误回复
};
// typed helper 的结果 (带具体值 value)
template
struct RedisValueResult {
int code;
RedisReply reply;
T value;
bool has_value;
bool ok() const; // code == 0 且非错误回复且 has_value
bool isNil() const; // code == 0 且回复为 nil (例如 GET 不存在的 key)
};
```
语义约定:
- `code == 0`:客户端侧调用成功,`reply` 有效。
- `code != 0`:客户端侧失败(未连接、连接失败、超时、断线、协议错误、参数非法等),`reply` 无效。
- `code == 0 && reply.isError()`:成功收到 Redis 服务端错误回复(如 `-ERR`、`-WRONGTYPE`、`-NOAUTH`)。
- `isNil()`:不是错误,例如 `GET` 不存在的 key。
## class RedisClient
面向多数使用者的主入口,同步 + 异步接口共存,用法与 `HttpClient` 类似。内部使用独立线程运行事件循环,同步接口在内部等待完成,对外无需操作 future。
```
/* Detailed source-code truncated for AI context efficiency. */
```
其中 `RedisCommand` 即 `std::vector`,`RedisCallback` 为 `std::function`。
typed helpers 是对原始命令的薄封装,复用统一的命令编码、reply 解析与错误语义,不构成第二套协议实现。
### 同步用法
```c++
using namespace hv;
RedisClient client;
client.setHost("127.0.0.1");
client.setPort(6379);
client.setConnectTimeout(3000);
client.setTimeout(3000);
// typed helper
if (client.set("key", "hello").ok()) {
RedisValueResult v = client.get("key");
if (v.ok()) {
printf("key => %s\n", v.value.c_str());
} else if (v.isNil()) {
printf("key not exists\n");
}
}
// 原始命令
RedisResult r = client.command(RedisCommand{"INCR", "counter"});
if (r.ok()) {
printf("counter = %lld\n", (long long)r.reply.asInt());
}
```
### 异步用法
异步命令采用单连接、FIFO 配对模型:按发送顺序取出对应 callback。回调保证只触发一次。
```c++
client.getAsync("key", [](const RedisValueResult& v) {
if (v.ok()) {
printf("key => %s\n", v.value.c_str());
}
});
```
> 注意:不要在异步回调线程里再调用同步接口(如 `client.get(...)`),会因处于事件循环线程内被拒绝并返回 `ERR_INVALID_HANDLE`。
## class RedisPipeline
批量命令对象:先累积命令,`exec` 时一次性发送,按顺序返回 N 条回复。整体失败(发送失败 / 超时 / 断线 / 回复不完整)通过 `RedisResult.code` 体现;单条命令的错误回复是 `replies` 中某一项的合法 error reply。
```c++
class RedisPipeline {
void appendCommand(const RedisCommand& command);
RedisResult exec(std::vector* replies = NULL); // 同步
int execAsync(const RedisRepliesCallback& cb); // 异步
};
```
```c++
RedisPipeline pipe = client.pipeline();
pipe.appendCommand(RedisCommand{"SET", "counter", "1"});
pipe.appendCommand(RedisCommand{"INCR", "counter"});
std::vector replies;
RedisResult result = pipe.exec(&replies);
if (result.ok()) {
printf("INCR => %lld\n", (long long)replies[1].asInt()); // 2
}
```
## class RedisTransaction
封装 `MULTI` / `EXEC` / `DISCARD`。`exec` 返回事务结果数组。第一版不把 `WATCH` 及其冲突重试作为重点能力。
```c++
class RedisTransaction {
void appendCommand(const RedisCommand& command);
RedisResult exec(std::vector* replies = NULL); // MULTI + 命令 + EXEC
RedisResult discard(); // DISCARD
};
```
```c++
RedisTransaction tx = client.transaction();
tx.appendCommand(RedisCommand{"SET", "k", "7"});
tx.appendCommand(RedisCommand{"GET", "k"});
std::vector replies;
RedisResult result = tx.exec(&replies);
if (result.ok()) {
printf("GET => %s\n", replies[1].asString().c_str()); // 7
}
```
## class AsyncRedisClient
面向事件循环模型的纯异步客户端,风格贴近 `TcpClient` / `AsyncHttpClient`。`RedisClient` 的异步能力即基于它实现。适合高并发、长连接、批量发送场景。
```c++
class AsyncRedisClient {
AsyncRedisClient(EventLoopPtr loop = NULL);
// 连接配置 (同 RedisClient)
void setHost(const std::string& host);
void setPort(int port);
void setAuth(const std::string& password);
void setDb(int db);
void setConnectTimeout(int ms);
void setTimeout(int ms);
void setReconnect(reconn_setting_t* setting);
// 生命周期
void start(bool wait_threads_started = true);
void stop(bool wait_threads_stopped = true);
bool isConnected() const;
bool isStarted() const;
bool isInLoopThread();
// 异步命令
int command(const RedisCommand& command, RedisCallback cb);
int commandBatch(const std::vector& commands, RedisRepliesCallback cb);
// 事件回调
std::function onConnect;
std::function onClose;
std::function onError;
};
```
> 连接断开时,所有尚未完成的 pending 异步请求会统一以客户端错误失败,且不会自动重放未完成命令(Redis 命令可能有副作用,自动重放不安全)。
## class RedisSubscriber
独立订阅客户端,专门处理 Pub/Sub。使用独立连接,不与普通命令连接复用状态机。
```c++
class RedisSubscriber {
RedisSubscriber(EventLoopPtr loop = NULL);
// 连接配置
void setHost(const std::string& host);
void setPort(int port);
void setAuth(const std::string& password);
void setDb(int db);
void setReconnect(reconn_setting_t* setting);
// 生命周期
void start(bool wait_threads_started = true);
void stop(bool wait_threads_stopped = true);
// 订阅 / 退订
int subscribe(const std::string& channel);
int psubscribe(const std::string& pattern);
int unsubscribe(const std::string& channel);
int punsubscribe(const std::string& pattern);
// 事件回调
std::function onMessage;
std::function onSubscribe;
std::function onUnsubscribe;
std::function onError;
};
```
```c++
RedisSubscriber subscriber;
subscriber.setHost("127.0.0.1");
subscriber.setPort(6379);
subscriber.onMessage = [](const std::string& channel, const std::string& message) {
printf("%s => %s\n", channel.c_str(), message.c_str());
};
subscriber.start();
subscriber.subscribe("news");
```
> 配置了 reconnect 时,连接恢复后会重放当前订阅集合并继续接收推送;已经显式退订的条目不会自动恢复。
## 说明
- 第一版明确不做:Redis Sentinel、Redis Cluster、RESP3、coroutine / future-first 的公开 API、`WATCH` 冲突重试、自动重放未完成命令、大而全的全命令 typed API 覆盖。
- 普通命令流与订阅流分离,是第一版设计中的硬边界。
测试代码见 [examples/redis_client_test.cpp](../../examples/redis_client_test.cpp) 和 [examples/redis_subscriber_test.cpp](../../examples/redis_subscriber_test.cpp)
---
### Cn/Rpc
# libhv RPC (hrpc)
hrpc 是 libhv 自带的轻量 RPC,两端均基于 libhv。传输走自定义 TCP,序列化用 protobuf,服务桩代码由 protoc 插件生成。默认不依赖 nghttp2,不追求与 gRPC 线上兼容。
分三层,自底向上:
1. **TLV 编解码层**:通用的 `type-length-value` 帧编解码,不依赖 IO,可被任意二进制协议复用。
2. **TLV 三件套**(`evpp/`):`TLVChannel` / `TLVClient` / `TLVServer`,把 TLV 编解码接到 evpp 事件循环,按整帧投递。
3. **RPC 层**(`rpc/`):`RpcClient` / `RpcServer` 继承 TLV 三件套,处理服务路由、请求应答关联、超时、心跳。
---
## 一、TLV 编解码层
TLV 是通用编解码单元,T 和 L 的字节宽度均可配置,让不同协议自由裁剪帧头。
```
+----------------+------------------+------------------+
| Type (T bytes) | Length (L bytes) | Value (Length) |
+----------------+------------------+------------------+
```
### 配置
```cpp
typedef struct tlv_setting_s {
unsigned char type_bytes; // T 宽度: 0/1/2/4/8, 默认 4; 0 表示无 Type (纯 LV)
unsigned char length_bytes; // L 宽度: 1/2/4, 默认 4; 底层拆包按 32 位处理长度字段,
// >4 会被 tlv_unpack_setting 钳制到 4
bool big_endian; // 默认 true (网络字节序)
} tlv_setting_t;
```
由配置推算底层 `unpack_setting_t`(`UNPACK_BY_LENGTH_FIELD`):
```
length_field_offset = type_bytes
length_field_bytes = length_bytes
body_offset = type_bytes + length_bytes
length_field_coding = big_endian ? ENCODE_BY_BIG_ENDIAN : ENCODE_BY_LITTLE_ENDIAN
```
### TLVMessage 类
Type 用定长字节数组承载(最大 8 字节),既能当整数用,也能按字节位切分子字段。类名用 `TLVMessage`(而非 `TLV`)以避免与全大写宏冲突;头文件为 `evpp/TLVMessage.h`。
```cpp
class TLVMessage {
public:
// Type: 原始字节 / 按位 / 整数三种视图
const unsigned char* type() const;
void setType(const void* data, int len);
unsigned char typeAt(int i) const;
void setTypeAt(int i, unsigned char b);
uint64_t typeInt(const tlv_setting_t* setting) const; // 按宽度/字节序解释为整数
void setTypeInt(uint64_t v, const tlv_setting_t* setting);
// Length: 只读,随 setValue 自动维护
uint64_t length() const;
// Value
const char* value() const;
void setValue(const void* data, uint64_t len);
// 编解码 (按 setting 决定 T/L 宽度与字节序)
// pack: 写出 [T|L|V] 到 buf, 返回写入字节数, 不足返回 <0
// unpack: 从 buf 解析 T/L/V (Value 拷贝到内部), 返回整帧长度, 不足返回 <0
int packSize(const tlv_setting_t* setting) const;
int pack(void* buf, int cap, const tlv_setting_t* setting) const;
int unpack(const void* buf, int len, const tlv_setting_t* setting);
};
```
`length` 用 `uint64_t`,声明宽度 8 字节时理论上限 16EB,但实际拆包仍受底层 `package_max_length` 限制,不会因为声明大宽度就分配巨型 buffer。
---
## 二、TLV 三件套(evpp/)
复刻 libhv 已有的 WebSocket 三件套模式(`WebSocketChannel` / `WebSocketClient` / `WebSocketServer`)。
- `TLVChannel : SocketChannel` —— 提供 `sendTLV(type, data, len)`,内部用 TLV 编解码封帧。
- `TLVClient : TcpClientTmpl` —— 构造接受 `tlv_setting_t`(默认 T=4/L=4/大端),自动推算并 `setUnpack`;内部接管基类 `onMessage` 做整帧解析,对上层暴露高层回调。
- `TLVServer : TcpServerTmpl` —— 同上。
对上层暴露的高层回调(与 WebSocket 风格一致):
```cpp
// 收到一整帧
std::function onmessage;
```
三件套只负责“完整帧的收发”,不含任何 RPC 语义,可被 IM、游戏等其它 TLV 协议直接继承复用。
---
## 三、RPC 层(rpc/)
### 帧头(Type 的 8 字节切分)
hrpc 令 `type_bytes = 8`,把 Type 切成子字段(等价于早期 protorpc 的定长头):
| 字节 | 含义 |
|-----------|-------------------------------------------------|
| type[0-3] | magic `"hrpc"` |
| type[4] | version = `1` |
| type[5] | 消息类型: REQUEST/RESPONSE/PING/PONG/CLOSE |
| type[6-7] | reserved |
- **REQUEST / RESPONSE**:Value 承载 protobuf 信封 `RpcMessage`。
- **PING / PONG**:心跳,Value 为空。
- **CLOSE**:优雅关闭,Value 为空。
`length_bytes = 4`(Value 即信封长度)。
### 信封 rpc.proto
```proto
syntax = "proto3";
package hv.rpc;
message RpcMessage {
uint64 id = 1; // 请求/响应关联 id
string method = 2; // "package.Service.Method"
int32 status = 3; // 0=OK, 非0=错误码 (仅 response)
string message = 4; // 错误描述 (仅 response)
bytes payload = 5; // 用户 request/response message 的序列化
}
```
一次 parse 信封即拿到路由信息(method)、关联 id、错误状态;`payload` 再由生成的桩代码按具体类型 parse。
### RpcServer
```cpp
class RpcServer : public TLVServer {
public:
// 注册一个 service (由 codegen 生成的 RpcService 子类)
void registerService(const std::shared_ptr& service);
};
```
`RpcService` 是 codegen 生成的服务基类,内部持有 `method -> handler` 表并向 `RpcServer` 注册。收到 REQUEST 帧后,`RpcServer` 解析信封、按 `method` 路由到对应 service,执行后把结果打包成 RESPONSE 帧回写。
> **线程约束(重要)**
> - `registerService()` 必须在 `start()` **之前**调用:method 表在 IO 线程无锁读取,start 后再注册是数据竞争。
> - service 方法**同步运行在连接所属的 IO 线程**上,同一连接的所有调用串行执行。慢 handler 会阻塞该 IO 线程上的其它连接。重活请交给自己的工作线程/线程池处理后再回复。
### RpcClient
```cpp
class RpcStatus { // RPC 调用结果 (区别于 evpp/Status.h 的生命周期状态)
public:
int code; // 0=OK
std::string message;
bool ok() const { return code == 0; }
};
class RpcClient : public TLVClient {
public:
// 同步 (禁止在所属 loop 线程内调用, 内部 future 等待)
RpcStatus call(const std::string& method, const std::string& reqData,
std::string* respData, int timeout_ms = 10000);
// 异步 (回调在 loop 线程触发)
void callAsync(const std::string& method, const std::string& reqData,
std::function cb,
int timeout_ms = 10000);
void setPingInterval(int ms); // 0 关闭心跳; 默认 3000ms
};
```
- 内部维护 `id -> 调用上下文` 表,收到 RESPONSE 按 id 匹配。
- 复用 `TcpClient` 的重连、EventLoop 所有权体系。
- **超时**:同步靠 future 等待 `timeout_ms`;异步挂一个 loop 定时器,到点回调 `HRPC_STATUS_TIMEOUT`。
- **断线**:连接关闭时,所有在途调用立即以 `HRPC_STATUS_NOT_CONNECTED` 失败(不会挂死、不泄漏上下文)。
- **心跳**:连接建立后定时发 PING(`setPingInterval`,默认 3000ms),收 PONG 重置计数;连续 3 次无 PONG 主动关闭连接(触发重连)。对端 PING 自动回 PONG。
---
## 四、代码生成(rpc/protoc-gen-hrpc)
标准 protobuf service 定义:
```proto
service Calc {
rpc Add (CalcRequest) returns (CalcReply);
}
```
`protoc-gen-hrpc` 是 protoc 插件(依赖 `libprotoc`),protoc 把已解析的 AST 从 stdin 传入,插件遍历 `ServiceDescriptor` 套模板输出 `calc.hrpc.h`,无需自己写任何 IDL 解析。
生成产物:
**Server —— 用户继承实现纯虚方法**
```cpp
class CalcService : public hv::rpc::RpcService {
public:
virtual hv::rpc::RpcStatus Add(const CalcRequest& req, CalcReply* reply) = 0;
// 自动填充 method 表: "Calc.Add" -> 内部 trampoline (parse payload -> Add -> serialize)
};
```
**Client —— 生成类型安全的 stub**
```cpp
class CalcStub {
public:
explicit CalcStub(hv::rpc::RpcClient* client);
// 同步
hv::rpc::RpcStatus Add(const CalcRequest& req, CalcReply* reply, int timeout_ms = 10000);
// 异步
void Add(const CalcRequest& req,
std::function cb);
};
```
生成方式沿用 `examples/protorpc/proto/protoc.sh` 的一键脚本风格:
```bash
protoc --plugin=protoc-gen-hrpc=./protoc-gen-hrpc \
--cpp_out=. --hrpc_out=. calc.proto
```
---
## 五、构建与使用
hrpc 依赖 protobuf,因此**单独编译成 `libhrpc`**,让 `libhv` 本身保持零 protobuf 依赖。三者关系:
```
用户代码 (含 protoc-gen-hrpc 生成的 xxx.hrpc.h)
→ libhrpc (RpcClient/RpcServer + rpc.pb 信封) 依赖 protobuf
→ libhv (TLV 三件套 + evpp + event...) 零 protobuf
```
- **TLV 三件套**(`evpp/TLVMessage.h` + `TLV{Channel,Client,Server}.h`)不依赖 protobuf,随 evpp 一起编译安装(`include/hv/`),可被其它二进制协议独立复用。
- **libhrpc**(可选,默认关):`RpcClient.cpp`/`RpcServer.cpp` + 信封 `rpc.pb.cc`,链接 libhv + protobuf。头文件安装到 **`include/hv/rpc/`**。
- **protoc-gen-hrpc** 插件安装到 `bin/`,用户用它生成 service stub。
- 现代 protobuf/abseil 头要求 C++17,故 libhrpc 以 C++17 编译;libhv/TLV 仍是 C++11。
### 编译 libhrpc
```bash
# Makefile (homebrew 环境指定 protobuf 前缀)
./configure --with-rpc
make libhv && make libhrpc PROTOBUF_PREFIX=/opt/homebrew
sudo make install WITH_RPC=yes # 安装 libhrpc + include/hv/rpc + bin/protoc-gen-hrpc
# CMake
cmake .. -DWITH_RPC=ON -DCMAKE_PREFIX_PATH=/opt/homebrew
cmake --build . --target hrpc
```
### 用户使用(基于发布产物)
```bash
# 1. 用安装的插件生成 stub
protoc --plugin=protoc-gen-hrpc=$(which protoc-gen-hrpc) \
--cpp_out=. --hrpc_out=. myservice.proto
# 2. 编译链接: -lhrpc -lhv -lprotobuf
g++ -std=c++17 myapp.cpp myservice.pb.cc \
-I/include/hv -I/include/hv/rpc \
-lhrpc -lhv -lprotobuf
```
代码里 `#include ` + `#include "myservice.hrpc.h"` 即可。
---
## 六、范围
**第一版包含**:unary RPC、同步 + 异步调用、service/method 路由、错误传递、超时、心跳(PING/PONG)、优雅关闭(CLOSE)、断线重连。
**暂不包含(后续)**:streaming(server/client/bidi stream)、TLS(后续复用 libhv ssl 层)、Lua 绑定。
---
### Cn/TcpClient
TCP 客户端类
```c++
class TcpClient {
// 返回所在的事件循环
const EventLoopPtr& loop();
// 创建套接字
int createsocket(int remote_port, const char* remote_host = "127.0.0.1");
int createsocket(struct sockaddr* remote_addr);
// 绑定端口
int bind(int local_port, const char* local_host = "0.0.0.0");
int bind(struct sockaddr* local_addr);
// 关闭套接字
void closesocket();
// 开始运行
void start(bool wait_threads_started = true);
// 停止运行
void stop(bool wait_threads_stopped = true);
// 是否已连接
bool isConnected();
// 发送
int send(const void* data, int size);
int send(Buffer* buf);
int send(const std::string& str);
// 设置SSL/TLS加密通信
int withTLS(hssl_ctx_opt_t* opt = NULL);
// 设置连接超时
void setConnectTimeout(int ms);
// 设置重连
void setReconnect(reconn_setting_t* setting);
// 是否是重连
bool isReconnect();
// 设置拆包规则
void setUnpack(unpack_setting_t* setting);
// 连接状态回调
std::function onConnection;
// 消息回调
std::function onMessage;
// 写完成回调
std::function onWriteComplete;
};
```
测试代码见 [evpp/TcpClient_test.cpp](../../evpp/TcpClient_test.cpp)
---
### Cn/TcpServer
TCP 服务端类
```c++
class TcpServer {
// 返回索引的事件循环
EventLoopPtr loop(int idx = -1);
// 创建套接字
int createsocket(int port, const char* host = "0.0.0.0");
// 关闭套接字
void closesocket();
// 设置最大连接数
void setMaxConnectionNum(uint32_t num);
// 设置负载均衡策略
void setLoadBalance(load_balance_e lb);
// 设置线程数
void setThreadNum(int num);
// 开始运行
void start(bool wait_threads_started = true);
// 停止运行
void stop(bool wait_threads_stopped = true);
// 设置SSL/TLS加密通信
int withTLS(hssl_ctx_opt_t* opt = NULL);
// 设置拆包规则
void setUnpack(unpack_setting_t* setting);
// 返回当前连接数
size_t connectionNum();
// 遍历连接
int foreachChannel(std::function fn);
// 广播消息
int broadcast(const void* data, int size);
int broadcast(const std::string& str);
// 连接到来/断开回调
std::function onConnection;
// 消息回调
std::function onMessage;
// 写完成回调
std::function onWriteComplete;
};
```
测试代码见 [evpp/TcpServer_test.cpp](../../evpp/TcpServer_test.cpp)
---
### Cn/UdpClient
UDP 客户端类
```c++
class UdpClient {
// 返回所在的事件循环
const EventLoopPtr& loop();
// 创建套接字
int createsocket(int remote_port, const char* remote_host = "127.0.0.1");
// 绑定端口
int bind(int local_port, const char* local_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::function onMessage;
// 写完成回调
std::function onWriteComplete;
};
```
测试代码见 [evpp/UdpClient_test.cpp](../../evpp/UdpClient_test.cpp)
---
### Cn/UdpServer
UDP 服务端类
```c++
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::function onMessage;
// 写完成回调
std::function onWriteComplete;
};
```
测试代码见 [evpp/UdpServer_test.cpp](../../evpp/UdpServer_test.cpp)
---
### Cn/WebSocketClient
WebSocket 客户端类
```c++
class WebSocketClient {
// 打开回调
std::function onopen;
// 关闭回调
std::function onclose;
// 消息回调
std::function onmessage;
// 打开
int open(const char* url, const http_headers& headers = DefaultHeaders);
// 关闭
int close();
// 发送
int send(const std::string& msg);
int send(const char* buf, int len, enum ws_opcode opcode = WS_OPCODE_BINARY);
// 设置心跳间隔
void setPingInterval(int ms);
// 设置WebSocket握手阶段的HTTP请求
void setHttpRequest(const HttpRequestPtr& req);
// 获取WebSocket握手阶段的HTTP响应
const HttpResponsePtr& getHttpResponse();
};
```
测试代码见 [examples/websocket_client_test.cpp](../../examples/websocket_client_test.cpp)
---
### Cn/WebSocketServer
WebSocket 服务端类
```c++
// WebSocketServer 继承自 HttpServer
class WebSocketServer : public HttpServer {
// 注册WebSocket业务类
void registerWebSocketService(WebSocketService* service);
};
// WebSocket业务类
struct WebSocketService {
// 打开回调
std::function onopen;
// 消息回调
std::function onmessage;
// 关闭回调
std::function onclose;
// 心跳间隔
int ping_interval;
};
```
测试代码见 [examples/websocket_server_test.cpp](../../examples/websocket_server_test.cpp)
---
### API
# libhv API Manual
## base
### hplatform.h
- OS: OS_WIN, OS_UNIX (OS_LINUX, OS_ANDROID, OS_DARWIN ...)
- ARCH: ARCH_X86, ARCH_X64, ARCH_ARM, ARCH_ARM64
- COMPILER: COMPILER_MSVC, COMPILER_MINGW, COMPILER_GCC, COMPILER_CLANG
- BYTE_ORDER: BIG_ENDIAN, LITTLE_ENDIAN
- stdbool.h: bool, true, false
- stdint.h: int8_t, int16_t, int32_t, int64_t
- hv_sleep, hv_msleep, hv_usleep, hv_delay
- hv_mkdir
- stricmp, strcasecmp
### hexport.h
- HV_EXPORT, HV_INLINE
- HV_SOURCE, HV_STATICLIB, HV_DYNAMICLIB
- HV_DEPRECATED
- HV_UNUSED
- EXTERN_C, BEGIN_EXTERN_C, END_EXTERN_C
- BEGIN_NAMESPACE, END_NAMESPACE, USING_NAMESPACE
- DEFAULT
- ENUM, STRUCT
- IN, OUT, INOUT
- OPTIONAL, REQUIRED, REPEATED
### hdef.h
- ABS, NABS
- ARRAY_SIZE
- BITSET, BITCLR, BITGET
- CR, LF, CRLF
- FLOAT_EQUAL_ZERO
- INFINITE
- IS_ALPHA, IS_DIGIT, IS_ALPHANUM
- IS_CNTRL, IS_GRAPH
- IS_HEX
- IS_LOWER, IS_UPPER
- LOWER, UPPER
- MAKEWORD, LOBYTE, HIBYTE
- MAKELONG, LOWORD, HIWORD
- MAKEINT64, LOINT, HIINT
- MAKE_FOURCC
- MAX, MIN, LIMIT
- MAX_PATH
- NULL, TRUE, FALSE
- SAFE_FREE, SAFE_DELETE, SAFE_DELETE_ARRAY, SAFE_RELEASE
- STRINGCAT
- STRINGIFY
- offsetof, offsetofend
- container_of
- prefetch
- printd, printe
### hatomic.h
- hatomic_flag_t, hatomic_t
- hatomic_flag_test_and_set
- hatomic_flag_clear
- hatomic_add
- hatomic_sub
- hatomic_inc
- hatomic_dec
### herr.h
- hv_strerror
### htime.h
- IS_LEAP_YEAR
- datetime_t
- gettick_ms
- gettimeofday
- gettimeofday_ms
- gettimeofday_us
- gethrtime_us
- datetime_now
- datetime_localtime
- datetime_mktime
- datetime_past
- datetime_future
- duration_fmt
- datetime_fmt
- gmtime_fmt
- days_of_month
- month_atoi
- month_itoa
- weekday_atoi
- weekday_itoa
- hv_compile_datetime
- cron_next_timeout
### hmath.h
- floor2e
- ceil2e
- varint_encode
- varint_decode
### hbase.h
- hv_malloc
- hv_calloc
- hv_realloc
- hv_zalloc
- hv_strncpy
- hv_strncat
- hv_strlower
- hv_strupper
- hv_strreverse
- hv_strstartswith
- hv_strendswith
- hv_strcontains
- hv_wildcard_match
- hv_strnchr
- hv_strrchr_dot
- hv_strrchr_dir
- hv_basename
- hv_suffixname
- hv_mkdir_p
- hv_rmdir_p
- hv_exists
- hv_isdir
- hv_isfile
- hv_islink
- hv_filesize
- get_executable_path
- get_executable_dir
- get_executable_file
- get_run_dir
- hv_rand
- hv_random_string
- hv_getboolean
- hv_parse_size
- hv_parse_time
- hv_parse_url
### hversion.h
- hv_version
- hv_compile_version
- version_atoi
- version_itoa
### hsysinfo.h
- get_ncpu
- get_meminfo
### hproc.h
- hproc_spawn
### hthread.h
- hv_getpid
- hv_gettid
- HTHREAD_RETTYPE
- HTHREAD_ROUTINE
- hthread_create
- hthread_join
- class HThread
### hmutex.h
- hmutex_t
- hmutex_init
- hmutex_destroy
- hmutex_lock
- hmutex_unlock
- hspinlock_t
- hspinlock_init
- hspinlock_destroy
- hspinlock_lock
- hspinlock_unlock
- hrwlock_t
- hrwlock_init
- hrwlock_destroy
- hrwlock_rdlock
- hrwlock_rdunlock
- hrwlock_wrlock
- hrwlock_wrunlock
- htimed_mutex_t
- htimed_mutex_init
- htimed_mutex_destroy
- htimed_mutex_lock
- htimed_mutex_lock_for
- htimed_mutex_unlock
- hcondvar_t
- hcondvar_init
- hcondvar_destroy
- hcondvar_wait
- hcondvar_wait_for
- hcondvar_signal
- hcondvar_broadcast
- hsem_init
- hsem_destroy
- hsem_wait
- hsem_post
- hsem_timedwait
- honce_t
- HONCE_INIT
- honce
- class `hv::MutexLock`
- class `hv::SpinLock`
- class `hv::RWLock`
- class `hv::LockGuard`
- synchronized
### hsocket.h
- INVALID_SOCKET
- closesocket
- blocking
- nonblocking
- Bind
- Listen
- Connect
- ConnectNonblock
- ConnectTimeout
- ResolveAddr
- Socketpair
- socket_errno
- socket_strerror
- sockaddr_u
- sockaddr_ip
- sockaddr_port
- sockaddr_set_ip
- sockaddr_set_port
- sockaddr_set_ipport
- sockaddr_len
- sockaddr_str
- sockaddr_print
- SOCKADDR_LEN
- SOCKADDR_STR
- SOCKADDR_PRINT
- tcp_nodelay
- tcp_nopush
- tcp_keepalive
- udp_broadcast
- ip_v6only
- so_sndtimeo
- so_rcvtimeo
- so_sndbuf
- so_rcvbuf
- so_reuseaddr
- so_reuseport
- so_linger
### hlog.h
- default_logger
- file_logger
- stderr_logger
- stdout_logger
- logger_create
- logger_destroy
- logger_enable_color
- logger_enable_fsync
- logger_fsync
- logger_print
- logger_set_file
- logger_set_handler
- logger_set_level
- logger_set_max_bufsize
- logger_set_max_filesize
- logger_set_remain_days
- logger_set_truncate_percent
- logger_get_cur_file
- hlogd, hlogi, hlogw, hloge, hlogf
- LOGD, LOGI, LOGW, LOGE, LOGF
### hbuf.h
- hbuf_t
- offset_buf_t
- class HBuf
- class HVLBuf
- class HRingBuf
### hmain.h
- main_ctx_init
- parse_opt
- parse_opt_long
- dump_opt_long
- get_arg
- get_env
- setproctitle
- signal_init
- signal_handle
- create_pidfile
- delete_pidfile
- getpid_form_pidfile
- master_workers_run
### hstring.h
- to_string
- from_string
- toupper
- tolower
- reverse
- startswith
- endswith
- contains
- asprintf
- trim
- ltrim
- rtrim
- trim_pairs
- split
- splitKV
- replace
- replaceAll
### hfile.h
- class HFile
### hpath.h
- exists
- isdir
- isfile
- islink
- basename
- dirname
- filename
- suffixname
- join
### hdir.h
- listdir
### hurl.h
- HUrl::escape
- HUrl::unescape
- HUrl::parse
- HUrl::dump
### hscope.h
- defer
- template ScopeCleanup
- template ScopeFree
- template ScopeDelete
- template ScopeDeleteArray
- template ScopeRelease
- template ScopeLock
### ifconfig.h
- ifconfig
## utils
### md5.h
- HV_MD5Init
- HV_MD5Update
- HV_MD5Final
- hv_md5
- hv_md5_hex
### sha1.h
- HV_SHA1Init
- HV_SHA1Update
- HV_SHA1Final
- HV_SHA1
- hv_sha1
- hv_sha1_hex
### base64.h
- hv_base64_decode
- hv_base64_encode
### json.hpp
- json::parse
- json::dump
### singleton.h
- DISABLE_COPY
- SINGLETON_DECL
- SINGLETON_IMPL
## event
### hloop.h
- hloop_create_tcp_client
- hloop_create_tcp_server
- hloop_create_udp_client
- hloop_create_udp_server
- hloop_create_ssl_client
- hloop_create_ssl_server
- hloop_new
- hloop_free
- hloop_run
- hloop_stop
- hloop_pause
- hloop_resume
- hloop_status
- hloop_pid
- hloop_tid
- hloop_now
- hloop_now_ms
- hloop_now_us
- hloop_update_time
- hloop_set_userdata
- hloop_userdata
- hloop_wakeup
- hloop_post_event
- hevent_loop
- hevent_type
- hevent_id
- hevent_priority
- hevent_userdata
- hevent_set_priority
- hevent_ser_userdata
- haccept
- hconnect
- hread
- hwrite
- hrecv
- hsend
- hrecvfrom
- hsendto
- hio_add
- hio_del
- hio_get
- hio_detach
- hio_attach
- hio_read
- hio_read_start
- hio_read_stop
- hio_read_once
- hio_read_until
- hio_read_until_length
- hio_read_until_delim
- hio_read_readline
- hio_read_readstring
- hio_read_readbytes
- hio_write
- hio_close
- hio_accept
- hio_connect
- hio_fd
- hio_id
- hio_type
- hio_error
- hio_localaddr
- hio_peeraddr
- hio_events
- hio_revents
- hio_is_opened
- hio_is_closed
- hio_enable_ssl
- hio_is_ssl
- hio_get_ssl
- hio_set_ssl
- hio_get_ssl_ctx
- hio_set_ssl_ctx
- hio_new_ssl_ctx
- hio_setcb_accept
- hio_setcb_connect
- hio_setcb_read
- hio_setcb_write
- hio_setcb_close
- hio_getcb_accept
- hio_getcb_connect
- hio_getcb_read
- hio_getcb_write
- hio_getcb_close
- hio_set_type
- hio_set_localaddr
- hio_set_peeraddr
- hio_set_readbuf
- hio_set_connect_timeout
- hio_set_close_timeout
- hio_set_read_timeout
- hio_set_write_timeout
- hio_set_keepalive_timeout
- hio_set_heartbeat
- hio_set_unpack
- hio_unset_unpack
- hio_read_upstream
- hio_write_upstream
- hio_close_upstream
- hio_setup_upstream
- hio_get_upstream
- hio_setup_tcp_upstream
- hio_setup_ssl_upstream
- hio_setup_udp_upstream
- hio_create_socket
- hio_create_pipe
- hio_context
- hio_set_context
- htimer_add
- htimer_add_period
- htimer_del
- htimer_reset
- hidle_add
- hidle_del
- hsignal_add
- hsignal_del
### nlog.h
- network_logger
- nlog_listen
## evpp
- class Buffer
- class Channel
- class Event
- class EventLoop
- class EventLoopThread
- class EventLoopThreadPool
- class TcpClient
- class TcpServer
- class UdpClient
- class UdpServer
## ssl
- hssl_ctx_init
- hssl_ctx_cleanup
- hssl_ctx_instance
- hssl_ctx_new
- hssl_ctx_free
- hssl_new
- hssl_free
- hssl_accept
- hssl_connnect
- hssl_read
- hssl_write
- hssl_close
- hssl_set_sni_hostname
## protocol
### dns.h
- dns_name_decode
- dns_name_encode
- dns_pack
- dns_unpack
- dns_rr_pack
- dns_rr_unpack
- dns_query
- dns_free
- nslookup
### ftp.h
- ftp_command_str
- ftp_connect
- ftp_login
- ftp_exec
- ftp_upload
- ftp_download
- ftp_download_with_cb
- ftp_quit
- ftp_status_str
### smtp.h
- smtp_command_str
- smtp_status_str
- smtp_build_command
- sendmail
### icmp.h
- ping
## http
- class HttpMessage
- class HttpRequest
- class HttpResponse
- class HttpParser
### httpdef.h
- http_content_type_enum
- http_content_type_enum_by_suffix
- http_content_type_str
- http_content_type_str_by_suffix
- http_content_type_suffix
- http_errno_description
- http_errno_name
- http_method_enum
- http_method_str
- http_status_enum
- http_status_str
### http_content.h
- parse_query_params
- parse_json
- parse_multipart
- dump_query_params
- dump_json
- dump_multipart
### HttpClient.h
- http_client_new
- http_client_del
- http_client_send
- http_client_send_async
- http_client_strerror
- http_client_set_timeout
- http_client_set_header
- http_client_del_header
- http_client_get_header
- http_client_clear_headers
- http_client_set_http_proxy
- http_client_set_https_proxy
- http_client_add_no_proxy
- class HttpClient
### requests.h
- requests::request
- requests::get
- requests::post
- requests::put
- requests::patch
- requests::Delete
- requests::head
- requests::async
### axios.h
- axios::axios
- axios::get
- axios::post
- axios::put
- axios::patch
- axios::Delete
- axios::head
- axios::async
### HttpServer.h
- http_server_run
- http_server_stop
- class HttpService
- class HttpServer
### WebSocketClient.h
- class WebSocketClient
### WebSocketServer.h
- websocket_server_run
- websocket_server_stop
- class WebSocketService
- class WebSocketServer
## mqtt
- mqtt_client_new
- mqtt_client_free
- mqtt_client_run
- mqtt_client_stop
- mqtt_client_set_id
- mqtt_client_set_will
- mqtt_client_set_auth
- mqtt_client_set_callback
- mqtt_client_set_userdata
- mqtt_client_get_userdata
- mqtt_client_get_last_error
- mqtt_client_set_ssl_ctx
- mqtt_client_new_ssl_ctx
- mqtt_client_set_reconnect
- mqtt_client_reconnect
- mqtt_client_set_connect_timeout
- mqtt_client_connect
- mqtt_client_is_connected
- mqtt_client_disconnect
- mqtt_client_publish
- mqtt_client_subscribe
- mqtt_client_unsubscribe
- class MqttClient
## other
- class HThreadPool
- class HObjectPool
- class ThreadLocalStorage
---
### PLAN
## Done
- base: cross platfrom infrastructure
- event: select/poll/epoll/wepoll/kqueue/port/io_uring
- ssl: openssl/gnutls/mbedtls/wintls/appletls
- rudp: KCP
- evpp: c++ EventLoop interface similar to muduo and evpp
- http client/server: include https http1/x http2
- http server sync/async/ctx/state/script handlers
- websocket client/server
- mqtt client
- redis client
- async DNS
- lua binding
- hrpc = libhv + protobuf
## Plan
- js binding
- rudp: FEC, ARQ, UDT, QUIC
- coroutine
- cppsocket.io
- IM-libhv
- MediaServer-libhv
- GameServer-libhv
---
### README
English | [中文](README-CN.md)
# libhv
[](https://github.com/ithewei/libhv/actions/workflows/CI.yml?query=branch%3Amaster)
[](https://github.com/ithewei/libhv/actions/workflows/CI.yml?query=branch%3Amaster)
[](https://github.com/ithewei/libhv/actions/workflows/CI.yml?query=branch%3Amaster)
[](https://github.com/ithewei/libhv/actions/workflows/CI.yml?query=branch%3Amaster)
[](https://github.com/ithewei/libhv/actions/workflows/CI.yml?query=branch%3Amaster)
[](https://github.com/ithewei/libhv/actions/workflows/benchmark.yml?query=branch%3Amaster)
[](https://github.com/ithewei/libhv/releases)
[](https://github.com/ithewei/libhv/stargazers)
[](https://github.com/ithewei/libhv/forks)
[](https://github.com/ithewei/libhv/issues)
[](https://github.com/ithewei/libhv/pulls)
[](https://github.com/ithewei/libhv/contributors)
[](LICENSE)
[](https://gitee.com/libhv/libhv)
[](https://github.com/oz123/awesome-c)
[](https://github.com/fffaraz/awesome-cpp)
Like `libevent, libev, and libuv`,
`libhv` provides event-loop with non-blocking IO and timer,
but simpler api and richer protocols.
## ✨ Features
- Cross-platform (Linux, Windows, macOS, Android, iOS, BSD, Solaris)
- High-performance EventLoop (IO, timer, idle, custom, signal)
- TCP/UDP client/server/proxy
- TCP supports heartbeat, reconnect, upstream, MultiThread-safe write and close, etc.
- Built-in common unpacking modes (FixedLength, Delimiter, LengthField)
- RUDP support: WITH_KCP
- SSL/TLS support: (via WITH_OPENSSL or WITH_GNUTLS or WITH_MBEDTLS)
- HTTP client/server (support https http1/x http2 grpc)
- HTTP supports static service, indexof service, forward/reverse proxy service, sync/async API handler
- HTTP supports RESTful, router, middleware, keep-alive, chunked, SSE, etc.
- WebSocket client/server
- MQTT client
- Redis client
## ⌛️ Build
see [BUILD.md](BUILD.md)
Makefile:
```shell
./configure
make
sudo make install
```
or cmake:
```shell
mkdir build
cd build
cmake ..
cmake --build .
```
or bazel:
```shell
bazel build libhv
```
or vcpkg:
```shell
vcpkg install libhv
```
or xmake:
```shell
xrepo install libhv
```
## ⚡️ Getting Started
run `./getting_started.sh`:
```shell
git clone https://github.com/ithewei/libhv.git
cd libhv
./configure
make
bin/httpd -h
bin/httpd -d
#bin/httpd -c etc/httpd.conf -s restart -d
ps aux | grep httpd
# http file service
bin/curl -v localhost:8080
# http indexof service
bin/curl -v localhost:8080/downloads/
# http api service
bin/curl -v localhost:8080/ping
bin/curl -v localhost:8080/echo -d "hello,world!"
bin/curl -v localhost:8080/query?page_no=1\&page_size=10
bin/curl -v localhost:8080/kv -H "Content-Type:application/x-www-form-urlencoded" -d 'user=admin&pswd=123456'
bin/curl -v localhost:8080/json -H "Content-Type:application/json" -d '{"user":"admin","pswd":"123456"}'
bin/curl -v localhost:8080/form -F 'user=admin' -F 'pswd=123456'
bin/curl -v localhost:8080/upload -d "@LICENSE"
bin/curl -v localhost:8080/upload -F "file=@LICENSE"
bin/curl -v localhost:8080/test -H "Content-Type:application/x-www-form-urlencoded" -d 'bool=1&int=123&float=3.14&string=hello'
bin/curl -v localhost:8080/test -H "Content-Type:application/json" -d '{"bool":true,"int":123,"float":3.14,"string":"hello"}'
bin/curl -v localhost:8080/test -F 'bool=1' -F 'int=123' -F 'float=3.14' -F 'string=hello'
# RESTful API: /group/:group_name/user/:user_id
bin/curl -v -X DELETE localhost:8080/group/test/user/123
# benchmark
bin/wrk -c 1000 -d 10 -t 4 http://127.0.0.1:8080/
```
### TCP
#### tcp server
**c version**: [examples/tcp_echo_server.c](examples/tcp_echo_server.c)
**c++ version**: [evpp/TcpServer_test.cpp](evpp/TcpServer_test.cpp)
```c++
#include "TcpServer.h"
using namespace hv;
int main() {
int port = 1234;
TcpServer srv;
int listenfd = srv.createsocket(port);
if (listenfd < 0) {
return -1;
}
printf("server listen on port %d, listenfd=%d ...\n", port, listenfd);
srv.onConnection = [](const SocketChannelPtr& channel) {
std::string peeraddr = channel->peeraddr();
if (channel->isConnected()) {
printf("%s connected! connfd=%d\n", peeraddr.c_str(), channel->fd());
} else {
printf("%s disconnected! connfd=%d\n", peeraddr.c_str(), channel->fd());
}
};
srv.onMessage = [](const SocketChannelPtr& channel, Buffer* buf) {
// echo
channel->write(buf);
};
srv.setThreadNum(4);
srv.start();
// press Enter to stop
while (getchar() != '\n');
return 0;
}
```
#### tcp client
**c version**: [examples/tcp_client_test.c](examples/tcp_client_test.c)
**c++ version**: [evpp/TcpClient_test.cpp](evpp/TcpClient_test.cpp)
```c++
#include
#include "TcpClient.h"
using namespace hv;
int main() {
int port = 1234;
TcpClient cli;
int connfd = cli.createsocket(port);
if (connfd < 0) {
return -1;
}
cli.onConnection = [](const SocketChannelPtr& channel) {
std::string peeraddr = channel->peeraddr();
if (channel->isConnected()) {
printf("connected to %s! connfd=%d\n", peeraddr.c_str(), channel->fd());
} else {
printf("disconnected to %s! connfd=%d\n", peeraddr.c_str(), channel->fd());
}
};
cli.onMessage = [](const SocketChannelPtr& channel, Buffer* buf) {
printf("< %.*s\n", (int)buf->size(), (char*)buf->data());
};
cli.start();
std::string str;
while (std::getline(std::cin, str)) {
if (str == "close") {
cli.closesocket();
} else if (str == "start") {
cli.start();
} else if (str == "stop") {
cli.stop();
break;
} else {
if (!cli.isConnected()) break;
cli.send(str);
}
}
return 0;
}
```
### HTTP
#### http server
see [examples/http_server_test.cpp](examples/http_server_test.cpp)
**golang gin style**
```c++
#include "HttpServer.h"
using namespace hv;
int main() {
HttpService router;
router.GET("/ping", [](HttpRequest* req, HttpResponse* resp) {
return resp->String("pong");
});
router.GET("/data", [](HttpRequest* req, HttpResponse* resp) {
static char data[] = "0123456789";
return resp->Data(data, 10);
});
router.GET("/paths", [&router](HttpRequest* req, HttpResponse* resp) {
return resp->Json(router.Paths());
});
router.GET("/get", [](HttpRequest* req, HttpResponse* resp) {
resp->json["origin"] = req->client_addr.ip;
resp->json["url"] = req->url;
resp->json["args"] = req->query_params;
resp->json["headers"] = req->headers;
return 200;
});
router.POST("/echo", [](const HttpContextPtr& ctx) {
return ctx->send(ctx->body(), ctx->type());
});
HttpServer server(&router);
server.setPort(8080);
server.setThreadNum(4);
server.run();
return 0;
}
```
#### http client
see [examples/http_client_test.cpp](examples/http_client_test.cpp)
**python requests style**
```c++
#include "requests.h"
int main() {
auto resp = requests::get("http://www.example.com");
if (resp == NULL) {
printf("request failed!\n");
} else {
printf("%s\n", resp->body.c_str());
}
resp = requests::post("127.0.0.1:8080/echo", "hello,world!");
if (resp == NULL) {
printf("request failed!\n");
} else {
printf("%s\n", resp->body.c_str());
}
return 0;
}
```
### WebSocket
#### WebSocket server
see [examples/websocket_server_test.cpp](examples/websocket_server_test.cpp)
```c++
#include "WebSocketServer.h"
using namespace hv;
int main(int argc, char** argv) {
WebSocketService ws;
ws.onopen = [](const WebSocketChannelPtr& channel, const HttpRequestPtr& req) {
printf("onopen: GET %s\n", req->Path().c_str());
};
ws.onmessage = [](const WebSocketChannelPtr& channel, const std::string& msg) {
printf("onmessage: %.*s\n", (int)msg.size(), msg.data());
};
ws.onclose = [](const WebSocketChannelPtr& channel) {
printf("onclose\n");
};
WebSocketServer server(&ws);
server.setPort(9999);
server.setThreadNum(4);
server.run();
return 0;
}
```
#### WebSocket client
see [examples/websocket_client_test.cpp](examples/websocket_client_test.cpp)
```c++
#include "WebSocketClient.h"
using namespace hv;
int main(int argc, char** argv) {
WebSocketClient ws;
ws.onopen = []() {
printf("onopen\n");
};
ws.onmessage = [](const std::string& msg) {
printf("onmessage: %.*s\n", (int)msg.size(), msg.data());
};
ws.onclose = []() {
printf("onclose\n");
};
// reconnect: 1,2,4,8,10,10,10...
reconn_setting_t reconn;
reconn_setting_init(&reconn);
reconn.min_delay = 1000;
reconn.max_delay = 10000;
reconn.delay_policy = 2;
ws.setReconnect(&reconn);
ws.open("ws://127.0.0.1:9999/test");
std::string str;
while (std::getline(std::cin, str)) {
if (!ws.isConnected()) break;
if (str == "quit") {
ws.close();
break;
}
ws.send(str);
}
return 0;
}
```
### Redis
see [examples/redis_client_test.cpp](examples/redis_client_test.cpp) and [examples/redis_subscriber_test.cpp](examples/redis_subscriber_test.cpp)
The Redis C++ module lives in the repository root `redis/` and follows the same `.h + .cpp` split used by other libhv modules. Redis is disabled by default; enable it explicitly with `./configure --with-redis` or `cmake -DWITH_REDIS=ON`.
```shell
./configure --with-redis
make unittest
cmake -S . -B build -DWITH_EVPP=ON -DWITH_REDIS=ON -DBUILD_UNITTEST=ON
cmake --build build --target redis_protocol_test redis_async_client_test redis_client_test redis_batch_test redis_subscriber_test
```
## 🍭 More examples
### c version
- [examples/hloop_test.c](examples/hloop_test.c)
- [examples/htimer_test.c](examples/htimer_test.c)
- [examples/pipe_test.c](examples/pipe_test.c)
- [examples/tcp_echo_server.c](examples/tcp_echo_server.c)
- [examples/tcp_chat_server.c](examples/tcp_chat_server.c)
- [examples/tcp_proxy_server.c](examples/tcp_proxy_server.c)
- [examples/udp_echo_server.c](examples/udp_echo_server.c)
- [examples/udp_proxy_server.c](examples/udp_proxy_server.c)
- [examples/socks5_proxy_server.c](examples/socks5_proxy_server.c)
- [examples/tinyhttpd.c](examples/tinyhttpd.c)
- [examples/tinyproxyd.c](examples/tinyproxyd.c)
- [examples/jsonrpc](examples/jsonrpc)
- [examples/mqtt](examples/mqtt)
- [examples/multi-thread/multi-acceptor-processes.c](examples/multi-thread/multi-acceptor-processes.c)
- [examples/multi-thread/multi-acceptor-threads.c](examples/multi-thread/multi-acceptor-threads.c)
- [examples/multi-thread/one-acceptor-multi-workers.c](examples/multi-thread/one-acceptor-multi-workers.c)
### c++ version
- [evpp/EventLoop_test.cpp](evpp/EventLoop_test.cpp)
- [evpp/EventLoopThread_test.cpp](evpp/EventLoopThread_test.cpp)
- [evpp/EventLoopThreadPool_test.cpp](evpp/EventLoopThreadPool_test.cpp)
- [evpp/TimerThread_test.cpp](evpp/TimerThread_test.cpp)
- [evpp/TcpServer_test.cpp](evpp/TcpServer_test.cpp)
- [evpp/TcpClient_test.cpp](evpp/TcpClient_test.cpp)
- [evpp/UdpServer_test.cpp](evpp/UdpServer_test.cpp)
- [evpp/UdpClient_test.cpp](evpp/UdpClient_test.cpp)
- [examples/http_server_test.cpp](examples/http_server_test.cpp)
- [examples/http_client_test.cpp](examples/http_client_test.cpp)
- [examples/websocket_server_test.cpp](examples/websocket_server_test.cpp)
- [examples/websocket_client_test.cpp](examples/websocket_client_test.cpp)
- [examples/redis_client_test.cpp](examples/redis_client_test.cpp)
- [examples/redis_subscriber_test.cpp](examples/redis_subscriber_test.cpp)
- [examples/protorpc](examples/protorpc)
- [hv-projects/QtDemo](https://github.com/hv-projects/QtDemo)
### simulate well-known command line tools
- [examples/nc](examples/nc.c)
- [examples/nmap](examples/nmap)
- [examples/httpd](examples/httpd)
- [examples/wrk](examples/wrk.cpp)
- [examples/curl](examples/curl.cpp)
- [examples/wget](examples/wget.cpp)
- [examples/consul](examples/consul)
- [examples/kcptun](examples/kcptun)
## 🥇 Benchmark
### `pingpong echo-servers`
```shell
cd echo-servers
./build.sh
./benchmark.sh
```
**throughput**:
```shell
libevent running on port 2001
libev running on port 2002
libuv running on port 2003
libhv running on port 2004
asio running on port 2005
poco running on port 2006
==============2001=====================================
[127.0.0.1:2001] 4 threads 1000 connections run 10s
total readcount=1616761 readbytes=1655563264
throughput = 157 MB/s
==============2002=====================================
[127.0.0.1:2002] 4 threads 1000 connections run 10s
total readcount=2153171 readbytes=2204847104
throughput = 210 MB/s
==============2003=====================================
[127.0.0.1:2003] 4 threads 1000 connections run 10s
total readcount=1599727 readbytes=1638120448
throughput = 156 MB/s
==============2004=====================================
[127.0.0.1:2004] 4 threads 1000 connections run 10s
total readcount=2202271 readbytes=2255125504
throughput = 215 MB/s
==============2005=====================================
[127.0.0.1:2005] 4 threads 1000 connections run 10s
total readcount=1354230 readbytes=1386731520
throughput = 132 MB/s
==============2006=====================================
[127.0.0.1:2006] 4 threads 1000 connections run 10s
total readcount=1699652 readbytes=1740443648
throughput = 165 MB/s
```
### `iperf tcp_proxy_server`
```shell
# sudo apt install iperf
iperf -s -p 5001 > /dev/null &
bin/tcp_proxy_server 1212 127.0.0.1:5001 &
iperf -c 127.0.0.1 -p 5001 -l 8K
iperf -c 127.0.0.1 -p 1212 -l 8K
```
**Bandwidth**:
```shell
------------------------------------------------------------
[ 3] local 127.0.0.1 port 52560 connected with 127.0.0.1 port 5001
[ ID] Interval Transfer Bandwidth
[ 3] 0.0-10.0 sec 20.8 GBytes 17.9 Gbits/sec
------------------------------------------------------------
[ 3] local 127.0.0.1 port 48142 connected with 127.0.0.1 port 1212
[ ID] Interval Transfer Bandwidth
[ 3] 0.0-10.0 sec 11.9 GBytes 10.2 Gbits/sec
```
### `webbench`
```shell
# sudo apt install wrk
wrk -c 100 -t 4 -d 10s http://127.0.0.1:8080/
# sudo apt install apache2-utils
ab -c 100 -n 100000 http://127.0.0.1:8080/
```
**libhv(port:8080) vs nginx(port:80)**
Above test results can be found on [Github Actions](https://github.com/ithewei/libhv/actions/workflows/benchmark.yml).
---