You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Ktor服务器大文件传输字节数精准日志实现方案咨询

Ktor大文件传输字节数精准统计方案

你之前的方案依赖ByteReadChannel.totalBytesRead,问题出在这个值包含了通道内部缓冲的字节,未必是实际发送到客户端的量——尤其是反向代理场景下,代理会缓存部分数据,通道提前关闭但代理还没把所有数据发完,导致统计数值不准。以下是两种精准的实现方式:

方案一:自定义计数ByteReadChannel(通用场景)

通过包装原始的ByteReadChannel,在每次实际读取(即发送)数据时累加计数,确保统计的是真正发出去的字节:

class CountingByteReadChannel(
    private val delegate: ByteReadChannel,
    private val onCountUpdated: (Long) -> Unit
) : ByteReadChannel by delegate {
    private var totalBytesSent = 0L

    override suspend fun read(dst: ByteBuffer): Int {
        val bytesRead = delegate.read(dst)
        if (bytesRead > 0) {
            totalBytesSent += bytesRead
            onCountUpdated(totalBytesSent)
        }
        return bytesRead
    }

    override suspend fun readFully(dst: ByteBuffer): Int {
        val bytesRead = delegate.readFully(dst)
        if (bytesRead > 0) {
            totalBytesSent += bytesRead
            onCountUpdated(totalBytesSent)
        }
        return bytesRead
    }

    fun getTotalBytesSent(): Long = totalBytesSent
}

在路由中使用,等待响应完成后记录最终数值:

route("/files") {
    get("/{fileName}") {
        val fileName = call.parameters["fileName"] ?: return@get call.respond(HttpStatusCode.BadRequest)
        val file = File("/path/to/your/files/$fileName")
        if (!file.exists()) return@get call.respond(HttpStatusCode.NotFound)

        var totalBytesSent = 0L
        val countingChannel = CountingByteReadChannel(file.readChannel()) { count ->
            totalBytesSent = count
            // 可选:实时打印传输进度
            // println("已发送 $count 字节")
        }

        call.respond(object : OutgoingContent.ReadChannelContent() {
            override val channel: ByteReadChannel = countingChannel
            override val contentType: ContentType? = ContentType.Application.OctetStream
            override val contentLength: Long? = file.length()
        })

        // 响应发送完成后执行统计逻辑
        call.response.complete().invokeOnCompletion {
            println("请求 ${call.request.uri} 累计发送 $totalBytesSent 字节")
            addTraffic(device, totalBytesSent)
        }
    }
}

方案二:扩展LocalFileContent(针对本地文件场景)

如果你习惯用LocalFileContent,可以直接扩展它,重写readFrom方法来统计实际写入响应通道的字节:

class CountingLocalFileContent(
    file: File,
    private val onComplete: (Long) -> Unit
) : LocalFileContent(file) {
    private var totalBytesSent = 0L

    override suspend fun readFrom(channel: ByteWriteChannel) {
        val countingSource = file.readChannel().let { source ->
            object : ByteReadChannel by source {
                override suspend fun read(dst: ByteBuffer): Int {
                    val bytesRead = super.read(dst)
                    if (bytesRead > 0) {
                        totalBytesSent += bytesRead
                    }
                    return bytesRead
                }
            }
        }
        channel.writeFully(countingSource)
        onComplete(totalBytesSent)
    }
}

路由中使用示例:

get("/{fileName}") {
    val fileName = call.parameters["fileName"] ?: return@get call.respond(HttpStatusCode.BadRequest)
    val file = File("/path/to/your/files/$fileName")
    if (!file.exists()) return@get call.respond(HttpStatusCode.NotFound)

    call.respond(CountingLocalFileContent(file) { totalBytes ->
        println("累计发送 $totalBytes 字节")
        addTraffic(device, totalBytes)
    })
}

这两种方案都是在数据实际写入响应通道时计数,完全不受缓冲或反向代理缓存的影响,统计结果精准。

内容的提问来源于stack exchange,提问作者Vodes

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 22:05:18