从 OkHttp 引入 Cronet 支持 quic 协议

转自:https://blog.csdn.net/keeng2008/article/details/119174331
初步进行了一些排版优化。

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协议。

  1. 使用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。

  1. 实现Okhttp调用链

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

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

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

  1. 实现超时,异常处理

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

  1. 请求中断

调用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.

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

四、后续优化方向

  1. 调研dns实现方式

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

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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
CronetOkHttp都是用于网络请求的库,但它们有一些区别。 1. 架构和用途: - Cronet是Google开发的网络库,它是基于Chromium网络栈构建的,主要用于Android平台上的网络请求。Cronet提供了高性能和低延迟的网络请求能力,并且支持HTTP/2和QUIC协议。 - OkHttp是Square开发的网络库,它是基于Java语言构建的,可以在Android和Java平台上使用。OkHttp提供了简洁易用的API,支持HTTP/1.1和HTTP/2协议,并且具有连接池、请求拦截器、缓存等功能。 2. 性能和功能: - Cronet在性能方面具有优势,它使用了Chromium网络栈,可以利用Chromium在网络请求方面的优化和经验。Cronet支持并发请求、请求优先级管理、请求流量控制等功能,可以满足高性能网络请求的需求。 - OkHttp也是一个高性能的网络库,它使用了连接池和异步请求等技术来提高性能。OkHttp支持请求重试、连接超时、连接池管理等功能,并且可以通过拦截器来实现自定义的网络请求处理逻辑。 3. 兼容性和依赖: - Cronet是Google官方推荐的网络库,但它目前仅在Android平台上可用,并且需要导入相应的Cronet库文件。 - OkHttp是一个跨平台的网络库,可以在Android和Java平台上使用,并且可以通过Gradle等构建工具方便地引入依赖。 总的来说,CronetOkHttp都是优秀的网络库,选择使用哪个取决于具体的需求和平台限制。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值