今天来聊一聊 Http协议 的请求客户端。
OkHttp 是一个由 Square 公司开发的高性能 HTTP 客户端库,专门为 Java 和 Android 应用程序设计。Square 是一家知名的金融科技公司,开发了多个流行的开源项目。OkHttp 的设计初衷是为了解决 Android 平台上原生 HTTP 客户端(如 HttpURLConnection)的某些局限性,特别是在处理连接管理和缓存等方面。
OkHttp 是一个开源项目,托管在 GitHub 上。它以其高效的资源管理、支持 HTTP/2、连接池和透明的 GZIP 压缩等特性而闻名。下面是 OkHttp 的组成模块、工作原理以及一些示例代码。
1. 功能模块
OkHttpClient:
OkHttpClient是 OkHttp 的核心类,负责配置和执行 HTTP 请求。它管理所有的网络操作,包括连接池、重定向、缓存和拦截器。示例:
OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .writeTimeout(15, TimeUnit.SECONDS) .build();
Request:
Request表示一个 HTTP 请求。使用Request.Builder来构建请求,包括 URL、请求头、请求方法(GET、POST 等)和请求体。示例:
Request request = new Request.Builder() .url("https://www.example.com") .header("User-Agent", "OkHttp Example") .build();
Response:
Response表示 HTTP 响应,包含响应码、响应头、响应体等信息。通过Response对象可以读取服务器返回的数据。示例:
Response response = client.newCall(request).execute(); if (response.isSuccessful()) { System.out.println(response.body().string()); }
Call:
Call代表一次 HTTP 请求的可执行调用,可以是同步的(execute())或异步的(enqueue())。示例:
Call call = client.newCall(request);
Interceptor:
- 拦截器用于对请求和响应进行拦截和修改,分为应用拦截器和网络拦截器。应用拦截器在请求和响应过程中调用一次,网络拦截器在请求和响应通过网络时调用。
示例:
client = new OkHttpClient.Builder() .addInterceptor(chain -> { Request newRequest = chain.request().newBuilder() .header("Authorization", "Bearer token") .build(); return chain.proceed(newRequest); }) .build();
Connection Pool:
- 管理 HTTP 连接的复用,减少建立连接的开销。OkHttp 默认启用连接池以提高性能。
Cache:
- 支持对响应进行缓存,减少不必要的网络请求。可以通过配置缓存目录和大小来启用缓存。
示例:
int cacheSize = 10 * 1024 * 1024; // 10 MiB Cache cache = new Cache(new File("cacheDirectory"), cacheSize); client = new OkHttpClient.Builder() .cache(cache) .build();
2. 工作原理
请求创建:
- 通过
Request.Builder创建请求对象,设置 URL、方法、头信息和请求体等。
- 通过
请求执行:
- 创建
Call对象并调用execute()方法进行同步请求,或者enqueue()方法进行异步请求。 - 异步请求由调度器管理,它负责请求的排队和线程管理。
- 创建
拦截器链:
- 请求在发送到服务器之前会经过拦截器链,拦截器可以修改请求或响应。
- 拦截器链的执行顺序是:应用拦截器 -> 网络拦截器 -> 网络请求 -> 网络拦截器 -> 应用拦截器。
连接池和缓存:
- OkHttp 使用连接池来复用 HTTP 连接,以减少延迟和资源消耗。
- 响应可以被缓存,后续请求如果命中缓存可以避免网络请求。
响应处理:
- 请求完成后,响应通过
Response对象返回,可以读取响应头和响应体。 - 如果使用异步请求,响应会通过回调函数返回。
- 请求完成后,响应通过
maven依赖
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.10.0</version> <!-- 请使用最新的稳定版本 -->
</dependency>示例代码
以下是一个简单的示例,展示了如何使用 OkHttp 发起同步和异步请求:
import okhttp3.*;
import java.io.IOException;
public class OkHttpExample {
public static void main(String[] args) throws IOException {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url("https://www.example.com")
.build();
// 同步请求
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful()) {
System.out.println("Synchronous response: " + response.body().string());
}
}
// 异步请求
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
e.printStackTrace();
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful()) {
System.out.println("Asynchronous response: " + response.body().string());
}
}
});
}
}3. 异步请求
3.1. 工作原理
OkHttp 是一个高效的 HTTP 客户端库,支持同步和异步请求。它的异步请求机制设计得非常精巧,基于 NIO 设计,有点像 Reactor 模型。
1. 请求创建
当你创建一个异步请求时,你通常会构建一个 Request 对象,并使用 OkHttpClient 的实例调用 newCall(request) 方法来创建一个 Call 对象。这个 Call 对象代表一个可执行的 HTTP 请求。
2. 调度请求
- 当你调用
Call.enqueue(Callback)方法时,OkHttp 会将这个请求提交给它的内部调度器(Dispatcher)。 - 调度器负责管理请求队列和线程池,它决定何时执行请求,并限制同时进行的请求数量以优化资源使用。
3. 线程池
- OkHttp 使用一个内部线程池来执行异步请求。默认情况下,这个线程池会限制同时进行的请求数量(通常为 64 个),以及对同一主机的并发请求数量(通常为 5 个)。
- 线程池的使用避免了为每个请求创建新线程的开销,线程可以被重用以处理多个请求。
4. 非阻塞 I/O
- OkHttp 利用 Java 的 NIO(Non-blocking I/O)库来执行网络操作。这意味着请求的执行不会阻塞线程,线程可以在等待网络响应时处理其他任务。
- 通过使用 Selector 和 Channel,OkHttp 能够在一个线程中管理多个连接,处理 I/O 事件(如连接建立、数据准备好读取等)。
5. 回调机制
- 当网络响应准备好时,OkHttp 调用用户提供的
Callback方法来处理响应。 - 回调在 OkHttp 的内部线程池中执行,主线程不会被阻塞。开发者可以在
onResponse方法中处理成功的响应,或者在onFailure方法中处理失败的请求。
6. 资源管理
- OkHttp 的连接池和线程池确保了资源的高效利用。连接池通过复用连接来减少建立连接的开销,而线程池则通过限制线程数量来减少上下文切换的开销。
- 这些机制使得 OkHttp 能够在处理大量并发请求时保持高性能。
7. 示例代码
以下是一个使用 OkHttp 进行异步请求的简单示例:
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;
public class OkHttpAsyncExample {
public static void main(String[] args) {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url("https://www.example.com")
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
e.printStackTrace();
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful()) {
System.out.println(response.body().string());
} else {
System.out.println("Request failed: " + response.code());
}
}
});
// 主线程可以继续执行其他任务
System.out.println("Request sent, continue with other tasks...");
}
}在这个示例中,enqueue 方法不会阻塞主线程,OkHttp 内部使用线程池来管理网络请求,避免了为每个请求创建新线程的开销。通过这种方式,应用可以高效地处理大量并发请求。
3.2. 调度器
在 OkHttp 中,调度器(Dispatcher)是一个关键组件,它负责管理请求队列和控制请求的执行。调度器的主要作用是优化资源使用和提高并发请求的处理能力,但只在异步请求时起作用。
1. 调度器的作用
管理并发请求:
- 调度器负责管理同时进行的请求数量,确保不会超过预设的最大并发请求数。这有助于避免过多的请求导致的资源耗尽或服务器过载。
请求排队和调度:
- 调度器将请求分为正在执行的请求和等待执行的请求。它会根据可用资源和配置的限制来决定何时执行哪些请求。
按主机限制请求:
- 调度器支持对同一主机的并发请求数量进行限制。这有助于避免对单个服务器的过度请求,防止服务器过载。
优先级和公平性:
- 虽然 OkHttp 默认没有复杂的请求优先级机制,但调度器通过公平性策略来确保请求被合理地调度和执行。
2. 工作原理
请求队列:
- 当一个新的请求被创建并通过
enqueue方法提交时,调度器将其添加到请求队列中。 - 调度器会定期检查当前正在执行的请求数量和等待队列中的请求,决定是否可以执行新的请求。
- 当一个新的请求被创建并通过
最大请求数限制:
- 调度器使用两个主要限制:
maxRequests和maxRequestsPerHost。 maxRequests控制全局并发请求的最大数量。maxRequestsPerHost控制对单个主机的并发请求数量。
- 调度器使用两个主要限制:
请求执行:
- 如果当前正在执行的请求数量少于
maxRequests,并且对某个主机的请求数量少于maxRequestsPerHost,调度器会从等待队列中选择请求并执行。
- 如果当前正在执行的请求数量少于
3. 配置调度器
OkHttp 的调度器可以通过 Dispatcher 类进行配置。以下是一些常用的配置示例:
import okhttp3.Dispatcher;
import okhttp3.OkHttpClient;
public class OkHttpDispatcherExample {
public static void main(String[] args) {
Dispatcher dispatcher = new Dispatcher();
// 设置最大并发请求数量
dispatcher.setMaxRequests(64);
// 设置对单个主机的最大并发请求数量
dispatcher.setMaxRequestsPerHost(5);
OkHttpClient client = new OkHttpClient.Builder()
.dispatcher(dispatcher)
.build();
// 使用 client 执行请求
}
}4. 调优策略
调整
maxRequests和maxRequestsPerHost:- 根据应用的需求和网络环境,合理设置这两个参数。对于高并发需求的应用,可以适当增加
maxRequests,但要注意服务器的承载能力。 - 对于访问同一主机的请求,可以根据服务器的负载能力调整
maxRequestsPerHost。
- 根据应用的需求和网络环境,合理设置这两个参数。对于高并发需求的应用,可以适当增加
监控和调整:
- 在实际应用中,监控请求的执行情况和服务器的响应时间,根据负载情况动态调整调度器的配置。
避免过度限制:
- 虽然限制并发请求有助于资源管理,但过度限制可能导致请求处理变慢。因此,在调优时要平衡并发性和资源使用。
3.3. 高并发更推荐异步
通过异步请求的介绍,我们能明显感受到异步请求性能更高,更适合高并发场景。但业务上很多请求是需要同步获取请求结果的,就只能选择同步请求了吗?
在高并发场景下,异步请求可以显著提高系统的响应性和性能。通过适当的机制(如 CompletableFuture、Handler 或 CountDownLatch),可以将异步请求的结果同步传递回主线程,从而满足业务需求。这种方式既保证了主线程的响应性,又充分利用了系统资源。
1. 为什么异步请求更适合高并发场景
避免主线程阻塞:
- 同步请求会导致主线程在等待网络请求完成期间被阻塞,无法处理其他任务。
- 异步请求可以让主线程继续执行其他任务,提高整体的响应性和性能。
资源利用率更高:
- 同步请求在等待网络响应时浪费了宝贵的计算资源,而异步请求可以利用这些资源处理其他任务。
- 异步请求通过多线程或异步调度器来处理网络请求,提高了资源利用率。
更好的并发控制:
- 异步请求可以更好地控制并发请求的数量,避免对服务器造成过大压力。
2. 异步请求与同步结果获取的实现方案
在高并发场景下,可以通过以下几种方式实现异步请求并将结果同步传递回主线程:
使用
Future或CompletableFuture:Future或CompletableFuture可以用来包装异步请求的结果,并在主线程中同步获取。
使用
Handler或Looper(Android 特定):- 在 Android 中,可以使用
Handler或Looper将结果传递回主线程。
- 在 Android 中,可以使用
使用
CountDownLatch:CountDownLatch可以用来同步等待异步请求的结果。
3. 示例代码
例如可以使用 CompletableFuture
import okhttp3.*;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public class AsyncSyncExample {
public static void main(String[] args) {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url("https://www.example.com")
.build();
CompletableFuture<String> future = new CompletableFuture<>();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
e.printStackTrace();
future.completeExceptionally(e);
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful()) {
String responseBody = response.body().string();
future.complete(responseBody);
} else {
future.completeExceptionally(new IOException("Unexpected code " + response));
}
}
});
try {
String result = future.get(); // 阻塞等待结果
System.out.println("Result: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}4. 连接池
OkHttp 的连接池是其高效网络通信的关键组件之一。通过连接池,OkHttp 可以复用已有的连接,减少每次请求时建立新连接的开销,从而显著提高性能。
4.1. 基本概念
1. 连接复用
- 连接复用:OkHttp 的连接池通过复用已有的连接来减少新建连接的开销。连接复用减少了 TCP 和 TLS 握手的频率,降低了请求延迟。
- 持久连接:连接池维持与服务器的持久连接,允许在多个请求之间共享这些连接,从而减少连接建立和关闭的频率。
2. 多路复用
- HTTP/2 支持:在支持 HTTP/2 的情况下,单个连接可以承载多个并发请求,这进一步提高了连接利用率。
3. 连接池的配置
OkHttp 的连接池可以通过 ConnectionPool 类进行配置,主要包括以下几个关键参数:
最大空闲连接数 (
maxIdleConnections):- 允许连接池中保持空闲状态的最大连接数。
- 如果连接池中的空闲连接数超过这个值,多余的连接将被关闭。
- 默认值通常为 5。
保持连接的时间 (
keepAliveDuration):- 空闲连接在被关闭之前保持活动状态的最长时间。
- 单位通常是
TimeUnit(如秒、分钟等)。 - 默认值通常为 5 分钟。
4. 示例代码
下面是一个完整的示例代码,展示了如何配置和使用 OkHttpClient 的连接池:
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
public class ConnectionPoolExample {
public static void main(String[] args) {
// 配置连接池
ConnectionPool connectionPool = new ConnectionPool(
10, // 最大空闲连接数
5, // 保持连接的时间
TimeUnit.MINUTES
);
// 创建 OkHttpClient 实例并设置连接池
OkHttpClient client = new OkHttpClient.Builder()
.connectionPool(connectionPool)
.build();
// 发起请求
Request request = new Request.Builder()
.url("https://example.com")
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
} catch (IOException e) {
e.printStackTrace();
}
}
}4.2. 工作机制
1. 连接获取
- 当一个请求需要连接时,连接池首先检查是否有可用的空闲连接。
- 如果有适合的空闲连接(与目标主机匹配),则复用该连接。
- 如果没有可用连接,连接池会创建一个新的连接。
2. 连接释放
- 请求完成后,连接返回到连接池中,供后续请求复用。
- 如果空闲连接超过
maxIdleConnections,则多余的连接将被关闭。 - 空闲连接在超过
keepAliveDuration后会被关闭。
3. 连接关闭
- 空闲连接在超过
keepAliveDuration后会被关闭。 - 连接池会定期清理过期的空闲连接。
4.3. 连接池作用范围
OkHttp 的连接池是与每个 OkHttpClient 实例绑定的。
有些业务使用时偷懒,消费者项目中定义一个全局的 OkHttpClient,请求不同服务提供者时都使用同一个 OkHttpClient 对象实例。如果不了解连接池的作用范围,可能无法发挥连接复用的作用。
4.3.1. 连接池的作用范围
单个
OkHttpClient实例:- 一个
OkHttpClient实例的连接池管理所有通过该实例发出的请求的连接,无论请求的目标服务有多少个或是否是集群。 - 连接池负责管理这些连接的复用和生命周期。
- 一个
请求多个不同的服务:
- 如果一个
OkHttpClient实例用于请求多个不同的服务,连接池会管理这些服务的连接。 - 每个服务的连接是独立管理的,但都在同一个连接池的上下文中。
- 如果一个
请求集群服务:
- 如果请求的目标服务是一个集群(即多个节点),连接池会为每个节点管理连接。
- 连接池根据主机和端口来管理连接,因此不同节点的连接是分开管理的。
4.3.2. 常见问题
问题1:同一实例请求多种服务提供者问题
有些业务使用时偷懒,消费者项目中定义一个全局的 OkHttpClient,请求不同服务提供者时都使用同一个 OkHttpClient 对象实例。
但针对不同的服务提供者,有些服务请求并发高,有些服务请求并发低,但由于公用同一个 OkHttpClient 对象,即同一个连接池。连接池中必然都被并发高的服务连接占满,其他服务享受不到连接复用的好处。
另外而且并发高的多个不同服务之间,也会因为竞争空闲连接而相互影响。请求A服务的并发高时占满了连接池中的空闲连接,当请求B服务的并发高时发现没有可以复用的空闲连接(请求B服务),因此会关闭请求A服务的连接,在连接池中创建请求B服务的连接。随后等到请求A服务的并发高时,又会恶性循环。
问题2:服务提供者集群多节点问题
如果服务提供者是集群服务,假设有10个节点(IP或端口不同),基于负载均衡提供服务。现在单独基于这个服务提供者创建一个 OkHttpClient 实例,那么连接池中 最大空闲连接数 (maxIdleConnections) 就应该要大于等于10。
因为连接是基于每个节点(IP和端口)匹配的,等10个节点都请求到之后,必然会创建10个连接。
假如只有5个最大空闲连接数,集群通过轮训访问10个节点的话,那么 1~5 节点在访问之后,连接池中创建了5个连接,但在 6~10 节点访问之后,原先的5个连接都会被关闭,又重新创建了5个新连接。如此循环,永远也复用不了连接。
4.3.3.对比dubbo连接池
OkHttp中,一个 OkHttpClient 对象实例对应一个连接池,单个连接是和单个服务提供者的单个节点(IP和端口)匹配的。
回顾一下 Dubbo协议 的连接池,其实也是一个 NettyChannelPool 对象实例对应一个连接池,单个连接也是和单个服务提供者的单个节点(IP和端口)匹配的。
二者的区别在于,Dubbo协议 中,开发者接触不到 NettyChannelPool 对象的创建,Dubbo协议的框架默认给 每个服务提供者的每个节点(IP和端口) 创建一个 NettyChannelPool 对象(即创建一个连接池)。
OkHttp中,OkHttpClient 是需要开发者自己创建的。开发者可以不用像 Dubbo协议 将连接池控制的那么细,追求极致的性能把控。但需要考虑好前面的 常见问题,充分发挥好连接复用的作用。
5. 调度器和连接池
首先前提是,在 OkHttp 中,Dispatcher 主要用于管理 异步请求 的调度和执行,同步请求不生效。
在 OkHttp 中,Dispatcher 管理着最大并发请求数,而最大连接数则由 ConnectionPool 管理。它们分别影响不同的层面,因此在配置时需要根据具体的应用需求来决定它们的值。
通常,最大并发请求数设置得比最大连接数大,以便在保持高效连接复用的同时,支持较高的并发请求量。
1. 最大并发请求数
设置方法:
- 通过
Dispatcher的setMaxRequests(int maxRequests)方法设置最大并发请求数。 - 通过
Dispatcher的setMaxRequestsPerHost(int maxRequestsPerHost)方法设置每个主机的最大并发请求数。
- 通过
作用:
- 限制同时可以执行的 HTTP 请求的数量。
- 有助于控制客户端的资源使用,防止过多并发请求导致的资源耗尽。
2. 最大连接数
设置方法:
OkHttpClient使用ConnectionPool来管理连接。可以通过构造ConnectionPool时指定最大连接数和连接保持时间。new ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit)用于设置连接池的最大空闲连接数及连接保持时间。
作用:
- 限制客户端可以同时打开的网络连接数量。
- 通过重用连接来减少连接建立的开销,从而提高性能。
3. 如何配置
最大并发请求数配置:
- 这个值通常需要设置得相对较高,以允许多个请求同时执行。
- 例如,
dispatcher.setMaxRequests(64)和dispatcher.setMaxRequestsPerHost(5)是常见的默认设置。
最大连接数配置:
- 最大连接数一般设置得相对较低,因为一个连接可以被多个请求复用。
- 例如,
new ConnectionPool(5, 5, TimeUnit.MINUTES)表示最多保持 5 个空闲连接,每个连接的保持时间为 5 分钟。
4. 哪个值更大?
- 通常情况下,最大并发请求数会比最大连接数大。
- 这是因为多个请求可以复用同一个连接,尤其是在 HTTP/1.1 或 HTTP/2 协议中,连接复用是常见的做法。
- 通过适当的设置,可以确保在高并发情况下,连接资源能够被有效地利用,同时又不至于因为过多连接而耗尽系统资源。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用。你还可以使用@来通知其他用户。