使用apache commons-net FTPClient传输原始二进制文件?

更新:解决了

登录调用FTPClient.setFileType() ,导致FTP服务器使用默认模式( ASCII ),无论我将其设置为什么 。 另一方面,客户端的行为就像文件类型已正确设置一样。 BINARY模式现在完全按照需要工作,在所有情况下逐字节传输文件。 我所要做的只是在wireshark中进行一些流量嗅探,然后使用netcat模拟FTP命令以查看发生了什么。 两天前为什么我没想到这个?? 谢谢,大家帮忙!

我有一个xml文件,utf-16编码,我使用apache的commons-net-2.0 java库的FTPClient从FTP站点下载。 它支持两种传输模式: ASCII_FILE_TYPEBINARY_FILE_TYPE ,区别在于ASCII将用适当的本地行分隔符替换行分隔符( '\r\n'或只是'\n' – hex, 0x0d0a或仅0x0a ) 。 我的问题是:我有一个测试文件,utf-16编码,包含以下内容:



blah

这是hex:
0000000: 003c 003f 0078 006d 006c 0020 0076 0065 .<.?.xml .ve
0000010: 0072 0073 0069 006f 006e 003d 0027 0031 .rsion=.'.1
0000020: 002e 0030 0027 0020 0065 006e 0063 006f ...0.'. .enco
0000030: 0064 0069 006e 0067 003d 0027 0075 0074 .ding=.'.ut
0000040: 0066 002d 0031 0036 0027 003f 003e 000a .f.-.1.6.'.?.>..
0000050: 003c 0064 0061 0074 0061 003e 000a 0009 .....
0000060: 003c 0062 006c 0061 0068 003e 0062 006c ..bl
0000070: 0061 0068 003c 002f 0062 006c 0061 0068 .ah<./.blah
0000080: 003e 000a 003c 002f 0064 0061 0074 0061 .>...<./.data
0000090: 003e 000a .>..

当我对这个文件使用ASCII模式时,它正确地逐字节传输; 结果具有相同的md5sum。 大。 当我使用BINARY传输模式时,除了将InputStream字节混合到OutputStream不应该执行任何操作,结果是换行符( 0x0a )被转换为回车符+换行符对( 0x0d0a )。 这是二进制传输后的hex:

0000000: 003c 003f 0078 006d 006c 0020 0076 0065 .<.?.xml .ve
0000010: 0072 0073 0069 006f 006e 003d 0027 0031 .rsion=.'.1
0000020: 002e 0030 0027 0020 0065 006e 0063 006f ...0.'. .enco
0000030: 0064 0069 006e 0067 003d 0027 0075 0074 .ding=.'.ut
0000040: 0066 002d 0031 0036 0027 003f 003e 000d .f.-.1.6.'.?.>..
0000050: 0a00 3c00 6400 6100 7400 6100 3e00 0d0a .....
0000060: 0009 003c 0062 006c 0061 0068 003e 0062 ....b
0000070: 006c 0061 0068 003c 002f 0062 006c 0061 .lah<./.bla
0000080: 0068 003e 000d 0a00 3c00 2f00 6400 6100 .h.>....<./.da
0000090: 7400 6100 3e00 0d0a ta>...

它不仅转换换行符(它不应该),但它不尊重utf-16编码(不是我希望它知道它应该,它只是一个愚蠢的FTP管道)。 如果没有进一步处理来重新排列字节,结果是不可读的。 我只会使用ASCII模式,但我的应用程序也将在同一个管道上移动真正的二进制数据(mp3文件和jpeg图像)。 在这些二进制文件上使用BINARY传输模式也会导致它们将随机0x0d s注入其内容,由于二进制数据通常包含合法的0x0d0a序列,因此无法安全删除。 如果我对这些文件使用ASCII模式,那么“聪明”的FTPClient会将这些0x0d0a转换为0x0a ,无论我做什么,都会使文件不一致。

我想我的问题是(是):有没有人知道任何好的FTP库,只是将该死的字节从那里移到这里,或者我将不得不破解apache commons-net-2.0并维护我自己的FTP客户端代码只是为这个简单的应用程序 还有其他人处理过这种奇怪的行为吗? 任何建议,将不胜感激。

我检查了commons-net源代码,当使用BINARY模式时,它看起来不像是负责奇怪的行为。 但它在BINARY模式下读取的InputStream只是一个围绕套接字InputStreamjava.io.BufferedInptuStream 。 这些较低级别的Java流是否会进行任何奇怪的字节操作? 如果他们这样做我会感到震惊,但我不知道还有什么可以在这里发生。

