网络库的选择
理论上来说几乎所有的网络库都是基于Socket实现的,在Socket的基础上可以实现各种应用层的通信协议,比http、ftp等。Java实现的网络库理论上来说都可以在Android端上使用,但由于Android这种嵌入式系统的特性,对网络请求库的要求可能会更严格。
HttpUrlConnection和HttpClient在API23之后已经从系统源码中移除了,开发时如果要使用到网络请求,可能需要额外引入一些第三方库。目前常用的网络请求库包括android-async-http、Volley、Okhttp以及Retrofit。Retrofit其实并不是一个网络请求库,更准确地说它是一个网络封装库,能够将底层的网络请求库封装成RESTful API设计风格。其中android-async-http是基于HttpClient封装的,主要是封装了异步线程与main线程之间的切换以及智能请求重试,持久化、cookie保存到SP等。Vollery是曾经最有名的Android请求库,基于HttpUrlConnection,支持图片加载,网络请求排序,优先级处理缓存与Activity生命周期联运等。其实Vollery的底层网络请求也可以使用其他网络请求库替换。OkHttp是近几年来最受欢迎的网络库了,现在几乎所有的app的网络请求库都使用OkHttp或者基于OkHttp来定制的。OkHttp已经不是基于HttpUrlConnection了,而是使用Socket来实现了一套网络请求。OkHttp支持同步、异步,封装了线程池、数据转换、参数使用,错误处理等。并且Socket还使用了NIO,在非阻塞的技术下更大程度地提高网络请求的性能。
OkHttp的demo使用
package com.benson.android.network
import android.graphics.Color
import android.os.Bundle
import android.view.Gravity
import android.view.ViewGroup
import android.widget.*
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import okhttp3.Request
import java.lang.StringBuilder
import java.util.concurrent.TimeUnit
class Okhttp3DemoActivity: AppCompatActivity() {
val client by lazy { OkHttpClient.Builder()
.retryOnConnectionFailure(true) // 连接超时重试
.connectTimeout(2L, TimeUnit.MINUTES) // 2s 连接超时
.readTimeout(2L, TimeUnit.MINUTES) // 2s 读超时
.writeTimeout(2L, TimeUnit.MINUTES) // 2s 写超时
.build()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val content = LinearLayout(this)
content.orientation = LinearLayout.VERTICAL
initView(content)
setContentView(content)
}
private fun initView(content: ViewGroup) {
val searchBar = LinearLayout(this)
searchBar.orientation = LinearLayout.HORIZONTAL
searchBar.gravity = Gravity.CENTER
val input = EditText(this)
input.textSize = DisplayUtil.sp2px(this, 10.0F) * 1.0F
searchBar.addView(input, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 7.0F))
val searchBtn = Button(this)
searchBtn.text = "search"
searchBtn.textSize = DisplayUtil.sp2px(this, 10.0F) * 1.0F
searchBar.addView(searchBtn, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 2.0F))
content.addView(searchBar, LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0, 1.0F))
val result = TextView(this)
result.setBackgroundColor(Color.BLACK)
result.setTextColor(Color.WHITE)
result.textSize = DisplayUtil.sp2px(this, 15.0F) * 1.0F
val scrollView = ScrollView(this)
scrollView.addView(result)
content.addView(scrollView, LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0, 10.0F))
searchBtn.setOnClickListener {
search(result, input.text.toString())
}
}
private fun search(result: TextView, url: String) {
GlobalScope.launch {
val request = Request.Builder()
.url(url)
.get()
.build()
val response = client.newCall(request).execute()
val content = response.body?.string()
val resultStr = StringBuilder()
resultStr.append("code=").append(response.code).append("\n")
.append("msg=").append(response.message).append("\n")
.append("headers=").append(response.headers).append("\n")
.append(content)
result.post {
result.text = resultStr.toString()
}
}
}
}
执行之后效果图如下
这里是使用了同步调用,执行了execute返回一个Response对象,在这个Response中就可以得到Http请求结果的参数。
源码解析——OkHttpClient的创建
OkHttp3的请求过程比较简单,使用也很方便。那还是以创建过程和请求过程两个方面来剖析OkHttp3的原理吧。
OkHttp的Manager对象是OkHttpClient,也是使用Builder模式创建出来的。以下是OkHttpClient.Builder的属性
// 异步请求调度器
internal var dispatcher: Dispatcher = Dispatcher()
// 请求连接池
internal var connectionPool: ConnectionPool = ConnectionPool()
// 各种拦截器
internal val interceptors: MutableList<Interceptor> = mutableListOf()
// 网络拦截器,非websocket的请求才会使用
internal val networkInterceptors: MutableList<Interceptor> = mutableListOf()
// 事件监听器工厂
internal var eventListenerFactory: EventListener.Factory = EventListener.NONE.asFactory()
// 是否支持连接失败重试
internal var retryOnConnectionFailure = true
// 校验相关
internal var authenticator: Authenticator = Authenticator.NONE
internal var followRedirects = true
internal var followSslRedirects = true
internal var cookieJar: CookieJar = CookieJar.NO_COOKIES
internal var cache: Cache? = null
internal var dns: Dns = Dns.SYSTEM
// 代理相关
internal var proxy: Proxy? = null
internal var proxySelector: ProxySelector? = null
internal var proxyAuthenticator: Authenticator = Authenticator.NONE
internal var socketFactory: SocketFactory = SocketFactory.getDefault()
internal var sslSocketFactoryOrNull: SSLSocketFactory? = null
internal var x509TrustManagerOrNull: X509TrustManager? = null
internal var connectionSpecs: List<ConnectionSpec> = DEFAULT_CONNECTION_SPECS
internal var protocols: List<Protocol> = DEFAULT_PROTOCOLS
internal var hostnameVerifier: HostnameVerifier = OkHostnameVerifier
internal var certificatePinner: CertificatePinner = CertificatePinner.DEFAULT
internal var certificateChainCleaner: CertificateChainCleaner? = null
internal var callTimeout = 0
// 连接超时
internal var connectTimeout = 10_000
// 读取数据超时
internal var readTimeout = 10_000
// 写数据超时
internal var writeTimeout = 10_000
internal var pingInterval = 0
internal var minWebSocketMessageToCompress = RealWebSocket.DEFAULT_MINIMUM_DEFLATE_SIZE
internal var routeDatabase: RouteDatabase? = null
每个属性的意义如注释所示,这些属性用Builder构造出一个OkHttpClient之后,相当于就是一个配置了,每次执行请求时都会应用这些配置。
源码解析——Request的创建
Request的创建过程也是使用了Builder模式,主要包括url、method、headers以及body四个参数,这也是http的request的参数了。url主要是请求的地址,可以包含GET请求的参数,method表示http请求类型,一般有GET、POST、PUT、DELETE等。header就是请求头了,主要是包含一些cookie和校验之类的信息,body一般用于POST请求的请求体,例如文件上传。Request也只是一个配置类,真正请求时会读取这些配置。
源码解析——请求过程
OkHttpClient首先使用request构造出一个Call对象,Call接口的实现类是RealCall。这个Call对象就有同步和异步两种执行方式了,分别是调用execute和enqueue方法。RealCall中execute实现代码如下
override fun execute(): Response {
check(executed.compareAndSet(false, true)) { "Already Executed" }
timeout.enter()
callStart()
try {
client.dispatcher.executed(this)
return getResponseWithInterceptorChain()
} finally {
client.dispatcher.finished(this)
}
}
首先检查executed是否为false,如果为false则置为true,否则抛异步,这里是保存每个Call对象只能执行一次。然后调用超时计时器接着调用callStart,这是为了通知事件监听,已经调用了callStart。execute执行的重点是最后三个调用,调用OkHttpClient的dispatcher的executed方法,将Call对象添加到runningSyncCalls队列中,getResponseWithInterceptorChain方法取得请求结果对象,在finally中调用OkHttpClient的dispatcher的finished方法,将call对象从call队列中移除,同时执行idleCallback的run方法。在这三个方法调用中getResponseWithInterceptorChain方法又是最重要的那一个。
看下RealCall中getResponseWithInterceptorChain的实现
@Throws(IOException::class)
internal fun getResponseWithInterceptorChain(): Response {
// Build a full stack of interceptors.
val interceptors = mutableListOf<Interceptor>()
interceptors += client.interceptors
interceptors += RetryAndFollowUpInterceptor(client)
interceptors += BridgeInterceptor(client.cookieJar)
interceptors += CacheInterceptor(client.cache)
interceptors += ConnectInterceptor
if (!forWebSocket) {
interceptors += client.networkInterceptors
}
interceptors += CallServerInterceptor(forWebSocket)
val chain = RealInterceptorChain(
call = this,
interceptors = interceptors,
index = 0,
exchange = null,
request = originalRequest,
connectTimeoutMillis = client.connectTimeoutMillis,
readTimeoutMillis = client.readTimeoutMillis,
writeTimeoutMillis = client.writeTimeoutMillis
)
var calledNoMoreExchanges = false
try {
val response = chain.proceed(originalRequest)
if (isCanceled()) {
response.closeQuietly()
throw IOException("Canceled")
}
return response
} catch (e: IOException) {
calledNoMoreExchanges = true
throw noMoreExchanges(e) as Throwable
} finally {
if (!calledNoMoreExchanges) {
noMoreExchanges(null)
}
}
}
首先创建了一个Interceptor的列表,将OkHttpClient创建时传入的interceptor添加进来,同时还添加请求重定向拦截器RetryAndFollowUpInterceptor、cookie处理拦截器BridgeInterceptor、缓存拦截器CacheInterceptor、以及网络连接拦截器ConnectInterceptor,如果这个请求不是WebSocket请求,还会把OkHttpClient中的networkInterceptors添加到拦截器列表中,最后一个是CallServerInterceptor,是为了真正执行网络请求,并解析请求结果的。拦截器列表构造好后,new出一个拦截器链RealInterceptorChain,调用RealInterceptorChain的proceed方法开始执行逐个拦截器的intercept方法。RealInterceptorChain的proceed方法中每次都会调用copy方法再创建出一个新的RealInterceptorChain对象,使用这个新的chain对象继续执行下一个Interceptor,我理解这里是为了让每个Interceptor都拥有一个新的执行环境,这个chain里就包含了拦截器的执行环境,这样每个interceptor执行都不会相互影响。
总体上来看,chain的拦截器执行是个职责链模式,每个拦截器都可以实现网络请求过程中的部分功能,最后由CallInterceptor收尾,进行最终的网络请求,每个中间interceptor在拦截处理方法中都最终调用chain的proceed方法返回结果,这就相当于递归调用,让职责链一节一节地执行。那OkHttp的请求执行过程其实就蕴含在提供的几个默认拦截器中了。
OkHttp的默认拦截器
首先是请求重定向拦截器RetryAndFollowUpInterceptor,这个拦截器还会处理请求错误重试的操作。在这个拦截器的intercept方法中有个while(true)循环,在循环体中调用chain的proceed方法,也就是会不断地执行后面的拦截器列表。proceed被catch起来,在catch中调用了RetryAndFollowUpInterceptor的recover方法,这个recover是用来判断是否还可以进行重试的。如果能执行重试,就调用followUpRequest,判断是否可重定向,floowUpRequest方法中取出了response中的code参数,如果code是HPPT_PROXY_AUTH(407),表示使用了代理,如果代理不是HTTP请求,则抛异常,否则使用代码认证并返回一个request对象。如果code是HTTP_UNAUTHORIZED(401)则表示要用户认证。如果code是HTTP_PERM_REDIRECT(308), HTTP_TEMP_REDIRECT(307), HTTP_MULT_CHOICE(300), HTTP_MOVED_PERM(301), HTTP_MOVED_TEMP(302), HTTP_SEE_OTHER(303)则表示重定向,调用buildRedirectRequest构造出一个新的重定向Request对象。如果code是HTTP_CLIENT_TIMEOUT(408)并且未命中过,则执行返回response的request。followUpRequest得到一个新的request对象后,继续while循环中的请求过程,把后面的拦截器再执行一遍。这个while循环中没有看到break关键字,所以这个while循环只有抛出异常和return两条出路。return就是返回一个response给开始的execute,抛异常则有多种,比如不支持重试并且请求异常了,或者重试次数达到最大限制数了,这里的最大重试数据为MAX_FOLLOW_UPS=20。
然后是BridgeInterceptor,这个拦截器是把OkHttp定义的字段转化成Http协议的字段同时处理cookie,这里主要是给request添加一些http请求的默认参数,当请求构造时未传入这些参数时,就会使用BridgeInterceptor的默认参数,以此来构造一个Http协议完整的请求头和请求体。在BridgeInterceptor的intercept中间位置,调用了chain.proceed继续执行后面的拦截器并得到一个Response,这个response被BridgeInterceptor进一步解析。因此这个BridgeInterceptor的真正作用就是在真正请求前给request添加一些默认参数,并将request中传入的参数转化为http协议的参数,例如将content_type转化为header中的"Content-Type",将请求后的结果进行进一步地解析处理。
第三个是缓存相关的CacheInterceptor,在其intercept方法中首先就从传入的cache对象中根据request取出一个cacheCandidate对象,而cache是个Cache对象,默认是以url作为缓存的key标识。然后使用CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()取出缓存的request和response。这里如果request为null并且response也为null,则表示本应从缓存中取,但缓存没有了,那就给一个空的response。如果request为null而response不为null,则表示从缓存中取成功,用缓存的response构造出最终的response。如果request也不为null,表表示还是要进行网络请求,执行chain.proceed,如果response的code是HTTP_NOT_MODIFIED(304),表示服务端告诉客户端,缓存还可以使用,则response还是使用缓存的response来构造,否则就使用网络请求回来的response,并将request和response存入缓存中。
ConnectInterceptor的作用就比较简单了,只是开启一个网络连接,这个网络连接可能会被已经返回的response使用,也可能会被失败了的缓存response使用,OkHttp框架的网络连接使用Exchange对象来表示。
最后就是CallServierInterceptor,最终会使用这个拦截器进行真正的网络请求。
override fun intercept(chain: Interceptor.Chain): Response {
val realChain = chain as RealInterceptorChain
val exchange = realChain.exchange!!
val request = realChain.request
val requestBody = request.body
val sentRequestMillis = System.currentTimeMillis()
exchange.writeRequestHeaders(request)
var invokeStartEvent = true
var responseBuilder: Response.Builder? = null
if (HttpMethod.permitsRequestBody(request.method) && requestBody != null) {
// If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100
// Continue" response before transmitting the request body. If we don't get that, return
// what we did get (such as a 4xx response) without ever transmitting the request body.
if ("100-continue".equals(request.header("Expect"), ignoreCase = true)) {
exchange.flushRequest()
responseBuilder = exchange.readResponseHeaders(expectContinue = true)
exchange.responseHeadersStart()
invokeStartEvent = false
}
if (responseBuilder == null) {
if (requestBody.isDuplex()) {
// Prepare a duplex body so that the application can send a request body later.
exchange.flushRequest()
val bufferedRequestBody = exchange.createRequestBody(request, true).buffer()
requestBody.writeTo(bufferedRequestBody)
} else {
// Write the request body if the "Expect: 100-continue" expectation was met.
val bufferedRequestBody = exchange.createRequestBody(request, false).buffer()
requestBody.writeTo(bufferedRequestBody)
bufferedRequestBody.close()
}
} else {
exchange.noRequestBody()
if (!exchange.connection.isMultiplexed) {
// If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection
// from being reused. Otherwise we're still obligated to transmit the request body to
// leave the connection in a consistent state.
exchange.noNewExchangesOnConnection()
}
}
} else {
exchange.noRequestBody()
}
if (requestBody == null || !requestBody.isDuplex()) {
exchange.finishRequest()
}
if (responseBuilder == null) {
responseBuilder = exchange.readResponseHeaders(expectContinue = false)!!
if (invokeStartEvent) {
exchange.responseHeadersStart()
invokeStartEvent = false
}
}
var response = responseBuilder
.request(request)
.handshake(exchange.connection.handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build()
var code = response.code
if (code == 100) {
// Server sent a 100-continue even though we did not request one. Try again to read the actual
// response status.
responseBuilder = exchange.readResponseHeaders(expectContinue = false)!!
if (invokeStartEvent) {
exchange.responseHeadersStart()
}
response = responseBuilder
.request(request)
.handshake(exchange.connection.handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build()
code = response.code
}
exchange.responseHeadersEnd(response)
response = if (forWebSocket && code == 101) {
// Connection is upgrading, but we need to ensure interceptors see a non-null response body.
response.newBuilder()
.body(EMPTY_RESPONSE)
.build()
} else {
response.newBuilder()
.body(exchange.openResponseBody(response))
.build()
}
if ("close".equals(response.request.header("Connection"), ignoreCase = true) ||
"close".equals(response.header("Connection"), ignoreCase = true)) {
exchange.noNewExchangesOnConnection()
}
if ((code == 204 || code == 205) && response.body?.contentLength() ?: -1L > 0L) {
throw ProtocolException(
"HTTP $code had non-zero Content-Length: ${response.body?.contentLength()}")
}
return response
}
首先调用了exchange.writeRequestHeaders将请求头写入到连接,如果请求不是GET或者PUT的,就要先处理请求体,使用exchange创建Sink对象并将requestBody写入到Sink中去。
这里需要再回到ConnectInterceptor中去看如果创建的一个连接,一个连接的底层是用什么实现的。前面说到过,interceptor chain中的exchange就是一个连接的抽象,在ConnectInterceptor中就对exchange进行了初始化。调用了RealCall的initExchange方法,在initExchange方法中使用了exchangeFinder,这个finder中拥有一个连接池和一个地址,地址由请求的url构建,而连接池是OkHttpClient的连接池,也就是说同一个OkHttpClient发出的请求,共用一个连接池,当同一个OkHttpClient对一个地址发送多个请求时,可能就可以复用连接。使用exchangeFinder调用其find方法是为创建一个ExchangeCodec对象,在find方法中调用findHealthyConnection方法先获取一个连接对象,根据方法名就可以看出,这个方法会去连接池找可以复用的连接。
findHealthConnection方法中在一个while循环中调用findConnection方法获取一个连接,如果判断连接isHealth,则return。继续跟到findConnection方法中去,首先判断这个call对象中是否已有connection,如果有则判断其可用的话,就复用这个connection。否则再调用RealConnectionPool的callAcquirePooledConnection方法从连接池中获取,连接池中有个RealConnection的ConcurrentLinkedQueue用来保存连接,循环判断连接池中的连接可以被传入的地址使用的话,就将这个连接设置给call对象并将这个连接对象返回。如果从连接池中取不到连接,则新建一个RealConnection对象,并调用其connect方法新建一个连接。connect方法中调用了connectSocket方法创建了Socket对象,并将Socket保存在RealConnection的rawSocket属性中,创建Socket后调用其connect方法与服务端建立一个网络连接。
再回到CallServerInterceptor中,当请求体写完后,调用exchanged的flushRequest方法,flushRequest方法中调用了BufferSink的flush方法,这个方法里最终调用了Socket的OutputStream的write,将数据写到服务端连接中去。
写完请求头数据了就需要开始读服务端返回的http结果了,先调用exchange.readResponseHeaders读取响应头,读取响应头时,把response的code、message、protocol也同时给解析了。如果判断响应体为null,则将请求体数据写到服务端。将读取到的responseBuilder构造成一个response对象,判断如果不是websocket并且code不是101,则还需要请求响应体数据,这样一个请求Response才算完全构造成功,最后将这个response返回给拦截链。
总结
OkHttp框架基于职责链模式,将一个请求的过程分拆为多个小过程,每个小过程由一个Interceptor来完成,并且使用者可以在所有内置interceptor之前插入自定义的interceptor对请求过程加以扩展。网络连接部分OkHttp框架使用Exchange对象予以抽象,Interceptor只实现请求逻辑,具体的数据读写由Exchange来完成,因此理论来说底层的数据请求我们还可以替换。整个框架对于逻辑处理与数据处理耦合较低,扩展性较强。