android ftp4j 源码分析

来源:互联网 发布:上海黄浦区java培训 编辑:程序博客网 时间:2024/05/21 11:56

android ftp4j 源码分析

FTP理论

FTP会话时包含了两个通道,一个叫控制通道,一个叫数据通道。重点这个两个通道就是下面源码分析里面为什么通道会这么多……
控制通道:控制通道是和FTP服务器进行沟通的通道,连接FTP,发送FTP指令都是通过控制通道来完成的。
数据通道:数据通道是和FTP服务器进行文件传输或者列表的通道。


FTP协议中,控制连接均有客户端发起,而数据连接有两种工作方式:PORT方式和PASV方式

PORT模式(主动方式)
FTP 客户端首先和FTP Server的TCP 21端口建立连接,通过这个通道发送命令,客户端需要接收数据的时候在这个通道上发送PORT命令。 PORT命令包含了客户端用什么端口(一个大于1024的端口)接收数据。在传送数据的时候,服务器端通过自己的TCP 20端口发送数据。 FTP server必须和客户端建立一个新的连接用来传送数据。
PASV模式(被动方式)
在建立控制通道的时候和PORT模式类似,当客户端通过这个通道发送PASV 命令的时候,FTP server打开一个位于1024和5000之间的随机端口并且通知客户端在这个端口上传送数据的请求,然后FTP server 将通过这个端口进行数据的传送,这个时候FTP server不再需要建立一个新的和客户端之间的连接传送数据。

 

好了理论就这么简单,再写下去自己就要睡着了,不过上面那个倒是重点,主要是为理解后面源码铺垫的,所以不能错过耶~~

 

FTP协议

ftp协议这里就不给那么多,给了估计大家看了也是白看(因为本人就是,多而枯燥,如果每样都去记这明显是笨蛋的行为,吃力而且效果不明显,这里就说到右脑开发,说白了就是文字图片记忆,虽然高中尝试过锻炼,不过后面自己觉得效果好像没什么显著,估计那个时候思维已经被这个社会的思维给框住了,多悲哀!不过如果代码跟图片结合一起来记忆,效果还是明显。举个例子和一个图片:这张图片是我从某人的网站窃来的^-^

 Android Service And Bound Service总结思维

android service 的思维导图,很明显这样代码跟图片一起结合使用,这个知识点你很快就记住了)

 

因此下面协议只列出本文章举例代码里面用到的,剩下的可以问谷老师。

命令

ACCT <account>
系统特权帐号

LIST <name>
如果是文件名列出文件信息,如果是目录则列出文件列表

MODE <mode>
传输模式(S=流模式,B=块模式,C=压缩模式)

NOOP
无动作,除了来自服务器上的承认

PASS <password>
系统登录密码

PASV
被动模式,等待服务器告诉客户端去哪个端口建立连接

PORT <address>
IP 地址和两字节的端口 ID

QUIT
从 FTP 服务器上退出登录

TYPE <data type>
数据类型(A=ASCII,E=EBCDIC,I=binary)

USER <username>>
系统登录的用户名

 

响应代码

120
服务器准备就绪的时间(分钟数)

125
打开数据连接,开始传输

150
打开连接

200
成功

220
服务就绪

221
退出网络

227
进入被动模式(IP 地址、ID 端口)

230
登录因特网

331
要求密码

332
要求帐号

 

FTP服务器与客户端搭建

这里以Serv-U作为服务器,以LeapFTP3.0.1.46_yfy作为客户端来测试,搭建这个东西主要是为了测试服务器是否建立正确,确认后就可以开始动手做android 客户端的。服务器的搭建很简单,连我这种读过大学的人都知道,更何况那些没读过小学的(^-^这句话没逻辑错误吧)。

 

总的思路

1.创建核心类对象

2.建立socket连接

3.建立控制通道(命令通道)类

4.登陆

5.获取服务器目录文件信息

6.关闭连接

 

源码分析

这个源码分析倒没什么难点,跟之前分析android smack包源码差不多,基本都是以socket为核心,然后根据不同的协议定义不同的解释类,设计不同的项目框架。

(声明在看下面的文章时,最好先理解一下ftp4j的使用,这样才能更容易明白)

FTPClassProgrammer

