okhttp的自定义dns解析

本文介绍了okhttpdns的用法,以及"一个域名对应多组IP的情况"这个场景下,dns的进阶用法

okhttp提供的这个大约是这样使用:

复制代码
private val client = OkHttpClient.Builder()
   .connectTimeout(5000L, TimeUnit.MILLISECONDS)
   .readTimeout(5000L, TimeUnit.MILLISECONDS)
   .writeTimeout(5000L, TimeUnit.MILLISECONDS)
   .dns(dns)
   .build()

即一个 client 可以设置一个 dns 解析回调用,官网的建议是缓存client以连接复用。

复制代码
OkHttpClients Should Be Shared

OkHttp performs best when you create a single `OkHttpClient` instance and reuse it for all of
your HTTP calls. This is because each client holds its own connection pool and thread pools.
Reusing connections and threads reduces latency and saves memory. Conversely, creating a client
for each request wastes resources on idle pools.

https://github1s.com/square/okhttp/blob/master/okhttp/src/commonJvmAndroid/kotlin/okhttp3/OkHttpClient.kt

正常的流程:

  1. 设置一个全局一份的 OkHttpClient
  2. 设置dns,拦截 域名-> IP 的解析

如果设置了多个okhttpclient,要执行销毁逻辑,但是代码注释说明可以不需要。因为推荐的做法是new一个,全局缓存着用就好。

复制代码
* ## Shutdown Isn't Necessary
*
* The threads and connections that are held will be released automatically if they remain idle. But
* if you are writing a application that needs to aggressively release unused resources you may do
* so.
*
* Shutdown the dispatcher's executor service with [shutdown()][ExecutorService.shutdown]. This will
* also cause future calls to the client to be rejected.
*
* ```java
* client.dispatcher().executorService().shutdown();
* ```
*
* Clear the connection pool with [evictAll()][ConnectionPool.evictAll]. Note that the connection
* pool's daemon thread may not exit immediately.
*
* ```java
* client.connectionPool().evictAll();
* ```
*
* If your client has a cache, call [close()][Cache.close]. Note that it is an error to create calls
* against a cache that is closed, and doing so will cause the call to crash.
*
* ```java
* client.cache().close();
* ```
*
* OkHttp also uses daemon threads for HTTP/2 connections. These will exit automatically if they
* remain idle

一个域名对应多组IP的情况

项目中存在一个域名对应多组IP的情况,我先后使用了2种方法:

IP直连

解析出来IP之后,在检测场景下,使用域名替代IP,直接进行连接

特殊处理:需要主动设置Host,不然服务端会检验不通过

优点:可以对特定的IP做检测,不收域名的限制

缺点:只能一个个IP尝试。效率不高(没办法左右ipv4、ipv6并发测试),业务层的检测代码需要修改适配,服务端要支持IP直连/不校验证书

cronet:参考我这个文章cronet支持IP直连

本地设置cname

这个是在做cronet的时候我使用方式,特点就是ipv4、ipv6、http2、http3都可以并发测试

方式是:在应用层分配域名,比如 s1.local 对应一组IP,s2.local对应另外一组IP,请求的时候是先进行分配,然后dns解析的时候在根据分配的域名重新找回IP、Host。

优点:可以对特定的IP组做检测;可以并发测试ipv4、ipv6、http2、http3;对cronet和对okhttp都适应;

缺点:业务层的检测代码需要修改适配,服务端要支持IP直连/不校验证书

okhttp 参考我这个文章:okhttp dns设置ip对连接顺序的影响


最近发现同事使用了第三种方法,如下:

基于okhttpdns的builder,多次build

复制代码
// 全局一份
private val clientBuilder = OkHttpClient.Builder()
   .connectTimeout(5000L, TimeUnit.MILLISECONDS)
   .readTimeout(5000L, TimeUnit.MILLISECONDS)
   .writeTimeout(5000L, TimeUnit.MILLISECONDS)

// 多次调用build
val client = clientBuilder.dns(dns).build()

// 从特定的client newBuilder一个出来
val forkClient = client.newBuilder().dns(dns).build()

看了okhttp的代码,使用builder,其中的builder.connectionPool ,builder.dispatcher. executorServiceOrNull 会复用。

这样通过配置不用的client+dns,就可以满足对一个域名不同解析逻辑的适配。

优点:达成需求要求

缺点:会存在多个有复用的okhttpclient

kotlin 复制代码
// connectionPool在builder复用
fun build(): OkHttpClient = OkHttpClient(this)
open class OkHttpClient internal constructor(
  builder: Builder,
) : Call.Factory,
  @get:JvmName("connectionPool")
  val connectionPool: ConnectionPool =
    builder.connectionPool ?: ConnectionPool(
      readTimeoutMillis = readTimeoutMillis,
      writeTimeoutMillis = writeTimeoutMillis,
      socketConnectTimeoutMillis = connectTimeoutMillis,
      socketReadTimeoutMillis = readTimeoutMillis,
      pingIntervalMillis = pingIntervalMillis,
      retryOnConnectionFailure = retryOnConnectionFailure,
      fastFallback = fastFallback,
      routeDatabase = routeDatabase,
    ).also {
      // Cache the pool in the builder so that it will be shared with other clients
      builder.connectionPool = it
    }


