Android开发老生新谈:从OkHttp原理看网络请求

GET /wxarticle/chapters/json HTTP/1.1

Host: wanandroid.com

Accept: application/json, text/plain, /

Accept-Encoding: gzip, deflate, br

Accept-Language: zh-CN,zh;q=0.9

Connection: keep-alive

User-Agent: Mozilla/5.0 xxx

你可能会问,BridgeInterceptor拦截器和这个有什么关系?其实BridgeInterceptor的作用就是帮用户处理网络请求,它会帮助用户填写服务器请求所需要的配置信息,如上面所展示的User-Agent、Connection、Host、Accept-Encoding等。同时也会对请求的结果进行相应处理。

BridgeInterceptor的内部实现主要分为以下三步:

  1. 为用户网络请求设置Content-Type、Content-Length、Host、Connection、Cookie等参数,也就是将一般请求转换为适合服务器解析的格式,以适应服务器端;

  2. 通过 chain.proceed(requestBuilder.build())方法,将转换后的请求移交给下一个拦截器CacheInterceptor,并接收返回的结果Response;

  3. 对结果Response也进行gzip、Content-Type转换,以适应应用程序端。

所以说BridgeInterceptor是应用程序和服务器端的一个桥梁。

4、CacheInterceptor

CacheInterceptor是一个处理网络请求缓存的拦截器。它的内部处理和一些图片缓存的逻辑相似,首先会判断是否存在可用的缓存,如果存在,则直接返回缓存,反之,调用chain.proceed(networkRequest)方法将请求移交给下一个拦截器,有了结果后,将结果put到cache中。

5、ConnectInterceptor

ConnectInterceptor是建立连接去请求的拦截器。

internal fun initExchange(chain: RealInterceptorChain): Exchange {

val exchangeFinder = this.exchangeFinder!!

val codec = exchangeFinder.find(client, chain)

val result = Exchange(this, eventListener, exchangeFinder, codec)

this.interceptorScopedExchange = result

this.exchange = result

if (canceled) throw IOException(“Canceled”)

return result

}

从它的源码可以看到,它首先会通过ExchangeFinder查询到codec,这个ExchangeFinder是不是很熟悉?在上面RetryAndFollowUpInterceptor分析中,每次循环都会先做创建ExchangeFinder的准备工作。

而这个codec是什么?它是一个编码解码器,来确定是用Http1的方式还是以Http2的方式进行请求。

在找到合适的codec后,作为参数创建Exchange。Exchange内部涉及了很多网络连接的实现,这个后面再详细说,我们先看看是如何找到合适的codec?

如何找到可用连接?

找到合适的codec,就必须先找到一个可用的网络连接,再利用这个可用的连接创建一个新的codec。 为了找到可用的连接,内部使用了大概5种方式进行筛选。

第一种:从连接池中查找

if (connectionPool.callAcquirePooledConnection(address, call, null, false)) {

val result = call.connection!!

return result

}

尝试在连接池中查找可用的连接,在遍历连接池中的连接时,就会判断每个连接是否可用,而判断连接是否可用的条件如下:

  1. 请求数要小于该连接最大能承受的请求数,Http2以下,最大请求数为1个,并且此连接上可创建新的交换;

  2. 该连接的主机和请求的主机一致;

如果从连接池中拿到了合格的连接connection,则直接返回。

如果没有拿到,那就进行第二种拿可用连接的方式。

第二种:传入Route,从连接池中查找

if (connectionPool.callAcquirePooledConnection(address, call, routes, false)) {

val result = call.connection!!

return result

}

第二种依然是从连接池中拿,但是这次不同的是,参数里传入了routes,这个routes是包含路由Route的一个List集合,而Route其实指的是连接的IP地址、TCP端口以及代理模式。

而这次从连接池中拿,主要是针对Http2,路由必须共用一个IP地址,此连接的服务器证书必须包含新主机且证书必须与主机匹配。

第三种:自己创建连接

如果前两次从连接池里都没有拿到可用连接,那么就自己创建连接。

val newConnection = RealConnection(connectionPool, route)

call.connectionToCancel = newConnection

try {

newConnection.connect(

connectTimeout,

readTimeout,

writeTimeout,

pingIntervalMillis,

connectionRetryEnabled,

call,

eventListener

)

}

创建连接其实是内部自己在进行socket,tls的连接,这里抛出一个问题在后面解答:TCP/TLS连接是如何实现的?

自己创建好连接后,又做了一次从连接池中查找的操作。

第四种:多路复用置为true,依然从连接池中查找

if (connectionPool.callAcquirePooledConnection(address, call, routes, true)) {

val result = call.connection!!

newConnection.socket().closeQuietly()

return result

}

这次从连接池中查找,requireMultiplexed置为了true,只查找支持多路复用的连接。并且在建立连接后,将新的连接保存到连接池中。

如何找到Http1和Http2的编/解码器?

上面已经分析出寻找可用且健康的连接的几种方式,那对于codec的创建则需要根据这些连接进行Http1和Http2的区分。如果http2Connection不为null,则创建Http2ExchangeCodec,反之创建Http1ExchangeCodec。

