HTTP/3在HTTP/2的基础上,增强了安全上的限制,且使用UDP传输降低丢包导致的头部阻塞、降低因为TCP的协议限制而导致的连接耗时高等问题,但是目前各大浏览器的支持范围不够广,暂时不建议在网页相关的服务上进行升级。但是其提高了传输效率,有必要在传输数据量较大的应用上进行升级,建议对HTTP/3支持的改造设计与研究,在规范成熟时发布支持HTTP/3协议的版本。

> 前期在调研quic选型中,选择了Cronet作为客户端访问quic协议的网络库。为了方便现有项目中能快速的支持quic网络协议,下面会对比OkHttp与Cronet网络库的使用区别。

## 一、不同网络库的使用方法差异

### 1.1. OkHttp使用方法

> 支持http1,http2; 使用广泛方便 ,可定义拦截器添加业务逻辑,支持同步和异步使用执行。与Retrofit结合定义api,可理解性强。

```

val builder: OkHttpClient.Builder = OkHttpClient.Builder()

builder.addInterceptor(LoggingInterceptor())

val client = builder.build()

var url = "http://www.baidu.com/"

// 可设置发送数据:.post(RequestBody())

val request: Request = Request.Builder().url(url).build()

val call = client.newCall(request)

call.enqueue(object : Callback {

override fun onFailure(call: Call, e: IOException) {

Log.d(TAG, "onFailure response: $e")

}

override fun onResponse(call: Call, response: Response) {

Log.d(TAG, "response: $response")

val bodyContent = response.body()?.string()

Log.d(TAG, "response body: $bodyContent")

}

})

```

### 1.2. Cronet使用方法

> 支持http1,http2,http3; 使用方法与OkHttp不同,只能异步调用,不使用Stream方式发送数据和接收数据。需要自定义数据断处理,各种超时逻辑,异步处理。不方便与Retrofit等结合使用。如下。

```

val myBuilder = CronetEngine.Builder(context)

val cronetEngine: CronetEngine = myBuilder.build()

// 创建一个请求

val requestBuilder = cronetEngine.newUrlRequestBuilder(

"https://www.example.com",

MyUrlRequestCallback(),

executor

)

// 可设置发送的body数据: requestBuilder.setUploadDataProvider(dataProvider, executor)

val request: UrlRequest = requestBuilder.build()

// 开始请求, 在callback中处理数据重定向,接收数据,错误处理。

request.start()

//定义一个请求回调类

class MyUrlRequestCallback : UrlRequest.Callback() {

override fun onRedirectReceived(request: UrlRequest?, info: UrlResponseInfo?, newLocationUrl: String?) {

// 决定是否重定向

request?.followRedirect()

}

override fun onResponseStarted(request: UrlRequest?, info: UrlResponseInfo?) {

// 收到回复开始,读取状态码和头部,提供接收的缓冲区

request?.read(ByteBuffer.allocateDirect(102400))

}

override fun onReadCompleted(request: UrlRequest?, info: UrlResponseInfo?, byteBuffer: ByteBuffer?) {

// 收到一段body数据,会回调多次

request?.read(ByteBuffer.allocateDirect(102400))

}

override fun onSucceeded(request: UrlRequest?, info: UrlResponseInfo?) {

Log.i(TAG, "onSucceeded method called.")

}

}

```

## 二、项目现状

### 2.1. 现在项目中使用方式修改

现在项目中,使用网络请求模式大部分是使用OkHttp,直接使用或者与Retrofit结合使用。还有少部分共用的api使用HttpClient。

需要进行的修改有:

> 所有OkHttp网络请求修改为Cronet的请求方式。

存在以下问题:

+ 代码改动大。请求方式变化较大并且读写操作,异常处理等方式比较原始,不易操作。

+ 不易回滚。修改完成后如果想要快速回滚到原来的方式,也同样面临麻烦。

+ 无法使用原有的Interceptor业务逻辑和Retrofit接口定义功能

因此此方法可行性不高,下面会考虑在OkHttp的拦截器方式接入Cronet。

### 2.2 在OkHttp拦截器中快速接入

参考网易分享的在OkHttp的拦截器中接收Cronet,经实际操作,有部分可行性。在最后一个业务Interceptor中添加一个拦截器转接到Cronet来进行请求,主要代码示例如下:

参考 https://github.com/akshetpandey/react-native-cronet/blob/master/android/src/main/java/com/akshetpandey/rncronet/RNCronetInterceptor.java

