OKHttp是Square公司辨析的一个网络请求框架,也是目前市面上使用最多的网络框架之一。OKHttp是基于HTTP协议封装的一套请求客户端,在请求底层支持连接同一个地址的链接共享同一个Socket。
OkHttp作为当前Android端最火热的网络请求框架之一,有很多的优点:
这篇文章主要针对OKHttp的工作原理进行分析,着重介绍OKHttp实现的原理以及工作流程。
以下是基于OKHttp 3.9.x分析
首先,我们先来看下OKHttp的使用。
OkHttpClient client = new OkHttpClient();//创建OkHttpClient对象 Request request = new Request.Builder() .url(url)//请求链接 .build();//创建Request对象 Response response = client.newCall(request).execute();//获取Response对象 复制代码
以上代码是OKHttp的GET请求的同步请求用法。可以看到,第一步是创建OKHttpClient对象,然后创建Request,最后发起请求并获取请求结果Response。我们针对上面的请求流程开始分析OKHttp的工作原理。
从代码中可以看出,在使用OkHttp时需要先创建OkHttpClient对象。
public OkHttpClient() { this(new Builder()); } OkHttpClient(Builder builder) { this.dispatcher = builder.dispatcher; this.proxy = builder.proxy; //...... this.connectTimeout = builder.connectTimeout; this.readTimeout = builder.readTimeout; this.writeTimeout = builder.writeTimeout; this.pingInterval = builder.pingInterval; } 复制代码
上面的代码就是OkHttpClient的构造方法。可以看到OkHttpClient有两个构造方法,在构造方法中我们可以看到会初始化一个Builder对象( OKHttp使用了建造者模式 ),根据构造方法的代码,很容易发现在构造方法中主要设置了一些OKHttp的属相。比如:超时设置、拦截器、HTTPS相关等。
接下来开始创建Request对象,Request描述了OkHttp将要发送的请求。比如:URL、HTTP header、请求类型(GET请求或者POST请求)等。
Request(Builder builder) { this.url = builder.url; this.method = builder.method; this.headers = builder.headers.build(); this.body = builder.body; this.tag = builder.tag != null ? builder.tag : this; } 复制代码
可以看到Request也是通过建造者模式创建的,在这里配置了url、请求头等信息。
在上面OKHttpClient和Request创建好之后,就开始发起HTTP请求了。OkHttp中请求方式分为同步请求( client.newCall(request).execute() )和异步请求( client.newCall(request).enqueue() )两种,其中同步请求和一部请求的区别就是同步请求会阻塞当前线程,一部请求会放到线程池中执行。
public Call newCall(Request request) { return RealCall.newRealCall(this, request, false /* for web socket */); } static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) { // Safely publish the Call instance to the EventListener. RealCall call = new RealCall(client, originalRequest, forWebSocket); call.eventListener = client.eventListenerFactory().create(call); return call; } 复制代码
可以看到通过newCall()方法创建了RealCall实例,然后通过RealCall发起请求。接下来我们同步OkHttp的异步请求分析。异步请求调用了RealCall的enqueue()方法。
public void enqueue(Callback responseCallback) { synchronized (this) { if (executed) throw new IllegalStateException("Already Executed"); executed = true; } captureCallStackTrace(); eventListener.callStart(this); client.dispatcher().enqueue(new AsyncCall(responseCallback)); } 复制代码
在这里,OkHttp通过调度器Dispatcher执行请求。
/**Dispatcher**/ synchronized void enqueue(AsyncCall call) { //这里判断队列是否已满,队列不满怎将请求放到线程池中执行,否则加入到队列中 if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) { runningAsyncCalls.add(call); executorService().execute(call); } else { readyAsyncCalls.add(call); } } 复制代码
可以看到enqueue()方法是一个同步方法,在这里首先判断了请求队列是否已满,如果不满,则开始在线程池中执行请求AsyncCall。AsyncCall继承了NamedRunnable抽象类,而NamedRunnable继承了Runnable接口,在run方法中调用了execute()方法。
protected void execute() { boolean signalledCallback = false; try { //通过责任链模式执行接下来请求任务 Response response = getResponseWithInterceptorChain(); if (retryAndFollowUpInterceptor.isCanceled()) { signalledCallback = true; //执行失败回调 responseCallback.onFailure(RealCall.this, new IOException("Canceled")); } else { signalledCallback = true; //执行成功回调 responseCallback.onResponse(RealCall.this, response); } } //...... finally { client.dispatcher().finished(this); } } 复制代码
在这里开始了OkHttp核心的请求部分。在OkHttp中使用了责任链模式处理这一部分的请求。getResponseWithInterceptorChain()开始请求。
Response getResponseWithInterceptorChain() throws IOException { // Build a full stack of interceptors. List<Interceptor> interceptors = new ArrayList<>(); interceptors.addAll(client.interceptors()); //自定义的拦截器 interceptors.add(retryAndFollowUpInterceptor); //重试拦截器,请求失败后重试 interceptors.add(new BridgeInterceptor(client.cookieJar())); //桥接拦截器,处理请求 interceptors.add(new CacheInterceptor(client.internalCache())); //缓存拦截器,处理请求缓存 interceptors.add(new ConnectInterceptor(client)); //连接拦截器,创建HTTP连接 if (!forWebSocket) { interceptors.addAll(client.networkInterceptors()); } interceptors.add(new CallServerInterceptor(forWebSocket)); //网络请求拦截器,开始网络请求 Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0, originalRequest, this, eventListener, client.connectTimeoutMillis(), client.readTimeoutMillis(), client.writeTimeoutMillis()); return chain.proceed(originalRequest); } 复制代码
在上面的代码中OkHttp通过各种拦截器处理请求。这里简单介绍下OkHttp的拦截器:
拦截器是OkHttp发起请求的核心部分,接下来我们针对各种拦截器进行分析。上面的代码中,通过RealInterceptorChain的proceed()方法开始执行拦截器。
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec, RealConnection connection) throws IOException { calls++; RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec, connection, index + 1, request, call, eventListener, connectTimeout, readTimeout, writeTimeout); Interceptor interceptor = interceptors.get(index); Response response = interceptor.intercept(next); //执行拦截器 //...... return response; } 复制代码
这里我们直接分析RetryAndFollowUpInterceptor的intercept()方法。
public Response intercept(Chain chain) throws IOException { //...... int followUpCount = 0; Response priorResponse = null; //通过一个循环来重新尝试请求 while (true) { if (canceled) { streamAllocation.release(); throw new IOException("Canceled"); } Response response; boolean releaseConnection = true; try { //1.调用下一个拦截器 response = realChain.proceed(request, streamAllocation, null, null); releaseConnection = false; } catch (RouteException e) { //...... } catch (IOException e) { //...... } //...... //2.检测response是否合法 Request followUp = followUpRequest(response); if (followUp == null) { if (!forWebSocket) { streamAllocation.release(); } //3.返回response,请求完成 return response; } //最多尝试20次 if (++followUpCount > MAX_FOLLOW_UPS) { streamAllocation.release(); throw new ProtocolException("Too many follow-up requests: " + followUpCount); } //4.重新设置请求 request = followUp; priorResponse = response; } } 复制代码
在RetryAndFollowUpInterceptor中我们可以看到请求的重试是由一个无限循环保持的,同时在代码里还限制了请求的次数,最多尝试20次。RetryAndFollowUpInterceptor的具体逻辑是:
我们看看BridgeInterceptor做了哪些事。
public Response intercept(Chain chain) throws IOException { Request userRequest = chain.request(); Request.Builder requestBuilder = userRequest.newBuilder(); RequestBody body = userRequest.body(); if (body != null) { MediaType contentType = body.contentType(); if (contentType != null) { requestBuilder.header("Content-Type", contentType.toString()); } long contentLength = body.contentLength(); if (contentLength != -1) { requestBuilder.header("Content-Length", Long.toString(contentLength)); requestBuilder.removeHeader("Transfer-Encoding"); } else { requestBuilder.header("Transfer-Encoding", "chunked"); requestBuilder.removeHeader("Content-Length"); } } if (userRequest.header("Host") == null) { requestBuilder.header("Host", hostHeader(userRequest.url(), false)); } if (userRequest.header("Connection") == null) { requestBuilder.header("Connection", "Keep-Alive"); } boolean transparentGzip = false; if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) { transparentGzip = true; requestBuilder.header("Accept-Encoding", "gzip"); } List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url()); if (!cookies.isEmpty()) { requestBuilder.header("Cookie", cookieHeader(cookies)); } if (userRequest.header("User-Agent") == null) { requestBuilder.header("User-Agent", Version.userAgent()); } Response networkResponse = chain.proceed(requestBuilder.build()); HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers()); Response.Builder responseBuilder = networkResponse.newBuilder() .request(userRequest); if (transparentGzip && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding")) && HttpHeaders.hasBody(networkResponse)) { GzipSource responseBody = new GzipSource(networkResponse.body().source()); Headers strippedHeaders = networkResponse.headers().newBuilder() .removeAll("Content-Encoding") .removeAll("Content-Length") .build(); responseBuilder.headers(strippedHeaders); String contentType = networkResponse.header("Content-Type"); responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody))); } return responseBuilder.build(); } 复制代码
从代码里可以看到,在BridgeInterceptor中出了HTTP的请求头,设置了请求头的各种参数,比如:Content-Type、Connection、User-Agent、GZIP等。
缓存拦截器主要是处理HTTP请求缓存的,通过缓存拦截器可以有效的使用缓存减少网络请求。
public Response intercept(Chain chain) throws IOException { Response cacheCandidate = cache != null? cache.get(chain.request()): null;//1.取缓存 long now = System.currentTimeMillis(); CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get(); //2.验证缓存 Request networkRequest = strategy.networkRequest; Response cacheResponse = strategy.cacheResponse; //获取缓存 if (cache != null) { cache.trackResponse(strategy); } // If we're forbidden from using the network and the cache is insufficient, fail. //这里表示禁止使用缓存 if (networkRequest == null && cacheResponse == null) { return new Response.Builder() .request(chain.request()) .protocol(Protocol.HTTP_1_1) .code(504) .message("Unsatisfiable Request (only-if-cached)") .body(Util.EMPTY_RESPONSE) .sentRequestAtMillis(-1L) .receivedResponseAtMillis(System.currentTimeMillis()) .build(); } // If we don't need the network, we're done. //3.直接返回缓存 if (networkRequest == null) { return cacheResponse.newBuilder() .cacheResponse(stripBody(cacheResponse)) .build(); } Response networkResponse = null; try { //4.没有缓存,执行下一个拦截器 networkResponse = chain.proceed(networkRequest); } // If we have a cache response too, then we're doing a conditional get. if (cacheResponse != null) { if (networkResponse.code() == HTTP_NOT_MODIFIED) { Response response = cacheResponse.newBuilder() .headers(combine(cacheResponse.headers(), networkResponse.headers())) .sentRequestAtMillis(networkResponse.sentRequestAtMillis()) .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis()) .cacheResponse(stripBody(cacheResponse)) .networkResponse(stripBody(networkResponse)) .build(); networkResponse.body().close(); // Update the cache after combining headers but before stripping the // Content-Encoding header (as performed by initContentStream()). cache.trackConditionalCacheHit(); //5.更新缓存 cache.update(cacheResponse, response); return response; } else { closeQuietly(cacheResponse.body()); } } //...... if (cache != null) { if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) { // Offer this request to the cache. //6.保存缓存 CacheRequest cacheRequest = cache.put(response); return cacheWritingResponse(cacheRequest, response); } } return response; } 复制代码
在上面的代码中可以看到,OkHttp首先会取出缓存,然后经过验证处理判断缓存是否可用。流程如下:
缓存拦截器主要的工作就是处理缓存,知道了大致流程后,我们接下来分析一下OkHttp是如何管理缓存的。首先我们分析缓存如何获取,在代码中可以看到通过cache.get()得到,我们直接跟代码看。
final InternalCache internalCache = new InternalCache() { @Override public Response get(Request request) throws IOException { return Cache.this.get(request); } @Override public CacheRequest put(Response response) throws IOException { return Cache.this.put(response); } @Override public void remove(Request request) throws IOException { Cache.this.remove(request); } @Override public void update(Response cached, Response network) { Cache.this.update(cached, network); } @Override public void trackConditionalCacheHit() { Cache.this.trackConditionalCacheHit(); } @Override public void trackResponse(CacheStrategy cacheStrategy) { Cache.this.trackResponse(cacheStrategy); } }; 复制代码
可以看到,缓存是通过InternalCache管理的,而InternalCache是Cache的内部了类,InternalCache又调用了Cache的方法。我们这里只分析一个get()方法。
@Nullable Response get(Request request) { String key = key(request.url()); DiskLruCache.Snapshot snapshot; Entry entry; try { snapshot = cache.get(key); if (snapshot == null) { return null; } } catch (IOException e) { return null; } try { entry = new Entry(snapshot.getSource(ENTRY_METADATA)); } catch (IOException e) { Util.closeQuietly(snapshot); return null; } Response response = entry.response(snapshot); //...... return response; } 复制代码
可以看到,缓存是通过DiskLruCache管理,那么不难看出OkHttp的缓存使用了LRU算法管理缓存。接下来,我们分析下OkHttp如何验证缓存。
在上面的代码中,缓存最终来自于CacheStrategy。我们直接分析下那里的代码。
public Factory(long nowMillis, Request request, Response cacheResponse) { //...... if (cacheResponse != null) { this.sentRequestMillis = cacheResponse.sentRequestAtMillis(); this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis(); Headers headers = cacheResponse.headers(); for (int i = 0, size = headers.size(); i < size; i++) { String fieldName = headers.name(i); String value = headers.value(i); //这里获取了缓存的时间限制 if ("Date".equalsIgnoreCase(fieldName)) { servedDate = HttpDate.parse(value); servedDateString = value; } else if ("Expires".equalsIgnoreCase(fieldName)) { expires = HttpDate.parse(value); } else if ("Last-Modified".equalsIgnoreCase(fieldName)) { lastModified = HttpDate.parse(value); lastModifiedString = value; } else if ("ETag".equalsIgnoreCase(fieldName)) { etag = value; } else if ("Age".equalsIgnoreCase(fieldName)) { ageSeconds = HttpHeaders.parseSeconds(value, -1); } } } } public CacheStrategy get() { CacheStrategy candidate = getCandidate(); if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) { // We're forbidden from using the network and the cache is insufficient. return new CacheStrategy(null, null); } return candidate; } private CacheStrategy getCandidate() { //...... long ageMillis = cacheResponseAge(); long freshMillis = computeFreshnessLifetime(); //计算缓存是否过期 //...... return new CacheStrategy(conditionalRequest, cacheResponse); } private long computeFreshnessLifetime() { //判断缓存是否过期 CacheControl responseCaching = cacheResponse.cacheControl(); if (responseCaching.maxAgeSeconds() != -1) { return SECONDS.toMillis(responseCaching.maxAgeSeconds()); } else if (expires != null) { long servedMillis = servedDate != null ? servedDate.getTime() : receivedResponseMillis; long delta = expires.getTime() - servedMillis; return delta > 0 ? delta : 0; } else if (lastModified != null && cacheResponse.request().url().query() == null) { long servedMillis = servedDate != null ? servedDate.getTime() : sentRequestMillis; long delta = servedMillis - lastModified.getTime(); return delta > 0 ? (delta / 10) : 0; } return 0; } 复制代码
我们知道在HTTP请求中判断缓存是否过期的依据就是根据请求头的字段来判断比如Last-Modified、expires( 不同的HTTP版本也有不同的字段表示 )。在OkHttp中也是根据这些依据。
在上面的代码中,我们可以看到首先会从缓存中获取请求头字段中的时间数据,然后在构造CacheStrategy判断缓存是否过期,最后决定缓存是否合法。
ConnectInterceptor的作用就是建立一个与服务端的连接。
public Response intercept(Chain chain) throws IOException { RealInterceptorChain realChain = (RealInterceptorChain) chain; Request request = realChain.request(); StreamAllocation streamAllocation = realChain.streamAllocation(); boolean doExtensiveHealthChecks = !request.method().equals("GET"); HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks); RealConnection connection = streamAllocation.connection(); return realChain.proceed(request, streamAllocation, httpCodec, connection); } 复制代码
在上面的代码中,可以看到连接来自于StreamAllocation的newStream()方法。
public HttpCodec newStream( OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) { int connectTimeout = chain.connectTimeoutMillis(); int readTimeout = chain.readTimeoutMillis(); int writeTimeout = chain.writeTimeoutMillis(); boolean connectionRetryEnabled = client.retryOnConnectionFailure(); try { RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled, doExtensiveHealthChecks); HttpCodec resultCodec = resultConnection.newCodec(client, chain, this); synchronized (connectionPool) { codec = resultCodec; return resultCodec; } } catch (IOException e) { throw new RouteException(e); } } 复制代码
可以看到在newStream()方法中会继续寻找连接。我们继续分析代码可以看到,OkHttp的连接是维护在一个连接池中的。
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout, boolean connectionRetryEnabled) throws IOException { boolean foundPooledConnection = false; RealConnection result = null; Route selectedRoute = null; Connection releasedConnection; Socket toClose; synchronized (connectionPool) { if (released) throw new IllegalStateException("released"); if (codec != null) throw new IllegalStateException("codec != null"); if (canceled) throw new IOException("Canceled"); // Attempt to use an already-allocated connection. We need to be careful here because our // already-allocated connection may have been restricted from creating new streams. releasedConnection = this.connection; toClose = releaseIfNoNewStreams(); if (this.connection != null) { // We had an already-allocated connection and it's good. result = this.connection; releasedConnection = null; } if (!reportedAcquired) { // If the connection was never reported acquired, don't report it as released! releasedConnection = null; } if (result == null) { // Attempt to get a connection from the pool. Internal.instance.get(connectionPool, address, this, null); if (connection != null) { foundPooledConnection = true; result = connection; } else { selectedRoute = route; } } } closeQuietly(toClose); if (releasedConnection != null) { eventListener.connectionReleased(call, releasedConnection); } if (foundPooledConnection) { eventListener.connectionAcquired(call, result); } if (result != null) { // If we found an already-allocated or pooled connection, we're done. return result; } // If we need a route selection, make one. This is a blocking operation. boolean newRouteSelection = false; if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) { newRouteSelection = true; routeSelection = routeSelector.next(); } synchronized (connectionPool) { if (canceled) throw new IOException("Canceled"); if (newRouteSelection) { // Now that we have a set of IP addresses, make another attempt at getting a connection from // the pool. This could match due to connection coalescing. List<Route> routes = routeSelection.getAll(); for (int i = 0, size = routes.size(); i < size; i++) { Route route = routes.get(i); Internal.instance.get(connectionPool, address, this, route); if (connection != null) { foundPooledConnection = true; result = connection; this.route = route; break; } } } if (!foundPooledConnection) { if (selectedRoute == null) { selectedRoute = routeSelection.next(); } // Create a connection and assign it to this allocation immediately. This makes it possible // for an asynchronous cancel() to interrupt the handshake we're about to do. route = selectedRoute; refusedStreamCount = 0; result = new RealConnection(connectionPool, selectedRoute); acquire(result, false); } } // If we found a pooled connection on the 2nd time around, we're done. if (foundPooledConnection) { eventListener.connectionAcquired(call, result); return result; } // Do TCP + TLS handshakes. This is a blocking operation. result.connect( connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled, call, eventListener); routeDatabase().connected(result.route()); Socket socket = null; synchronized (connectionPool) { reportedAcquired = true; // Pool the connection. Internal.instance.put(connectionPool, result); // If another multiplexed connection to the same address was created concurrently, then // release this connection and acquire that one. if (result.isMultiplexed()) { socket = Internal.instance.deduplicate(connectionPool, address, this); result = connection; } } closeQuietly(socket); eventListener.connectionAcquired(call, result); return result; } 复制代码
以上是OkHttp获取连接的主要逻辑,方法比较复杂,我们这里总结一下获取连接的流程,具体的细节可以自行查看。
CallServerInterceptor是最后一个拦截器,理所当然这个拦截器负责向服务端发送数据。
public Response intercept(Chain chain) throws IOException { //...... //写入请求头数据 httpCodec.writeRequestHeaders(request); realChain.eventListener().requestHeadersEnd(realChain.call(), request); Response.Builder responseBuilder = null; if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) { //...... if (responseBuilder == null) { // Write the request body if the "Expect: 100-continue" expectation was met. realChain.eventListener().requestBodyStart(realChain.call()); long contentLength = request.body().contentLength(); //这里写入请求体 CountingSink requestBodyOut = new CountingSink(httpCodec.createRequestBody(request, contentLength)); BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut); request.body().writeTo(bufferedRequestBody); bufferedRequestBody.close(); realChain.eventListener() .requestBodyEnd(realChain.call(), requestBodyOut.successfulCount); } else if (!connection.isMultiplexed()) { streamAllocation.noNewStreams(); } } //完成请求 httpCodec.finishRequest(); if (responseBuilder == null) { //这里请求返回,读取返回请求头 realChain.eventListener().responseHeadersStart(realChain.call()); responseBuilder = httpCodec.readResponseHeaders(false); } Response response = responseBuilder .request(request) .handshake(streamAllocation.connection().handshake()) .sentRequestAtMillis(sentRequestMillis) .receivedResponseAtMillis(System.currentTimeMillis()) .build(); realChain.eventListener() .responseHeadersEnd(realChain.call(), response); int code = response.code(); if (forWebSocket && code == 101) { response = response.newBuilder() .body(Util.EMPTY_RESPONSE) .build(); } else { //读取返回内容 response = response.newBuilder() .body(httpCodec.openResponseBody(response)) .build(); } //...... return response; } 复制代码
在上面的代码上可以看到主要是由HttpCodec执行的数据写入以及读取。HttpCodec是一个接口,它实现有两个类,分别是Http1Codec(处理HTTP1.1请求)和Http2Codec(处理HTTP2请求)。在HttpCodec的实现中主要通过okio与服务端通信。在上一节的ConnectInterceptor我们知道,OkHttp与服务端建立了一个TCP连接,所以客户端的与服务端的通信是直接通过TCP协议层的,当数据返回时,OkHttp会将数据构造HTTP形式的数据。
OkHttp的工作原理就分析到这里了。在上面的文章中,首先分析了OkHttp在发起请求的准备阶段工作,构造OkHttpClient以及Request,然后通过调度器Dispatcher处理请求任务(请求又分为同步请求和异步请求)。最后通过拦截器处理请求。拦截器作为OkHttp中处理请求的核心部分,我们再文章中对各种拦截器都进行了分型,当然其中还有很多细节没有讲到,感兴趣的同学可以更加深入的去了解。