libfv:基于 C++20 的异步 HTTP 库

2022-04-23 20:52:46 +08:00
 fawdlstty

仓库地址:https://github.com/fawdlstty/libfv

介绍一款船新 HTTP 库。C++的 HTTP 库很多,但基于 C++20 的异步网络 HTTP 库几乎没有。我没找到好用的,因此写了一个。在讲解这个库之前,我先说说为什么我们需要这样的库。

C++ HTTP 库有两种主要的实现方式,第一种是同步 HTTP 网络访问,比如这样的代码:

// 伪代码
Response _r = HttpGet ("https://t.cn");
std::cout << _t.text;

这样的代码写起来很简单,但它存在一个问题:HTTP 网络访问比较耗时,可能需要几百毫秒,这么长时间,这个线程将阻塞在这里,比较消耗线程资源。假如遇到需要同时发起几十、几百个请求,将较大消耗系统资源。很显然,它不是一个较好的设计。

第二种是回调通知,比如这样的代码:

// 伪代码
HttpGet ("https://t.cn", [] (Response _r) {
	std::cout << _t.text;
});

这种方式解决了线程问题,也就是,几十、几百个请求可以同时发起,只需要极少量或者一个线程就行,HTTP 库内部实现了请求的内部管理,在收到请求的回复后,调用回调函数,从而实现请求的高效处理。但这种方式有个问题,假如我们需要根据请求结果内容转给下一个请求,这会带来一个回调地狱问题,比如这样的代码:

// 伪代码
HttpGet ("https://t.cn", [] (Response _r) {
    HttpGet (_t.text, [] (Response _r) {
        HttpGet (_t.text, [] (Response _r) {
            HttpGet (_t.text, [] (Response _r) {
                HttpGet (_t.text, [] (Response _r) {
                    std::cout << _t.text;
                });
            });
        });
    });
});

那么,有没更好的处理方式呢?有,通过 C++20 的 co_await 实现异步等待。下面给出 libfv 的发起请求的代码:

fv::Response _r = co_await fv::Get ("https://t.cn");

一方面它能获得回调方式的好处,也就是少量线程支撑同时大量的请求任务,同时它不会带来回调地狱问题。上面的代码通过 libfv 实现,代码可以这样写:

fv::Response _r = co_await fv::Get ("https://t.cn");
_r = co_await fv::Get (_r.text);
_r = co_await fv::Get (_r.text);
_r = co_await fv::Get (_r.text);
_r = co_await fv::Get (_r.text);
std::cout << _t.text;

这儿特别说明一下。单 CPU 处理效率来说,C++20 的异步性能比回调要低,大概 10%左右,也就是假设理论上跑满网络 IO 带宽情况,回调需要 10%的 CPU ,那么使用 C++20 的异步需要 11%,这是 stackless 需要付出的代价。当然,在我看来这个特性完全可以忽略,毕竟 IO 密集型应用首先需要考虑的是跑满网络带宽,一般不太需要关注 CPU 使用率。

libfv 使用方法见仓库:https://github.com/fawdlstty/libfv

4455 次点击
所在节点    C++
61 条回复
fawdlstty
2022-04-24 10:21:11 +08:00
@ysc3839 其实所有语言的 stackless 都是一样的,实现都是状态机+线程池管理异步任务,区别只有底层实现的代码不同、用起来给人感觉不同。对其他语言来说,我不是深度体验用户(因此对这个议题具有比较强的主观看法),不过你可以参考我以前写的一个库的源码,https://github.com/fawdlstty/SMLite ,里面有 js 和 python 的异步实现( c 艹的还没写进去)
ysc3839
2022-04-24 10:23:56 +08:00
@fawdlstty co_await 不一定要配合线程池用,可以当成回调函数来用,可以配合事件循环。但是事件循环需要跑在一个线程上,侵入性较大,复杂度也高。所以我认为要求不高的情况下用线程池+同步的 http 库会更简单,同时配合 coroutine 可以解决回调地狱的问题。
forcecharlie
2022-04-24 10:24:11 +08:00
有没有一种可能,C++ 标准在网络这块不给力,大家都慢慢少用 C++ 开发网络程序了,比如可以使用 Golang/Rust 。

