基于Socket编程实现FTP客户端

实验环境

  1. Cygwin
  2. Python 2.7
  3. FileZella Server

原理

利用ftp协议进行文件传输时,主要利用两个端口:命令端口(也叫作控制端口)和数据端口。控制端口主要用来传输命令,数据端口主要用于传输数据。
这两个端口一般是20/21,其中20代表主动模式下的数据端口,21代表控制端口。而被动模式下,我们将使用x/21。其中x代表我们被动模式下的数据端口,而21仍然为控制端口(x的获取我们将在后文说明)。
更详细的原理参见一篇讲得很好的博文。

实验过程

连接server

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

PASV

当我们登录成功后,就可以做想做的事情了。
但是我们需要传输数据时,需要先通过被动模式打开一个数据端口用于传输数据,并且新建立一个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作为利用数据端口传输数据的例子。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()

Download

通过上面的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

你可能感兴趣的:(socket编程)