libhv
是一个比libevent、libev、libuv
更易用的跨平台国产网络库,可用来开发TCP/UDP/SSL/HTTP/WebSocket
客户端/服务端。
项目地址:https://github.com/ithewei/libhv.git
码云镜像:https://gitee.com/libhv/libhv.git
QQ技术交流群:739352073
libhv入门教程:https://hewei.blog.csdn.net/article/details/113733758
libhv源码剖析:https://hewei.blog.csdn.net/article/details/123295998
A:libhv
是一个类似于libevent、libev、libuv
的跨平台网络库,提供了带非阻塞IO和定时器的事件循环。
libhv
的名称也正是继承此派,寓意高性能的事件循环High-performance event loop library
。
A:
uv_write
个人感觉难用;A:精妙小巧跨平台,简单实用易上手
configure
/cmake
自动生成的hconfig.h
和hplatform.h
两个头文件中提供的平台宏、编译器宏等实现的;Linux
使用epoll
,Windows
使用IOCP
、Mac
使用kqueue
、Solaris
使用evport
,感兴趣的可以读一读event下的源码;A:基于event模块实现更多的常见应用层协议,如MQTT
、redis
、kafka
、mysql
等;
更多发展规划详见docs/PLAN.md
A:
libev = libhv > libuv > libevent = asio = POCO
A:libhv自2018年5月创建,至今已有三年多迭代,700+提交,广泛用于公司IoT和HTTP API服务中,此外QQ群里也是有不少水友成功用在各种项目中,反馈很好;
请放心使用,开源且保证长期维护,QQ群里也有很多大神积极解答。
A:
getting_started.sh
脚本开始, 你会被libhv的httpd所展示的便利性所吸引;examples
下的示例代码;base->event->http
;A:libhv可通过Makefile
或cmake
编译出动态库和静态库,make install
后包含相关头文件(base模块下头文件比较分散,可直接#include "hv.h"
)和链接库文件即可使用;当然libhv模块划分清晰,低耦合,你也可以直接把源文件拿到自己项目中去编译,如日志功能hlog.h
和hlog.c
就可以直接拿去用。
A:以ubuntu下编译arm为例:
Makefile方式:
sudo apt install gcc-arm-linux-gnueabi g++-arm-linux-gnueabi
export CROSS_COMPILE=arm-linux-gnueabi-
./configure
make clean
make libhv
cmake方式:
mkdir build
cd build
cmake .. -DCMAKE_C_COMPILER=arm-linux-gnueabi-gcc -DCMAKE_CXX_COMPILER=arm-linux-gnueabi-g++
cmake --build . --target libhv libhv_static
更多编译平台和编译选项介绍见BUILD.md
A:Windows下编译libhv请先使用cmake
生成VS工程。
附VS各版本下载地址VS2008 ~ VS2019下载地址
cmake官网下载过慢的可以到gitee
下载cmake release
包https://gitee.com/ithewei/cmake-release
cmake不会使用的请自行百度
A:Windows下VS编译最低要求VS2015版本(VS2015要求Update3版本,推荐使用VS2017或更高版本VS),这是因为http模块中使用了一个modern c++ JSON解析库nlohmann::json,该json库使用方法见 https://github.com/nlohmann/json
如果想使用vs低版本编译或只使用c语言的,可以在cmake时关闭使用了c++11
的模块WITH_EVPP
、WITH_HTTP
,只编译base
、event
等c模块,当然你就不能使用http
模块的功能了。
A: Windows下cmake生成vs工程,打开hv.sln
编译后会生成头文件include/hv、静态库lib/hv_static.lib和动态库lib/hv.dll
,所以有动态库和静态库两种链库方式:
方案一:工程-> 属性 -> Linker -> Input -> Addtional Dependencies 加 hv.lib
方案二:代码里添加#pragma comment(lib, "hv.lib")
c/c++
–>预处理器–>预处理器定义中添加HV_STATICLIB
预编译宏,以屏蔽hexport.h
头文件中动态库导入宏#define HV_EXPORT __declspec(dllimport)
CURL_STATICLIB
预编译宏hv_static.lib
或#pragma comment(lib, "hv_static.lib")
A:hlog_disable
:即logger_set_level(hlog, LOG_LEVEL_SILENT)
,设置日志等级为SILENT。
hlog默认输出到当天日志文件,带truncate功能,不用担心长时间运行磁盘不足,记录了libhv内部的关键信息,方便出错时排查,非必要不建议禁用。
A:libhv
中集成了openssl
来支持SSL/TLS
加密通信,通过打开config.mk
或CMakeList.txt
中WITH_OPENSSL
选项,编译即可。
Makefile方式:
./configure --with-openssl
make clean && make && sudo make install
cmake方式:
mkdir build
cd build
cmake .. -DWITH_OPENSSL=ON
cmake --build .
sudo cmake --install .
测试https:
bin/httpd -s restart -d
bin/curl -v http://localhost:8080
bin/curl -v https://localhost:8443
# curl -v https://127.0.0.1:8443 --insecure
https代码示例可以参考examples/http_server_test.cpp中TEST_HTTPS
相关内容
wss代码示例可以参考examples/websocket_server_test.cpp中TEST_WSS
相关内容
当然你也可以用nginx
做https
代理。
A:Windows下请自行下载或编译openssl
,将openssl
头文件include
和库文件lib
放到libhv可搜索路径(如libhv
根目录下include
和lib
)。
附gitee
上Windows openssl
已编译好的https://gitee.com/ithewei/openssl-release.git
(需将libssl.dll.a改名为ssl.lib
,libcrypto.dll.a改名为crypto.lib
)
1、确认是否已集成SSL/TLS
库
以集成openssl
为例,确认方法如下:
ldd libhv.so
,查看动态库依赖项中是否有libssl.so、libcrypto.so
dumpbin /DEPENDENTS hv.dll
或者图形界面工具dependency
查看hssl_backend()
,如打印openssl
则表示使用了openssl
2、连接失败后,日志里查看是否有ssl handshake failed
失败的字样,如有表示开启了SSL,但是握手失败,具体原因可能是对端开启了证书验证,需要调用hssl_ctx_init
输入有效的证书。
接口定义:
typedef struct {
const char* crt_file;
const char* key_file;
const char* ca_file;
const char* ca_path;
short verify_peer;
short endpoint;
} hssl_ctx_init_param_t;
HV_EXPORT hssl_ctx_t hssl_ctx_init(hssl_ctx_init_param_t* param);
调用示例:
hssl_ctx_init_param_t param;
memset(¶m, 0, sizeof(param));
param.crt_file = "cert/server.crt";
param.key_file = "cert/server.key";
if (hssl_ctx_init(¶m) == NULL) {
fprintf(stderr, "hssl_ctx_init failed!\n");
return -20;
}
A:日志里查看是否有websocket no pong
的错误打印,如有表示对方不支持心跳响应,而libhv
的websocket
客户端、服务端都默认开启了应用层心跳检测,即websocket
的PING/PONG
帧,一段时间没收到PONG
帧就会主动断链,可以通过如下方式关闭心跳检测功能来规避。
websocket
客户端:WebSocketClient::setPingInterval(0);
websocket
服务端:WebSocketService::ping_interval = 0;
A:
上传文件:
Content-Type
,如image/jpeg
,将文件内容读入body即可,见HttpMessage::File、requests::uploadFile
接口;Content-Type: multipart/form-data
,见HttpMessage::FormFile、requests::uploadFormFile
接口;下载文件:
document_root
,即可通过url下载该目录下的文件,如wget http://ip:port/path/to/filename
A:编写http服务端,强烈建议通读examples/httpd,里面有你想要的一切
/async
;Handler::setTimeout
;Handler::json
;Handler::form
;Handler::kv
;Handler::restful
// 同步handler: 适用于非阻塞型的快速响应
typedef std::function<int(HttpRequest* req, HttpResponse* resp)> http_sync_handler;
// 异步handler: 适用于耗时处理和响应
typedef std::function<void(const HttpRequestPtr& req, const HttpResponseWriterPtr& writer)> http_async_handler;
// 类似nodejs koa的ctx handler: 兼容以上两种handler的最新写法,可在回调里自己决定同步响应还是异步响应
typedef std::function<int(const HttpContextPtr& ctx)> http_ctx_handler;
因为历史兼容原因,同时保留支持以上三种格式的handler
,用户可根据自己的业务和接口耗时选择合适的handler
,如果使用的较新版libhv
,推荐使用带HttpContext
参数的http_ctx_handler
。
三种handler
的等同写法见:
// 同步handler: 回调函数运行在IO线程
router.POST("/echo", [](HttpRequest* req, HttpResponse* resp) {
resp->content_type = req->content_type;
resp->body = req->body;
return 200;
});
// 异步handler:回调函数运行在hv::async全局线程池
router.POST("/echo", [](const HttpRequestPtr& req, const HttpResponseWriterPtr& writer) {
writer->Begin();
writer->WriteStatus(HTTP_STATUS_OK);
writer->WriteHeader("Content-Type", req->GetHeader("Content-Type"));
writer->WriteBody(req->body);
writer->End();
});
// 带HttpContext参数的handler是兼容同步/异步handler的最新写法,推荐使用
// 回调函数运行在IO线程,可通过hv::async丢到全局线程池处理,或者自己的消费者线程/线程池
// HttpContext里包含了HttpRequest和HttpResponseWriter成员变量,参照nodejs koa提供了一系列操作HttpRequest和HttpResponse的成员函数,写法更加简洁
router.POST("/echo", [](const HttpContextPtr& ctx) {
return ctx->send(ctx->body(), ctx->type());
});
router.POST("/echo", [](const HttpContextPtr& ctx) {
// demo演示丢到hv::async全局线程池处理,实际使用推荐丢到自己的消费者线程/线程池
hv::async([ctx]() {
ctx->send(ctx->body(), ctx->type());
});
return 0;
});
Tips:
std::async
在不同c++运行库下有着不同的实现,有的是线程池,有的就是当场另起一个线程,而且返回值析构时也会阻塞等待,不推荐使用,可以使用hv::async
代替(需要#include “hasync.h”
),可以通过hv::async::startup
配置全局线程池的最小线程数、最大线程数、最大空闲时间,hv::async::cleanup
用于销毁全局线程池;worker_threads
起多线程就可以满足了,不能满足的(并发很高不能容忍阻塞后面请求、handler回调里耗时秒级以上)才考虑将HttpContextPtr
丢到消费者线程池处理;largeFileHandler
,单独起线程循环读文件->发送
,但是要注意做好流量控制,因为磁盘IO总是快于网络IO的,或者对方接受过慢,都会导致发送数据积压在发送缓存里,耗费大量内存,示例里是通过判断WriteBody
返回值调整sleep
睡眠时间从而控制发送速度的,当然你也可以通过ctx->writer->fd()
获取到套接字,设置成阻塞来发;或者设置ctx->writer->onwrite
监听写完成事件统计写数据来决定是否继续发送;或者通过ctx->writer->writeBufsize()
获取当前写缓存积压字节数来决定是否继续发送;chunked
方式,回调里基本流程是Begin -> EndHeaders("Transfer-Encoding", "chunked") -> WriteChunked -> WriteChunked -> ... -> End
A:libhv提供了设置拆包规则接口,c接口见hio_set_unpack
,c++接口见SocketChannel::setUnpack
,支持固定包长、分隔符、头部长度字段
三种常见的拆包方式,调用该接口设置拆包规则后,内部会根据拆包规则处理粘包与分包,保证回调上来的是完整的一包数据,大大节省了上层处理粘包与分包的成本,该接口具体定义如下:
typedef enum {
UNPACK_BY_FIXED_LENGTH = 1, // 根据固定长度拆包
UNPACK_BY_DELIMITER = 2, // 根据分隔符拆包,如常见的“\r\n”
UNPACK_BY_LENGTH_FIELD = 3, // 根据头部长度字段拆包
} unpack_mode_e;
#define DEFAULT_PACKAGE_MAX_LENGTH (1 << 21) // 2M
// UNPACK_BY_DELIMITER
#define PACKAGE_MAX_DELIMITER_BYTES 8
// UNPACK_BY_LENGTH_FIELD
typedef enum {
ENCODE_BY_VARINT = 1, // varint编码
ENCODE_BY_LITTEL_ENDIAN = LITTLE_ENDIAN, // 小端编码
ENCODE_BY_BIG_ENDIAN = BIG_ENDIAN, // 大端编码
} unpack_coding_e;
typedef struct unpack_setting_s {
unpack_mode_e mode; // 拆包模式
unsigned int package_max_length; // 最大包长度限制
// UNPACK_BY_FIXED_LENGTH
unsigned int fixed_length; // 固定包长度
// UNPACK_BY_DELIMITER
unsigned char delimiter[PACKAGE_MAX_DELIMITER_BYTES]; // 分隔符
unsigned short delimiter_bytes; // 分隔符长度
// UNPACK_BY_LENGTH_FIELD
unsigned short body_offset; // body偏移量(即头部长度)real_body_offset = body_offset + varint_bytes - length_field_bytes
unsigned short length_field_offset; // 头部长度字段偏移量
unsigned short length_field_bytes; // 头部长度字段所占字节数
unpack_coding_e length_field_coding; // 头部长度字段编码方式,支持varint、大小端三种编码方式,通常使用大端字节序(即网络字节序)
#ifdef __cplusplus
unpack_setting_s() {
// Recommended setting:
// head = flags:1byte + length:4bytes = 5bytes
mode = UNPACK_BY_LENGTH_FIELD;
package_max_length = DEFAULT_PACKAGE_MAX_LENGTH;
fixed_length = 0;
delimiter_bytes = 0;
body_offset = 5;
length_field_offset = 1;
length_field_bytes = 4;
length_field_coding = ENCODE_BY_BIG_ENDIAN;
}
#endif
} unpack_setting_t;
HV_EXPORT void hio_set_unpack(hio_t* io, unpack_setting_t* setting);
以ftp
为例(分隔符方式)可以这样设置:
unpack_setting_t ftp_unpack_setting;
memset(&ftp_unpack_setting, 0, sizeof(unpack_setting_t));
ftp_unpack_setting.package_max_length = DEFAULT_PACKAGE_MAX_LENGTH;
ftp_unpack_setting.mode = UNPACK_BY_DELIMITER;
ftp_unpack_setting.delimiter[0] = '\r';
ftp_unpack_setting.delimiter[1] = '\n';
ftp_unpack_setting.delimiter_bytes = 2;
以mqtt
为例(头部长度字段方式)可以这样设置:
unpack_setting_t mqtt_unpack_setting = {
.mode = UNPACK_BY_LENGTH_FIELD,
.package_max_length = DEFAULT_PACKAGE_MAX_LENGTH,
.body_offset = 2,
.length_field_offset = 1,
.length_field_bytes = 1,
.length_field_coding = ENCODE_BY_VARINT,
};
具体实现代码在event/unpack.c中,在内部readbuf
的基础上直接原地拆包与组包,基本做到零拷贝,比抛给上层处理更高效,感兴趣的可以研究一下。
具体示例可参考examples/jsonrpc、examples/protorpc
A:
1、c++标准库提取的是所有操作系统的共性,所以它甚至不能像其它语言(没有操作系统包袱,只需要满足主流操作系统)那样提供通用的时间日期操作,也没有提供差异化的锁(自旋锁、读写锁),你可以发现java中锁的类型一大堆,而c++只有一个mutex
;至于没有提供标准网络库,更是c++一直被诟病之处。
2、event
模块是纯c实现的,libevent、libuv
也是如此,底层库使用c++性能有损、库大小、复杂度也会增加,并不会带来编码上的简化。如果只把libhv当作libevent
来使用,关闭WITH_HTTP
选项,是可以做到不依赖stdc++
的。event
模块本身也是封装了各种操作系统的IO多路复用机制(如linux的epoll
、bsd的kqueue
、通用的select、poll
等),提供出了统一的非阻塞IO接口。
3、http模块使用c++的考量,是为了接口使用上的便利性(HttpRequest
、HttpResponse
中使用了map、string
来表示headers、body
,json、form、kv
来存储各种Content-Type
解析后的结构化数据,Get、Set
模板函数屏蔽了int、float、string
之间的类型转化),你如果使用过libevent
的evhttp
就会发现,c写这些会非常痛苦。
4、没有任何贬低或者褒奖c、c++,归根结底它们只是有各自特色的编程语言,只是你实现业务的工具,避其糟粕、用其精华、为你所有,才是其价值。
如果你是写数据库的CRUD
应用,提供http api服务,我也并不推荐使用libhv,使用golang、python、ruby
它不香吗?c++ http
库使用场景可能就是需要将c接口SDK
的算法功能以http api服务的方式提供出去。
A:
c/c++本身是一种支持多编程范式的语言,简单的函数式编程,流行的OOP面向对象编程
、还有c++的GP泛型编程
,也就是模板编程。语言没有谁好谁坏,只有其适用场景,编程范式亦是如此。c with class
我认为恰恰是c++
最精华之处。
所以event模块中将IO、timer、idle
统一抽象成事件,方便放入事件队列中统一调度,也是一种OOP的思想,而http模块中也不是全是class
,也有很多函数式,强行封装成类,反而显得别扭。
而模板编程的核心是使静态类型语言具有动态类型的泛化,STL
就是泛型编程的典范,其提供的容器如vector、list、deque、map、set
、算法如max、min、sort、count、find、search、transform
,应该是每个c++ coder应该熟练掌握的,即使如此,它的源码可读性还是很低,所以没有一定的功底和必要性,不推荐烂用模板编程。