Reqable是使用Flutter
和C++
实现的API测试+调试应用程序,目前已正式发布桌面端版本,不久将来还会支持移动端。今天,给大家带来的是Reqable API请求引擎(网络框架)的实现过程。
1. 需求
在API测试工具这个领域,有非常成熟的产品,例如国外的有Postman
和Hoppscotch
等等,国内的有ApiFox
和ApiPost
等等。这些产品的功能都已经非常得完善,所以我并不打算做一个同质化的功能出来,但是API测试对Reqable
来说又是一个必不可少的功能。所以,我打算另辟蹊径,希望在HTTP协议版本的支持上更胜一筹。
经过调查,知名的API测试工具Postman
至今仍然没有支持HTTP2
协议,详见此问题讨论,可以看到此需求的用户热度很高。如果Reqable能够支持HTTP2
,将会是一个非常棒的亮点。那我为什么不连HTTP3
也一起支持了呢?
技术上与时俱进,这非常符合Reqable先进HTTP生产力
的理念!
2. 技术预研
HTTP2
和HTTP3
对我来说并不是一个陌生的协议。早在2016年,我就已经在正式产品中使用HTTP2
,我对其协议非常地熟悉。HTTP3
则非常的新,它由QUIC
发展而来,其RFC规范rfc9114去年(2022)才正式发布。当然,QUIC
我也曾经做过一段时间的研究,并在移动端的产品中正式上线了。
上面的经历省去了我大量的技术预研和技术选型的时间。完全自己去实现那是不可能的,想起swankjesse(okhttp的开发者)的原话:
I could write a basic implementation of HTTP/1 in about a week.
I could write a basic implementation of HTTP/2 in about a month.
I fear HTTP/3 is larger still; maybe 6 months of work? Not having TCP as a baseline means there’s a lot more to do in timeouts, retransmissions, recovering from out-of-order delivery, and other things we’ve been able to take advantage of to date. The upside is also less clear! It might be slower than HTTP/2, especially early on as integrations with UDP and TLSv1.3 are optimized.
事实上确实如此,HTTP3
的协议复杂度导致根本不可能短时间内实现。Reqable是基于Flutter
开发的,使用dart语言重新实现一遍HTTP3
基本是痴人说梦,唯一的方案就是只有使用外部库进行接入了。
根据我之前的项目经验,当时可靠的方案有两个:curl
vs Cronet
,最终我选择了 Cronet
,下面分别来说下两个方案的优劣。
curl
是一个非常老牌的网络库了,curl
已经内置到大部分系统中,例如MacOS、Linux等。非常多常用的软件也都是基于curl
发送网络请求,例如Git
、Unity
等等。另外,curl
很早就开始支持HTTP3
的草案版本了,但是至今仍然处于实验功能阶段,详见这里。我曾经在2021年,编译并测试过curl
的HTTP3
协议版本,测试结果让我迅速放弃了它。
Cronet
是chromium
项目的一个子模块,源码详见这里。Cronet
本身并不是HTTP
众多版本协议的实现者(真正的实现在这个net目录),只是对net
目录代码封装,而net
才是整个chromium
的网络核心。当然,Cronet
的好处是作为一个模块,可以编译出单独的模块制品,另外API设计得也更加地简单。据我之前了解,B站、字节系等相关产品都采用了Cronet
的方案,但大多数是移动端。也许是因为Google
本身的大量移动端产品使用了此方案,另外,Cronet
还提供了Java
等移动端语言的接口和成品库。
Reqable的规划是先出桌面端版本,但Cronet
并没有提供桌面端的成品库,只好自动动手了哎。
3. 框架实现
下面,讲下Reqable
网络框架的完整实现过程,包括Cronet
的编译、定制修改、Flutter接入等步骤。
3.1 准备Cronet
首先阅读下chromium
项目编译指引,检查下各个桌面端平台下必要的开发工具和环境变量。例如Windows需要安装VS(不是VS Code),去微软官网下载即可,但是不建议选择最新的版本,编译工具链不一定支持(一般选择VS最新前一年的版本比较好)。
具体的项目配置过程不多讲,准备好必要的高速梯子和充足时间并认真阅读文档即可。完整命令如下:
mkdir chromium && cd chromium
fetch --nohooks --no-history chromium
gclient runhooks
# 114.0.5735.199是tag名,可以自行根据需求修改
cd src && git fetch origin +refs/tags/114.0.5735.199:reqable-114.0.5735.199 --depth 1
git checkout -b reqable-114.0.5735.199
cd .. && gclient sync --no-history
下面简单说一些小技巧。
由于编译Cronet
需要克隆完整的chromium
项目,为了速度和节省磁盘空间考虑,只需要选定一个提交节点(或者tag)。可以打开这个网站,选择相应平台下的某个Stable
版本,例如这里的114.0.5735.199
版本:
git fetch origin +refs/tags/114.0.5735.199:reqable-114.0.5735.199 --depth 1
由于仓库是托管在Google
自己的服务器上,即使拉取单个节点仍然可能很慢。我们可以使用Github
的镜像仓库:github.com/chromium/ch… ,修改项目根目录下的.gclient
文件中的url
字段:
solutions = [
{ "name" : 'src',
"url" : 'https://github.com/chromium/chromium.git',
"deps_file" : 'DEPS',
"managed" : False,
"custom_deps" : {
},
"custom_vars": {},
},
]
.gclient
文件会在第一个运行fetch
命令的时候创建,如果没有的话把上面内容复制新建一个即可。
3.2 编译Cronet
编译命令非常简单,只需要两行命令:
gn args out/arm64
然后在打开的文本编辑器中填入下面的内容:
is_debug = false
is_component_build =false
is_debug
用于控制Debug和Release版本,这一步我们还可以加上cpu架构配置交叉编译:
target_cpu="x64"
配置完成后,输入下面开始编译:
ninja -C out/arm64 Cronet
编译完成后,我们可以在out/arm64
目录下面找到编译出来的制品了。
3.3 定制Cronet
上一步编译出来的Cronet
库基本是可以使用的,但是有些功能Cronet
是没有实现或者默认实现有问题的,就需要我进行一些修改。
a. 修改库名
Cronet
编译出来的默认库的名称是包含版本号的,需要改成我们自己的名称。修改components/cronet
目录下的BUILD.gn
文件中的_cronet_shared_lib_name
的值:
_cronet_shared_lib_name = "reqable_cronet"
b. 禁用HTTP自动转HTTPS机制
在Chrome浏览器中,我们在地址栏输入http://www.baidu.com
之后,浏览器会自动转成https://www.baidu.com
。浏览器直接构造了一个重定向的响应数据,自动去请求HTTPS地址,发给服务器的请求也是HTTPS请求。这个机制对于API测试工具来讲是非常不合适的,可以采用下面的方法禁用掉此功能。
可以在gn args out/arm64
这一步命令后填入下面一行配置:
include_transport_security_state_preload_list = false
c. 移除端口限制
Cronet
对一些端口进行了限制,导致请求无法发送,例如10080
端口等。这个是写在代码里的,只有通过修改下面的代码文件来实现:
src/net/base/port_util.cc
d. 添加自定义代理支持
Cronet
库不支持配置自定义代理,默认使用系统配置的代理,这一点就非常不友好了。Reqable
中有个非常重要的API功能跟随调试
,可以直接将API请求连接Reqable
的代理服务器进行流量分析。另外,还需要能支持配置其他代理服务器,或者禁用任何代理,如果都需要通过配置系统代理来实现体验就非常反人类了。而这个功能真的只能自己加接口来实现了。
这里就不得不提chromium
里面的idl
模板机制了,Cronet
库的C/C++接口头文件都是通过idl
模板通过脚本自动生成的。修改C/C++接口,需要通过修改idl
模板来实现,不熟悉这个流程的容易走弯路,编码倒是很简单。
3.4 接入Cronet
Dart
语言可以通过FFI
来接入C/C++库,接入Cronet
库也是通过这种方式。但是由于Cronet
库的头文件接口相对比较复杂,另外我也希望为以后提供更好的扩展,所以决定对Cronet
库进行封装。提供统一的实现接口,将Cronet
作为其中的一种实现,未来可以接入curl
等其他库的实现。
如上图所示,我提供了统一的http_api.h
头文件作为对外接口,然后通过FFI
进行接入,对外隐藏Cronet
的实现。
3.5 FFI配置
我们需要通过FFI
生成Dart
语言接口来接入C/C++库。在项目根目录下新建ffi.yaml
文件,配置好头headers等参数,如下:
name: 'NativeReqableHttp'
description: 'Bindings to reqable http library'
output: 'lib/src/gen/_generated_bindings.dart'
headers:
entry-points:
- 'src/http_api.h'
include-directives:
- 'src/*.h'
- 'src/utils/*.h'
- 'src/cronet/*.h'
compiler-opts:
- '-Ithird_party/cronet/include'
- '-Ithird_party/dart-sdk/include'
- '-DDART_SHARED_LIB'
然后,执行下面的命令生成Dart
语言接口:
dart run ffigen --config ffi.yaml
现在来看这一步其实很简单了,之前还需要安装和配置llvm
,还好后来更新不需要了。
3.6 Dart实现
接下来就是Dart
语言层面的实现了,相对来说简单了很多,主要的精力都是花在了API的设计上面。我们可以通过下面的请求,发送一个HTTP3
协议的请求。
final ReqableHttp client = ReqableHttpBuilder()
.protocol(HttpProtocol.http3)
.build();
final HttpResponse response = await client.post('https://quic.aiortc.org/httpbin/post', "hello");
此外,还提供了拦截器的接口:
abstract class HttpInterceptor {
Future<HttpResponse> intercept(Chain chain);
}
Reqable
中对于Cookie
的处理逻辑就是基于拦截器来实现的。
3.7 单元测试
为了保证整个框架的鲁棒性,搭建了Mock Server
以及编写了大量的单元测试,来保证框架的稳定性,结果证明Cronet
库非常地稳定。
3.8 实测效果
网络框架开发完成后,需要在Reqable
项目中进行实测。curl
库的作者整理了一些HTTP3
的测试服务器地址(貌似大部分都失效了):bagder.github.io/HTTP3-test/
结语
欢迎各位阅读!也欢迎大家来下载和体验 Reqable-先进HTTP生产力工具,也欢迎与我一起交流更多Flutter的开发经验和心得!