利用ftp协议进行文件传输时,主要利用两个端口:命令端口
(也叫作控制端口)和数据端口
。控制端口主要用来传输命令,数据端口主要用于传输数据。
这两个端口一般是20/21
,其中20
代表主动模式下的数据端口,21
代表控制端口。而被动模式下,我们将使用x/21
。其中x
代表我们被动模式下的数据端口,而21
仍然为控制端口(x
的获取我们将在后文说明)。
更详细的原理参见一篇讲得很好的博文。
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(url, 21)
其中url为我们要连接的服务器的IP地址,这里是localhost
。
如果连接成功,我们将收到从服务器返回的响应码220
,返回具体信息如下:
220-FileZilla Server 0.9.60 beta
220-written by Tim Kosse (tim.kosse@filezilla-project.org)
220 Please visit https://filezilla-project.org/
当我们成功连接server后,需要做的事情就是以用户的身份登录进我们的FTP服务器。
在我的FileZella Server上预设了一个用户,用户名为lz
,密码为123456
。接下来我们将以该身份登录进去。
首先,向服务器发送用户名
sock.sendall('USER lz\r\n')
cmd = self.sock_.recv(1024)
print cmd
cmd
是从服务器返回的状态信息,服务器将返回331告诉我们需要输入密码
331 Password required for lz
接下来向服务器发送密码
sock.sendall('PASS 123456\r\n')
cmd = sock.recv(1024)
print cmd
如果成功服务器将以230
响应,并且返回信息
230 Logged on
当我们登录成功后,就可以做想做的事情了。
但是我们需要传输数据时,需要先通过被动模式打开一个数据端口用于传输数据,并且新建立一个socket用于传输数据。
首先,向服务器发起被动模式请求
sock.sendall('PASV \r\n')
cmd = sock_.recv(1024)
print cmd
如果成功,服务器会返回响应码227,并返回如下数据
227 Entering Passive Mode (127,0,0,1,211,69)
最后有6个数字,其中前4个代表我们的IP地址。最后2个,我们令为x和y,那么port = x * 256 + y
即为ftp服务器打开给用于传输数据的端口。我们接下来的数据传输将使用这个端口(注意每次传输数据都需要打开该端口,用完关闭该端口)。
我们新建名为sock_pasv
的socket用于传输数据
sock_pasv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock_pasv.connect((url, port))
我们用list作为利用数据端口传输数据的例子。list的含义是列出当前目录下的文件。
首先,我们通过上面PASV
已经打开了一个端口名为port
的数据端口,接下来,我们将使用该端口进行数据传输。
发送list
的请求信息
sock.sendall('LIST\r\n')
cmd = sock.recv(1024)
如果命令发送成功,服务器将返回响应码150
。接下来,接受返回的数据:
content = self.sock_pasv_.recv(1024 * 8)
print content
这里content即为我们list
后目录下的内容:
-rw-r--r-- 1 ftp ftp 173 May 22 15:00 a.cpp
-rw-r--r-- 1 ftp ftp 4156 May 22 18:05 a.py
-rw-r--r-- 1 ftp ftp 0 May 22 16:32 c.cpp
-rw-r--r-- 1 ftp ftp 902202966 May 22 18:39 c1.mkv
-rw-r--r-- 1 ftp ftp 272425904 May 22 23:24 c2.mp4
-rw-r--r-- 1 ftp ftp 1411 May 22 15:01 d.cpp
-rw-r--r-- 1 ftp ftp 0 May 22 18:04 ftp.py
-rw-r--r-- 1 ftp ftp 308888890 May 22 21:45 num
-rw-r--r-- 1 ftp ftp 308888890 May 22 21:50 num1
-rw-r--r-- 1 ftp ftp 348888890 May 22 22:18 out_1
-rw-r--r-- 1 ftp ftp 348888890 May 22 22:36 out_3
-rw-r--r-- 1 ftp ftp 348888890 May 22 22:47 out_4
-rw-r--r-- 1 ftp ftp 190 May 22 22:12 small
-rw-r--r-- 1 ftp ftp 4156 May 22 18:18 test
-rw-r--r-- 1 ftp ftp 0 May 22 16:48 xxx
-rw-r--r-- 1 ftp ftp 214 May 22 16:58 yyy
当所有数据成功发送后,ftp服务器会返回:
226 Successfully transferred "/"
代表所有数据成功传输。
最后,我们将关闭这个用于传输数据的socket:
sock_pasv.close()
通过上面的list
的例子,我们可以看出ftp服务器传输数据的流程为:
1. 打开被动模式并获取用于传输数据的端口
2. 发送数据请求命令,如list\r\n
3. 接受数据
4. 关闭数据传输端口
这里,我们将进行文件下载。
首先,向服务器请求被动模式并发送下载命令:
PASV() #上面的PASV过程
sock.sendall('RETR ' + filein + '\r\n')
cmd = sock.recv(1024)
这里的filein
是我们要从服务器上下载的文件名,如果成功,ftp服务器将以150
响应,返回信息:
150 Opening data channel for file download from server of "/a.cpp"
接下来获取文件大小,才能判断什么时候传输完了,我们通过向ftp发送SIZE
命令来实现:
sock.sendall('SIZE ' + filename + '\r\n')
cmd = sock.recv(1024)
如果成功,我们的ftp服务器将以213
响应,并返回:
213 173
其中第一个数据是响应码,第二个数字是我们文件的大小,则size = 173
。
获取到文件大小后,在本地创建一个文件作为我们保存下载内容的文件,每次写入1024个字节:
fp = open(fileout, 'wb')
while size > 0:
to_receive = min(size, 1024)
content = sock_pasv.receive(to_receive)
size -= to_receive
fp.write(content)
文件传输完毕后,关闭文件指针和数据端口:
fp.close()
sock_pasv.close()
最后,从服务器接收传输是否成功的响应码:
226 Successfully transferred "/a.cpp"
上传文件和下载文件思路本质上是一样的,这里就不说了。
我们还将实现断点续传,断点续传将利用FTP服务器的REST
命令,其命令格式是:REST offset\r\n
,其中offset
是一个偏移量,代表我们要续传的文件的起始位置。要明确的是REST
并不是单独使用的,而是配合我们的上传下载等命令使用。我们通过REST
命令设置偏移量后,然后调用上传或者下载将从该偏移量的位置开始传输。
假设我们上次传输的文件名为filea
,我们保存在本地命名为fileb
,假设在传输过程中,我们的网络突然断掉了,那么传输是不完整的。我们下次不需要重新传输而是上次传输的结束位置开始传输,即我们的偏移量offset
。
首先,我们获取偏移量,这里将通过获取fileb
的大小作为偏移量:
offset = os.path.getsize('fileb')
然后,通过REST
设置偏移量:
self.sock_.sendall('REST ' + str(offset) + '\r\n')
cmd = self.sock_.recv(1024)
print cmd
服务器将会返回响应码350
代表成功设置断点续传的位置:
350 Rest supported. Restarting at 173
173
代表我们续传的偏移量为173
接下来,我们只需要调用Download()
或者Upload()
来继续传输即可。但是注意比如我们下载文件时,往本地写入文件时,不再是从文件头开始写了,而是从上次结束的位置开始写。
FTP标准参见FTP 0959
我的完整代码参考我的Github