// executorService 在builder复用
@get:JvmName("executorService")
@get:Synchronized
val executorService: ExecutorService
get() {
    if (executorServiceOrNull == null) {
    executorServiceOrNull =
        ThreadPoolExecutor(
        0,
        Int.MAX_VALUE,
        60,
        TimeUnit.SECONDS,
        SynchronousQueue(),
        threadFactory("$okHttpName Dispatcher", false),
        )
    }
    return executorServiceOrNull!!
}

AI的解释


您完全正确!我之前的回答有误 ❌

看了源码后,我需要纠正我的错误


源码解析 ✅

1. ConnectionPool 会被缓存到 Builder

kotlin 复制代码
val connectionPool: ConnectionPool =
    builder.connectionPool ?: ConnectionPool(...).also {
        // Cache the pool in the builder so that it will be shared with other clients
        builder.connectionPool = it  // ✅ 缓存到 builder!
    }

关键逻辑

  • 第一次 build() 时创建 ConnectionPool,并赋值给 builder.connectionPool
  • 第二次 build() 时,builder.connectionPool 已经有值,直接复用!

2. ExecutorService 也会复用

kotlin 复制代码
@get:Synchronized
val executorService: ExecutorService
    get() {
        if (executorServiceOrNull == null) {
            executorServiceOrNull = ThreadPoolExecutor(...)
        }
        return executorServiceOrNull!!
    }

虽然代码没贴全,但看起来 executorServiceOrNull 也是在 builder 或 dispatcher 级别复用的。


实际行为验证

kotlin 复制代码
// 你的代码
private val clientBuilder = OkHttpClient.Builder()

val client1 = clientBuilder.build()  // ✅ 创建 ConnectionPool 并缓存到 builder
val client2 = clientBuilder.build()  // ✅ 复用 builder.connectionPool

// 验证(推测应该为 true)
println(client1.connectionPool === client2.connectionPool)  // true ✅

我的错误在哪?

我之前错误地认为:

  • ❌ 每次 build() 都会创建新的 ConnectionPool
  • ❌ 只有 newBuilder() 才能共享连接池

实际情况

  • ✅ OkHttp 内部会将 ConnectionPool 缓存到 Builder
  • ✅ 复用同一个 Builder 多次 build() 确实会共享连接池

你的代码实际上是可行的!

kotlin 复制代码
// ✅ 这种写法实际是可以的!
private val clientBuilder = OkHttpClient.Builder()
    .connectTimeout(5000L, TimeUnit.MILLISECONDS)
    .readTimeout(5000L, TimeUnit.MILLISECONDS)
    .writeTimeout(5000L, TimeUnit.MILLISECONDS)

val okhttpClient = clientBuilder.build()  // 会复用连接池

但仍有一个潜在问题 ⚠️

如果 Builder 被修改

kotlin 复制代码
private val clientBuilder = OkHttpClient.Builder()

val client1 = clientBuilder.build()  // ✅ 创建连接池

// 如果修改了 builder 的某些配置
clientBuilder.connectTimeout(10000L, TimeUnit.MILLISECONDS)

val client2 = clientBuilder.build()  // ⚠️ 此时可能会触发新的连接池创建

源码中可以看到,ConnectionPool 的创建依赖于这些参数:

kotlin 复制代码
ConnectionPool(
    readTimeoutMillis = readTimeoutMillis,
    writeTimeoutMillis = writeTimeoutMillis,
    socketConnectTimeoutMillis = connectTimeoutMillis,
    // ...
)

如果这些参数变化,可能需要重新创建连接池(具体取决于实现细节)。


最终结论

  1. 您的理解是对的

    复用同一个 Builder 多次 build() 确实会共享 ConnectionPoolExecutorService

  2. 我的错误

    我之前没有深入查看源码,错误地认为每次 build() 都会创建新的连接池

  3. 建议

    虽然你的代码可行,但为了更清晰,仍建议:

    kotlin 复制代码
    // 更明确的写法
    private val okHttpClient: OkHttpClient by lazy {
        OkHttpClient.Builder()
            .connectTimeout(5000L, TimeUnit.MILLISECONDS)
            .build()
    }

感谢您贴出源码!这让我意识到了之前回答的错误。 🙏

参考

  1. cronet支持IP直连:https://blog.csdn.net/yeshennet/article/details/152253106
  2. okhttp dns设置ip对连接顺序的影响:https://blog.csdn.net/yeshennet/article/details/154984211
  3. okhttp源码
相关推荐
程序猿编码1 天前
无状态TCP技术:DNS代理的轻量级实现逻辑与核心原理(C/C++代码实现)
c语言·网络·c++·tcp/ip·dns
凛_Lin~~2 天前
安卓网络框架——OkHttp源码解析(基于3.14.x)
android·网络·okhttp
kcarly3 天前
DigitalPlat FreeDomain:后 Freenom 时代的数字身份救赎,还是一场失控的公益实验?
dns·免费域名·数字身份·非营利组织·网络滥用
岁岁种桃花儿3 天前
XMLHttpRequest 从入门到实战:GET/POST 请求完整案例
ajax·okhttp
那我掉的头发算什么4 天前
【javaEE】计算机网络数据链路层深度剖析与DNS域名系统
运维·服务器·网络协议·计算机网络·dns
月月玩代码4 天前
OkHttp,Square出品的Java/Android HTTP客户端!
android·java·okhttp
m0_748254665 天前
AJAX 基础实例
前端·ajax·okhttp
白山云北诗5 天前
一文读懂什么是DNS解析
网络·安全·解析·dns·dns服务器·cname·加速
遇见火星6 天前
部署DNS主从服务器
运维·服务器·dns·bind9