从一次请求看懂 OkHttp:架构、调度与连接管理

很多人第一次接触 OkHttp,是从这样几行代码开始的:

vbscript 复制代码
Request request = new Request.Builder()
    .url("https://example.com/api")
    .build();
​
try (Response response = client.newCall(request).execute()) {
    String body = response.body().string();
}

代码看起来很简单:构造一个 Request,创建一个 Call,然后执行它。但如果这个调用发生在生产环境里,事情就没有这么简单了。它背后会经历请求调度、拦截器链、连接复用、DNS、TLS、HTTP/1.1 或 HTTP/2 编码、超时控制、失败重试、响应释放等一整套流程。

OkHttp 好用的地方,也正是在这里。它把 HTTP 客户端里很多容易出错、又很影响性能的细节封装好了。但如果只是把它当成一个"发请求工具",很容易在生产环境里踩坑,比如每次请求都 new 一个 client、忘记关闭 response、把 Dispatcher 并发调到几万、误以为连接池限制的是最大连接数,或者把 OkHttp 的连接失败重试当成业务重试。

这篇文章试着从一次请求的生命周期出发,把 OkHttp 的核心设计讲清楚:它怎么调度请求,怎么通过拦截器链组织逻辑,怎么管理连接,HTTP/2 对并发意味着什么,以及在后端服务里应该如何配置和使用它。

OkHttp 到底是什么

OkHttp 是一个成熟的 HTTP 客户端。它不是简单封装 socket,也不只是把 URL 打出去然后拿回响应。一个完整的 HTTP 客户端要处理很多事情:请求构造、同步和异步执行、连接池、DNS、代理、TLS、证书校验、HTTP/1.1、HTTP/2、多路复用、缓存、重试、重定向、超时控制、事件观测等。

在后端系统里,OkHttp 常见于服务间 HTTP 调用、内部 SDK、网关访问下游服务、第三方 API 调用、模型服务调用、批处理任务等场景。Android 里也大量使用 OkHttp,不过本文更偏后端工程视角。

先看几个最核心的对象。

OkHttpClient 是客户端实例,里面持有 Dispatcher、ConnectionPool、拦截器、DNS、代理、TLS、超时、缓存等配置。它是线程安全的,应该被复用,而不是每次请求都创建一个新的实例。

Request 表示一次 HTTP 请求,包括 URL、method、headers、body 等。

Call 表示一次已经准备好执行的请求。client.newCall(request) 会创建一个 Call,一个 Call 只能执行一次。

Response 表示 HTTP 响应。它必须被关闭,否则底层连接不能及时回收到连接池。

一个基础同步调用通常长这样:

scss 复制代码
OkHttpClient client = new OkHttpClient.Builder()
    .connectTimeout(Duration.ofSeconds(3))
    .readTimeout(Duration.ofSeconds(30))
    .writeTimeout(Duration.ofSeconds(30))
    .callTimeout(Duration.ofSeconds(60))
    .build();
​
Request request = new Request.Builder()
    .url("https://example.com/api")
    .get()
    .build();
​
try (Response response = client.newCall(request).execute()) {
    if (!response.isSuccessful()) {
        throw new IOException("HTTP error: " + response.code());
    }
    String body = response.body().string();
}

这里有一个非常重要的细节:try (Response response = ...)。这个写法不是为了优雅,而是为了确保响应被关闭。OkHttp 能不能复用连接,很大程度上取决于你有没有正确释放 response。

从整体上看 OkHttp 的架构

可以把 OkHttp 的一次请求想象成一条流水线:业务代码把请求交给 OkHttpClientCall 负责承载这次请求,异步请求会经过 Dispatcher 调度,然后进入拦截器链。拦截器链会依次处理重试、桥接、缓存、连接获取和网络读写。最后,请求落到底层 socket,通过 HTTP/1.1 或 HTTP/2 与服务端通信。

整体结构可以抽象成这样:

sql 复制代码
业务代码
  ↓
