建立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(); 获取服务器的信息,判断是否成功。
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) 发送信息,每次发送与接收都会触发监听事件
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(); } }
该方法主要根据换行符分离字符串,然后加上回车符,直到要发送的文字结束才加上换行符。
FTPReply wm = this.communication.readFTPReply();读取并分析服务器返回的数据,返回一个包括服务器的返回码和信息的FTPReply 类。代码比较无聊,就不贴出来了,可以去查看项目源码。
接下来是登陆服务器。该方法一步一步验证用户名、密码最后是
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()方法,前一个是获取服务器支持哪些功能,后一个是启动循环等待计时,每段时间都去请求服务器的承认。
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^看得这么辛苦,最后给个该流程的思维导图,导图没有什么规范,就是按照我觉得比较容易理解的方式画出来。(图片好像过大了……)
红色线发送数据请求,蓝色线获取数据分析,主的线索就是这么简单。剩余的就是socket类建立的设计,信息协议类的设计。
个人观点:可能是协议的不同,我看的这个ftp源码跟smack源码比较发现还是smack源码项目设计得比较好,有很多地方可以扩展自定义消息,当然整个设计也是复杂多一点。