3

今天来聊一聊 Http协议 的请求客户端。

OkHttp 是一个由 Square 公司开发的高性能 HTTP 客户端库,专门为 Java 和 Android 应用程序设计。Square 是一家知名的金融科技公司,开发了多个流行的开源项目。OkHttp 的设计初衷是为了解决 Android 平台上原生 HTTP 客户端(如 HttpURLConnection)的某些局限性,特别是在处理连接管理和缓存等方面。

OkHttp 是一个开源项目,托管在 GitHub 上。它以其高效的资源管理、支持 HTTP/2、连接池和透明的 GZIP 压缩等特性而闻名。下面是 OkHttp 的组成模块、工作原理以及一些示例代码。

1. 功能模块

  1. OkHttpClient

    • OkHttpClient 是 OkHttp 的核心类,负责配置和执行 HTTP 请求。它管理所有的网络操作,包括连接池、重定向、缓存和拦截器。
    • 示例:

      OkHttpClient client = new OkHttpClient.Builder()
          .connectTimeout(10, TimeUnit.SECONDS)
          .readTimeout(30, TimeUnit.SECONDS)
          .writeTimeout(15, TimeUnit.SECONDS)
          .build();
  2. Request

    • Request 表示一个 HTTP 请求。使用 Request.Builder 来构建请求,包括 URL、请求头、请求方法(GET、POST 等)和请求体。
    • 示例:

      Request request = new Request.Builder()
          .url("https://www.example.com")
          .header("User-Agent", "OkHttp Example")
          .build();
  3. Response

    • Response 表示 HTTP 响应,包含响应码、响应头、响应体等信息。通过 Response 对象可以读取服务器返回的数据。
    • 示例:

      Response response = client.newCall(request).execute();
      if (response.isSuccessful()) {
          System.out.println(response.body().string());
      }
  4. Call

    • Call 代表一次 HTTP 请求的可执行调用,可以是同步的(execute())或异步的(enqueue())。
    • 示例:

      Call call = client.newCall(request);
  5. Interceptor

    • 拦截器用于对请求和响应进行拦截和修改,分为应用拦截器和网络拦截器。应用拦截器在请求和响应过程中调用一次,网络拦截器在请求和响应通过网络时调用。
    • 示例:

      client = new OkHttpClient.Builder()
          .addInterceptor(chain -> {
              Request newRequest = chain.request().newBuilder()
                  .header("Authorization", "Bearer token")
                  .build();
              return chain.proceed(newRequest);
          })
          .build();
  6. Connection Pool

    • 管理 HTTP 连接的复用,减少建立连接的开销。OkHttp 默认启用连接池以提高性能。
  7. Cache

    • 支持对响应进行缓存,减少不必要的网络请求。可以通过配置缓存目录和大小来启用缓存。
    • 示例:

      int cacheSize = 10 * 1024 * 1024; // 10 MiB
      Cache cache = new Cache(new File("cacheDirectory"), cacheSize);
      client = new OkHttpClient.Builder()
          .cache(cache)
          .build();

2. 工作原理

  1. 请求创建

    • 通过 Request.Builder 创建请求对象,设置 URL、方法、头信息和请求体等。
  2. 请求执行

    • 创建 Call 对象并调用 execute() 方法进行同步请求,或者 enqueue() 方法进行异步请求。
    • 异步请求由调度器管理,它负责请求的排队和线程管理。
  3. 拦截器链

    • 请求在发送到服务器之前会经过拦截器链,拦截器可以修改请求或响应。
    • 拦截器链的执行顺序是:应用拦截器 -> 网络拦截器 -> 网络请求 -> 网络拦截器 -> 应用拦截器。
  4. 连接池和缓存

    • OkHttp 使用连接池来复用 HTTP 连接,以减少延迟和资源消耗。
    • 响应可以被缓存,后续请求如果命中缓存可以避免网络请求。
  5. 响应处理

    • 请求完成后,响应通过 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. 调度器的作用
  1. 管理并发请求

    • 调度器负责管理同时进行的请求数量,确保不会超过预设的最大并发请求数。这有助于避免过多的请求导致的资源耗尽或服务器过载。
  2. 请求排队和调度

    • 调度器将请求分为正在执行的请求和等待执行的请求。它会根据可用资源和配置的限制来决定何时执行哪些请求。
  3. 按主机限制请求

    • 调度器支持对同一主机的并发请求数量进行限制。这有助于避免对单个服务器的过度请求,防止服务器过载。
  4. 优先级和公平性

    • 虽然 OkHttp 默认没有复杂的请求优先级机制,但调度器通过公平性策略来确保请求被合理地调度和执行。