找到编解码器后,我们就回到ConnectInterceptor的一开始,利用编解码器codec创建了一个Exchange,而这个Exchange的内部其实是利用Http1解码器或者Http2解码器,分别进行请求头的编写writeRequestHeaders,或者创建Request Body,发送给服务器。

Exchange初始化成功后,就又将请求移交给了下一个拦截器CallServerInterceptor。

6、CallServerInterceptor

CallServerInterceptor是链中最后一个拦截器,主要用于向服务器发送内容,主要传输http的头部和body信息。

其内部利用上面创建的Exchange进行请求头编写,创建Request body,发送请求,得到结果后,对结果进行解析并回传。

7、NetworkInterceptor

networkInterceptor也是属于用户自定义的一种拦截器,它的位置在ConnectInterceptor之后,CallServerInterceptor之前。我们知道第一个拦截器便是用户自定义,那和这个有什么区别呢?

networkInterceptor前面已经存在有多个拦截器的使用,在请求到达该拦截器时,请求信息已经相当复杂了,其中就包括RetryAndFollowUpInterceptor重试拦截器,经过分析知道,每当重试一次,其后面的拦截器也都会被调用一次,这样就导致networkInterceptor也会被调用多次,而第一个自定义拦截器只会调用一次。当我们需要自定义拦截器时,如token、log,为了资源消耗这一点,一般都是使用第一个。

到这里为止,7种拦截器都分析完成。在分析ConnectInterceptor时抛出了一个问题:TCP/TLS连接是如何实现的?

如何建立TCP/TLS连接?

================================================================================

TCP连接


fun connect(

connectTimeout: Int,

readTimeout: Int,

writeTimeout: Int,

pingIntervalMillis: Int,

connectionRetryEnabled: Boolean,

call: Call,

eventListener: EventListener

) {

while (true) {

try {

if (route.requiresTunnel()) {

connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener)

if (rawSocket == null) {

// We were unable to connect the tunnel but properly closed down our resources.

break

}

} else {

connectSocket(connectTimeout, readTimeout, call, eventListener)

}

establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener)

eventListener.connectEnd(call, route.socketAddress, route.proxy, protocol)

break

} catch (e: IOException) {

}

  1. 在connect的内部开启了一个while循环,可以看到第一步就是route.requiresTunnel()判断,这个requiresTunnel()方法表示该请求是否使用了Proxy.Type.HTTP代理且目标是Https连接;

  2. 如果是,则创建一个代理隧道连接Tunnel(connectTunnel)。创建这个隧道的目的在于利用Http来代理请求Https;

  3. 如果不是,则直接建立一个TCP连接(connectSocket);

  4. 建立请求协议。

代理隧道是如何创建的?它的内部会先通过Http代理创建一个TLS的请求,也就是在地址url上增加Host、Proxy-Connection、User-Agent首部。接着最多21次的尝试,利用connectSocket开启TCP连接且利用TLS请求创建一个代理隧道。

从这里可以看见,不管是否需要代理隧道,都会开始建立一个TCP连接(connectSocket),那又是如何建立TCP连接的?

private fun connectSocket(

connectTimeout: Int,

readTimeout: Int,

call: Call,

eventListener: EventListener

) {

val proxy = route.proxy

val address = route.address

val rawSocket = when (proxy.type()) {

Proxy.Type.DIRECT, Proxy.Type.HTTP -> address.socketFactory.createSocket()!!

else -> Socket(proxy)

}

this.rawSocket = rawSocket

eventListener.connectStart(call, route.socketAddress, proxy)

rawSocket.soTimeout = readTimeout

try {

Platform.get().connectSocket(rawSocket, route.socketAddress, connectTimeout)

} catch (e: ConnectException) {

throw ConnectException(“Failed to connect to ${route.socketAddress}”).apply {

initCause(e)

}

}

}

从源码上看,如果代理类型为直连或者HTTP/FTP代理,则直接创建一个socket,反之,则指定代理类型进行创建。我们看到创建后返回了一个rawSocket,这个就代表着TCP连接。在最后 调用Platform.get().connectSocket,而这实际就是调用socket的connect方法来打开一个TCP连接。

TLS连接


在建立TCP连接或者创建Http代理隧道后,就会开始建立连接协议(establishProtocol)。

private fun establishProtocol(

connectionSpecSelector: ConnectionSpecSelector,

pingIntervalMillis: Int,

call: Call,

eventListener: EventListener

) {

if (route.address.sslSocketFactory == null) {

if (Protocol.H2_PRIOR_KNOWLEDGE in route.address.protocols) {

socket = rawSocket

protocol = Protocol.H2_PRIOR_KNOWLEDGE

startHttp2(pingIntervalMillis)

return

}

socket = rawSocket

protocol = Protocol.HTTP_1_1

return

}

eventListener.secureConnectStart(call)

connectTls(connectionSpecSelector)

eventListener.secureConnectEnd(call, handshake)

if (protocol === Protocol.HTTP_2) {

startHttp2(pingIntervalMillis)

}

}

  1. 判断当前地址是否是HTTPS;

  2. 如果不是HTTPS,则判断当前协议是否是明文HTTP2,如果是的则调用startHttp2,开始Http2的握手动作,如果是Http/1.1则直接return返回;

  3. 如果是HTTPS,就开始建立TLS安全协议连接了(connectTls);

  4. 如果是HTTPS且为HTTP2,除了建立TLS连接外,还会调用startHttp2,开始Http2的握手动作。

