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 有两种方法。

  1. gradle 引用,最新只能引用到 113 版别,见:Maven Repository: org.chromium.net cronet-embedded (mvnrepository.com)

Android QUIC 实践 - 根据 OKHttp 扩展出 Cronet 拦截器

  1. 直接去 GoogleCloud 里去下载 chromium-cronet 的最新版别。比方:下面这个链接就可以下载最新的 121.0.6167.0 版别,优点是版别新,缺陷是或许不稳定 chromium-cronet – 存储桶详情 – Cloud Storage – Google Cloud Console

Android QUIC 实践 - 根据 OKHttp 扩展出 Cronet 拦截器

3. 直接用 cronet 不就行了吗?为啥还调配 OKHTTP 呢?

我之前共享的文章里边就反复提及三个点 – 危险、收益、本钱

  • 低危险: 为了最小化线上危险,可以满意快速回退的需求,最好仍是让 cronet 可以插拔,变成一个现有网络库的可选功能项
  • 低本钱: 许多项目都是深度运用 okhttp,现已根据 okhttp 干了许多基建了,要把那些东西悉数切到 cronet 上本钱仍是比较高的
  • 网络库切换有必要是比及线上验证完 cronet 的功能/稳定性后才可以被讨论的论题

当然,考虑归考虑,最终要上线 cronet 的时分,仍是引荐大家要么进行网络库替换,要么改造 cronet,让 cronet 可以完美和 okHttp 融合。举个最简略的例子,OKHttp 和 cronet 的线程池是不共用的,那app 运行过程中建议网络恳求时,就比单独用 okHttp 或许 cronet 时,会创立更加多的线程。

google 官方其实有相关完成,但个人觉得完成的太过复杂:

google/cronet-transport-for-okhttp: This package allows OkHttp and Retrofit users to use Cronet as their transport layer, benefiting from features like QUIC/HTTP3 support or connection migration. (github.com)

4. 完成计划 – Cronet 逻辑

咱们已知,OKHttp 的拦截器规划支撑调用者可以自定义自己的拦截器,每个拦截器都是有机会去消费恳求生产响应的。所以咱们为了完成如下需求:

  • cronet 嵌入到 okhttp 的恳求流程中
  • cronet 接纳 okhttp 的建议恳求流程
  • 保存原先用于设置恳求 UA 等自定义功能的拦截器

咱们将自定义一个 Cronet 的拦截器,并且将这个拦截器添加到自定义拦截器的末尾,确保 CronetInteceptor 执行前,之前自定义功能的拦截器都能正常工作。

咱们提前料想一下,看看咱们的 cronet 拦截器需求完成哪些东西:

  1. OKhttp request 转 cronet request
  2. cronet 恳求流程
  3. cronet response 转 OKHttp response
  4. 或许需求的功能计算逻辑

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,实际上还需求做的更多,如:

  • 齐备的功能监控
  • 齐备的报警机制
  • 及时的容灾机制
  • 或许的针对 cronet 的功能优化
  • cronet的完全改造或许网络库的完全替换(两个人在一起总得抹平棱角,当然,也或许是大家保存各自特色各自安好)

你或许感兴趣

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)

一类有趣的无限缓存OOM现象 – (juejin.cn)

Android – 一种别致的冷发动速度优化思路(Fragment极度懒加载 + Layout子线程预加载) – (juejin.cn)

Android – 完全消除OOM的实战经验共享(千分之1.5 -> 万分之0.2) – (juejin.cn)