```

class RNCronetInterceptor implements okhttp3.Interceptor {

@Override

public Response intercept(Chain chain) throws IOException {

if (RNCronetNetworkingModule.cronetEngine() != null) {

return proceedWithCronet(chain.request(), chain.call());

} else {

return chain.proceed(chain.request());

}

}

private Response proceedWithCronet(Request request, Call call) throws IOException {

RNCronetUrlRequestCallback callback = new RNCronetUrlRequestCallback(request, call);

UrlRequest urlRequest = RNCronetNetworkingModule.buildRequest(request, callback);

urlRequest.start();

return callback.waitForDone();

}

}

static UrlRequest buildRequest(Request request, UrlRequest.Callback callback) throws IOException {

String url = request.url().toString();

UrlRequest.Builder requestBuilder = cronetEngine.newUrlRequestBuilder(url, callback, executorService);

requestBuilder.setHttpMethod(request.method());

Headers headers = request.headers();

for (int i = 0; i < headers.size(); i += 1) {

requestBuilder.addHeader(headers.name(i), headers.value(i));

}

RequestBody requestBody = request.body();

if (requestBody != null) {

MediaType contentType = requestBody.contentType();

if (contentType != null) {

requestBuilder.addHeader("Content-Type", contentType.toString());

}

Buffer buffer = new Buffer();

requestBody.writeTo(buffer);

requestBuilder.setUploadDataProvider(UploadDataProviders.create(buffer.readByteArray()), executorService);

}

return requestBuilder.build();

}

```

> 上面这种做法,对于一些数据量比较小的请求和回复没有问题。但是其中有明显的缺点,就是需要把请求的body数据全部构造出来,设置到Cronet的DataProvider中;读取回复时,也是同样的等所有数据接收完成到内存中时,才构造Response对象返回给okhttp的调用链。

也就是没有实现数据的流式传输,也没有实现请求超时,异常等情况的对接。

这样,对于数据量小问题不明显,但是对于一些大文件上传,下载等,无法达到内存和效率要求。作为app基层的网络请求模块也不能依赖于上层应用的数据量和使用方式。

> 但是通过okhttp拦截器的接入Cronet给我们提供了思路。如果我们使用Cronet实现了OkHttp的拦截器,数据流式处理,网络超时参数的逻辑,异常与OkHttp对接,事件回调对接,就相当于实现了一个“继承”OkHttp的子类网络库,也能通过简单的参数实现2个网络库的快速切换。

## 三、解决方案

### 3.1 自定义网络通信组件MdHttpClient接口考虑

1. 底层使用Cronet和OkHttp实现,接口尽量跟OkHttp接口兼容,可在项目中快速接入使用。

2. 新的HttpClient需要实现Call.Factory接口,方便Retrofit框架结合使用。

3. 使用OkHttp时,可通过简单封装连接起来实现。

缺点:不支持http3.0, quic协议。

4. 使用Cronet时,需要实现OkHttp的Request和Response流式数据发送接收接口,实现拦截器模式接口。

缺点:需要自己实现流式数据接口。代理,dns等功能在Cronet暂无接口可使用。

### 3.2 网络通信组件MdHttpClient接口设计

```

MdHttpClient

Call newCall(Request request)

MdHttpClient.Builder

// 指定使用Cronet或者OkHttp,如开启http3则只能使用cronet

Builder useNetCore(cronet/okhttp)

// 开启则只能使用cronet, 未开启默认使用okHttp

Builder enableHttp3(boolean)

Builder enableHttp2(boolean)

Builder connectTimeout(long timeout, TimeUnit unit)

Builder readTimeout(long timeout, TimeUnit unit)

Builder writeTimeout(long timeout, TimeUnit unit)

Builder callTimeout(long timeout, TimeUnit unit)

Builder retryOnConnectionFailure(boolean retryOnConnectionFailure)

Builder addInterceptor(Interceptor)

Builder addNetworkInterceptor(Interceptor)

Builder followRedirects(boolean followRedirects)

Builder followSslRedirects(boolean followProtocolRedirects)

Builder eventListener(EventListener eventListener)

Builder dispatcher(Dispatcher dispatcher)

// dns仅在OkHttp时生效

Builder dns(Dns dns)

// 代理仅在OkHttp时有效

Builder proxy(@Nullable Proxy proxy)

Builder proxyAuthenticator(Authenticator proxyAuthenticator)

MdHttpClient build()

```

### 3.3 实现要点

1. 实现相同接口,快速替换不同实现

在 MdHttpClient.Builder 在调用build方法时,判断当前使用的底层库,生成对应的CallFactory对象。