编辑1:

这是一段模仿我正在下载文件的代码。 要编译,就这样做

 javac -classpath /path/to/commons-net-2.0.jar Main.java 

要运行,您需要目录/ tmp / ascii和/ tmp / binary来下载文件,以及设置文件的ftp站点。 代码还需要配置适当的ftp主机,用户名和密码。 我把文件放在test /文件夹下的测试ftp站点上,并调用文件test.xml。 测试文件至少应该有多行,并且是utf-16编码的(这可能不是必需的,但有助于重新创建我的确切情况)。 我在打开一个新文件并输入上面引用的xml文本后使用了vim’s :set fileencoding=utf-16命令。 最后,要运行,就这样做

 java -cp .:/path/to/commons-net-2.0.jar Main 

码:

(注意:此代码已修改为使用自定义FTPClient对象,在“编辑2”下面链接)

 import java.io.*; import java.util.zip.CheckedInputStream; import java.util.zip.CheckedOutputStream; import java.util.zip.CRC32; import org.apache.commons.net.ftp.*; public class Main implements java.io.Serializable { public static void main(String[] args) throws Exception { Main main = new Main(); main.doTest(); } private void doTest() throws Exception { String host = "ftp.host.com"; String user = "user"; String pass = "pass"; String asciiDest = "/tmp/ascii"; String binaryDest = "/tmp/binary"; String remotePath = "test/"; String remoteFilename = "test.xml"; System.out.println("TEST.XML ASCII"); MyFTPClient client = createFTPClient(host, user, pass, org.apache.commons.net.ftp.FTP.ASCII_FILE_TYPE); File path = new File("/tmp/ascii"); downloadFTPFileToPath(client, "test/", "test.xml", path); System.out.println(""); System.out.println("TEST.XML BINARY"); client = createFTPClient(host, user, pass, org.apache.commons.net.ftp.FTP.BINARY_FILE_TYPE); path = new File("/tmp/binary"); downloadFTPFileToPath(client, "test/", "test.xml", path); System.out.println(""); System.out.println("TEST.MP3 ASCII"); client = createFTPClient(host, user, pass, org.apache.commons.net.ftp.FTP.ASCII_FILE_TYPE); path = new File("/tmp/ascii"); downloadFTPFileToPath(client, "test/", "test.mp3", path); System.out.println(""); System.out.println("TEST.MP3 BINARY"); client = createFTPClient(host, user, pass, org.apache.commons.net.ftp.FTP.BINARY_FILE_TYPE); path = new File("/tmp/binary"); downloadFTPFileToPath(client, "test/", "test.mp3", path); } public static File downloadFTPFileToPath(MyFTPClient ftp, String remoteFileLocation, String remoteFileName, File path) throws Exception { // path to remote resource String remoteFilePath = remoteFileLocation + "/" + remoteFileName; // create local result file object File resultFile = new File(path, remoteFileName); // local file output stream CheckedOutputStream fout = new CheckedOutputStream(new FileOutputStream(resultFile), new CRC32()); // try to read data from remote server if (ftp.retrieveFile(remoteFilePath, fout)) { System.out.println("FileOut: " + fout.getChecksum().getValue()); return resultFile; } else { throw new Exception("Failed to download file completely: " + remoteFilePath); } } public static MyFTPClient createFTPClient(String url, String user, String pass, int type) throws Exception { MyFTPClient ftp = new MyFTPClient(); ftp.connect(url); if (!ftp.setFileType( type )) { throw new Exception("Failed to set ftpClient object to BINARY_FILE_TYPE"); } // check for successful connection int reply = ftp.getReplyCode(); if (!FTPReply.isPositiveCompletion(reply)) { ftp.disconnect(); throw new Exception("Failed to connect properly to FTP"); } // attempt login if (!ftp.login(user, pass)) { String msg = "Failed to login to FTP"; ftp.disconnect(); throw new Exception(msg); } // success! return connected MyFTPClient. return ftp; } } 

编辑2:

好的,我按照CheckedXputStream建议,这是我的结果。 我制作了一个名为MyFTPClient的apache的FTPClient MyFTPClient ,并使用CRC32校验和将MyFTPClientBufferedInputStream包装在CheckedInputStream 。 此外,我将FileOutputStream包装到FTPClient以将输出存储在具有CRC32校验和的CheckOutputStream 。 MyFTPClient的代码发布在这里 ,我修改了上面的测试代码以使用这个版本的FTPClient(试图将gist URL发布到修改后的代码,但我需要10个信誉点才能发布多个URL!), test.xmltest.mp3因此结果如下:

 14:00:08,644 DEBUG [main,TestMain] TEST.XML ASCII 14:00:08,919 DEBUG [main,MyFTPClient] Socket CRC32: 2739864033 14:00:08,919 DEBUG [main,MyFTPClient] Buffer CRC32: 2739864033 14:00:08,954 DEBUG [main,FTPUtils] FileOut CRC32: 866869773 14:00:08,955 DEBUG [main,TestMain] TEST.XML BINARY 14:00:09,270 DEBUG [main,MyFTPClient] Socket CRC32: 2739864033 14:00:09,270 DEBUG [main,MyFTPClient] Buffer CRC32: 2739864033 14:00:09,310 DEBUG [main,FTPUtils] FileOut CRC32: 2739864033 14:00:09,310 DEBUG [main,TestMain] TEST.MP3 ASCII 14:00:10,635 DEBUG [main,MyFTPClient] Socket CRC32: 60615183 14:00:10,635 DEBUG [main,MyFTPClient] Buffer CRC32: 60615183 14:00:10,636 DEBUG [main,FTPUtils] FileOut CRC32: 2352009735 14:00:10,636 DEBUG [main,TestMain] TEST.MP3 BINARY 14:00:11,482 DEBUG [main,MyFTPClient] Socket CRC32: 60615183 14:00:11,482 DEBUG [main,MyFTPClient] Buffer CRC32: 60615183 14:00:11,483 DEBUG [main,FTPUtils] FileOut CRC32: 60615183 

这基本上没有任何意义,因为这里是相应文件的md5sums:

 bf89673ee7ca819961442062eaaf9c3f ascii/test.mp3 7bd0e8514f1b9ce5ebab91b8daa52c4b binary/test.mp3 ee172af5ed0204cf9546d176ae00a509 original/test.mp3 104e14b661f3e5dbde494a54334a6dd0 ascii/test.xml 36f482a709130b01d5cddab20a28a8e8 binary/test.xml 104e14b661f3e5dbde494a54334a6dd0 original/test.xml 

我很茫然。 我发誓我在这个过程中的任何一点都没有置换文件名/路径,而且我已经对每一步进行了三重检查。 它必须是简单的东西,但我没有最模糊的想法,在哪里看下一步。 为了实用性,我将通过调用shell来进行FTP传输,但我打算继续这样做,直到我明白到底是怎么回事。 我会用我的发现更新这个post,我会继续感谢任何人可能有的贡献。 希望这在某些方面对某人有用!

登录ftp服务器后

 ftp.setFileType(FTP.BINARY_FILE_TYPE); 

以下这行不解决它:

 //ftp.setFileTransferMode(org.apache.commons.net.ftp.FTP.BINARY_FILE_TYPE); 

听起来好像你的应用程序代码可能已经选择了ASCII和BINARY模式。 ASCII没有变化,BINARY执行行尾字符翻译 FTP应该如何工作完全相反

如果这不是问题,请编辑您的问题以添加代码的相关部分。

编辑

其他一些可能的(但IMO不太可能)解释:

  • FTP服务器已损坏/配置错误。 (您是否可以使用非Java命令行FTP实用程序以ASCII / BINARY模式成功下载文件?)
  • 您正在通过损坏或配置错误的代理与FTP服务器通信。
  • 你以某种方式设法得到Apache FTP客户端JAR文件的一个狡猾(被黑客入侵)的副本。 (是的,是的,非常不可能…)

我发现Apache retrieveFile(…)有时不能使用超过一定限制的文件大小。 为了克服这个问题,我会使用retrieveFileStream()代替。 在下载之前,我已经设置了Correct FileType并将Mode设置为PassiveMode

所以代码看起来像

  .... ftpClientConnection.setFileType(FTP.BINARY_FILE_TYPE); ftpClientConnection.enterLocalPassiveMode(); ftpClientConnection.setAutodetectUTF8(true); //Create an InputStream to the File Data and use FileOutputStream to write it InputStream inputStream = ftpClientConnection.retrieveFileStream(ftpFile.getName()); FileOutputStream fileOutputStream = new FileOutputStream(directoryName + "/" + ftpFile.getName()); //Using org.apache.commons.io.IOUtils IOUtils.copy(inputStream, fileOutputStream); fileOutputStream.flush(); IOUtils.closeQuietly(fileOutputStream); IOUtils.closeQuietly(inputStream); boolean commandOK = ftpClientConnection.completePendingCommand(); ....