一、okhttp是什么
本次讲解的okhttp基于版本
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.14.9</version>
</dependency>
GitHub地址:github.com/square/okht…
See the project website for documentation and APIs.
HTTP is the way modern applications network. It’s how we exchange data & media. Doing HTTP efficiently makes your stuff load faster and saves bandwidth.
OkHttp is an HTTP client that’s efficient by default:
- HTTP/2 support allows all requests to the same host to share a socket.
- Connection pooling reduces request latency (if HTTP/2 isn’t available).
- Transparent GZIP shrinks download sizes.
- Response caching avoids the network completely for repeat requests.
OkHttp perseveres when the network is troublesome: it will silently recover from common connection problems. If your service has multiple IP addresses, OkHttp will attempt alternate addresses if the first connect fails. This is necessary for IPv4+IPv6 and services hosted in redundant data centers. OkHttp supports modern TLS features (TLS 1.3, ALPN, certificate pinning). It can be configured to fall back for broad connectivity.
Using OkHttp is easy. Its request/response API is designed with fluent builders and immutability. It supports both synchronous blocking calls and async calls with callbacks.
okhttp是安卓和后端发起http请求的框架。
二、okhttp的优势有哪些
- 支持HTTP2/SPDY
- socket自动选择最好路线,并支持自动重连
- 拥有自动维护的socket连接池,减少握手次数
- 拥有队列线程池,轻松写并发
- 拥有Interceptors轻松处理请求与响应(比如透明GZIP压缩)
- 基于Headers的缓存策略
- 请求恢复:okhttp处理了很多网络疑难杂症:会从很多常用的连接问题中自动恢复。如果您的服务器配置了多个IP地址,当第一个IP连接失败的时候,okhttp会自动尝试下一个IP
三、okhttp架构图
| 组件 | 作用 |
|---|---|
| Interface | 对外暴露使用的类 |
| Protocol | 处理协议、代理相关 |
| InterceptorChain | okhttp采用责任链执行任务,每个责任链负责不同的任务,支持用户自定义责任链 |
| Connection | 处理网络连接相关,包括连接池,路由,DNS等 |
| Cache | 支持处理http协议中的缓存 |
| I/O | 数据IO |
四、okhttp流程图
-
创建一个okHttpClient对象(如果是spring项目一般整个项目创建一个okhttpClient对象即可,避免内存溢出),构建Request对象用于存储请求的url、method、header和body。
-
构建RealCall作为一个请求的载体。
-
okhttp里支持同步请求和异步请求,我们一般常用的是同步请求.
OkHttpClient okHttpClient = new OkHttpClient();
Request request = new Request.Builder().url("https://www.baidu.com").build();
MediaType MEDIA_JSON = MediaType.parse("application/json; charset=utf-8");
RequestBody body = RequestBody.create(MEDIA_JSON, "{}".getBytes(StandardCharsets.UTF_8));
// 同步请求示例
Response response = okHttpClient.newCall(request).execute();
// 异步请求示例
okHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
}
});
对于同步请求来说,直接就是主线程发起请求没有限制,对于一个新的请求来说直接就执行请求了。
而异步请求,如果当前请求数大于Dispathcer里面的maxRequest = 64 或者 maxRequestsPerHost = 5(同一个host,例如www.baodu.com 就是一个host,的链接是否小于maxRequestsPerHost),则会将请求先放入准备队列readyAsyncCalls等待执行,否则放入runningAsyncCalls队列立即执行。每个请求执行完成就会从running队列移除,同时进行查看准备队列readyAsyncCalls是否有满足条件的任务可以执行。
/** Ready async calls in the order they'll be run. */
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
/** Running asynchronous calls. Includes canceled calls that haven't finished yet. */
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
而不是被异步调用里面的线程池所限制住。
public synchronized ExecutorService executorService() {
if (executorService == null) {
executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
new SynchronousQueue<>(), Util.threadFactory("OkHttp Dispatcher", false));
}
return executorService;
}
当一个任务通过execute(Runnable)方法添加到线程池时:
- 线程数小于corePoolSize:新建线程来处理被添加的任务;
线程数量大于等于corePoolSize,新任务被添加到等待队列,添加失败:
- 线程数小于maxmumPoolSize,新建线程执行新任务
- 线程数等于maxmumPoolSize,使用RejectedExecutionHandler拒绝策略
这个线程池的核心线程数是0,意味着没有一直保持的线程。最大线程数是整数的最大值,说明并发没有上限。因为线程池的队列是SynchronousQueue,这个队列的容量为0,入不了队列,所以当请求来的时候先看是否有可以复用的线程,没有则会创建。每个线程的空闲生存时间最长是60s。这个线程池就是一个没有并发上线的线程池。如果单独使用这个线程池,如果请求量大了则极可能引发OOM。
因此,okhttp用了两个队列来限制并发度。
- okhttp的主要流程是以责任链的方式实现的,里面包含了okhttp的五个主要的Interceptor(拦截器),也可以自定义Interceptor,我们的网络请求就是这样经过责任链一级一级的递推下去,最终执行到CallServerInterceptor的intercept方法,此方法会将网络响应的结果封装成一个Response对象并return。之后沿着责任链一级一级的回溯,最终就回到getResponseWithInterceptorChain方法的返回。
| 拦截器 | 作用 |
|---|---|
| 应用拦截器 | 拿到的是原始请求,可以添加一些自定义header、通用参数、参数加密、网关接入等等。 |
| RetryAndFollowUpInterceptor | 处理错误重试和重定向 |
| BridgeInterceptor | 应用层和网络层的桥接拦截器,主要工作是为请求添加cookie、添加固定的header,比如Host、Content-Length、Content-Type、User-Agent等等,然后保存响应结果的cookie,如果响应使用gzip压缩过,则还需要进行解压 |
| CacheInterceptor | 缓存拦截器,如果命中缓存则不会发起网络请求 |
| ConnectInterceptor | 连接连接器,内部会维护一个连接池,负责连接复用、创建连接(减少三次握手所需的时间)、释放连接以及创建连接上的socket |
| networkInterceptors(网络拦截器) | 用户自定义拦截器,常用于监控网络层的数据传输 |
| CallServerInterceptor | 请求拦截器,在前置准备工作完成后,真正发起了网络请求 |
五、后端开发常用的调整参数
| 参数 | 作用 |
|---|---|
| maxIdleConnections | 最大空闲连接数 |
| keepAliveDuration | 空闲连接存活时间 |
| connectTimeout | 连接超时 |
| writeTimeout | 写超时 |
| readTimeout | 读超时 |
int maxIdleConnections = 500;
long keepAliveDuration = 60 * 1000;
OkHttpClient okHttpClient =new OkHttpClient.Builder()
.connectionPool(new ConnectionPool(maxIdleConnections, keepAliveDuration, TimeUnit.MILLISECONDS))
.connectTimeout(500, TimeUnit.MILLISECONDS)
.writeTimeout(200, TimeUnit.MILLISECONDS)
.readTimeout(200, TimeUnit.MILLISECONDS)
.build();
connectTimeout、writeTimeout、readTimeout 这三个超时参数分别在不同的网络请求阶段起作用,阐述如下:
-
connectTimeout(连接超时):此参数在TCP握手阶段起作用,当你的客户端尝试与服务器建立连接时,connectTimeout会限制TCP握手的最长时间。如果在此时间段内仍未建立起连接,连接将被中断。 例子:假设你的请求是要登录www.example.com,当你点击登录按钮时,客户端会尝试与服务器建立连接。如果在指定的connectTimeout时间内没有和服务器建立连接,连接将失败。
-
writeTimeout(写入超时):此参数在客户端向服务器发送请求时起作用。当客户端将请求数据写入输出流时,writeTimeout会限制发送请求的最长时间。如果在此时间内没有将请求数据写入输出流,连接会被中断。 例子:继续刚刚登录www.example.com的例子,当客户端与服务器建立连接后,客户端需要将登录请求(包括用户名、密码等)发送给服务器。如果客户端在指定的writeTimeout时间内没有将登录请求发送给服务器,请求将被中止。
-
readTimeout(读取超时):readTimeout参数在客户端接收服务器响应时起作用。当客户端从输入流中读取服务器响应时,readTimeout会限制读取响应数据的最长时间。在此时间内,如果客户端没有获取到完整的服务器响应,连接会被中断。 例子:还是刚才登录www.example.com的例子,当服务器接收到客户端的登录请求后,它会处理请求并将响应发送给客户端。假设服务器发送的响应数据包括登录成功或失败信息。如果在指定的readTimeout时间内客户端没有读取到完整的响应数据,连接将失败。
总结:在整个HTTP请求中,connectTimeout、writeTimeout和readTimeout分别限制了三个关键的网络请求阶段的最长时间,分别是TCP连接、发送请求数据和接收响应数据。这将有助于避免客户端在遇到网络阻塞时导致的长时间等待,从而提高用户体验。
5.1 连接池逻辑
-
直接先从连接池里面拿可以复用的(复用的条件是host相同,连接有效等等),如果没有就新建一个放到队列里面执行,标记为正在执行,然后执行完了,这个标记就会被清除
-
后面有个线程会去执行cleanup任务的时候会检查队列里面没有标记正在执行的任务(也就是空闲任务)是不是超过了keepalive时间,他们的个数是不是超过了maxidleconnection,超过了就移除那几个时间最长的
-
对于如果找不到连接池里面可以复用的连接的请求,不会被maxidleconnection所制约,会直接new一个新的连接出来
5.1 获取不到可用路由
// 当日志中出现关键字,说明获取不到可用路由
exhausted all routes
5.2 异步请求监控readyAsyncCalls 调整maxRequests maxRequestsPerHost
这个readyAsyncCalls队列里面存着即将执行的任务,监控这个队列的变化我们就能知道okhttp是否有很多的任务挤压着。
新建连接时只要满足了条件:
连接数 < maxRequests 并且 同一域名(例如:www.baidu.com )的连接数 < maxRequestsPerHost,就可以加入执行队列(runningAsyncCalls)执行。调整这两个值对okhttp的并发度提升是有帮助的。
参考资料
- okhttp源码
- okhttp ConnectInterceptor源码介绍
- okhttp源码学习资料