2. 工作原理
  • 请求队列

    • 当一个新的请求被创建并通过 enqueue 方法提交时,调度器将其添加到请求队列中。
    • 调度器会定期检查当前正在执行的请求数量和等待队列中的请求,决定是否可以执行新的请求。
  • 最大请求数限制

    • 调度器使用两个主要限制:maxRequestsmaxRequestsPerHost
    • 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. 调优策略
  1. 调整 maxRequestsmaxRequestsPerHost

    • 根据应用的需求和网络环境,合理设置这两个参数。对于高并发需求的应用,可以适当增加 maxRequests,但要注意服务器的承载能力。
    • 对于访问同一主机的请求,可以根据服务器的负载能力调整 maxRequestsPerHost
  2. 监控和调整

    • 在实际应用中,监控请求的执行情况和服务器的响应时间,根据负载情况动态调整调度器的配置。
  3. 避免过度限制

    • 虽然限制并发请求有助于资源管理,但过度限制可能导致请求处理变慢。因此,在调优时要平衡并发性和资源使用。

3.3. 高并发更推荐异步

通过异步请求的介绍,我们能明显感受到异步请求性能更高,更适合高并发场景。但业务上很多请求是需要同步获取请求结果的,就只能选择同步请求了吗?

在高并发场景下,异步请求可以显著提高系统的响应性和性能。通过适当的机制(如 CompletableFutureHandlerCountDownLatch),可以将异步请求的结果同步传递回主线程,从而满足业务需求。这种方式既保证了主线程的响应性,又充分利用了系统资源。

1. 为什么异步请求更适合高并发场景
  1. 避免主线程阻塞

    • 同步请求会导致主线程在等待网络请求完成期间被阻塞,无法处理其他任务。
    • 异步请求可以让主线程继续执行其他任务,提高整体的响应性和性能。
  2. 资源利用率更高

    • 同步请求在等待网络响应时浪费了宝贵的计算资源,而异步请求可以利用这些资源处理其他任务。
    • 异步请求通过多线程或异步调度器来处理网络请求,提高了资源利用率。
  3. 更好的并发控制

    • 异步请求可以更好地控制并发请求的数量,避免对服务器造成过大压力。
2. 异步请求与同步结果获取的实现方案

在高并发场景下,可以通过以下几种方式实现异步请求并将结果同步传递回主线程:

  1. 使用 FutureCompletableFuture

    • FutureCompletableFuture 可以用来包装异步请求的结果,并在主线程中同步获取。
  2. 使用 HandlerLooper(Android 特定)

    • 在 Android 中,可以使用 HandlerLooper 将结果传递回主线程。
  3. 使用 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 类进行配置,主要包括以下几个关键参数:

  1. 最大空闲连接数 (maxIdleConnections)

    • 允许连接池中保持空闲状态的最大连接数。
    • 如果连接池中的空闲连接数超过这个值,多余的连接将被关闭。
    • 默认值通常为 5。
  2. 保持连接的时间 (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. 连接池的作用范围

  1. 单个 OkHttpClient 实例

    • 一个 OkHttpClient 实例的连接池管理所有通过该实例发出的请求的连接,无论请求的目标服务有多少个或是否是集群。
    • 连接池负责管理这些连接的复用和生命周期。
  2. 请求多个不同的服务

    • 如果一个 OkHttpClient 实例用于请求多个不同的服务,连接池会管理这些服务的连接。
    • 每个服务的连接是独立管理的,但都在同一个连接池的上下文中。
  3. 请求集群服务

    • 如果请求的目标服务是一个集群(即多个节点),连接池会为每个节点管理连接。
    • 连接池根据主机和端口来管理连接,因此不同节点的连接是分开管理的。

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. 最大并发请求数
  • 设置方法:

    • 通过 DispatchersetMaxRequests(int maxRequests) 方法设置最大并发请求数。
    • 通过 DispatchersetMaxRequestsPerHost(int maxRequestsPerHost) 方法设置每个主机的最大并发请求数。
  • 作用:

    • 限制同时可以执行的 HTTP 请求的数量。
    • 有助于控制客户端的资源使用,防止过多并发请求导致的资源耗尽。
2. 最大连接数
  • 设置方法:

    • OkHttpClient 使用 ConnectionPool 来管理连接。可以通过构造 ConnectionPool 时指定最大连接数和连接保持时间。
    • new ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) 用于设置连接池的最大空闲连接数及连接保持时间。
  • 作用:

    • 限制客户端可以同时打开的网络连接数量。
    • 通过重用连接来减少连接建立的开销,从而提高性能。
3. 如何配置
  1. 最大并发请求数配置:

    • 这个值通常需要设置得相对较高,以允许多个请求同时执行。
    • 例如,dispatcher.setMaxRequests(64)dispatcher.setMaxRequestsPerHost(5) 是常见的默认设置。
  2. 最大连接数配置:

    • 最大连接数一般设置得相对较低,因为一个连接可以被多个请求复用。
    • 例如,new ConnectionPool(5, 5, TimeUnit.MINUTES) 表示最多保持 5 个空闲连接,每个连接的保持时间为 5 分钟。
4. 哪个值更大?
  • 通常情况下,最大并发请求数会比最大连接数大。
  • 这是因为多个请求可以复用同一个连接,尤其是在 HTTP/1.1 或 HTTP/2 协议中,连接复用是常见的做法。
  • 通过适当的设置,可以确保在高并发情况下,连接资源能够被有效地利用,同时又不至于因为过多连接而耗尽系统资源。

KerryWu
641 声望159 粉丝

保持饥饿


引用和评论

0 条评论