- 如果为使用OkHttp,在内部新建一个 OkHttpClient对象,在newCall时直接使用OkHttp.newCall,不需要过多处理。

- 如果为使用Cronet,则创建一个 CronetClient, newCall时创建自定义的 CronetRealCall

保存使用 Dispatcher作为异步线程调度器,需要用到其中的executorService。

2. 实现Okhttp调用链

参考OkHttp的实现方式,需要新建一个CronetRealCall。

3. 实现数据发送接收流式对接

需要实现一个可堵塞的缓冲区BlockableBuffer,发送数据时,如果缓冲区已满,则堵塞等待;读取数据时,如果缓冲区为空,则堵塞等待。

4. 实现超时,异常处理

BlockableBuffer实现了Sink,Source接口,然后通过okhttp的Timeout包装成TimerSink, TimerSource。在read/write等待超过一定时间,则抛出超时异常。

5. 请求中断

调用cronet的cancel接口,如果BlockableBuffer在堵塞中,就使用ConditionVariable通知取消堵塞,抛出Cancell异常。

类设计图:

![avatar]

### 3.4 发布,支持快速接入

使用方式与OkHttp一致,在创建Builder和OkHttpClient对象时,需要修改为MdHttpClient,后续不需要改动。可以配置支持的协议(如quic)等。

```

/**

* 使用Cronet进行post请求,发送大数据接收大数据,不中断

*/

fun getWithCronetCoreBigReqBigRes() {

var bodyContent = "Hello World!"+...自定义body内容

Log.i(TAG, "send body Length: ${bodyContent.length}")

val builder: MdHttpClient.Builder = MdHttpClient.Builder()

builder.useNetCore(MdHttpClient.NetCore.Cronet)

builder.addInterceptor(LoggingInterceptor())

val client = builder.build()

var url = "http://api.wps.cn/getBigFile?page=1&count=5"

val request: Request =

Request.Builder().url(url)

.post(MyRequestBody(bodyContent))

.build()

val call = client.newCall(request)

call.enqueue(object : Callback {

override fun onFailure(call: Call, e: IOException) {

Log.d(TAG, "onFailure response: $e")

e.printStackTrace()

}

override fun onResponse(call: Call, response: Response) {

val bodyLength: Long = response.body()?.contentLength()!!

Log.d(TAG, "response: $response, bodyLen: $bodyLength")

ResponseConsumer(response).consume()

}

})

}

```

与Retrofic结合使用,跟OkHttp使用几乎一样。其中设置client要修改为设置callFactory:

```

/**

* 获取OpenApi接口对象

*/

fun getOpenApi():OpenApi {

/**

* 创建Client对象,与OkHttpClient类似,此对象可重复使用,节省资源消耗

*/

if (mdHttpClient == null) {

val builder: MdHttpClient.Builder = MdHttpClient.Builder()

builder.useNetCore(MdHttpClient.NetCore.Cronet)

builder.callTimeout(60000, TimeUnit.MILLISECONDS)

builder.readTimeout(15000, TimeUnit.MILLISECONDS)

builder.writeTimeout(15000, TimeUnit.MILLISECONDS)

builder.addInterceptor(LoggingInterceptor())

mdHttpClient = builder.build()

}

val retrofit:Retrofit = Retrofit.Builder()

.baseUrl("http://cloud.wps.cn/")

.callFactory(mdHttpClient!!)

.addConverterFactory(GsonConverterFactory.create()) //设置数据解析器

.addCallAdapterFactory(RxJava2CallAdapterFactory.create()) // 支持rxjava

.build()

return retrofit.create(OpenApi::class.java)

}

```

### 3.5 快速接入及切换网络库

在创建MdHttpClient时,可以通过参数配置使用的网络库,可以选择 OkHttp或者Cronet 库作为底层支持库。

```

val builder: MdHttpClient.Builder = service!!.newBuilder()

builder.useNetCore(MdHttpClient.NetCore.Cronet)

// 或者 builder.useNetCore(MdHttpClient.NetCore.OkHttp)

```

> 更多使用示例,请参考 EXAMPLE.md

#### 注意如果使用了NetCore.Cronet时,有一些区别的地方:

1) 使用Cronet支持库时,不允许设置Header:Accept-Encoding,否则它会提示并抛异常:

It's not necessary to set Accept-Encoding on requests - cronet will do this automatically for you, and setting it yourself has no effect. See https://crbug.com/581399 for details.

2) 在NetworkInterceptor中,无法获取Connection的详细信息(如IP,端口等),因为连接是由Cronet内部来执行,并未向调用者提供连接的信息。

## 四、后续优化方向