C++ 网络标准就是一群人的零和博弈,互不相让,最后一拖再拖。

免责声明:个人意见,并且本人在开源项目中大量使用 C++。
ysc3839
2022-04-24 10:30:36 +08:00
@fawdlstty 不同语言的 stackless coroutine 不完全一样,比如我前面提到的 Python ,不能实现 C++ 这种“由被 await 的对象控制恢复执行”,必须要一个事件循环,由事件循环来控制恢复执行。js 和 C++的有点类似,也可以由被 await 方恢复执行。C++是给被 await 方一个 coroutine handle ,“调用”这个 handle 恢复执行,而 js 是让被 await 方返回一个 Promise ,通过 Promise 这个中介来恢复执行。
fawdlstty
2022-04-24 10:36:09 +08:00
@forcecharlie 截止 c 艹 20 前没任何标准,这也是 c 艹标准委员会效率低下的体现。但大家并没减少 c 艹开发网络程序,都开始尝试用三方库去做网络。go 和 c 艹不完全对标,没有可比性。rust 的话,相比 c 艹 03 及以前具有极大优势,但对于 c 艹 20 及以后版本,难说。
fawdlstty
2022-04-24 10:39:42 +08:00
@ysc3839 所以这就是实现方式的不同咯。实际用户体验一致,比如 c 艹异步代码迁移到 python ,需要做的也是语法变为 python ,co_await 改为 await ,不会说换一种逻辑去实现
tulongtou
2022-04-24 10:53:28 +08:00
@fawdlstty 没有 libav 么?
ysc3839
2022-04-24 10:55:20 +08:00
@fawdlstty C++或 js 的 coroutine 不能直接迁移到 Python 。比如有个函数要求传递回调函数进去,js 可以用 Promise 转为在 async function 中 await ,C++也可以这么做。但是 Python 不行,Python 最多只能用 generator ,在回调函数中调用 next(generator)可以恢复执行。
fawdlstty
2022-04-24 11:08:54 +08:00
@ysc3839 c 艹、c#、python 回调转异步做法是,创建原子信号量然后异步等待,同步回调里设置信号。异步等待任务收到信号后恢复执行
ysc3839
2022-04-24 12:18:56 +08:00
@fawdlstty 并不是这样,C++ co_await 一个 awaitable 对象时,会调用 awaitable 对象中的 await_suspend() 函数,并传递 coroutine handle ,当需要恢复执行时,只需要调用 coroutine handle 的 resume() 函数即可恢复执行。此处 coroutine handle 就类似一个回调函数。
fawdlstty
2022-04-24 13:38:17 +08:00
@ysc3839 你说的是底下一层,这层的做法不兼容;我说的是顶上一层,这层的做法就兼容。让我选做法我会选择最简单通用的方式。
hankai17
2022-04-24 14:58:51 +08:00
关注了 问两个性能问题
1. c++20 的协程切换需要多久?
2. 协程调度器效率问题 从一个协程 await 保存上下文 到 resume 恢复上下文 需要多久?
fawdlstty
2022-04-24 15:06:54 +08:00
@hankai17 很难回答。使用不同品牌 cpu 、编译为不同指令集、不同编译器的不同优化等级,估计对这个都会有影响。你可以看看这个,对原理说的很详细。http://purecpp.org/detail?id=2288 。我个人看法,效率就是 O(1),因此忽略性能问题(狗头
wanguorui123
2022-04-24 16:39:53 +08:00
C 艹 没有 C 井 的 await ?如何用?
Calatrava
2022-04-24 16:40:54 +08:00
@fawdlstty dns 信息如果需要用户传进去就不太好用了呀,用户要维护很复杂的数据。而且,谁来做 dns 解析呢?你好像也没有把每个请求访问的 IP 地址传给用户,用户还要再搞一套异步 dns+dns 缓存。C++20 的开发者不能只局限于写 demo 啊。
fawdlstty
2022-04-24 16:42:17 +08:00
@wanguorui123 c 艹 20 有 co_await
fawdlstty
2022-04-24 16:44:41 +08:00
@Calatrava 你说的有道理,不过你见过哪个流行的库有做 dns 缓存的嘛?这个需求非常个性化,只能用户自己处理。libcurl 、asio 、libev/uv/hv 等等不都没做。不做这个不代表局限于 demo ,而是给用户自由发挥流有余地
Calatrava
2022-04-24 16:45:38 +08:00
FrankHB
2022-04-24 16:54:24 +08:00
@fawdlstty 我说 C++硬塞半成品进去不针对 co_xxx ,而是适用于这些年来各种已有的主要特性的迭代升级。
(从 C++20 开始这坑特别集中。)
比如 lambda ,C++20 调过 this 的 capture 还加了 deprecation ,于是一些 capture 不写全永远别想兼容 C++11 onwards ,要么#ifdef ,默认 capture 这功能约等于没有。
再如 char8_t ,变更了类型直接搞得想要兼容的 u8 string literal 没法写。
就算只用最新版本的嘛……有些可预见的东西是残的。比如 strcuctrual binding 还缺 nested match 。是不是会顺便在补全功能的同时进来一丢 breaking compatibility 的东西进来,到时候还真没底。
这种不会一步到位倒是也挺符合微软特色的(都不限于 C 艹,e.g. v2ex/t/845526#r_11546877 ;硬点的话,WG21 浮躁的“敏捷”发半吊子版的歪风一部分就得归功于微软)。
结果……这不就是等等党永不为奴么。(就算 C++98 太残太恶心了,C++11/14 大多数用户还是能凑数的。)

C++23 的 co_xxx 补充主要是 std 内部的东西,你既然都能积极从头造库的轮子了,和这里的关系反而不大。我说的 co_xxxx 的半吊子,主要是指 xxxx 本身理论上就有的局限性,而不是 C++里 co_这样的具体语法设计。
如果你看懂了我提到的一些文献就不应该容易有这个理解偏差。不过如果你只是用过“工业语言”特别是 async 类似物,因为糊 IO 库的主力确实也就是这些圈子在倒腾,那我倒是可以理解你这样想的原因(不过俺寻思 Lua 也不咋学术啊……)。

至于 asio ,你更该关心的是为什么推了那么多年还没进去,半路杀出来的 executor 又难产了。(当然这主要还 WG21 的原因。)既然 chriskohlhoff 那么高效热脸怼冷屁股的作风都推不大动,就更别指望别人了。
另外,基于 asio 的 http 库这题材实在不新鲜了。虽然我没怎么关心,比如 beast 这种,没在这里跟进么?
考虑这点,想要用你的库,就得长个心眼关心作为作者你和这些更大众得项目的维护持续性的差距了,这也是你要推广时需要考虑的点(当然开源嘛,基调都是爱用用不用润,所以不关心这个也无妨)。虽然这根本上不是技术问题,很遗憾,大多数用户现实能体验到的差异就是在这里。
fawdlstty
2022-04-24 17:10:36 +08:00
@FrankHB 1 、你上面说的是对的,所以 u8string 之类的难用或者不稳定的特性我都没用到,新特性里我只用到了 co_await
2 、beast 不好用,我是想一行 co_await 就能发起请求,beast 只能说,挺规范的
3 、作为个人开发者我没法保证任何情况都能积极维护。用的人少就不说了,用的人多了,像 duilib 那样,就算作者不维护其实也能开枝散叶

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/848811

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX