今天来聊一聊 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) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。