1. 调研dns实现方式

2. 持续更新cronet对更多协议的支持

现已支持quic Q050以下协议版本;编译新版cronet源码,支持更多quic协议版本以及http3草案协议。

从OkHttp引入Cronet支持quic协议相关推荐

  1. 实战|QUIC协议助力腾讯业务提速30%

    hi ,大家周五好,之前分享过一篇QUIC在蚂蚁金服落地的文章: 实战|QUIC协议在蚂蚁集团落地 今天我们分析一篇,QUIC在腾讯落地的文章,希望大家了解和学习新技术是如何在大厂落地,其中会遇到什么 ...

  2. QUIC协议初探-iOS实践

    本文来自于腾讯Bugly公众号(weixinBugly), 作者:emilymmwang,未经作者同意,请勿转载,原文地址:https://mp.weixin.qq.com/s/NbewZ1NU49q ...

  3. QUIC 协议初探 - iOS 实践

    本文来自于腾讯 Bugly 公众号(weixinBugly), 作者:emilymmwang,未经作者同意,请勿转载,原文地址:https://mp.weixin.qq.com/s/NbewZ1NU4 ...

  4. E百科 | 第2期 扒一扒能加速互联网的QUIC协议

    简介: 众所周知,QUIC(Quick UDP Internet Connection)是谷歌制定的一种互联网传输层协议,它基于UDP传输层协议,同时兼具TCP.TLS.HTTP/2等协议的可靠性与安 ...

  5. 扒一扒能加速互联网的QUIC协议

    简介:众所周知,QUIC(Quick UDP Internet Connection)是谷歌制定的一种互联网传输层协议,它基于UDP传输层协议,同时兼具TCP.TLS.HTTP/2等协议的可靠性与安全 ...

  6. 科普:QUIC协议原理分析

    作者介绍:lancelot,腾讯资深研发工程师.目前主要负责腾讯 stgw(腾讯安全云网关)的相关工作,整体推进腾讯内部及腾讯公有云,混合云的七层负载均衡及全站 HTTPS 接入.对 HTTPS,SP ...

  7. QUIC 协议是如何在蚂蚁集团落地的?

    点击上方"芋道源码",选择"设为星标" 管她前浪,还是后浪? 能浪的浪,才是好浪! 每天 10:33 更新文章,每天掉亿点点头发... 源码精品专栏 原创 | ...

  8. 实战|QUIC协议在蚂蚁集团落地

    自 2015 年以来,QUIC 协议开始在 IETF 进行标准化并被国内外各大厂商相继落地.鉴于 QUIC 具备"0RTT 建联"."支持连接迁移"等诸多优势, ...

  9. 积跬步至千里:QUIC 协议在蚂蚁集团落地之综述

    自 2015 年以来,QUIC 协议开始在 IETF 进行标准化并被国内外各大厂商相继落地.鉴于 QUIC 具备"0RTT 建联"."支持连接迁移"等诸多优势, ...

最新文章

  1. xp系统安装oracle乱码,linux中安装Oracle汉字乱码完整解决方案
  2. 独立重复实验与二项分布
  3. 【基础算法】 GBDT/XGBoost 常见问题
  4. P5643-[PKUWC2018]随机游走【min-max容斥,dp】
  5. gradle构建_指定Gradle构建属性
  6. 小程序 遮罩层(阻止事件穿透)
  7. django--cookie与session
  8. Java中的JsonConfig详解
  9. Socket 套接字和解决粘包问题
  10. 【养眼美女win7主题】主题世界
  11. Android APK安装后资源文件(res/assets)位置
  12. flutter APP自动更新
  13. HNU暑假程序设计训练 0419
  14. UIPATH IE浏览器下载问题
  15. 因果关系发现:推开认知世界的大门
  16. 基于自然语言处理的垃圾信息过滤方法
  17. Python编程基础-函数
  18. 基于WEB 的实时事件通知
  19. 鸭子心包积液是怎么回事怎么治32天的鸭子心包积液是什么病
  20. Security+ 5. 实现主机/软件安全性

热门文章

  1. 2021年3月全国MySQL二级考试笔记
  2. 百度apollo——启动脚本
  3. Twitter开源软件列表
  4. 刚刚买了 MacBook,要如何保护?
  5. 中国消费电子高频主轴市场趋势报告、技术动态创新及市场预测
  6. [AHK]一键发送选中的文件给微信好友
  7. 如何帮助孩子结交新朋友
  8. 数据告诉你:那些成功出道的男团女团,现在怎么样了
  9. 管理员页面的班级功能
  10. Docker——修改镜像源