OkHttpClient
  ↓
Call / RealCall
  ↓
Dispatcher
  ↓
Interceptor Chain
  ↓
ConnectionPool / Route / RealConnection
  ↓
Socket / TLS / HTTP/1.1 / HTTP/2
  ↓
远端服务

其中最值得关注的是四块:Dispatcher 负责异步请求调度;拦截器链负责组织 HTTP 请求处理流程;ConnectionPool 负责连接复用;底层协议层负责真正的网络读写。

不同 OkHttp 版本内部类名会有变化,比如 OkHttp 3、4、5 在实现上并不完全一致,但这条主线基本稳定。理解这条主线,比记住某个版本的内部类细节更重要。

一次 execute 请求是怎么执行的

先看同步请求,也就是 execute()

同步请求的特点是:谁调用它,谁就会被阻塞。网络请求会在当前调用线程里完成,直到响应返回、失败或超时。

它的大致流程是:

vbscript 复制代码
client.newCall(request)
  ↓
RealCall.execute()
  ↓
Dispatcher.executed(call)
  ↓
进入拦截器链
  ↓
RetryAndFollowUpInterceptor
  ↓
BridgeInterceptor
  ↓
CacheInterceptor
  ↓
ConnectInterceptor
  ↓
CallServerInterceptor
  ↓
返回 Response
  ↓
关闭 Response,连接释放或回收到连接池

这里容易产生一个误解:既然 execute() 也会经过 Dispatcher.executed(call),是不是同步请求也会被 Dispatcher 的线程池调度?不是。

同步请求只是被 Dispatcher 记录到 running sync calls 里,真正执行请求的是调用方线程。也就是说,如果你在业务线程池里调用 execute(),占用的是你的业务线程;如果你在主线程里调用它,就会阻塞主线程。

对于后端服务来说,这反而通常是一个优点。因为大多数后端系统本来就有自己的并发模型,比如 Tomcat/Jetty 工作线程、业务线程池、CompletableFuture、Kotlin coroutine、Reactor wrapper 或批处理 worker。用 execute() 时,你可以把并发、限流、熔断、trace、超时和错误处理统一放在自己的体系里。

enqueue 又做了什么

enqueue() 是异步请求。调用 enqueue() 后,当前线程不会等待 HTTP 响应,而是把请求交给 OkHttp 的 Dispatcher。Dispatcher 再决定这个请求是立刻执行,还是先放到等待队列里。

流程大致是:

scss 复制代码
client.newCall(request).enqueue(callback)
  ↓
Dispatcher.enqueue(asyncCall)
  ↓
检查 maxRequests / maxRequestsPerHost
  ↓
未超过限制:提交到 executor 执行
超过限制:进入 readyAsyncCalls 等待队列
  ↓
后台线程执行拦截器链
  ↓
回调 onResponse / onFailure

异步调用示例:

less 复制代码
client.newCall(request).enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
        // 请求失败,回调运行在 OkHttp 后台线程
    }
​
    @Override
    public void onResponse(Call call, Response response) throws IOException {
        try (response) {
            String body = response.body().string();
        }
    }
});

注意,onResponseonFailure 默认运行在 OkHttp 的后台线程里,不会自动切回 Android 主线程,也不会自动进入你业务系统的线程上下文。如果你有 MDC、trace context、租户上下文之类的东西,需要自己考虑上下文传递。

什么时候用 execute(),什么时候用 enqueue()?一个简单判断是:如果当前代码已经在可阻塞的 worker 线程里,并且你想自己管理并发,用 execute();如果当前线程不能阻塞,或者你想让 OkHttp 的 Dispatcher 管理这批 HTTP 请求,用 enqueue()

在后端服务里,我通常更偏向 execute() 外面套业务自己的并发控制。它的调用链更直,错误处理更自然,也更容易和限流、熔断、tracing 体系整合。

Dispatcher:别把它当成队列容量

