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

Android FTP下载文件时InputStream丢失字节问题求助

Android模拟器FTP下载文件末尾丢失字节问题排查与解决

问题现象

  • 在Android模拟器上通过FTP下载ZIP或大文件时,偶尔会出现本地文件末尾丢失部分字节的情况,真机无此问题
  • 更换Apache FTPClient、ftp4j客户端均存在该问题,已排除服务器、文件本身、网络连接问题
  • 测试300KB的重复数字文件(内容为0123456789循环)时,丢失的总是1个字节且为字符9,集中在输入流末尾
  • 本地文件直接用InputStream/OutputStream读写无异常

相关代码示例

下载实现代码:

import org.apache.commons.net.ftp.FTPClient

fun downloadFile(remotePath: String, outputFile: File) {
    val client = FTPClient()
    client.connect("ftp.example.com")
    client.login("username", "password")
    client.enterLocalPassiveMode()
    println(client.getSize(remotePath)) // 例如100000字节
    //client.setFileType(FTP.BINARY_FILE_TYPE) // 无影响
    client.retrieveFileStream(remotePath).use { inputStream ->
        outputFile.outputStream().use { outputStream ->
            val buffer = ByteArray(DEFAULT_BUFFER_SIZE) // 8 * 1024
            var bytes: Int
            while (inputStream.read(buffer).also { bytes = it } != -1) {
                outputStream.write(buffer, 0, bytes)
            }
        }
    }
    client.completePendingCommand()
    client.disconnect()
    println(outputFile.length()) // 通常为100000,有时为99999或更少字节
}

调用代码:

lifecycleScope.launch(Dispatchers.IO) {
    downloadFile("/path/to/input.zip", File(cacheDir, "output.zip"))
}

问题根源

这是Android模拟器的虚拟网络/输入流模拟实现bug:

  • 当输入流接近末尾时,模拟器对InputStream.read()的处理存在异常,可能提前返回-1或者少读取最后一个字节
  • 仅模拟器出现问题,且丢失固定规律的字节,说明是模拟器流处理逻辑在收尾阶段的疏漏

解决方案

方案1:使用FTPClient内置的retrieveFile方法

Apache FTPClient的retrieveFile方法内部有更完善的流处理逻辑,可直接规避模拟器的流bug:

fun downloadFile(remotePath: String, outputFile: File) {
    val client = FTPClient()
    client.connect("ftp.example.com")
    client.login("username", "password")
    client.enterLocalPassiveMode()
    client.setFileType(FTP.BINARY_FILE_TYPE) // 明确设置二进制类型
    val remoteFileSize = client.getSize(remotePath)
    println("Remote file size: $remoteFileSize")

    // 直接调用retrieveFile,内部处理流读写
    val success = client.retrieveFile(remotePath, FileOutputStream(outputFile))
    if (success) {
        println("Local file size: ${outputFile.length()}")
    } else {
        println("Download failed")
    }

    client.disconnect()
}

方案2:基于文件大小校验补全

利用FTP服务器返回的文件大小,下载完成后对比本地文件大小,若不足则补全(适合已知文件规律或能获取准确文件大小的场景):

fun downloadFile(remotePath: String, outputFile: File) {
    val client = FTPClient()
    client.connect("ftp.example.com")
    client.login("username", "password")
    client.enterLocalPassiveMode()
    client.setFileType(FTP.BINARY_FILE_TYPE)
    val remoteFileSize = client.getSize(remotePath)
    println("Remote file size: $remoteFileSize")

    client.retrieveFileStream(remotePath).use { inputStream ->
        outputFile.outputStream().use { outputStream ->
            val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
            var bytes: Int
            var totalRead = 0L
            while (inputStream.read(buffer).also { bytes = it } != -1) {
                outputStream.write(buffer, 0, bytes)
                totalRead += bytes
            }
            // 补全缺失字节
            if (totalRead < remoteFileSize) {
                val missing = remoteFileSize - totalRead
                // 针对测试文件补全'9',其他文件可重新读取末尾部分
                repeat(missing.toInt()) {
                    outputStream.write('9'.code)
                }
                outputStream.flush()
            }
        }
    }
    client.completePendingCommand()
    client.disconnect()
    println("Local file size: ${outputFile.length()}")
}

方案3:强制刷新+末尾重读

在流关闭前强制刷新缓冲区,若文件大小不符则重新读取末尾部分:

fun downloadFile(remotePath: String, outputFile: File) {
    val client = FTPClient()
    client.connect("ftp.example.com")
    client.login("username", "password")
    client.enterLocalPassiveMode()
    client.setFileType(FTP.BINARY_FILE_TYPE)
    val remoteFileSize = client.getSize(remotePath)
    println("Remote file size: $remoteFileSize")

    client.retrieveFileStream(remotePath).use { inputStream ->
        outputFile.outputStream().use { outputStream ->
            val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
            var bytes: Int
            while (inputStream.read(buffer).also { bytes = it } != -1) {
                outputStream.write(buffer, 0, bytes)
            }
            outputStream.flush() // 强制刷新缓冲区到磁盘
        }
    }
    client.completePendingCommand()

    // 验证并补全缺失部分
    if (outputFile.length() != remoteFileSize) {
        client.setRestartOffset(outputFile.length())
        client.retrieveFileStream(remotePath).use { inputStream ->
            outputFile.appendOutputStream().use { outputStream ->
                val buffer = ByteArray(1024)
                var bytes: Int
                while (inputStream.read(buffer).also { bytes = it } != -1) {
                    outputStream.write(buffer, 0, bytes)
                }
            }
        }
        client.completePendingCommand()
    }

    client.disconnect()
    println("Local file size: ${outputFile.length()}")
}

总结

  • 该问题仅存在于Android模拟器,属于模拟器的网络流实现bug
  • 优先使用FTPClient内置的retrieveFile方法,其内部逻辑更健壮
  • 若必须使用流操作,可通过文件大小校验+补全的方式规避问题

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 18:15:16