在上述第3步时就提到了TLS的连接(connectTls),那我们就来看一下它的内部实现:

private fun connectTls(connectionSpecSelector: ConnectionSpecSelector) {

val address = route.address

val sslSocketFactory = address.sslSocketFactory

var success = false

var sslSocket: SSLSocket? = null

try {

// Create the wrapper over the connected socket.

sslSocket = sslSocketFactory!!.createSocket(

rawSocket, address.url.host, address.url.port, true /* autoClose */) as SSLSocket

// Configure the socket’s ciphers, TLS versions, and extensions.

val connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket)

if (connectionSpec.supportsTlsExtensions) {

Platform.get().configureTlsExtensions(sslSocket, address.url.host, address.protocols)

}

// Force handshake. This can throw!

sslSocket.startHandshake()

// block for session establishment

val sslSocketSession = sslSocket.session

val unverifiedHandshake = sslSocketSession.handshake()

// Verify that the socket’s certificates are acceptable for the target host.

if (!address.hostnameVerifier!!.verify(address.url.host, sslSocketSession)) {

val peerCertificates = unverifiedHandshake.peerCertificates

if (peerCertificates.isNotEmpty()) {

val cert = peerCertificates[0] as X509Certificate

throw SSLPeerUnverifiedException(“”"

|Hostname ${address.url.host} not verified:

| certificate: ${CertificatePinner.pin(cert)}

| DN: ${cert.subjectDN.name}

| subjectAltNames: ${OkHostnameVerifier.allSubjectAltNames(cert)}

“”".trimMargin())

} else {

throw SSLPeerUnverifiedException(

“Hostname ${address.url.host} not verified (no certificates)”)

}

}

val certificatePinner = address.certificatePinner!!

handshake = Handshake(unverifiedHandshake.tlsVersion, unverifiedHandshake.cipherSuite,

unverifiedHandshake.localCertificates) {

certificatePinner.certificateChainCleaner!!.clean(unverifiedHandshake.peerCertificates,

address.url.host)

}

// Check that the certificate pinner is satisfied by the certificates presented.

certificatePinner.check(address.url.host) {

handshake!!.peerCertificates.map { it as X509Certificate }

}

// Success! Save the handshake and the ALPN protocol.

val maybeProtocol = if (connectionSpec.supportsTlsExtensions) {

Platform.get().getSelectedProtocol(sslSocket)

} else {

null

}

socket = sslSocket

文末

好了,今天的分享就到这里,如果你对在面试中遇到的问题,或者刚毕业及工作几年迷茫不知道该如何准备面试并突破现状提升自己,对于自己的未来还不够了解不知道给如何规划,可以来看看同行们都是如何突破现状,怎么学习的,来吸收他们的面试以及工作经验完善自己的之后的面试计划及职业规划。

这里放上一部分我工作以来以及参与过的大大小小的面试收集总结出来的相关的几十套腾讯、头条、阿里、美团等公司21年的面试专题,其中把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分免费分享给大家,主要还是希望大家在如今大环境不好的情况下面试能够顺利一点,希望可以帮助到大家~

还有 高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

【Android核心高级技术PDF文档,BAT大厂面试真题解析】

【延伸Android必备知识点】

这里只是整理出来的部分面试题,后续会持续更新,希望通过这些高级面试题能够降低面试Android岗位的门槛,让更多的Android工程师理解Android系统,掌握Android系统。喜欢的话麻烦点击一个喜欢在关注一下~

遇到的问题,或者刚毕业及工作几年迷茫不知道该如何准备面试并突破现状提升自己,对于自己的未来还不够了解不知道给如何规划,可以来看看同行们都是如何突破现状,怎么学习的,来吸收他们的面试以及工作经验完善自己的之后的面试计划及职业规划。

这里放上一部分我工作以来以及参与过的大大小小的面试收集总结出来的相关的几十套腾讯、头条、阿里、美团等公司21年的面试专题,其中把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分免费分享给大家,主要还是希望大家在如今大环境不好的情况下面试能够顺利一点,希望可以帮助到大家~

还有 高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

【Android核心高级技术PDF文档,BAT大厂面试真题解析】

[外链图片转存中…(img-vC8xK3FE-1726020107793)]

【延伸Android必备知识点】

[外链图片转存中…(img-LEw7y9O6-1726020107794)]

这里只是整理出来的部分面试题,后续会持续更新,希望通过这些高级面试题能够降低面试Android岗位的门槛,让更多的Android工程师理解Android系统,掌握Android系统。喜欢的话麻烦点击一个喜欢在关注一下~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值