Apache Commons FTPClient下载文件丢字节问题排查与解决咨询
问题分析与解决方案
首先,从你的描述和代码来看,字节丢失的核心原因在于手动处理FTP流时没有正确完成整个传输生命周期,而Apache Commons FTPClient提供的retrieveFile方法(你的版本1)之所以成功率高,是因为它内部已经封装了可靠的传输逻辑,避免了手动处理流的常见坑。
为什么会出现末尾丢字节?
- 手动流处理的缺陷:版本2和3中使用
retrieveFileStream后,虽然调用了completePendingCommand,但在高速传输场景下,服务器可能还在发送最后一批数据,而你的read循环已经提前退出(比如read返回-1时直接结束),导致末尾的部分字节没有被读取。缓冲区越大,这种情况越容易发生——大缓冲区一次读取大量数据,最后一次读取可能只拿到部分剩余数据,但循环直接终止,遗漏了后续的字节。 - 流关闭时机不当:手动关闭流时,如果没有确保所有数据都被flush并写入磁盘,也可能导致末尾数据丢失。
- 服务器响应处理不及时:
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
相关产品推荐
相关产品推荐

