Reqable项目日志:Flutter如何实现一个支持HTTP3的网络框架

1,755 阅读9分钟

Reqable是使用FlutterC++实现的API测试+调试应用程序,目前已正式发布桌面端版本,不久将来还会支持移动端。今天,给大家带来的是Reqable API请求引擎(网络框架)的实现过程。

1. 需求

在API测试工具这个领域,有非常成熟的产品,例如国外的有PostmanHoppscotch等等,国内的有ApiFoxApiPost等等。这些产品的功能都已经非常得完善,所以我并不打算做一个同质化的功能出来,但是API测试对Reqable来说又是一个必不可少的功能。所以,我打算另辟蹊径,希望在HTTP协议版本的支持上更胜一筹。

screenshot_01.png

经过调查,知名的API测试工具Postman至今仍然没有支持HTTP2协议,详见此问题讨论,可以看到此需求的用户热度很高。如果Reqable能够支持HTTP2,将会是一个非常棒的亮点。那我为什么不连HTTP3也一起支持了呢?

技术上与时俱进,这非常符合Reqable先进HTTP生产力的理念!

2. 技术预研

HTTP2HTTP3对我来说并不是一个陌生的协议。早在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发送网络请求,例如GitUnity等等。另外,curl很早就开始支持HTTP3的草案版本了,但是至今仍然处于实验功能阶段,详见这里。我曾经在2021年,编译并测试过curlHTTP3协议版本,测试结果让我迅速放弃了它。

Cronetchromium项目的一个子模块,源码详见这里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等其他库的实现。

screenshot_02.png

如上图所示,我提供了统一的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/

screenshot_03.png

结语

欢迎各位阅读!也欢迎大家来下载和体验 Reqable-先进HTTP生产力工具,也欢迎与我一起交流更多Flutter的开发经验和心得!