Dispatcher 是 OkHttp 的请求调度器,重点管理异步请求的并发和排队。它不是 Android 的主线程调度器,也不是 Kotlin coroutine 的 dispatcher。

它内部可以粗略理解成维护三组请求:

复制代码
readyAsyncCalls      等待执行的异步请求
runningAsyncCalls    正在执行的异步请求
runningSyncCalls     正在执行的同步请求

默认情况下,OkHttp 异步请求的并发限制通常是:

ini 复制代码
maxRequests = 64;
maxRequestsPerHost = 5;

也就是说,全局最多同时运行 64 个异步请求,同一个 host 最多同时运行 5 个异步请求。超过任一限制,请求就会进入等待队列。

配置方式如下:

ini 复制代码
Dispatcher dispatcher = new Dispatcher();
dispatcher.setMaxRequests(256);
dispatcher.setMaxRequestsPerHost(64);

OkHttpClient client = new OkHttpClient.Builder()
    .dispatcher(dispatcher)
    .build();

这里有一个生产中很常见的问题:如果配置成 dispatcher-max-requests: 20000,是不是超过 20000 才进队列?

如果这个配置最终映射到:

ini 复制代码
dispatcher.setMaxRequests(20000);

那答案是:对,它表示最多允许 20000 个异步请求同时运行,超过这个运行中数量后,新的异步请求会进入等待队列。但它不是"队列容量",而是"运行中异步请求的并发上限"。

而且 maxRequestsPerHost 仍然会生效。比如全局并发是 20000,但单 host 并发还是默认 5,那么同一个域名下第 6 个异步请求依然会排队。

更重要的是,20000 这样的值通常非常危险。OkHttp 异步请求背后会使用 executor 执行,过高的并发可能带来线程膨胀、内存上涨、连接数增加、文件描述符消耗、端口消耗,以及下游服务被瞬间打爆。很多时候,把这个值调大不是在提高吞吐,而是在允许更多请求堆积到网络层。

估算并发可以用一个很朴素的公式:

复制代码
所需并发 ≈ QPS × 平均耗时秒数

如果你要打 1000 QPS,下游平均耗时 100ms,那么平均并发大约是:

ini 复制代码
1000 × 0.1 = 100

真实配置还要考虑 P95/P99 长尾、重试、下游限流、HTTP/2 多路复用、业务隔离和机器资源。但无论如何,Dispatcher 并发都不应该凭感觉调到一个特别大的数字。

拦截器链:OkHttp 的骨架

OkHttp 的拦截器链是它架构里非常漂亮的一部分。它用责任链模式把一次 HTTP 请求拆成多个阶段,每个阶段只负责自己的事情,然后调用 chain.proceed(request) 把请求交给下一个阶段。

典型顺序可以理解为:

复制代码
Application Interceptors
  ↓
RetryAndFollowUpInterceptor
  ↓
BridgeInterceptor
  ↓
CacheInterceptor
  ↓
ConnectInterceptor
  ↓
Network Interceptors
  ↓
CallServerInterceptor

应用层拦截器,也就是 addInterceptor() 加进去的 interceptor,适合处理业务侧通用逻辑,比如添加鉴权 header、trace id、统一日志、指标埋点、租户信息等。

ini 复制代码
OkHttpClient client = new OkHttpClient.Builder()
    .addInterceptor(chain -> {
        Request oldRequest = chain.request();
        Request newRequest = oldRequest.newBuilder()
            .header("X-Request-Id", UUID.randomUUID().toString())
            .build();
        return chain.proceed(newRequest);
    })
    .build();

RetryAndFollowUpInterceptor 负责连接失败重试、重定向、认证挑战等 follow-up 请求。这里的重试主要是 HTTP 客户端语义和连接层面的处理,不等同于业务状态码重试。

BridgeInterceptor 负责把用户请求转换成网络请求,比如补充 HostConnectionAccept-Encoding、Cookie 等 header,也会处理 gzip 解压。

