1. QUIC 介绍
QUIC是快速UDP网络衔接(英语:Quick UDP Internet Connections)的缩写,这是一种实验性的传输层网络传输协议,由Google公司开发,在2013年完成。QUIC运用UDP协议,它在两个端点间创立衔接,且支撑多路复用衔接。在规划之初,QUIC希望可以提供等同于SSL/TLS层级的网络安全保护,削减数据传输及创立衔接时的延迟时间,双向控制带宽,以避免网络拥塞。Google希望运用这个协议来取代TCP协议,使网页传输速度加速,计划将QUIC提交至互联网工程任务小组(IETF),让它成为下一代的正式网络规范。 [百度百科]
2. CRONET 引入
官方项目里或许说高星开源项目中,支撑 Android 上 quic 的靠谱网络库只此一家,Android 工程引入 cronet release 有两种方法。
- gradle 引用,最新只能引用到 113 版别,见:Maven Repository: org.chromium.net cronet-embedded (mvnrepository.com)
- 直接去 GoogleCloud 里去下载 chromium-cronet 的最新版别。比方:下面这个链接就可以下载最新的 121.0.6167.0 版别,优点是版别新,缺陷是或许不稳定 chromium-cronet – 存储桶详情 – Cloud Storage – Google Cloud Console
3. 直接用 cronet 不就行了吗?为啥还调配 OKHTTP 呢?
我之前共享的文章里边就反复提及三个点 – 危险、收益、本钱。
- 低危险: 为了最小化线上危险,可以满意快速回退的需求,最好仍是让 cronet 可以插拔,变成一个现有网络库的可选功能项
- 低本钱: 许多项目都是深度运用 okhttp,现已根据 okhttp 干了许多基建了,要把那些东西悉数切到 cronet 上本钱仍是比较高的
- 网络库切换有必要是比及线上验证完 cronet 的功能/稳定性后才可以被讨论的论题
当然,考虑归考虑,最终要上线 cronet 的时分,仍是引荐大家要么进行网络库替换,要么改造 cronet,让 cronet 可以完美和 okHttp 融合。举个最简略的例子,OKHttp 和 cronet 的线程池是不共用的,那app 运行过程中建议网络恳求时,就比单独用 okHttp 或许 cronet 时,会创立更加多的线程。
google 官方其实有相关完成,但个人觉得完成的太过复杂:
4. 完成计划 – Cronet 逻辑
咱们已知,OKHttp 的拦截器规划支撑调用者可以自定义自己的拦截器,每个拦截器都是有机会去消费恳求生产响应的。所以咱们为了完成如下需求:
- cronet 嵌入到 okhttp 的恳求流程中
- cronet 接纳 okhttp 的建议恳求流程
- 保存原先用于设置恳求 UA 等自定义功能的拦截器
咱们将自定义一个 Cronet 的拦截器,并且将这个拦截器添加到自定义拦截器的末尾,确保 CronetInteceptor 执行前,之前自定义功能的拦截器都能正常工作。
咱们提前料想一下,看看咱们的 cronet 拦截器需求完成哪些东西:
- OKhttp request 转 cronet request
- cronet 恳求流程
- cronet response 转 OKHttp response
- 或许需求的功能计算逻辑
CronetEnv.kt
初始化 cronet,配置支撑 quic 的域名、端口,注入网络功能打点逻辑
object CronetEnv {
private var cronetEngine: CronetEngine? = null
private val executorService = Executors.newSingleThreadExecutor()
fun cronetEngine(): CronetEngine? {
return cronetEngine
}
fun initializeCronetEngine(
context: Context,
) {
if (cronetEngine != null) {
return
}
val builder = CronetEngine.Builder(context)
.enableHttp2(true)
.enableQuic(true)
.enableBrotli(true)
.addQuicHint("test.quic.com", 443, 443)
}
cronetEngine = builder.build()
cronetEngine?.addRequestFinishedListener(object :
RequestFinishedInfo.Listener(Executors.newSingleThreadExecutor()) {
override fun onRequestFinished(requestInfo: RequestFinishedInfo) {
CronetLogHelper.logMatrix(requestInfo)
}
})
}
}
CronetInterceptor.kt
首要担任:
- OKHttp request 转换成 cronet request
- 运用 cronet request 建议恳求,并接纳响应
class CronetInterceptor : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
if (chain.call().isCanceled()) {
throw IOException("Canceled")
}
//查看 cronet 初始化
CronetEnv.cronetEngine() ?: return chain.proceed(chain.request())
//用 cronet 来署理网络恳求
return try {
val callback = CronetRequestCallback(chain.request())
val urlRequest = buildRequest(chain.request(), callback, CronetEnv.cronetEngine(), CronetEnv.executorService)
urlRequest.start()
callback.blockForResponse()
} catch (e: Exception) {
Log.e(TAG, "got exception for ${chain.request().url}, message=${e.message}")
if (e is IOException) {
throw e
} else {
throw IOException("canceled due to $e", e)
}
}
}
fun buildRequest(
request: Request,
callback: UrlRequest.Callback?,
cronetEngine: CronetEngine,
executorService: ExecutorService
): UrlRequest {
val url = request.url.toString()
val requestBuilder = cronetEngine.newUrlRequestBuilder(url, callback, executorService)
requestBuilder.setHttpMethod(request.method)
request.headers.forEach {
requestBuilder.addHeader(it.first, it.second)
}
val requestBody = request.body
if (requestBody != null) {
val contentType = requestBody.contentType()
if (contentType != null) {
requestBuilder.addHeader("Content-Type", contentType.toString())
}
val buffer = Buffer()
requestBody.writeTo(buffer)
val uploadDataProvider = UploadDataProviders.create(buffer.readByteArray())
requestBuilder.setUploadDataProvider(uploadDataProvider, executorService)
}
return requestBuilder.build()
}
}
CronetRequestCallback
首要担任:
- 接纳 croent response 转成 OKHttp response
- 处理成功、失利、重定向逻辑
class CronetRequestCallback internal constructor(
private val originRequest: Request
) : UrlRequest.Callback() {
private var redirectCount = 0
private var response: Response
private var iOException: IOException? = null
private val responseLock = ConditionVariable()
private val receivedByteArrayOutputStream = ByteArrayOutputStream()
private val receiveChannel = Channels.newChannel(receivedByteArrayOutputStream)
init {
response = Response.Builder()
.sentRequestAtMillis(System.currentTimeMillis())
.request(originRequest)
.build()
}
@Throws(IOException::class)
fun blockForResponse(): Response {
responseLock.block()
if (iOException != null) {
throw iOException as IOException
}
return response
}
override fun onRedirectReceived(
request: UrlRequest,
responseInfo: UrlResponseInfo,
newLocationUrl: String
) {
if (redirectCount > 20) {
request.cancel()
iOException = ProtocolException("Too many follow-up requests: $redirectCount")
return
}
redirectCount += 1
request.followRedirect()
}
override fun onResponseStarted(request: UrlRequest, responseInfo: UrlResponseInfo) {
response = toOkResponse(response, responseInfo)
request.read(ByteBuffer.allocateDirect(32 * 1024))
}
@Throws(Exception::class)
override fun onReadCompleted(
request: UrlRequest,
responseInfo: UrlResponseInfo,
byteBuffer: ByteBuffer
) {
byteBuffer.flip()
try {
receiveChannel.write(byteBuffer)
} catch (e: IOException) {
iOException = e
}
byteBuffer.clear()
request.read(byteBuffer)
}
override fun onSucceeded(request: UrlRequest, responseInfo: UrlResponseInfo) {
val contentType = response.header("Content-Type", "text/html")
val mediaType: MediaType? = (contentType
?: """text/plain; charset="utf-8"""").toMediaTypeOrNull()
val responseBody = receivedByteArrayOutputStream.toByteArray().toResponseBody(mediaType)
val httpStatusCode = responseInfo.httpStatusCode
if ((httpStatusCode == 204 || httpStatusCode == 205) && responseBody.contentLength() > 0) {
iOException =
ProtocolException("HTTP " + httpStatusCode + " had non-zero Content-Length: " + responseBody.contentLength()); }
val newRequest = originRequest.newBuilder()
.url(responseInfo.url)
.build()
response = response.newBuilder()
.body(responseBody)
.request(newRequest).build()
responseLock.open()
}
override fun onFailed(request: UrlRequest, info: UrlResponseInfo?, error: CronetException?) {
iOException = error
responseLock.open()
}
override fun onCanceled(request: UrlRequest, info: UrlResponseInfo?) {
if (iOException == null) {
iOException = IOException("The request was canceled!")
}
responseLock.open()
}
companion object {
private fun toOkResponse(response: Response, responseInfo: UrlResponseInfo): Response {
val protocol = CronetLogHelper.protocolFromCronet(
responseInfo.negotiatedProtocol
)
val headersBuilder = Headers.Builder()
for ((key, value) in responseInfo.allHeadersAsList) {
headerBuilder.add(key, value)
}
return response.newBuilder()
.receivedResponseAtMillis(System.currentTimeMillis())
.protocol(protocol)
.code(responseInfo.httpStatusCode)
.message(responseInfo.httpStatusText)
.headers(headersBuilder.build())
.build()
}
}
}
CronetLogHelper
首要担任:
- 收集 cronet 恳求的功能指标数据,对标 OKHttp 的 EventListener,对各个阶段进行计算。
- 如果还想计算更多上下文内容,可以经过在前置经过 requestBuilder.addRequestannotation 注入,后续经过 requestFinishedInfo.annotations 取出
object CronetLogHelper {
fun logMatrix(requestFinishedInfo: RequestFinishedInfo) {
val matrix = requestFinishedInfo.metrics
val success = requestFinishedInfo.finishedReason == RequestFinishedInfo.SUCCEEDED
val requestStart = matrix.requestStart?.time ?: 0
val dnsStart = matrix.dnsStart?.time ?: 0
val dnsEnd = matrix.dnsEnd?.time ?: 0
val connectStart = matrix.connectStart?.time ?: 0
val connectEnd = matrix.connectEnd?.time ?: 0
val sslStart = matrix.sslStart?.time ?: 0
val sslEnd = matrix.sslEnd?.time ?: 0
val sendStart = matrix.sendingStart?.time ?: 0
val sendEnd = matrix.sendingEnd?.time ?: 0
val responseStart = matrix.responseStart?.time ?: 0
val responseEnd = matrix.requestEnd?.time ?: 0
val requestEnd = matrix.requestEnd?.time ?: 0
val responseBodySize = matrix.receivedByteCount ?: 0
val totalTime = matrix.totalTimeMs ?: 0
val map: MutableMap<String, Any> = mutableMapOf()
map["url"] = requestFinishedInfo.url.toString()
map["call_begin"] = requestStart
map["dns_begin"] = dnsStart
map["dns_end"] = dnsEnd
map["connect_begin"] = connectStart
map["secure_connect_begin"] = sslStart
map["secure_connect_end"] = sslEnd
map["connect_end"] = connectEnd
map["request_begin"] = sendStart
map["request_end"] = sendEnd
map["response_begin"] = responseStart
map["response_end"] = responseEnd
map["response_body_size"] = responseBodySize
map["call_end"] = requestEnd
map["task_interval"] = totalTime
map["quic_jetsam"] = false
requestFinishedInfo.responseInfo?.let {
map["result"] = it.httpStatusCode
map["protocol"] = it.negotiatedProtocol
map["is_cache"] = if (it.wasCached()) 1 else 0
}
if (!success) {
map["result"] = -1
map["error_desc"] = requestFinishedInfo.exception?.message ?: ""
}
//拿着数据进行日志上报或许其他
}
}
5.完成计划 – 将 CronetInterceptor 嵌入到 OKHttp
OKhttpClient.Builder builder = xxxx;
builder.addInterceptor(xxxx);
builder.addInterceptor(xxxx);
builder.addInterceptor(CronetIntecerptor())
6. 收工
至此咱们就在 OKhttp 的基础上,经过嵌入 cronet 来完成了 quic 协议栈的接入。 当然,如果想要在大项目中上线 quic,实际上还需求做的更多,如:
你或许感兴趣
Android QUIC 实践 – 根据 OKHttp 扩展出 Cronet 拦截器 – (juejin.cn)
Android发动优化实践 – 秒开率从17%提升至75% – (juejin.cn)
如何科学的进行Android包体积优化 – (juejin.cn)
Android稳定性:Looper兜底结构完成线上容灾(二) – (juejin.cn)
根据 Booster ASM API的配置化 hook 计划封装 – (juejin.cn)
记 AndroidStudio Tracer东西导致的编译失利 – (juejin.cn)
Android 发动优化事例-WebView非预期初始化排查 – (juejin.cn)
chromium-net – 跟随 Cronet 的脚步探索大致流程(1) – (juejin.cn)
Android稳定性:可远程配置化的Looper兜底结构 – (juejin.cn)
Android – 一种别致的冷发动速度优化思路(Fragment极度懒加载 + Layout子线程预加载) – (juejin.cn)