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

Apache Commons FTPClient下载文件丢字节问题排查与解决咨询

问题分析与解决方案

首先,从你的描述和代码来看,字节丢失的核心原因在于手动处理FTP流时没有正确完成整个传输生命周期,而Apache Commons FTPClient提供的retrieveFile方法(你的版本1)之所以成功率高,是因为它内部已经封装了可靠的传输逻辑,避免了手动处理流的常见坑。

为什么会出现末尾丢字节?

  1. 手动流处理的缺陷:版本2和3中使用retrieveFileStream后,虽然调用了completePendingCommand,但在高速传输场景下,服务器可能还在发送最后一批数据,而你的read循环已经提前退出(比如read返回-1时直接结束),导致末尾的部分字节没有被读取。缓冲区越大,这种情况越容易发生——大缓冲区一次读取大量数据,最后一次读取可能只拿到部分剩余数据,但循环直接终止,遗漏了后续的字节。
  2. 流关闭时机不当:手动关闭流时,如果没有确保所有数据都被flush并写入磁盘,也可能导致末尾数据丢失。
  3. 服务器响应处理不及时:completePendingCommand需要等待服务器返回传输完成的响应,如果在高速传输后立即调用,可能因为网络延迟导致响应未到达,客户端误以为传输完成,提前断开连接。

你的代码疏漏点

  • 版本2、3中,依赖缓冲区大小来控制读取逻辑,这是不可靠的——网络传输的数据包大小是不确定的,固定大缓冲区无法适配所有场景。
  • 版本2中加入的LockSupport.parkNanos是不必要的,反而可能导致线程阻塞,错过读取数据的时机。
  • 所有版本中,流的关闭没有放在finally块或使用try-with-resources,一旦发生异常,流可能无法正确关闭,导致资源泄漏或数据截断。
  • 没有设置FTP客户端的超时时间,高速传输时如果出现短暂网络卡顿,可能导致传输中断但未被捕获。

解决方案

1. 优先使用retrieveFile方法(版本1优化)

这是FTPClient官方推荐的可靠下载方式,它内部已经处理了流的读取、服务器响应校验和资源清理,是成功率最高的方案。优化后的代码如下:

public void downloadFiles(final String remoteFolder, final String localFolder, final ArrayList<String> filenames) {
    // 替换AsyncTask为ExecutorService(AsyncTask已被弃用)
    Executors.newSingleThreadExecutor().execute(() -> {
        if (!login()) {
            Log.e(TAG, "FTP login failed");
            logout();
            return;
        }
        try {
            ftpClient.enterLocalPassiveMode();
            ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
            ftpClient.setDataTimeout(30000); // 设置30秒数据传输超时
            ftpClient.setConnectTimeout(30000); // 设置连接超时

            if (!ftpClient.changeWorkingDirectory(remoteFolder)) {
                Log.e(TAG, "Failed to enter remote folder: " + remoteFolder);
                return;
            }

            for (String filename : filenames) {
                FTPFile[] remoteFiles = ftpClient.listFiles(filename);
                if (remoteFiles == null || remoteFiles.length == 0) {
                    Log.w(TAG, "Remote file not found: " + filename);
                    continue;
                }
                FTPFile remoteFile = remoteFiles[0];
                long expectedSize = remoteFile.getSize();
                String localPath = localFolder + File.separator + filename;
                File localFile = new File(localPath);

                // 使用try-with-resources自动关闭输出流
                try (FileOutputStream out = new FileOutputStream(localFile)) {
                    boolean downloadSuccess = ftpClient.retrieveFile(filename, out);
                    if (!downloadSuccess) {
                        Log.e(TAG, "Download failed for: " + filename);
                        localFile.delete(); // 删除不完整文件
                        continue;
                    }
                } catch (IOException e) {
                    Log.e(TAG, "Error writing file: " + filename, e);
                    localFile.delete();
                    continue;
                }

                // 校验文件大小,不一致则重试(可根据需求调整重试次数)
                if (localFile.length() != expectedSize) {
                    Log.w(TAG, "File size mismatch for " + filename + ": expected " + expectedSize + ", got " + localFile.length());
                    localFile.delete();
                    // 这里可以加入重试逻辑,比如重新调用下载逻辑
                    // 示例:downloadSingleFile(remoteFolder, localFolder, filename);
                } else {
                    Log.d(TAG, filename + " - OKAY");
                }
            }
        } catch (IOException e) {
            Log.e(TAG, "FTP operation failed", e);
        } finally {
            logout();
        }
    });
}

// 优化login方法,加入异常日志
private boolean login() {
    try {
        ftpClient.connect(url, port);
        if (ftpClient.isConnected()) {
            boolean loginSuccess = ftpClient.login(username, password);
            if (loginSuccess) {
                Log.d(TAG, "FTP logged in successfully");
                return true;
            } else {
                Log.e(TAG, "FTP login credentials failed");
                ftpClient.disconnect();
            }
        } else {
            Log.e(TAG, "Failed to connect to FTP server");
        }
    } catch (IOException e) {
        Log.e(TAG, "FTP connection error", e);
    }
    return false;
}

2. 若必须手动处理流(不推荐)

如果你因为某些原因需要使用retrieveFileStream,请确保以下几点:

  • 使用固定小缓冲区(比如4KB或8KB)循环读取,直到流完全结束
  • 读取完成后必须调用completePendingCommand,并检查其返回值
  • 确保所有流都正确关闭,使用try-with-resources

示例代码:

try (InputStream is = ftpClient.retrieveFileStream(filename);
     FileOutputStream out = new FileOutputStream(localPath)) {
    byte[] buffer = new byte[4096]; // 4KB缓冲区
    int bytesRead;
    long totalRead = 0;
    while ((bytesRead = is.read(buffer)) != -1) {
        out.write(buffer, 0, bytesRead);
        totalRead += bytesRead;
    }
    // 必须调用completePendingCommand并检查结果
    boolean commandSuccess = ftpClient.completePendingCommand();
    if (!commandSuccess) {
        Log.e(TAG, "FTP command failed after retrieving file: " + filename);
        localFile.delete();
        continue;
    }
    if (totalRead != expectedSize) {
        Log.w(TAG, "File size mismatch for " + filename);
        localFile.delete();
    }
} catch (IOException e) {
    Log.e(TAG, "Error processing file stream: " + filename, e);
    localFile.delete();
}

3. 其他优化建议

  • 替换AsyncTask:AsyncTask在Android API 26+已被弃用,改用ExecutorService或Coroutines(Kotlin)来处理异步任务,更稳定可靠。
  • 增加重试机制:对于下载失败或大小不一致的文件,加入有限次数的重试逻辑,提升成功率。
  • 网络环境适配:同网高速传输时,可尝试设置ftpClient.setBufferSize(8192)来调整内部缓冲区,优化传输性能。

总结

你的核心问题是手动流处理没有适配FTP的传输协议细节,而retrieveFile方法已经封装了这些逻辑,所以成功率最高。通过优化版本1的代码,加入超时设置、文件校验和重试逻辑,就能解决字节丢失的问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.07 21:38:15