CacheInterceptor 负责 HTTP 缓存。如果配置了 Cache,它会根据 Cache-ControlETagLast-Modified 等响应头决定是否直接用缓存,或者是否发条件请求。

ConnectInterceptor 负责获取连接。到了这里,请求开始和连接池、DNS、路由选择、TCP 连接、TLS 握手打交道。

网络层拦截器,也就是 addNetworkInterceptor() 加进去的 interceptor,更接近真实网络层。它可以看到重定向、重试之后的实际网络请求。

最后是 CallServerInterceptor,它真正把请求写到连接上,并从连接里读回响应。到这里,HTTP/1.1 或 HTTP/2 的 codec 会参与实际的数据编解码。

拦截器链的好处是结构清晰。应用层逻辑、HTTP 语义、缓存、连接、网络读写被拆开了。用户也能在合适的位置插入自己的逻辑,而不是把所有东西塞进一个巨大的 HTTP 执行函数里。

连接管理:OkHttp 性能的关键

HTTP 客户端性能很大一部分来自连接复用。一次 HTTPS 请求最贵的部分通常不是传几个字节,而是 DNS 查询、TCP 握手、TLS 握手和连接建立。如果每个请求都重新建连接,延迟和资源消耗都会很高。

OkHttp 用 ConnectionPool 复用连接。一次请求获取连接的过程大致是:

arduino 复制代码
请求 https://api.example.com
  ↓
查连接池里有没有可复用连接
  ↓
有:直接复用
  ↓
没有:DNS 解析
  ↓
选择 Route
  ↓
建立 TCP 连接
  ↓
如果是 HTTPS,进行 TLS 握手
  ↓
如果协商到 HTTP/2,创建 HTTP/2 连接
  ↓
发送请求并读取响应
  ↓
响应结束后,连接释放或回收到连接池

连接池可以这样配置:

ini 复制代码
ConnectionPool connectionPool = new ConnectionPool(
    100,                 // maxIdleConnections
    5, TimeUnit.MINUTES  // keepAliveDuration
);

OkHttpClient client = new OkHttpClient.Builder()
    .connectionPool(connectionPool)
    .build();

这里也有一个容易误解的点:maxIdleConnections 不是最大总连接数,而是最大空闲连接数。正在使用中的连接不算 idle。

连接能不能复用,不是只看 host 一不一样。OkHttp 会综合判断 scheme、host、port、代理、TLS 配置、证书校验、连接状态、协议类型等因素。对于 HTTP/1.1,通常要求连接对应的 address 兼容。对于 HTTP/2,还可能发生 connection coalescing,也就是在证书、DNS、IP、TLS 等条件满足时,不同 hostname 复用同一个 HTTP/2 连接。

所以连接池不是一个简单的 Map,也不是"host 到 socket"的粗暴缓存。它背后有完整的路由、协议和安全校验逻辑。

HTTP/1.1 和 HTTP/2 对连接的影响

HTTP/1.1 和 HTTP/2 的并发模型很不一样。

在 HTTP/1.1 下,一个连接通常同一时刻处理一个请求/响应。虽然 keep-alive 可以让连接被复用,但多个并发请求通常需要多条连接:

rust 复制代码
HTTP/1.1:
connection 1 -> request A
connection 2 -> request B
connection 3 -> request C

HTTP/2 支持多路复用,一个 TCP/TLS 连接上可以同时承载多个 stream:

less 复制代码
HTTP/2:
connection 1
  ├─ stream A
  ├─ stream B
  ├─ stream C

这会显著影响连接数和并发模型。如果下游支持 HTTP/2,高并发请求不一定意味着大量 TCP 连接。少量连接也能承载较高并发。

但要注意,OkHttp 的几个限制不在同一层:Dispatcher.setMaxRequestsPerHost() 是异步请求调度层面的同 host 并发;ConnectionPool 管理的是空闲连接复用;HTTP/2 stream 并发是协议层能力。这三者有关联,但不能混为一个参数。

