okhttp3中如何在不读取响应体的情况下关闭响应体

How to close response body without reading the response body in okhttp3

环境

我工作的环境对网络带宽非常敏感。
有时,我不需要阅读所有的响应主体。响应主体的一部分可能足以决定结果。

我想关闭响应(=响应正文)而不阅读正文

我想做的事情如下图

try (Response response = client.newCall(request).execute()) {
    assertThat(response.code(), is(200))
    // do nothing with the response body
}

但是,当与 HTTP1 建立连接时,ResponseBody::close 最后调用 Http1ExchangeCodec.FixedLengthSource::close

Http1ExchangeCodec.FixedLengthSource::关闭

override fun close() {
    if (closed) return

    if (bytesRemaining != 0L &&
        !discard(ExchangeCodec.DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) {
    connection.noNewExchanges() // Unread bytes remain on the stream.
    responseBodyComplete()
    }

    closed = true
}

然后,discard 方法读取所有响应正文源,如下所示。

Util.kt

fun Source.discard(timeout: Int, timeUnit: TimeUnit): Boolean = try {
    this.skipAll(timeout, timeUnit)
} catch (_: IOException) {
    false
}

@Throws(IOException::class)
fun Source.skipAll(duration: Int, timeUnit: TimeUnit): Boolean {
    val nowNs = System.nanoTime()
    val originalDurationNs = if (timeout().hasDeadline()) {
        timeout().deadlineNanoTime() - nowNs
    } else {
        Long.MAX_VALUE
    }
    timeout().deadlineNanoTime(nowNs + minOf(originalDurationNs, timeUnit.toNanos(duration.toLong())))
    return try {
        val skipBuffer = Buffer()
        while (read(skipBuffer, 8192) != -1L) {
            skipBuffer.clear()
        }
        true // Success! The source has been exhausted.
    } catch (_: InterruptedIOException) {
        false // We ran out of time before exhausting the source.
    } finally {
        if (originalDurationNs == Long.MAX_VALUE) {
            timeout().clearDeadline()
        } else {
            timeout().deadlineNanoTime(nowNs + originalDurationNs)
        }
    }
}

它读取所有正文并清除缓冲区。就我而言,这是浪费 CPU 时间和网络带宽。

有没有办法直接关闭它?

不,无法立即关闭响应正文。如果您从不在该连接上发出另一个 HTTP 请求,那将是对带宽和 CPU 的浪费。但是连接池是必须的。

如果您对响应正文不感兴趣,请考虑使用 HEAD 方法或添加 range header。这样您还可以节省服务器的 CPU 和带宽。

解决方法

尽管我不能重用连接,但我想在不读取所有缓冲区的情况下关闭连接以节省网络带宽和CPU。

为了跳过全部读取,我尝试将 Http1ExchangeCodec.FixedLengthSourcebytesRemaining 字段更改为 0。bytesRemaining 由“Content-Length”响应设置 header值。
但是,即使使用自定义拦截器更改响应 header,也无法更改 bytesRemaining。 因为,“Content-Length”是在 CallServerInterceptor(拦截器链的最后一个拦截器)处理请求时检查的,所以,bytesRemaining 由原始响应设置 header。

终于发现可以使用Source的超时期限了。 Source.skipAll() 可以中断属于超时期限。将超时期限设置为 0 以确保始终发生超时。这使得连接在不读取剩余字节的情况下直接关闭。

try (Response response = client.newCall(request).execute()) {
    assertThat(response.code(), is(200))
    
    response.body()
        .source()
        .timeout()
        .deadlineNanoTime(0);
    // close without reading remaining bytes
}