自学内容网 自学内容网

揭开 OkHttp3 高效处理 HTTP 请求的神秘面纱:池技术与拦截器

在之前的文章《Spring Boot 项目高效 HTTP 通信:常用客户端大比拼!》里,我们提到了RestTemplat和OkHttp3等Springboot项目开发中最常用的Http客户端。前一篇文章深入 Spring RestTemplate 源码:掌握 HTTP 通信核心技术剖析了RestTemplate客户端的源码,今天,我们就来深入探究一下 OkHttp3 的源码。
在OkHttp3在处理Http请求的过程中,用到了池技术和责任链模式实现的拦截器机制。

一、连接池管理

在 HTTP 通信中,建立连接是一个相对耗时的操作。为了提高性能,OkHttp3 通过连接池复用已经建立的 TCP 连接,减少连接建立和销毁的开销。

1. 连接池的实现

OkHttp3使用了RealConnectionPool和Dispatcher实现连接池的调度。
其内部维护了四个双端队列

  • connections:用于管理连接池中的连接
  • runningAsyncCalls:存储正在运行的异步调用,包括未完成的已取消调用。
  • runningSyncCalls:存储正在运行的同步调用,同样包括未完成的已取消调用。
  • readyAsyncCalls:存储待处理的异步调用,按照执行顺序排列。

2. 连接的获取和释放

当发起一个 HTTP 请求时,OkHttpClient 会将请求委托给 Dispatcher 进行调度。

void enqueue(AsyncCall call) {
  synchronized (this) {
    readyAsyncCalls.add(call);

    // Mutate the AsyncCall so that it shares the AtomicInteger of an existing running call to
    // the same host.
    if (!call.get().forWebSocket) {
      AsyncCall existingCall = findExistingCallWithHost(call.host());
      if (existingCall != null) call.reuseCallsPerHostFrom(existingCall);
    }
  }
  promoteAndExecute();
}

Dispatcher 在调度请求时,会将请求放入 readyAsyncCalls 队列。然后通过 promoteAndExecute 方法将符合条件的请求提升到 runningAsyncCalls 队列,并执行这些请求。

private boolean promoteAndExecute() {
  assert (!Thread.holdsLock(this));

  List<AsyncCall> executableCalls = new ArrayList<>();
  boolean isRunning;
  synchronized (this) {
    for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
      AsyncCall asyncCall = i.next();

      if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
      if (asyncCall.callsPerHost().get() >= maxRequestsPerHost) continue; // Host max capacity.

      i.remove();
      asyncCall.callsPerHost().incrementAndGet();
      executableCalls.add(asyncCall);
      // 满足条件的会被加入runningAsyncCalls队列中
      runningAsyncCalls.add(asyncCall);
    }
    isRunning = runningCallsCount() > 0;
  }

  for (int i = 0, size = executableCalls.size(); i < size; i++) {
    AsyncCall asyncCall = executableCalls.get(i);
    asyncCall.executeOn(executorService());
  }

  return isRunning;
}

请求实际执行时,AsyncCall 对象会调用 RealCall 的executeOn方法来执行。

void executeOn(ExecutorService executorService) {
      assert (!Thread.holdsLock(client.dispatcher()));
      boolean success = false;
      try {
        // 核心方法
        executorService.execute(this);
        success = true;
      } catch (RejectedExecutionException e) {
        InterruptedIOException ioException = new InterruptedIOException("executor rejected");
        ioException.initCause(e);
        transmitter.noMoreExchanges(ioException);
        responseCallback.onFailure(RealCall.this, ioException);
      } finally {
        if (!success) {
          client.dispatcher().finished(this); // This call is no longer running!
        }
      }
    }

而RealConnectionPool会定期清理空闲和过期的连接,这个操作是通过cleanupRunnable完成的。它是一个在特定时间间隔运行的线程,它遍历连接,检查是否有连接已经不再需要或者过期,然后将其清除。

private final Runnable cleanupRunnable = () -> {
    while (true) {
      // 核心方法
      long waitNanos = cleanup(System.nanoTime());
      if (waitNanos == -1) return;
      if (waitNanos > 0) {
        long waitMillis = waitNanos / 1000000L;
        waitNanos -= (waitMillis * 1000000L);
        synchronized (RealConnectionPool.this) {
          try {
            RealConnectionPool.this.wait(waitMillis, (int) waitNanos);
          } catch (InterruptedException ignored) {
          }
        }
      }
    }
  };

核心方法是cleanup(long now), 用于清理闲置的连接, 返回值指示下一次执行清理的时间或立即再次执行
主要功能如下:

  • 统计正在使用和闲置的连接数。
  • 找出最久未使用的连接。
  • 同步处理:
    • 若有连接超过存活期限或闲置连接数过多,则移除并关闭该连接。
    • 否则,计算下次检查时间。
    • 若移除连接,则立即再次执行清理。