举个例子,即使 HTTP/2 可以在一条连接上跑多个 stream,如果 maxRequestsPerHost 设得很小,OkHttp 的异步调度层仍然可能让请求排队。反过来,如果 Dispatcher 并发很大,也不意味着一定会创建同样数量的 TCP 连接,因为 HTTP/2 可以复用连接。

DNS、Route 和代理

当连接池里没有可复用连接时,OkHttp 需要找到一条可用的连接路线。这个过程通常包括代理选择、DNS 解析和 IP 尝试。

scss 复制代码
URL
  ↓
ProxySelector 选择代理
  ↓
DNS 解析 host 到 IP 列表
  ↓
组合 Route(proxy + ip + tls)
  ↓
逐个尝试连接

如果一个域名解析出多个 IP,OkHttp 可以在某个 IP 连接失败后尝试下一个 route。失败信息也会被记录,避免短时间内反复选择明显失败的路线。

后端系统里,有时会自定义 DNS,比如接入内部服务发现、私有 DNS、多机房路由、灰度解析或容灾切换:

ini 复制代码
OkHttpClient client = new OkHttpClient.Builder()
    .dns(hostname -> {
        // 可以接入内部服务发现、DNS 缓存、灰度解析等
        return Dns.SYSTEM.lookup(hostname);
    })
    .build();

自定义 DNS 很有用,但也容易引入新的问题。比如 DNS 缓存时间过长,会导致客户端一直访问已经不可用的 IP;缓存时间过短,又可能增加解析压力和延迟。服务发现、负载均衡、故障恢复这些逻辑,最好和业务的整体治理体系一起设计。

TLS 和证书校验

HTTPS 请求会经历 TLS 握手。OkHttp 默认使用平台信任链校验服务端证书。大多数情况下,直接使用默认配置就足够。

OkHttp 也支持 Certificate Pinning:

java 复制代码
CertificatePinner certificatePinner = new CertificatePinner.Builder()
    .add("example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
    .build();

OkHttpClient client = new OkHttpClient.Builder()
    .certificatePinner(certificatePinner)
    .build();

证书固定可以提高安全性,但要谨慎。它会增加证书轮换风险。一旦 pin 配错,或者服务端证书更新而客户端没有同步,线上请求可能会大面积失败。

企业内部环境还可能涉及私有 CA、mTLS、代理 TLS 拦截、证书自动轮换等问题。无论哪种情况,都不建议为了"先跑通"就信任所有证书。这种做法会直接破坏 HTTPS 的安全边界,后续也很容易遗留成生产风险。

超时:不要只配 readTimeout

OkHttp 常见的超时有四类:

scss 复制代码
OkHttpClient client = new OkHttpClient.Builder()
    .connectTimeout(Duration.ofSeconds(2))
    .readTimeout(Duration.ofSeconds(30))
    .writeTimeout(Duration.ofSeconds(30))
    .callTimeout(Duration.ofSeconds(35))
    .build();

connectTimeout 控制建立 TCP 连接的时间。

readTimeout 控制读取响应数据时的等待时间。如果服务端长时间没有返回数据,会触发读超时。

writeTimeout 控制写请求体的时间。上传大 body 或网络拥塞时,它会比较重要。

callTimeout 控制整个调用的总耗时,从请求开始到响应结束都算。它是后端服务里非常重要的兜底超时。

生产环境不建议只配置 connectTimeoutreadTimeout。在重试、重定向、连接等待、慢响应体读取等复杂情况下,如果没有总超时,一次请求的整体耗时可能比你预期长得多。

更稳妥的做法是:用 callTimeout 控制总预算,再用 connect/read/write timeout 控制具体阶段。比如一次下游调用最多允许 35 秒,那 callTimeout 就不应该缺失。

重试:连接失败重试不等于业务重试

OkHttp 默认支持连接层失败重试和 HTTP 重定向:

java 复制代码
OkHttpClient client = new OkHttpClient.Builder()
    .retryOnConnectionFailure(true)
    .followRedirects(true)
    .followSslRedirects(true)
    .build();

retryOnConnectionFailure(true) 主要处理连接层问题,比如某个 IP 连接失败后尝试另一个 route。它不是对所有失败状态码都重试,也不是完整的业务重试机制。

如果你需要对 500502503504429 等状态码做重试,通常应该在业务层或 resilience 框架里实现,并且明确最大重试次数、退避策略、抖动、错误分类、熔断和限流。

还要特别注意幂等性。GET、HEAD 通常更适合自动重试;POST、PATCH 这类请求如果没有 idempotency key,不应该随便重试。否则可能导致重复创建、重复下单、重复扣费或重复提交任务。

最常见的坑:忘记关闭 Response

如果只记住一个 OkHttp 使用规则,那就是:一定要关闭 Response

推荐写法:

vbscript 复制代码
try (Response response = client.newCall(request).execute()) {
    String body = response.body().string();
}

异步调用里也一样:

less 复制代码
client.newCall(request).enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
        // handle failure
    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {
        try (response) {
            String body = response.body().string();
        }
    }
});

