概述
FTP 是File Transfer Protocol(文件传输协议)的英文简称,用于Internet上文件的双向传输。FTP的主要作用就是让客户端连接上一个远程计算机(这些计算机上运行着FTP服务器程序)从而察看远程计算机上的文件,然后把文件从远程计算机上拷到本地计算机,或把本地的文件上传到远程计算机。
FTP是仅基于TCP的服务,不支持UDP。FTP协议使用2个连接,一个数据连接和一个控制连接(用来传输客户端向FTP服务器发送的命令、传输FTP服务器向客户端命令的响应)。通常来说控制连接的端口号是21,数据连接的端口号是20,但由于FTP协议工作方式的不同,数据连接的端口号并不总是20,这也是FTP的主动与被动模式的最大不同之处。
FTP协议的两种工作模式
1. 主动模式(Port模式)
1> 客户端发起从一个任意的非特权端口N(N大于等于1024)连接到FTP服务器的控制端口21,从而建立控制连接。
2> 客户端开始监听客户端的端口N+1,并发送FTP命令“port N+1”到FTP服务器。
3> 服务器从自己的数据端口20连接到客户端指定的数据端口N+1,从而建立数据连接。
2. 被动模式(Pasv模式)
1> 客户端发起从一个任意的非特权端口N(N大于等于1024)连接到FTP服务器的控制端口21,从而建立控制连接。
2> 与主动模式不同,客户端不会发送PORT命令要求服务器来回连它的数据端口,而是发送PASV命令。
3> 服务器接收到会PASV命令后会开启一个任意的非特权端口P(大于等于1024),并发送PORT P命令给客户端。
4> 客户端接收到PORT P命令后会发起从非特权端口N+1连接到服务器的端口P,从而建立数据连接。
注意:
1> 由于控制连接和数据连接都由客户端发起,可以解决从FTP服务器到客户端的数据端口的入方连接被防火墙过滤掉的问题。
FTP协议命令与响应信息
在FTP服务的执行过程中,FTP客户端与FTP服务器之间需要传输控制信息,这些信息用于完成某个具体的FTP操作,它们可以分为两种类型:FTP命令与FTP响应信息。其中,FTP命令是FTP客户端向FTP服务器发送的操作请求,FTP响应信息是FTP服务器根据操作结果向FTP客户端返回的响应信息。FTP协议详细规定了每种协议命令的顺序--首先需要顺序发送USER与PASS命令,最后需要发送QUIT命令,其他命令的顺序没有特殊要求。
1. FTP协议命令
FTP 每个命令都有 3 到 4 个字母组成,命令后面跟参数,用空格分开。每个命令都以 "\r\n"结束。FTP命令的标准格式为:
命令名 <参数>
常用的FTP命令如下表所示:
命令 | 描述 |
---|---|
USER <username> | 参数是标记用户的Telnet串。用户标记是访问服务器必须的,此命令通常是控制连接后第一个发出的命令,有些主机还会要求口令和帐户。服务器可以在任何时间接收新的USER命令以改变访问控制和(或)帐户信息。这可以重新开始登录过程,所以传输参数不变,在进行中的文件传输在过去的访问控制参数下完成。 |
PASS <password> | 参数是标记用户口令的Telnet串。此命令紧跟USER命令,在某些站点它是完成访问控制不可缺少的一步。因此口令是个重要的东西,因此不能显示出来,服务器方没有办法隐藏口令,所以这一任务得由用户FTP进程完成。 |
ACCT <account> | 参数是标记用户帐户的Telnet串。此命令不需要与USER相关,一些站点可能需要帐户用于登录,另一些可以限制帐户的权限,在后一种情况下,此命令可在任何时候发送。应答的不同可以区别不同的情况:当登录需要帐户信息时,对PASS命令的响应是332。另外,如果不需要帐户信息,对PASS的响应是230,如果需要帐户信息在以后需要,服务器会返回332或532,这要看它是保存此命令还是拒绝此命令了。 |
CWD <dir path> | 此命令使用户可以在不同的目录或数据集下工作而不用改变它的登录或帐户信息。传输参数也不变。参数一般是目录名或与系统相关的文件集合。 |
CDUP | 该命令要求系统回到上一级目录 |
SMNT <pathname> | 此命令使用户在不改变登录或帐户信息的情况下加载另一个文件系统数据结构。传输参数也不变。参数是文件目录或与系统相关的文件集合。 |
REIN | 此命令终止USER,将所有I/O和帐户信息写入,但不许进行中的数据传输完成。重置所有参数,控制连接打开,可以再次开始USER命令。 |
OUIT | 此命令终止USER,如果没有数据传输,服务器关闭控制连接;如果有数据传输,在得到传输响应后服务器关闭控制连接。如果用户进程正在向不同的USER传输数据,不希望对每个USER关闭然后再打开,可以使用REIN。对控制连接的意外关闭,可以导致服务器运行中止(ABOR)和退出登录(QUIT)。 |
PORT <address> | 参数是要使用的数据连接端口,通常情况下对此不需要命令响应。如果使用此命令时,要发送32位的IP地址和16位的TCP端口号。上面的信息以8位为一组,逗号间隔十进制传输,如下例: PORT h1,h2,h3,h4,p1,p2 其中h1是IP地址的最高8位。 |
PASV | 此命令要求服务器DTP在指定的数据端口侦听,进入被动接收请求的状态,参数是主机和端口地址。 |
TYPE <data type> | 该命令定义文件类型以及打印格式 |
STRU <type> | 参数是一个Telnet字符代码指定文件结构。下面是代码及其意义: F - 文件(非记录结构),它是默认值 ;R - 记录结构; P - 页结构 |
MODE <mode> | 参数是一个Telnet字符代码指定传输模式。下面是代码及其意义: S - 流(默认值); B - 块; C - 压缩 |
RETR <filename> | 此命令使服务器DTP传送指定路径内的文件复本到服务器或用户DTP。这边服务器上文件的状态和内容不受影响。 |
STOR <filename> | 此命令使服务器DTP接收数据连接上传送过来的数据,并将数据保存在服务器的文件中。如果文件已存在,原文件将被覆盖。如果文件不存在,则新建文件。 |
STOU <filename> | 此命令和STOR差不多,此命令要求在此目录下的文件名是唯一的,对此命令的响应必须包括产生的用户名。 |
APPE <filename> | 它和STOR的功能差不多,但是如果文件在指定路径内已存在,则把数据附加到原文件尾部,如果不存在则新建文件。 |
ALLO <bytes> | 此命令用于在一些主机上为新传送的文件分配足够的存储空间。参数是十进制的逻辑字节数。如果是记录或页结构,页或记录的最大大小也需要,这在第二个参数内以十进制指定。第二个参数是可选的,如果有它,它和第一个参数以Telnet字符<SP> R <SP>分隔。此命令在STOR或APPE命令后,对于不需要分配存储空间的机器,它的作用等于NOOP。 |
REST <offset> | 参数域代表服务器要重新开始的那一点,此命令并不传送文件,而是略过指定点后的数据,此命令后应该跟其它要求文件传输的FTP命令。 |
RNFR <old path> | 这个命令和我们在其它操作系统中使用的一样,只不过后面要跟"rename to"指定新的文件名。 |
RNTO <new path> | 此命令和上面的命令共同完成对文件的重命名。 |
ABOR | 此命令通知服务中止以前的FTP命令和与之相关的数据传送。如果先前的操作已经完成,则没有动作,返回226。如果没有完成,返回426,然后再返回226。关闭控制连接,数据连接不关闭。 |
DELE <filename> | 此命令删除指定路径下的文件。用户进程负责对删除的提示。 |
RMD <directory> | 此命令删除目录。 |
MKD <directory> | 此命令在指定路径下创建新目录。 |
PWD | 在响应时返回当前工作目录。 |
LIST <name> | 服务器传送列表到被动DTP,如果路径指定一个目录或许多文件,返回指定路径下的文件列表。如果路径名指定一个文件,服务器返回文件的当前信息,参数为空表示用户当前的工作目录或默认目录。数据传输在ASCII或EBCDIC下进行,用户必须确认这一点。因为文件信息因系统不同而不同,所以不可能被程序自动利用,但是人类用户却很需要。 |
NLST <directory> | 服务器传送目录表名到用户,路径名应指定目录或其它系统指定的文件群描述子;空参数指当前目录。服务器返回文件名数据流,以ASCII或EBCDIC形式传送,并以<CRLF>或<NL>分隔。这里返回的信息有时可以供程序进行进一步处理。 |
SITE <params> | 服务器用来提供服务器系统信息,信息因系统不同而不同,格式在HELP SITE命令应答中给出。 |
SYST | 用于确定服务器上运行的操作系统。 |
STAT <directory> | 此命令返回控制连接状态,它可以在文件传送过程中发送,服务器返回操作进行的状态。也可以在文件传送之间发送,这时命令有参数,参数是路径名,此命令的功能除了数据在控制连接上传送以外和列表命令相似。如果指定部分路径,服务器以文件名或与说明相关的属性返回;如没有参数,服务器返回服务器FTP进程的状态信息,包括传输参数的当前值和连接状态。 |
HELP <command> | 这条命令我们在平常系统中得到的帮助没有什么区别,响应类型是211或214。建议在使用USER命令前使用此命令。 |
NOOP | 此命令不产生什么实际动作,它仅使服务器返回OK。 |
2. FTP协议响应信息
FTP响应信息由两部分组成:响应码与描述信息(中间以空格隔开)。其中,响应码是由3位数字组成的字符串,它是对响应信息的数字标识,例如200表示用户登录成功;描述信息是对响应码的文字描述,例如200的描述信息是"Command okay."。FTP响应的标准格式为:
响应码 描述信息
常见的FTP响应如下表所示:
响 应 码 | 含 义 |
---|---|
110 | 重新启动标记应答 |
120 | 服务器准备就绪的时间(分钟数) |
125 | 打开数据连接,开始传输 |
150 | 文件状态良好,打开数据连接 |
200 | 命令成功 |
202 | 命令未执行 |
211 | 系统状态 |
212 | 目录状态 |
213 | 文件状态 |
214 | 帮助信息 |
215 | 系统类型 |
220 | 服务就绪 |
221 | 服务关闭控制连接,可以退出登录 |
225 | 打开数据连接 |
226 | 关闭数据连接,请求的文件操作成功 |
227 | 进入被动模式(IP地址、ID端口) |
230 | 登录因特网 |
250 | 请求的文件操作完成 |
257 | 路径名建立 |
331 | 用户名正确,需要密码 |
332 | 登录时需要账户信息 |
350 | 请求的文件操作需要进一步命令 |
421 | 不能提供服务,关闭控制连接 |
425 | 无法打开数据连接 |
426 | 关闭连接,中止传输 |
450 | 请求的文件操作未执行 |
451 | 遇到本地错误 |
452 | 磁盘空间不足 |
500 | 格式错误,无效命令 |
501 | 参数语法错误 |
502 | 命令未执行 |
503 | 命令顺序错误 |
504 | 此参数下的命令功能未执行 |
530 | 未登录网络 |
532 | 存储文件需要账户信息 |
550 | 未执行请求的操作 |
551 | 不知道的页类型 |
552 | 超过存储分配 |
553 | 文件名不合法 |
在Android上的应用(实现一个FTP服务器)
- 首先通过启动一个Service来实现在后台守护ServerScoket模拟的FTP服务器线程(下面称为服务器线程),由于Service是在主线程中执行的,所以守护服务器线程的工作是在新建的线程中(下面称为守护线程)完成的,代码如下所示:
@Override
public void run() {
Log.d(TAG, "Server thread running");
if (isConnectedToLocalNetwork() == false) {
Log.w(TAG, "run: There is no local network, bailing out");
stopSelf();
sendBroadcast(new Intent(ACTION_FAILEDTOSTART));
return;
}
// Initialization of wifi, set up the socket
try {
setupListener();
} catch (IOException e) {
Log.w(TAG, "run: Unable to open port, bailing out.");
stopSelf();
sendBroadcast(new Intent(ACTION_FAILEDTOSTART));
return;
}
// @TODO: when using ethernet, is it needed to take wifi lock?
WifiUtil.takeWifiLock(getApplicationContext(), wifiLock);
PowerUtil.takeWakeLock(getApplicationContext(), wakeLock);
// A socket is open now, so the FTP server is started, notify rest of world
Log.i(TAG, "Ftp Server up and running, broadcasting ACTION_STARTED");
sendBroadcast(new Intent(ACTION_STARTED));
while (!shouldExit) {
if (wifiListener != null) {
if (!wifiListener.isAlive()) {
Log.d(TAG, "Joining crashed wifiListener thread");
try {
wifiListener.join();
} catch (InterruptedException e) {
}
wifiListener = null;
}
}
if (wifiListener == null) {
// Either our wifi listener hasn't been created yet, or has crashed,
// so spawn it
wifiListener = new TcpListener(serverSocket, this);
wifiListener.start();
}
try {
// TODO: think about using ServerSocket, and just closing
// the main socket to send an exit signal
Thread.sleep(WAKE_INTERVAL_MS);
} catch (InterruptedException e) {
Log.d(TAG, "Thread interrupted");
}
}
terminateAllSessions();
if (wifiListener != null) {
wifiListener.quit();
wifiListener = null;
}
shouldExit = false; // we handled the exit flag, so reset it to acknowledge
Log.d(TAG, "Exiting cleanly, returning from run()");
stopSelf();
sendBroadcast(new Intent(ACTION_STOPPED));
}
// This opens a listening socket on all interfaces.
void setupListener() throws IOException {
serverSocket = new ServerSocket();
serverSocket.setReuseAddress(true);
serverSocket.bind(new InetSocketAddress(FtpServerSettings.getPortNumber()));
}
上面setupListener方法就是用来为serverScoket绑定端口用的,由于FtpServerSettings.getPortNumber()得到的返回值就是2121,因此serverScoket绑定了2121端口;由于shouldExit的值是false,因此守护线程会每间隔WAKE_INTERVAL_MS时长就会检查wifiListener线程是否存活,如果不存活就会重新建立wifiListener线程,从而完成了守护wifiListener线程的工作。
wifiListener线程就是serverScoket监听2121端口的工作线程,即上面提到的ServerScoket模拟的FTP服务器线程。
- wifiListener对应的TcpListener类(坚持Thread类)中实现监听2121端口的过程如下:
@Override
public void run() {
try {
while (true) {
Socket clientSocket = listenSocket.accept();
Log.i(TAG, "New connection, spawned thread");
SessionThread newSession = new SessionThread(clientSocket,
new LocalDataSocket());
newSession.start();
ftpServerService.registerSessionThread(newSession);
}
} catch (Exception e) {
Log.d(TAG, "Exception in TcpListener");
}
}
可以看到是通过调用serverSocket的accept方法实现的,当执行accept方法后,FTP服务器线程就会进入等待的状态,只要有客户端发起连接FTP服务器线程,FTP服务器线程才会继续向下执行,现在在windows操作系统的“计算机”上的地址栏上输入上输入ftp://192.168.10.52:2121/,然后回车,连接FTP服务器线程成功后的效果图如下所示:
然后你就可以像在本地操作文件夹一样操作android中的文件目录。
在mac系统中通过finder或者通过浏览器的地址栏中输入ftp://192.168.10.52:2121/也可以连接FTP服务器线程,效果图如下:
在mac系统中通过finder或者通过浏览器连接虽然是成功的,但是只能浏览和下载文件,修改和上传是不行的,所以如果想在mac上修改和上传,就需要下载一个ftp客户端。
注意:
我现在实现的是在同一个局域网上才可以,上面的IP地址192.168.10.52就是手机的wifi的IP。
- 当第2步中发起连接(就是上面FTP协议中提到的控制连接)后,就会创建SessionThread类(继承自Thread类)的实例sessionThread,就相当为这个控制连接创建一个会话线程,该会话线程用来专门处理客户端向ServerScoket模拟的FTP服务器发送的命令并且对客户端的命令做出响应。代码如下所示:
@Override
public void run() {
Log.i(TAG, "SessionThread started");
if (sendWelcomeBanner) {
writeString("220 SwiFTP " + MyApplication.getVersion() + " ready\r\n");
}
// Main loop: read an incoming line and process it
try {
BufferedReader in = new BufferedReader(new InputStreamReader(
cmdSocket.getInputStream()), 8192); // use 8k buffer
while (true) {
String line;
line = in.readLine(); // will accept \r\n or \n for terminator
if (line != null) {
FTPServerService.writeMonitor(true, line);
Log.d(TAG, "Received line from client: " + line);
FtpCmd.dispatchCommand(this, line);
} else {
Log.i(TAG, "readLine gave null, quitting");
break;
}
}
} catch (IOException e) {
Log.i(TAG, "Connection was dropped");
}
closeSocket();
}
可以看到,会话线程会先解析出FTP客户端请求的命令,然后通过FtpCmd.dispatchCommand方法处理命令,处理命令的代码如下:
protected static CmdMap[] cmdClasses = { new CmdMap("SYST", CmdSYST.class),
new CmdMap("USER", CmdUSER.class), new CmdMap("PASS", CmdPASS.class),
new CmdMap("TYPE", CmdTYPE.class), new CmdMap("CWD", CmdCWD.class),
new CmdMap("PWD", CmdPWD.class), new CmdMap("LIST", CmdLIST.class),
new CmdMap("PASV", CmdPASV.class), new CmdMap("RETR", CmdRETR.class),
new CmdMap("NLST", CmdNLST.class), new CmdMap("NOOP", CmdNOOP.class),
new CmdMap("STOR", CmdSTOR.class), new CmdMap("DELE", CmdDELE.class),
new CmdMap("RNFR", CmdRNFR.class), new CmdMap("RNTO", CmdRNTO.class),
new CmdMap("RMD", CmdRMD.class), new CmdMap("MKD", CmdMKD.class),
new CmdMap("OPTS", CmdOPTS.class), new CmdMap("PORT", CmdPORT.class),
new CmdMap("QUIT", CmdQUIT.class), new CmdMap("FEAT", CmdFEAT.class),
new CmdMap("SIZE", CmdSIZE.class), new CmdMap("CDUP", CmdCDUP.class),
new CmdMap("APPE", CmdAPPE.class), new CmdMap("XCUP", CmdCDUP.class), // synonym
new CmdMap("XPWD", CmdPWD.class), // synonym
new CmdMap("XMKD", CmdMKD.class), // synonym
new CmdMap("XRMD", CmdRMD.class), // synonym
new CmdMap("MDTM", CmdMDTM.class), //
new CmdMap("MFMT", CmdMFMT.class), //
new CmdMap("REST", CmdREST.class), //
new CmdMap("SITE", CmdSITE.class), //
};
protected static void dispatchCommand(SessionThread session, String inputString) {
String[] strings = inputString.split(" ");
String unrecognizedCmdMsg = "502 Command not recognized\r\n";
if (strings == null) {
// There was some egregious sort of parsing error
String errString = "502 Command parse error\r\n";
Log.d(TAG, errString);
session.writeString(errString);
return;
}
if (strings.length < 1) {
Log.d(TAG, "No strings parsed");
session.writeString(unrecognizedCmdMsg);
return;
}
String verb = strings[0];
if (verb.length() < 1) {
Log.i(TAG, "Invalid command verb");
session.writeString(unrecognizedCmdMsg);
return;
}
FtpCmd cmdInstance = null;
verb = verb.trim();
verb = verb.toUpperCase();
for (int i = 0; i < cmdClasses.length; i++) {
if (cmdClasses[i].getName().equals(verb)) {
// We found the correct command. We retrieve the corresponding
// Class object, get the Constructor object for that Class, and
// and use that Constructor to instantiate the correct FtpCmd
// subclass. Yes, I'm serious.
Constructor<? extends FtpCmd> constructor;
try {
constructor = cmdClasses[i].getCommand().getConstructor(
new Class[] { SessionThread.class, String.class });
} catch (NoSuchMethodException e) {
Log.e(TAG, "FtpCmd subclass lacks expected " + "constructor ");
return;
}
try {
cmdInstance = constructor.newInstance(new Object[] { session,
inputString });
} catch (Exception e) {
Log.e(TAG, "Instance creation error on FtpCmd");
return;
}
}
}
if (cmdInstance == null) {
// If we couldn't find a matching command,
Log.d(TAG, "Ignoring unrecognized FTP verb: " + verb);
session.writeString(unrecognizedCmdMsg);
return;
}
if (session.isUserLoggedIn()) {
cmdInstance.run();
} else if (session.isAnonymouslyLoggedIn() == true) {
boolean validCmd = false;
for (Class<?> cl : allowedCmdsWhileAnonymous) {
if (cmdInstance.getClass().equals(cl)) {
validCmd = true;
break;
}
}
if (validCmd == true) {
cmdInstance.run();
} else {
session.writeString("530 Guest user is not allowed to use that command\r\n");
}
} else if (cmdInstance.getClass().equals(CmdUSER.class)
|| cmdInstance.getClass().equals(CmdPASS.class)
|| cmdInstance.getClass().equals(CmdQUIT.class)) {
cmdInstance.run();
} else {
session.writeString("530 Login first with USER and PASS, or QUIT\r\n");
}
}
上面的代码很简单,首先是对格式不正确的命令做出响应(响应码事502,表示命令未执行);接着根据命令名称找到对应的处理这个命令的类(例如CmdUSER.class就是用来处理USER命令),然后通过反射创建创建该类的实例,然后执行实例的run方法,继而完成命令的处理,处理完成后,一般都会返回给FTP客户端响应信息,具体可以参考上面 常见的FTP响应表。
上面的源码是参考SwiFTP开源软件,源码地址:
FTP Server (swiftp)
有兴趣的同学可以自己研究一下。