long cleanup(long now) {
    int inUseConnectionCount = 0;
    int idleConnectionCount = 0;
    RealConnection longestIdleConnection = null;
    long longestIdleDurationNs = Long.MIN_VALUE;

    // Find either a connection to evict, or the time that the next eviction is due.
    synchronized (this) {
      for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
        RealConnection connection = i.next();

        // If the connection is in use, keep searching.
        if (pruneAndGetAllocationCount(connection, now) > 0) {
          inUseConnectionCount++;
          continue;
        }

        idleConnectionCount++;

        // If the connection is ready to be evicted, we're done.
        long idleDurationNs = now - connection.idleAtNanos;
        if (idleDurationNs > longestIdleDurationNs) {
          longestIdleDurationNs = idleDurationNs;
          longestIdleConnection = connection;
        }
      }

      if (longestIdleDurationNs >= this.keepAliveDurationNs
          || idleConnectionCount > this.maxIdleConnections) {
        // We've found a connection to evict. Remove it from the list, then close it below (outside
        // of the synchronized block).
        connections.remove(longestIdleConnection);
      } else if (idleConnectionCount > 0) {
        // A connection will be ready to evict soon.
        return keepAliveDurationNs - longestIdleDurationNs;
      } else if (inUseConnectionCount > 0) {
        // All connections are in use. It'll be at least the keep alive duration 'til we run again.
        return keepAliveDurationNs;
      } else {
        // No connections, idle or in use.
        cleanupRunning = false;
        return -1;
      }
    }

    closeQuietly(longestIdleConnection.socket());

    // Cleanup again immediately.
    return 0;
  }

3. 连接池的优势

  • 减少连接建立和关闭的开销,提升性能;
  • 控制连接数量,防止服务器过载;
  • 提高连接利用率,降低资源消耗。

二、拦截器

OkHttp3 使用责任链模式实现了灵活的拦截器机制,允许开发者在不修改核心代码的情况下,自定义请求和响应处理逻辑。

1. 拦截器类型

OkHttp3 中定义了以下两种类型的拦截器

  • 应用程序拦截器(Application Interceptor): 由开发者自定义,用于添加业务逻辑,例如日志记录、请求重试等。
  • 网络拦截器(Network Interceptor): 由 OkHttp3 内部实现,用于处理网络请求的具体细节,例如缓存、重定向等。

2. 拦截器执行流程

每个拦截器负责处理请求的一部分,并将处理结果传递给下一个拦截器。所有拦截器构成一个链条,按照顺序依次执行。

  • 请求阶段: 拦截器链按照添加顺序依次执行 intercept() 方法,每个拦截器可以选择修改请求、添加请求头等操作。
  • 响应阶段: 最后一个拦截器执行完毕后,响应会逆序经过拦截器链,每个拦截器可以读取响应内容、修改响应头等。
  Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    // 添加
    interceptors.addAll(client.interceptors());
    interceptors.add(new RetryAndFollowUpInterceptor(client));
    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, transmitter, null, 0,
        originalRequest, this, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());
    
    boolean calledNoMoreExchanges = false;
    try {
      // 责任链
      Response response = chain.proceed(originalRequest);
      if (transmitter.isCanceled()) {
        closeQuietly(response);
        throw new IOException("Canceled");
      }
      return response;
    } catch (IOException e) {
      calledNoMoreExchanges = true;
      throw transmitter.noMoreExchanges(e);
    } finally {
      if (!calledNoMoreExchanges) {
        transmitter.noMoreExchanges(null);
      }
    }
  }

3. 常用拦截器

OkHttp3 提供了一些常用的拦截器,例如:

  • RetryAndFollowUpInterceptor: 处理重试和重定向;
  • BridgeInterceptor: 将请求转换为网络请求,例如设置请求头、处理 Cookie 等;
  • CacheInterceptor: 处理缓存;
  • ConnectInterceptor: 建立 TCP 连接。

4. 自定义拦截器

我们可以自定义拦截器,只需实现 Interceptor 接口,并在 intercept() 方法中添加自定义逻辑即可。例如:我们实现一个log的拦截器,类似于切面的角色,在请求前后分别打印一行日志。

public class LoggingInterceptor implements Interceptor {
  @Override
  public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    long t1 = System.nanoTime();
    logger.info(String.format("Sending request %s on %s%n%s",
        request.url(), chain.connection(), request.headers()));

    Response response = chain.proceed(request);

    long t2 = System.nanoTime();
    logger.info(String.format("Received response for %s in %.1fms%n%s",
        response.request().url(), (t2 - t1) / 1e6d, response.headers()));
    return response;
  }
}

5. 拦截器的优势

  • 提高代码复用性: 将通用的网络逻辑封装成拦截器,方便复用。
  • 增强代码可扩展性: 通过添加自定义拦截器,轻松扩展功能,无需修改核心代码。
  • 提高代码可维护性: 将不同的网络逻辑隔离在不同的拦截器中,降低代码耦合度,方便维护。

三、总结

OkHttp3 通过连接池和拦截器机制实现了高效、灵活的 HTTP 通信。连接池提升了性能,拦截器增强了代码的复用性、可扩展性和可维护性。这些精妙的设计使得OkHttp3能够在保持简洁的API的同时提供强大的功能,从而成为SpringBoot项目开发中最受欢迎的HTTP客户端之一。


原文地址:https://blog.csdn.net/weixin_45593273/article/details/142668326

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!