如果 response 没有关闭,OkHttp 无法及时把连接回收到连接池。短期看可能只是连接复用率下降,长期看可能变成连接泄漏、请求变慢、文件描述符耗尽,甚至拖垮整个服务。

还有一个相关坑:在拦截器里读取 response body 后没有重建 body。响应体通常只能读一次。如果日志拦截器里写了下面这种代码:

ini 复制代码
String body = response.body().string();
return response;

后续业务代码再读 body,就可能读不到了。如果确实要读取并继续传递,需要重新构造 response body。

OkHttpClient 应该复用

不要这样写:

scss 复制代码
for (...) {
    OkHttpClient client = new OkHttpClient();
    client.newCall(request).execute();
}

这会让每个 client 都有自己的连接池和线程资源,连接无法有效复用,资源也会重复创建。

更合理的方式是创建长生命周期 client:

java 复制代码
private static final OkHttpClient CLIENT = new OkHttpClient.Builder()
    .connectionPool(new ConnectionPool(100, 5, TimeUnit.MINUTES))
    .build();

如果只是少量配置不同,可以从已有 client 派生:

ini 复制代码
OkHttpClient shortTimeoutClient = CLIENT.newBuilder()
    .callTimeout(Duration.ofSeconds(10))
    .build();

这种方式适合在统一基础配置上,对某个下游或某类请求做局部调整。

缓存:不是所有后端调用都需要,但要知道它存在

OkHttp 支持 HTTP 缓存,但需要显式配置:

ini 复制代码
Cache cache = new Cache(
    new File("/tmp/okhttp-cache"),
    100L * 1024 * 1024
);

OkHttpClient client = new OkHttpClient.Builder()
    .cache(cache)
    .build();

它遵循标准 HTTP 缓存语义,比如 Cache-ControlETagLast-ModifiedExpiresVary 等。

在服务间调用里,很多团队更依赖业务缓存、Redis、本地缓存或网关缓存,所以 OkHttp 自带 cache 不一定常用。但在第三方 API、配置拉取、静态资源、低频可缓存数据等场景中,它是一个值得考虑的能力。

用 EventListener 看清请求慢在哪里

线上排查 HTTP 慢请求时,只看"总耗时"通常不够。慢可能发生在 DNS、建连、TLS、等待服务端响应、下载响应体,甚至是连接池复用失败。

OkHttp 提供了 EventListener,可以观察一次请求内部的多个阶段:

typescript 复制代码
OkHttpClient client = new OkHttpClient.Builder()
    .eventListenerFactory(call -> new EventListener() {
        @Override
        public void dnsStart(Call call, String domainName) {
            System.out.println("dnsStart: " + domainName);
        }

        @Override
        public void connectStart(Call call, InetSocketAddress address, Proxy proxy) {
            System.out.println("connectStart: " + address);
        }

        @Override
        public void responseHeadersEnd(Call call, Response response) {
            System.out.println("responseHeadersEnd: " + response.code());
        }
    })
    .build();

