前言介绍
大约在2年半之前,就想写一篇关于OKHttp原理的文章,一来深入了解一下其原理,二来希望能在了解原理之后进行更好的使用。但是因为种种原因,一直无限往后推迟,最近因为我们情景智能半个月一次的分享轮到我了,所以有了压力之后,抽出了一整天的时间完成了这篇文章。之所以每半个月搞一次技术分享,其目标之一也是为了督促分享人能够在压力之后,达成设定的目标,所以技术分享最大的收益其实恰恰就是分享者自身。
另外这里也顺便给大家带来一些我对AI工具使用的心得。完成这次分享的时候,充分借助了AI工具。完成简单工作时,AI工具确实能够提供很大的帮助,甚至直接帮助我完成,但是在完成一些具有一定深度的任务时,AI工具给的结果不是很理想往往给的不准确或者就是错误,这也正常,因为具有深度的问题被搜索的频次较少,样本量较低,则准确性就不高。
言归正传,回到OKHttp,OKHttp是一个开源的HTTP客户端库,由Square公司开发,广泛应用于Java和Kotlin应用程序中进行网络请求和处理响应。目前OKHttp已成为安卓端最为主流通信的框架,之前存在的一些自带的框架如HttpClient已经逐渐从源码中都已废弃,那么为什么都在使用OKHttp,OKHttp有哪些优势,我们一起来看一下。
一.基本用法
1.1 构建OKHttpClient
通过三步流程进行进行创建,使用创建者模式。
首先创建Builder;
然后对Builder配置一些参数;
最后通过build生成OKHttpClient对象。
Java val builder = OkHttpClient.Builder() builder.cache(Cache(File(context?.filesDir?.absolutePath + File.separator + "ok"), 100)) client = builder.build() |
1.2 构建Request
通过三步流程进行创建,使用创建者模式。
首先创建Builder对象;
然后对Builder配置一些参数;
最后通过build生成Request对象。
Java val builder = Request.Builder() val cacheBuilder = CacheControl.Builder() cacheBuilder.noStore() builder.cacheControl(cacheBuilder.build()) val request = builder.url("https://www.baidu.com").get().build() |
1.3 构建Call
通过client和request构造生成Call。
Java val newCall = client.newCall(request) |
1.4 同步发送请求
直接使用call.execute()方法发送请求,execute方法阻塞线程。
Java val response = newCall.execute() |
1.5 异步发送请求
直接使用call.enqueue()方法发送请求,enqueue方法不阻塞线程。
Java newCall.enqueue(object : Callback { override fun onFailure(call: okhttp3.Call, e: IOException) { }
override fun onResponse(call: okhttp3.Call, response: okhttp3.Response) { } }) |
1.6 解析Reponse
Java val content = IOHelper.readStrByCode(response.body()?.byteStream(), "utf-8") |
1.7 支持状态监听
Java //OKHttpClient.Builder public Builder eventListener(EventListener eventListener) { if (eventListener == null) throw new NullPointerException("eventListener == null"); this.eventListenerFactory = EventListener.factory(eventListener); return this; } //EventListener public abstract class EventListener { public void callStart(Call call) {} public void dnsStart(Call call, String domainName) {} ... } |
二.请求流程
正常网络请求流程
OKHttp请求流程
1.构建RealCall;
Java val newCall = client.newCall(request) |
2.构建责任链
Java Response getResponseWithInterceptorChain() throws IOException { // Build a full stack of interceptors. //2.构建5+2层拦截器; 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)); 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()); //3.开始一层层处理拦截器流程; Response response = chain.proceed(originalRequest); //4.返回结果。 return response; } |
三.5+2层拦截器
3.1 责任链
调用一层层的往下层递归嵌套调用,结果一层层的向上层返回。
3.2 RetryAndFollowUpInterceptor
RetryAndFollowUpInterceptor是第一个拦截器,主要作用就是两个:重试和重定向。
需要说明的是,这里的重试并不是通常意义上的只要失败了无论怎么样都会再试一次的那个重试,而是满足特定条件下因为某种小的错误才会尝试的重试。
重试逻辑中,如果下层返回异常,如3.2.1代码和3.2.2代码,则进入到Exception的逻辑。
该逻辑中,会通过recover方法进行判断是否需要重试,如果需要则继续循环,否则如3.2.3代码返回reponse结束流程。
Java @Override public Response intercept(Chain chain) throws IOException { while (true) { try { response = realChain.proceed(request, streamAllocation, null, null); releaseConnection = false; } catch( RouteException e ) { //3.2.1代码 if (!recover(e.getLastConnectException(), streamAllocation, false, request)) { throw e.getLastConnectException(); } releaseConnection = false; continue; } catch( IOException e ) { //3.2.2代码 boolean requestSendStarted = !(e instanceof ConnectionShutdownException); if (!recover(e, streamAllocation, requestSendStarted, request)) throw e; releaseConnection = false; continue; } finally { ... } Request followUp = followUpRequest(response, streamAllocation.route()); if (followUp == null) { if (!forWebSocket) { streamAllocation.release(); } //3.2.3代码 return response; } ... if (++followUpCount > MAX_FOLLOW_UPS) { streamAllocation.release(); throw new ProtocolException("Too many follow-up requests: " + followUpCount); } ... } }
private boolean recover(IOException e, StreamAllocation streamAllocation, boolean requestSendStarted, Request userRequest) { streamAllocation.streamFailed(e);
// The application layer has forbidden retries. if (!client.retryOnConnectionFailure()) return false;
// We can't send the request body again. if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody) return false;
// This exception is fatal. if (!isRecoverable(e, requestSendStarted)) return false;
// No more routes to attempt. if (!streamAllocation.hasMoreRoutes()) return false;
// For failure recovery, use the same route selector with a new connection. return true; } |
重试逻辑中
判断是否需要重试的逻辑在recover方法中,主要分为4种。
应用层禁止重试,不能再次请求,发生致命的异常,没有路由可以尝试。
重定向逻辑中
会尝试最多进行20次的重定向,超过20次则认为失败。
3.3 BridgeInterceptor
桥接拦截器的的主要作用有三个:
则顾名思义,header中的各种基础信息,如gzip、keep-alive、text/html等等,进行内容的组装。
接下来看代码
Java public Response intercept(Chain chain) throws IOException { RequestBody body = userRequest.body(); //拼装请求的基础信息 if (body != null) { MediaType contentType = body.contentType(); if (contentType != null) { requestBuilder.header("Content-Type", contentType.toString()); } ... } ... //拼装Cookie List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url()); if (!cookies.isEmpty()) { requestBuilder.header("Cookie", cookieHeader(cookies)); } ... //责任链传递 Response networkResponse = chain.proceed(requestBuilder.build()); //解析响应的基础信息 HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers()); //gzip解压 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(); } |
3.4 CacheInterceptor
CacheInterceptor的主要作用,就是对数据进行缓存以及确认是否使用缓存。
执行流程:
主要分为3块:
Java @Override public Response intercept(Chain chain) throws IOException { //找到缓存 Response cacheCandidate = cache != null ? cache.get(chain.request()) : null; long now = System.currentTimeMillis(); //构建缓存策略,通过缓存策略决定是否使用此次的缓存Reponse CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get(); Request networkRequest = strategy.networkRequest; Response cacheResponse = strategy.cacheResponse; //如果使用缓存,则会把networkRequest设置为空,则返回CacheResponse。 if (networkRequest == null) { return cacheResponse.newBuilder().cacheResponse(stripBody(cacheResponse)).build(); } networkResponse = chain.proceed(networkRequest); //更新缓存Response if (cacheResponse != null) { ... cache.update(cacheResponse, response); return response; } //添加新的缓存,或者根据请求的配置删除缓存。 Response response = networkResponse.newBuilder().cacheResponse(stripBody(cacheResponse)).networkResponse(stripBody(networkResponse)).build(); if (cache != null) { if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) { // Offer this request to the cache. CacheRequest cacheRequest = cache.put(response); return cacheWritingResponse(cacheRequest, response); } if (HttpMethod.invalidatesCache(networkRequest.method())) { cache.remove(networkRequest); } } return response; } |
缓存和缓存策略:
简单看一下缓存和缓存策略。
使用的是LruCache,key为url。
Java public final class Cache implements Closeable, Flushable { final DiskLruCache cache; Cache(File directory, long maxSize, FileSystem fileSystem) { ... this.cache = DiskLruCache.create(fileSystem, directory, 201105, 2, maxSize); } Response get(Request request) { String key = key(request.url()); DiskLruCache.Snapshot snapshot; snapshot = this.cache.get(key); Entry entry = new Entry(snapshot.getSource(0)); Response response = entry.response(snapshot); return response; } } |
3.5 ConnectInterceptor
ConnectInterceptor的主要作用就是创建连接。
请求流程:
Java @Override 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"); //创建,通过域名DNS解析,获取到IP地址和端口,创建连接对象HttpCodec。 HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks); //这里的connection就是上面创建好的 RealConnection connection = streamAllocation.connection(); //交给下一个拦截器处理 return realChain.proceed(request, streamAllocation, httpCodec, connection); } |
- 创建协调类StreamAllocation,它负责协调Collections、Stream、Calls的关系。
- 创建HttpCodec对象,这个对象是经过DNS解析后,直接存储IP地址和端口的类,分为HTTP1.1和HTTP2.0两个版本。下面是我断点获取到的访问baidu的信息
Java Connection{www.baidu.com:443, proxy=DIRECT hostAddress=www.baidu.com/180.101.50.242:443 cipherSuite=TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 protocol=http/1.1} |
我们主要看一下newStream中的流程。这里的核心就是获取一个有效的连接,核心代码在StreamAllocation的findConnection方法中。
主要有三部分,首先尝试从连接池中查找,如果找不到则通过路由生成一个新的连接,最后更新连接到连接池。
Java private RealConnection findConnection() throws IOException { if (result == null) { //获取连接之前,先查询连接缓存池 Internal.instance.get(connectionPool, address, this, null); if (connection != null) { foundPooledConnection = true; result = connection; } else { selectedRoute = route; } } ... //创建新的连接 if (!foundPooledConnection) { if (selectedRoute == null) { selectedRoute = routeSelection.next(); } } //上面找到了连接,则添加到缓存池 Internal.instance.put(connectionPool, result); } |
缓存池获取连接
Java //获取连接 RealConnection get(Address address, StreamAllocation streamAllocation, Route route) { for (RealConnection connection : connections) { if (connection.isEligible(address, route)) { //确定使用直接把connection指向streamAllocation,其实这里返回值并没有用到。 streamAllocation.acquire(connection, true); return connection; } } return null; }
//合法性判断 public boolean isEligible(Address address, @Nullable Route route) { ... //基础类,判断是否正在被使用 if (allocations.size() >= allocationLimit || noNewStreams) return false; //判断地址是否匹配 if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false; //判断URL是否匹配,如果完全匹配则直接返回。 if (address.url().host().equals(this.route().address().url().host())) { return true; // This connection is a perfect match. } // 必须支持HTTP/2 if (http2Connection == null) return false; // 路由必须共享一个IP地址,不支持代理连接 if (route == null) return false; if (route.proxy().type() != Proxy.Type.DIRECT) return false; if (this.route.proxy().type() != Proxy.Type.DIRECT) return false; if (!this.route.socketAddress().equals(route.socketAddress())) return false; // 连接的证书支持新的域名 if (route.address().hostnameVerifier() != OkHostnameVerifier.INSTANCE) return false; ... return true; } |
开始连接
首先通过DNS获取地址
然后通过路由创建连接
Java private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException { ... if (!foundPooledConnection) { if (selectedRoute == null) { selectedRoute = routeSelection.next(); } route = selectedRoute; refusedStreamCount = 0; //创建连接 result = new RealConnection(connectionPool, selectedRoute); acquire(result, false); } //进行握手 result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis, connectionRetryEnabled, call, eventListener); routeDatabase().connected(result.route()); } |
添加连接到连接池
Java private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException { ... Socket socket = null; synchronized (connectionPool) { reportedAcquired = true; // Pool the connection. Internal.instance.put(connectionPool, result); ... } ... eventListener.connectionAcquired(call, result); return result; } |
3.6 CallServerInterceptor
到了这一步,连接已经创建好了。所以CallServerInterceptor的作用就是发送最终的请求到后台。
主要分为几步:
写入header;
写入requestBody;
发送请求流;
读取ResponseHeader;
读取ResponseBody。
Java @Override public Response intercept(Chain chain) throws IOException { //材料准备齐全,开始干活了。 RealInterceptorChain realChain = (RealInterceptorChain) chain; HttpCodec httpCodec = realChain.httpStream(); StreamAllocation streamAllocation = realChain.streamAllocation(); RealConnection connection = (RealConnection) realChain.connection(); Request request = realChain.request(); //写入header httpCodec.writeRequestHeaders(request); //写入requestBody long contentLength = request.body().contentLength(); CountingSink requestBodyOut = new CountingSink(httpCodec.createRequestBody(request, contentLength)); BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut); request.body().writeTo(bufferedRequestBody); bufferedRequestBody.close(); //IO流发送 httpCodec.finishRequest(); //解析ReponseHeader responseBuilder = httpCodec.readResponseHeaders(false); //解析ReponseBody if (forWebSocket && code == 101) { } else { response = response.newBuilder().body(httpCodec.openResponseBody(response)).build(); } return response; } |
写入header的逻辑。
Java @Override public void writeRequestHeaders(Request request) throws IOException { String requestLine = RequestLine.get( request, streamAllocation.connection().route().proxy().type()); writeRequest(request.headers(), requestLine); }
public void writeRequest(Headers headers, String requestLine) throws IOException { if (state != STATE_IDLE) throw new IllegalStateException("state: " + state); sink.writeUtf8(requestLine).writeUtf8("\r\n"); for (int i = 0, size = headers.size(); i < size; i++) { sink.writeUtf8(headers.name(i)) .writeUtf8(": ") .writeUtf8(headers.value(i)) .writeUtf8("\r\n"); } sink.writeUtf8("\r\n"); state = STATE_OPEN_REQUEST_BODY; } |
读取header的逻辑。
Java //Http1Codec public Response.Builder readResponseHeaders(boolean expectContinue) throws IOException { //这里阻塞,等到有响应了才继续往下走。 StatusLine statusLine = StatusLine.parse(readHeaderLine()); Response.Builder responseBuilder = new Response.Builder().protocol(statusLine.protocol).code(statusLine.code).message(statusLine.message).headers(readHeaders()); ... return responseBuilder; } |
3.7 自定义外层拦截器
举2个例子典型的例子:
例子1:内容转换为对象
客户端和服务端进行通信,如果采用的是Protobuff或者其它的数据类型,那么把返回的二进制流,转换为我们想要的对象类型,就可以在外层拦截器执行。
例子2:MOCK数据
我们现在和后台一般是并行开发,并行开发会存在一个问题,客户端前期开发时没有后台接口可用。这时候我们往往会选择mock数据,但是mock的数据是写在逻辑层的,侵入性较高。但是如果我们把mock的逻辑放在外层拦截器中,就会方便很多。首先,逻辑层完全不需要改动;其次拦截器可以进行配置,生产缓存不是使用该拦截器;最后,拦截器还可以设置读取磁盘上的文件甚至本地后台服务,方便内容修改。
还可以有以下作用:
- 修改请求:可以在请求发送之前修改请求的URL头信息或请求体。
- 处理响应:可以在响应接收之后修改响应的头信息或响应体。
- 日志记录:可以记录请求和响应的详细信息,用于调试和监控。
3.8 自定义连接拦截器
- 网络层拦截:networkInterceptors在网络请求和响应的过程中拦截,而普通的Interceptor可以在缓存、重定向等过程中拦截。
- 访问原始数据:networkInterceptors可以访问网络请求和响应的原始数据,包括未解码的响应体。
- 顺序执行:networkInterceptors必须按顺序执行,并且必须调用Chain.proceed()方法继续请求或响应的处理。
3.9 小结
通过一系列的拦截器,完成不同的任务,从而实现一个完整的请求。
四.线程设计
4.1 请求任务线程池
OKHttp任务调度,使用的线程池的设计。
添加任务:
相关代码如下:
Java //如果未超过最大请求数,则直接执行,否则加入等待队列 synchronized void enqueue(AsyncCall call) { if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) { runningAsyncCalls.add(call); executorService().execute(call); } else { readyAsyncCalls.add(call); } } //OKHttp中是一个不设置上限数量的线程池,PS:这个支持外部配置 public synchronized ExecutorService executorService() { if (executorService == null) { executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false)); } return executorService; }
//6个参数 public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, defaultHandler); } |
线程池6个参数:
参数名 | 参数含义 |
corePoolSize | 核心线程数 |
maximumPoolSize | 最大线程数 |
keepAliveTime | 持续存活时间 |
unit | 时间单位 |
workQueue | 任务队列 |
任务执行
Java final class AsyncCall extends NamedRunnable { @Override protected void execute() { try{ Response response = getResponseWithInterceptorChain(); ... } catch (Exception e) { ... responseCallback.onFailure(RealCall.this, e); } } } |
4.2 连接池线程池
连接池线程池用于连接池定期清理一些过期的连接。
创建一个最大线程数量为1,过期时间为60秒的线程池。
Java private final Executor executor = new ThreadPoolExecutor( 0 /* corePoolSize */, 1 /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true)); |
当连接数量大于0时,设置任务定期清理连接。
Java private Runnable cleanupRunnable = new Runnable() { @Override public void run() { while (true) { //cleanup为根据各种条件计算出来的等待时间,这里不是重点就不详细介绍了。 long waitNanos = cleanup(System.nanoTime()); ... ConnectionPool.this.wait(waitMillis, (int) waitNanos); } } }
|
4.3 缓存线程池
缓存线程池用于定期清理一些过期的缓存。
创建最大线程数量为1的线程池。
Java Executor executor = new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), Util.threadFactory("OkHttp DiskLruCache", true)); |
缓存线程池,是每次使用缓存的时候进行过期检查。而只执行一次,清理完成则任务执行完成。
Java //DisLruCache public synchronized Snapshot get(String key) throws IOException { if (journalRebuildRequired()) { executor.execute(cleanupRunnable); } }
// private final Runnable cleanupRunnable = new Runnable() { public void run() { synchronized (DiskLruCache.this) { ... trimToSize(); } } }
private void trimToSize() throws IOException { while (size > maxSize) { Entry toEvict = lruEntries.values().iterator().next(); removeEntry(toEvict); } } |
五.Socket连接
socket连接过程
整个请求过程中,Socket完整连接时间分为写入,等待,读取三个部分。
如果使用读写线程不分离:
如果使用读写线程分离:
OKHttp的socket连接
因此,OKHttp的OKIO,其实就是对SocketInputStream的一层封装,最终还是依赖底层的能力。
反思
所以:为什么OKHttp不是用读写分离?
这个问题就留给读者了。
六.本篇文章未涉及部分
自定义DNS、OKIO、HTTP2.0多路复用等等。
七.随堂小问题
问:OKHttp中涉及到哪些设计模式?
答:构建者、责任链、工厂、观察者。
问:连接池有什么作用?
答:避免重复的握手连接,提高通道复用效率。
问:OKHttp有哪些优势?
答:使用方便、监听->丰富的监听方便排查问题;
自定义责任链->可扩展性强;
责任链->问题排查方便;
连接池复用、支持GZIP压缩、缓存机制等。
问:OKHttp可能存在哪些缺点?
答:频繁创建对象、读写未分离。
问:五层拦截器,第1,2,3,4,5层的作用是什么?
答:RetryAndFollowUpInterceptor:重试和重定向;
BridgeInterceptor:请求和响应时实现对象和文本的转换;
CacheInterceptor:缓存响应,方便复用;
ConnectInterceptor:创建合适的连接并完成握手;
CallServerInterceptor:完成最后的请求发送和响应的解析。
问:统一失败配置,比如不同的请求,无网或若网时失败返回话术不一样,应该怎么做?
答:自定义外层拦截器
问:如果想修改原有的缓存策略,比如后台返回不缓存的,仍要缓存,应该怎么做?
答:自定义连接拦截器中,修改request的url地址即可。
问:再举一些外层拦截器可能使用到的场景?
答:重试、日志记录、加密解密、压缩解压、MOCK工具
问:HTTP1.1和HTTP2.0区别?
答:
多路复用(Multiplexing)
头部压缩(Header Compression)
服务器推送(Server Push)
数据帧(Data Frames)
连接管理(Connection Management)
流量控制(Flow Control)
优先级(Priority)
问:为什么OKHttp使用只包含1个线程的线程池,而不是使用安卓的Handler?
答:个人推测:OKHttp面向的对象并不仅仅只是安卓。