转自:https://blog.csdn.net/keeng2008/article/details/119174331
初步进行了一些排版优化。
HTTP/3 在 HTTP/2 的基础上,增强了安全上的限制,且使用 UDP 传输降低丢包导致的头部阻塞、降低因为 TCP 的协议限制而导致的连接耗时高等问题,但是目前各大浏览器的支持范围不够广,暂时不建议在网页相关的服务上进行升级。但是其提高了传输效率,有必要在传输数据量较大的应用上进行升级,建议对 HTTP/3 支持的改造设计与研究,在规范成熟时发布支持 HTTP/3 协议的版本。
前期在调研 quic 选型中,选择了 Cronet 作为客户端访问 quic 协议的网络库。为了方便现有项目中能快速的支持 quic 网络协议,下面会对比 OkHttp 与 Cronet 网络库的使用区别。
支持 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")
}
})
支持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.")
}
}
现在项目中,使用网络请求模式大部分是使用 OkHttp,直接使用或者与 Retrofit 结合使用。还有少部分共用的 api 使用 HttpClient。
需要进行的修改有:
所有 OkHttp 网络请求修改为 Cronet 的请求方式。
存在以下问题:
代码改动大。请求方式变化较大并且读写操作,异常处理等方式比较原始,不易操作。
不易回滚。修改完成后如果想要快速回滚到原来的方式,也同样面临麻烦。
无法使用原有的 Interceptor 业务逻辑和 Retrofit 接口定义功能
因此此方法可行性不高,下面会考虑在 OkHttp 的拦截器方式接入 Cronet。
参考网易分享的在 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个网络库的快速切换。
底层使用Cronet和OkHttp实现,接口尽量跟OkHttp接口兼容,可在项目中快速接入使用。
新的HttpClient需要实现Call.Factory接口,方便Retrofit框架结合使用。
使用OkHttp时,可通过简单封装连接起来实现。
缺点:不支持http3.0, quic协议。
缺点:需要自己实现流式数据接口。代理,dns等功能在Cronet暂无接口可使用。
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()
在 MdHttpClient.Builder 在调用build方法时,判断当前使用的底层库,生成对应的CallFactory对象。
如果为使用OkHttp,在内部新建一个 OkHttpClient对象,在newCall时直接使用OkHttp.newCall,不需要过多处理。
如果为使用Cronet,则创建一个 CronetClient, newCall时创建自定义的 CronetRealCall
保存使用 Dispatcher作为异步线程调度器,需要用到其中的executorService。
参考OkHttp的实现方式,需要新建一个CronetRealCall。
需要实现一个可堵塞的缓冲区BlockableBuffer,发送数据时,如果缓冲区已满,则堵塞等待;读取数据时,如果缓冲区为空,则堵塞等待。
BlockableBuffer实现了Sink,Source接口,然后通过okhttp的Timeout包装成TimerSink, TimerSource。在read/write等待超过一定时间,则抛出超时异常。
调用cronet的cancel接口,如果BlockableBuffer在堵塞中,就使用ConditionVariable通知取消堵塞,抛出Cancell异常。
类设计图:
![avatar]
使用方式与 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)
}
在创建MdHttpClient时,可以通过参数配置使用的网络库,可以选择 OkHttp或者Cronet 库作为底层支持库。
val builder: MdHttpClient.Builder = service!!.newBuilder()
builder.useNetCore(MdHttpClient.NetCore.Cronet)
// 或者 builder.useNetCore(MdHttpClient.NetCore.OkHttp)
更多使用示例,请参考 EXAMPLE.md
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.
调研dns实现方式
持续更新cronet对更多协议的支持
现已支持quic Q050以下协议版本;编译新版cronet源码,支持更多quic协议版本以及http3草案协议。