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

Posted

技术标签:

【中文标题】使用 apache commons-net FTPClient 传输原始二进制文件?【英文标题】:Transfer raw binary with apache commons-net FTPClient? 【发布时间】:2011-03-09 21:57:05 【问题描述】:

更新:已解决

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

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

<?xml version='1.0' encoding='utf-16'?><data><blah>blah</blah></data>

这是十六进制:0000000: 003c 003f 0078 006d 006c 0020 0076 0065 .<.?.x.m.l. .v.e0000010: 0072 0073 0069 006f 006e 003d 0027 0031 .r.s.i.o.n.=.'.10000020: 002e 0030 0027 0020 0065 006e 0063 006f ...0.'. .e.n.c.o0000030: 0064 0069 006e 0067 003d 0027 0075 0074 .d.i.n.g.=.'.u.t0000040: 0066 002d 0031 0036 0027 003f 003e 000a .f.-.1.6.'.?.>..0000050: 003c 0064 0061 0074 0061 003e 000a 0009 .<.d.a.t.a.>....0000060: 003c 0062 006c 0061 0068 003e 0062 006c .<.b.l.a.h.>.b.l0000070: 0061 0068 003c 002f 0062 006c 0061 0068 .a.h.<./.b.l.a.h0000080: 003e 000a 003c 002f 0064 0061 0074 0061 .>...<./.d.a.t.a0000090: 003e 000a                                                                     .>..

当我对该文件使用ASCII 模式时,它会正确地逐字节传输;结果具有相同的 md5sum。伟大的。当我使用BINARY 传输模式时,除了将字节从InputStream 洗牌到OutputStream 之外,它不应该做任何事情,结果是换行符(0x0a)被转换为回车符+换行符对( 0x0d0a)。这是二进制传输后的十六进制:

0000000: 003c 003f 0078 006d 006c 0020 0076 0065 .<.?.x.m.l. .v.e0000010: 0072 0073 0069 006f 006e 003d 0027 0031 .r.s.i.o.n.=.'.10000020: 002e 0030 0027 0020 0065 006e 0063 006f ...0.'. .e.n.c.o0000030: 0064 0069 006e 0067 003d 0027 0075 0074 .d.i.n.g.=.'.u.t0000040: 0066 002d 0031 0036 0027 003f 003e 000d .f.-.1.6.'.?.>..0000050: 0a00 3c00 6400 6100 7400 6100 3e00 0d0a ..<.d.a.t.a.>...0000060: 0009 003c 0062 006c 0061 0068 003e 0062 ...<.b.l.a.h.>.b0000070: 006c 0061 0068 003c 002f 0062 006c 0061 .l.a.h.<./.b.l.a0000080: 0068 003e 000d 0a00 3c00 2f00 6400 6100 .h.>....<./.d.a.0000090: 7400 6100 3e00 0d0a                                     t.a.>...

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

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

我查看了 commons-net 源代码,它看起来与使用 BINARY 模式时的奇怪行为无关。但是它在BINARY 模式下读取的InputStream 只是一个包裹在套接字InputStream 上的java.io.BufferedInptuStream。这些较低级别的 java 流是否做过任何奇怪的字节操作?如果他们这样做了,我会感到震惊,但我看不出这里还会发生什么。

编辑 1:

这是一段模仿我下载文件的最小代码。要编译,只需执行

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

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

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

代码:

(注意:此代码已修改为使用自定义 FTPClient 对象,链接在下面的“EDIT 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 的建议,这是我的结果。我复制了 apache 的 FTPClient,名为 MyFTPClient,并使用 CRC32 校验和将 SocketInputStreamBufferedInputStream 包装在 CheckedInputStream 中。此外,我包装了 FileOutputStreamFTPClient 以将输出存储在带有 CRC32 校验和的 CheckOutputStream 中。 MyFTPClient 的代码发布在here,我已经修改了上面的测试代码以使用这个版本的 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

这基本上是零意义,因为这里是相应文件的 md5sum:

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 传输,但我打算继续这样做,直到我了解到底发生了什么。我会用我的发现更新这个帖子,我会继续感谢任何人可能做出的任何贡献。希望这在某些时候对某人有用!

【问题讨论】:

哇,这很奇怪。我检查了BufferedInputStreamSocketInputStream 的源代码(至少是Java 部分),我没有看到任何可以改变字节的东西。我建议制作FTPClient 的副本并将输入流层次结构更改为CheckedInputStream(BufferedInputStream(CheckedInputStream(SocketInputStream()))),并使用校验和来查看您是否可以识别字节被更改的位置。这将是问题中有用的信息。 (更好的是,将您的测试代码放到网上并链接到它) 另外,为写得很好的问题 +1 ;-) 我会试试这个;谢谢。我从未听说过 CheckedInputStream。太酷了!! 还有其他人(例如这里)尝试重现此内容吗? 【参考方案1】:

在我看来,您的应用程序代码可能已经反转了 ASCII 和 BINARY 模式的选择。 ASCII 保持不变,执行行尾字符转换的 BINARY 与 FTP 的工作方式完全相反

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

编辑

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

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

【讨论】:

看起来是这样,但我至少运行了 5 次代码并尽可能多地删除了变量。我编辑了我的帖子以包含我已验证的代码重新创建了问题。不幸的是,我不能提供一个 ftp 站点来下载文件,所以希望你可以访问一个(我只是在 localhost 上测试)。感谢您的回复,如果您有任何想法可以分享,我将不胜感激! 如果代码全部正确,我认为您提到的第一种情况是最可能的解释。这是 Ubuntu 上一个相当默认的 proftp 安装。我刚刚尝试使用标准的 ftp 命令行客户端下载,并且 xml 文件运行良好(这在某种程度上是意料之中的,因为客户端可能使用的是 ascii 模式,它确实使用 FTPClient 正确传输了 xml)。它还正确传输 mp3 文件(相同的 md5sum),因此它看起来不像是服务器,除非 FTPClient 使用与 cmd 行客户端不同的设置连接到它(有可能)。 另外,我会支持你的帮助,但我还没有 15 个代表点! :)【参考方案2】:

登陆ftp服务器后

ftp.setFileType(FTP.BINARY_FILE_TYPE);

下面这行没有解决:

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

【讨论】:

谢谢,这是为我做的。奇怪的是文本模式是默认的。【参考方案3】:

我发现 Apache retrieveFile(...) 有时不适用于文件大小超过特定限制。为了克服这个问题,我会改用retrieveFileStream()。在下载之前,我已经设置了正确的文件类型并将模式设置为 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();
    ....

【讨论】:

以上是关于使用 apache commons-net FTPClient 传输原始二进制文件?的主要内容,如果未能解决你的问题,请参考以下文章

基于commons-net实现ftp创建文件夹上传下载功能.

apache FTP

ftp上传文件

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

commons-net 兼容 ssh-2.0 协议

maven中,POM.XML中怎么配置org.apache.commons.net.ftp引用包,求配置代码- -