(谨记:上图只显示本文章解释所要用到的类和方法,减缩了一些跟本文主题无关的代码或者一些判断,只留一条贯穿着从建立连接——获取服务器目录文件信息——关闭连接的线。)

1.

建立FTPClient对象,连接服务器

ftp.connect("169.254.xxx.xxx", 21);

 

 

public String[] connect(String host, int port)            throws IllegalStateException, IOException,            FTPIllegalReplyException, FTPException {        synchronized (this.lock) {            if (this.connected) {                throw new IllegalStateException("Client already connected to "                        + host + " on port " + port);            }            Socket connection = null;            try {                connection = this.connector.connectForCommunicationChannel(                        host, port);                if (this.security == 1) {                    connection = ssl(connection, host, port);                }                this.communication = new FTPCommunicationChannel(connection,                        "UTF-8");                for (Iterator i = this.communicationListeners.iterator(); i                        .hasNext();) {                    this.communication                            .addCommunicationListener((FTPCommunicationListener) i                                    .next());                }                FTPReply wm = this.communication.readFTPReply();                if (!wm.isSuccessCode()) {                    throw new FTPException(wm);                }                this.connected = true;                // this.authenticated = false;                // this.parser = null;                this.host = host;                this.port = port;                this.username = null;                this.password = null;                this.utf8Supported = false;                this.restSupported = false;                this.mlsdSupported = false;                this.modezSupported = false;                // this.dataChannelEncrypted = false;                // Returns the welcome message.                return wm.getMessages();            } catch (IOException e) {                // D'oh!                throw e;            } finally {                // If connection has failed...                if (!connected) {                    if (connection != null) {                        // Close the connection, 'cause it should be open.                        try {                            connection.close();                        } catch (Throwable t) {                            ;                        }                    }                }            }        }    }

 

在连接服务器过程中,

connection = this.connector.connectForCommunicationChannel( host, port);

创建了控制通道的socket

this.communication= new FTPCommunicationChannel(connection,"UTF-8");

然后管理socket创建控制通道的管理类,监听信息入口的接收与发送,建立socket完成后通过FTPReply wm = this.communication.readFTPReply(); 获取服务器的信息,判断是否成功。

 

2.

public FTPCommunicationChannel(Socket connection, String charsetName)            throws IOException {        this.connection = connection;        this.charsetName = charsetName;        InputStream inStream = connection.getInputStream();        OutputStream outStream = connection.getOutputStream();        // Wrap the streams into reader and writer objects.        reader = new NVTASCIIReader(inStream, charsetName);        writer = new NVTASCIIWriter(outStream, charsetName);    }

 

private String read() throws IOException {        // Read the line from the server.        String line = reader.readLine();        if (line == null) {            throw new IOException("FTPConnection closed");        }        // Call received() method on every communication listener        // registered.        for (Iterator iter = communicationListeners.iterator(); iter.hasNext();) {            FTPCommunicationListener l = (FTPCommunicationListener) iter.next();            l.received(line);        }        // Return the line read.        return line;    }

 

public void sendFTPCommand(String command) throws IOException {        writer.writeLine(command);        for (Iterator iter = communicationListeners.iterator(); iter.hasNext();) {            FTPCommunicationListener l = (FTPCommunicationListener) iter.next();            l.sent(command);        }    }

 

监听socket信息接收与发送的管理类,该类创建了两个继承Reader和Writer的类来接收与发送信息——NVTASCIIReader与NVTASCIIWriter,read() 读取方法,sendFTPCommand(String command) 发送信息,每次发送与接收都会触发监听事件

3.

NVTASCIIReader 类读取信息方法:

public String readLine() throws IOException {        StringBuffer buffer = new StringBuffer();        int previous = -1;        int current = -1;        while (true) {            int i = this.reader.read();            if (i == -1) {                if (buffer.length() == 0) {                    return null;                }                return buffer.toString();            }            previous = current;            current = i;            if (/* previous == '\r' && */current == '\n') {                // End of line.                return buffer.toString();            } else if (previous == '\r' && current == 0) {                // Literal new line.                buffer.append(SYSTEM_LINE_SEPARATOR);            } else if (current != 0 && current != '\r') {                buffer.append((char) current);            }        }    }

该方法每次读取一行一旦遇到\n就返回。相反NVTASCIIReader 类发送信息也是如此:

public void writeLine(String str) throws IOException {        StringBuffer buffer = new StringBuffer();        boolean atLeastOne = false;        StringTokenizer st = new StringTokenizer(str, LINE_SEPARATOR);        int count = st.countTokens();        for (int i = 0; i < count; i++) {            String line = st.nextToken();            if (line.length() > 0) {                if (atLeastOne) {                    buffer.append('\r');                    buffer.append('\000');                }                buffer.append(line);                atLeastOne = true;            }        }        if (buffer.length() > 0) {            String statement = buffer.toString();            this.writer.write(statement);            this.writer.write("\r\n");            this.writer.flush();        }    }

该方法主要根据换行符分离字符串,然后加上回车符,直到要发送的文字结束才加上换行符。

 

4.

FTPReply wm = this.communication.readFTPReply();读取并分析服务器返回的数据,返回一个包括服务器的返回码和信息的FTPReply 类。代码比较无聊,就不贴出来了,可以去查看项目源码。

5.

接下来是登陆服务器。该方法一步一步验证用户名、密码最后是

public void login(String username, String password, String account)            throws IllegalStateException, IOException,            FTPIllegalReplyException, FTPException {        synchronized (this.lock) {            this.authenticated = false;            this.communication.sendFTPCommand("USER " + username);            FTPReply r = this.communication.readFTPReply();            boolean passwordRequired;            boolean accountRequired;            switch (r.getCode()) {            case 230:                passwordRequired = false;                accountRequired = false;                break;            case 331:                passwordRequired = true;                accountRequired = false;                break;            case 332:                passwordRequired = false;                accountRequired = true;            default:                throw new FTPException(r);            }            if (passwordRequired) {                if (password == null) {                    throw new FTPException(331);                }                this.communication.sendFTPCommand("PASS " + password);                r = this.communication.readFTPReply();                switch (r.getCode()) {                case 230:                    accountRequired = false;                    break;                case 332:                    accountRequired = true;                    break;                default:                    throw new FTPException(r);                }            }            if (accountRequired) {                if (account == null) {                    throw new FTPException(332);                }                this.communication.sendFTPCommand("ACCT " + account);                r = this.communication.readFTPReply();                switch (r.getCode()) {                case 230:                    break;                default:                    throw new FTPException(r);                }            }            this.authenticated = true;            this.username = username;            this.password = password;        }        postLoginOperations();        startAutoNoopTimer();    }

登陆成功后,运行postLoginOperations() 和startAutoNoopTimer()方法,前一个是获取服务器支持哪些功能,后一个是启动循环等待计时,每段时间都去请求服务器的承认。

 

6.

FTPFile[] list = ftp.list();这里是重点,主要是获取服务器当前目录的文件。该方法去除了很多判断的枝末^0^,如果想要完整地看它是如何处理的就要去研究查看源码,也不难就是变量多了点。这里使用被动方式,这个方式在文章开头理论就有说明。很简单,先在控制通道(比如A通道)发送一个PASV这个协议(说:hey,man 我想建立socket来传输数据,给个端口我),服务器返回一个随机端口告诉客户端,客户端分析出这个端口,然后与服务器建立一个新的socket。

private FTPDataTransferConnectionProvider openPassiveDataTransferChannel()            throws IOException, FTPIllegalReplyException, FTPException {        // Send the PASV command.        communication.sendFTPCommand("PASV");        // Read the reply.        FTPReply r = communication.readFTPReply();        touchAutoNoopTimer();        if (!r.isSuccessCode()) {            throw new FTPException(r);        }        // Use a regexp to extract the remote address and port.        String addressAndPort = null;        String[] messages = r.getMessages();        for (int i = 0; i < messages.length; i++) {            Matcher m = PASV_PATTERN.matcher(messages[i]);            if (m.find()) {                int start = m.start();                int end = m.end();                addressAndPort = messages[i].substring(start, end);                break;            }        }        if (addressAndPort == null) {            // The remote server has not sent the coordinates for the            // data transfer connection.            throw new FTPIllegalReplyException();        }        // Parse the string extracted from the reply.        StringTokenizer st = new StringTokenizer(addressAndPort, ",");        int b1 = Integer.parseInt(st.nextToken());        int b2 = Integer.parseInt(st.nextToken());        int b3 = Integer.parseInt(st.nextToken());        int b4 = Integer.parseInt(st.nextToken());        int p1 = Integer.parseInt(st.nextToken());        int p2 = Integer.parseInt(st.nextToken());        final InetAddress remoteAddress;        // Ignore address?        // String useSuggestedAddress = System        // .getProperty(FTPKeys.PASSIVE_DT_USE_SUGGESTED_ADDRESS);        String useSuggestedAddress = "IP";        if ("true".equalsIgnoreCase(useSuggestedAddress)                || "yes".equalsIgnoreCase(useSuggestedAddress)                || "1".equals(useSuggestedAddress)) {            remoteAddress = InetAddress.getByAddress(new byte[] { (byte) b1,                    (byte) b2, (byte) b3, (byte) b4 });        } else {            remoteAddress = InetAddress.getByName(host);        }        final int remotePort = (p1 << 8) | p2;        FTPDataTransferConnectionProvider provider = new FTPDataTransferConnectionProvider() {            public Socket openDataTransferConnection() {                // Establish the connection.                Socket dtConnection = null;                String remoteHost = remoteAddress.getHostAddress();                try {                    dtConnection = connector.connectForDataTransferChannel(                            remoteHost, remotePort);                } catch (IOException e) {                }                return dtConnection;            }            public void dispose() {                // nothing to do            }        };        return provider;    }

该方法返回一个新的socket。

然后就是通过新的socket来接收服务器端返回的file列表,(谨记:所有请求协议都是通过控制通道(A通道)发送的)

FTPDataTransferConnectionProvider provider = openDataTransferChannel();            String command = "LIST";            // Adds the file/directory selector.            if (fileSpec != null && fileSpec.length() > 0) {                command += " " + fileSpec;            }            // Sends the command.            communication.sendFTPCommand(command);            Socket dtConnection;            try {                try {                    dtConnection = provider.openDataTransferConnection();                } finally {                    r = communication.readFTPReply();                    touchAutoNoopTimer();                    if (r.getCode() != 150 && r.getCode() != 125) {                        throw new FTPException(r);                    }                }            } finally {                provider.dispose();            }            // Fetch the list from the data transfer connection.            ArrayList lines = new ArrayList();            NVTASCIIReader dataReader = null;            try {                // Opens the data transfer connection.                dataTransferInputStream = dtConnection.getInputStream();                // MODE Z enabled?                if (modezEnabled) {                    dataTransferInputStream = new InflaterInputStream(                            dataTransferInputStream);                }                // Let's do it!                dataReader = new NVTASCIIReader(dataTransferInputStream,                        "UTF-8");                String line;                while ((line = dataReader.readLine()) != null) {                    if (line.length() > 0) {                        lines.add(line);                    }                }            } catch (IOException e) {            } finally {                if (dataReader != null) {                    try {                        dataReader.close();                    } catch (Throwable t) {                        ;                    }                }                try {                    dtConnection.close();                } catch (Throwable t) {                    ;                }                // Consume the result reply of the transfer.                communication.readFTPReply();                // Set to null the instance-level input stream.                dataTransferInputStream = null;            }

这样一个文件目录的获取就完成了,举一反三,下载、上传同样道理。

 

^0^看得这么辛苦,最后给个该流程的思维导图,导图没有什么规范,就是按照我觉得比较容易理解的方式画出来。(图片好像过大了……)

android FTP获取server文件的流程

 

总结:

未命名

红色线发送数据请求,蓝色线获取数据分析,主的线索就是这么简单。剩余的就是socket类建立的设计,信息协议类的设计。

个人观点:可能是协议的不同,我看的这个ftp源码跟smack源码比较发现还是smack源码项目设计得比较好,有很多地方可以扩展自定义消息,当然整个设计也是复杂多一点。

 

项目下载:http://files.cnblogs.com/not-code/simualteFTP.zip

原文地址:http://www.cnblogs.com/not-code/archive/2011/08/10/2134318.html

本文为原创,如需转载,请注明作者和出处,谢谢!

 

原创粉丝点击