生产环境可以把这些事件接到 metrics 或 tracing 系统里,观察 DNS 耗时、建连耗时、TLS 握手耗时、连接复用率、响应头耗时、响应体下载耗时等指标。这样排查问题时,就不会只剩下一句"HTTP 调用很慢"。

一个后端服务的配置起点

下面这个配置不是通用最优值,只能作为后端服务的起点参考:

scss 复制代码
Dispatcher dispatcher = new Dispatcher();
dispatcher.setMaxRequests(256);
dispatcher.setMaxRequestsPerHost(64);

ConnectionPool connectionPool = new ConnectionPool(
    200,
    5, TimeUnit.MINUTES
);

OkHttpClient client = new OkHttpClient.Builder()
    .dispatcher(dispatcher)
    .connectionPool(connectionPool)
    .connectTimeout(Duration.ofSeconds(2))
    .readTimeout(Duration.ofSeconds(30))
    .writeTimeout(Duration.ofSeconds(30))
    .callTimeout(Duration.ofSeconds(35))
    .retryOnConnectionFailure(true)
    .build();

真正调优时,要看 QPS、平均耗时、P95/P99、下游 host 数量、是否使用 HTTP/2、请求体和响应体大小、机器 CPU/内存/FD 限制、下游承载能力、业务幂等性等因素。

如果某个下游调用量很大,或者调用耗时明显更长,最好不要和所有请求共享同一套无限制资源。可以考虑为不同下游设计独立 client、独立 Dispatcher、独立连接池或业务层限流隔离。高优先级下游、低优先级下游、第三方 API、长耗时模型服务,通常不应该无差别共享同一组并发预算。

最后总结

OkHttp 的核心设计可以概括为一句话:用 OkHttpClient 承载全局配置,用 Call 表示一次请求,用 Dispatcher 管理异步请求并发,用拦截器链分层处理 HTTP 语义,用 ConnectionPoolRealConnection 复用底层连接,最终通过 HTTP/1.1 或 HTTP/2 完成网络通信。

对于后端工程来说,稳定使用 OkHttp 的关键不在于会不会写 newCall(request).execute(),而在于理解这行代码背后的资源模型。

客户端要复用,response 要关闭,超时要有总预算,Dispatcher 并发要根据吞吐和下游能力设置,连接池不是最大连接数,HTTP/2 会改变连接和并发关系,OkHttp 的连接失败重试也不能替代业务重试。

理解了这些,再看 OkHttp 就不只是一个 HTTP 工具库,而是一个围绕请求调度、协议处理和连接复用构建起来的完整客户端运行时。用得好,它可以帮你把 HTTP 调用做得稳定、高效、可观测;用得随意,它也会把资源泄漏、过载和长尾延迟放大到生产环境里。

相关推荐
爱勇宝2 小时前
深扒 Anthropic 1680 位工程师简历:应届生几乎没机会,AI 公司最缺的不是博士
前端·后端·程序员
AskHarries2 小时前
工具失败时怎么办:重试、回滚、人工确认和风险提示
后端·程序员
苏三说技术4 小时前
Claude Code从失控到起飞,只用了这些技巧
后端
长栎5 小时前
写 for 循环写了十年,你却从没用过迭代器模式最狠的那一面
后端
LiaCode5 小时前
Redis 在生产项目的使用
前端·后端
用户559822481225 小时前
Docker Compose Down 导致容器数据误删——ext4 日志恢复全记录
后端
LiaCode5 小时前
一天学完 redis 的爽翻版核心知识总结
前端·后端
大刚测试开发实战5 小时前
如何内网穿透访问本地私有化部署的TestHub
前端·后端·github
xiaodaoluanzha5 小时前
迄今為止,最簡單的編程語言 Nolang
前端·后端