为了从FTP服务器下载文件,需要要实现一个简单的FTP客户端。
FTP(文件传输协议) 是 TCP/IP 协议组中的应用层协议。
FTP协议使用字符串格式命令字,每条命令都是一行字符串,以“\r\n”结尾。
客户端发送格式是:命令+空格+参数+"\r\n"的格式
服务器返回格式是以:状态码+空格+提示字符串+"\r\n"的格式,代码只要解析状态码就可以了。
读写文件需要登陆服务器,特殊用户名:anonymous,表示匿名。注意大小写敏感。
从FTP服务器下载文件的基本流程如下:
1. 建立TCP连接,该协议默认使用21端口,当然可以指定其它端口,取决于服务器的配置。
2. 连接成功之后,服务器会发送一行欢迎文字,例如:220 welcome.
其中左边的数字220表示就绪状态,220后面有一个空格,空格后面是提示文字。
在解析命令应答的时候,只需要获取前面的数字即可。
3. 收到欢迎信息后,就要开始登陆了,先用USER命令发送用户名,服务器返回331状态。
然后再用PASS命令发送登陆密码,服务器返回530表示密码错误,返回230表示密码正确。
发送:USER anonymous
接收:331 Anonymous login ok, send your complete email address as your password
发送:PASS anonymous
接收:230 Anonymous access granted, restrictions apply
4. 登陆成功之后,再发送一条TYPE I命令,进入二进制模式,这样获取文件数据的时候,就会以二进制字节流发送。
避免以ASCII码格式发送文件数据。
5. 获取文件长度
发送:SIZE /path/filename
失败:550 /path/filename: No such file or directory
成功:213 [filesize]
返回[filesize]是十进制数字,表示该文件在大小,字节为单位
6. 下载文件
下载文件前,先发送PASV命令,进入被动模式,这样FTP服务器就会开放一个新的端口,用于接收文件数据。 客户端成功连接到这个数据端口后,再发送RETR命令请求下载文件,这时文件数据就会从新的端口发送过来,文件传输完毕,服务器自动关闭数据端口。
发送:PASV
接收:227 Entering Passive Mode (145,24,145,107,207,235).
后面括号内的5个数字,分别表示IP地址和端口号,端口号分为高8位和低8位,高8位在前
这里的例子表示IP地址为145.24.145.107,端口号为53227(计算公式:207 * 256 + 235)。
发送:RETR /path/filename
接收:150 Opening BINARY mode data connection for /path/filename (54 bytes)
>>>从数据端口接收文件数据
接收:226 Transfer complete
7. 上传文件
上传文件与下载文件类似,也是发送PASV命令,获取到数据端口,然后发送STOR命令请求上传文件。 不同的是上传文件是往这个数据端口写文件数据,写完数据后,客户端主动断开与数据端口的TCP连接,服务器会返回一条上传成功的状态。
发送:PASV
接收:227 Entering Passive Mode (145,24,145,107,207,235).
发送:STOR /path/filename
接收:150 Opening BINARY mode data connection for /path/filename
>>>往数据端口写数据
接收:226 Transfer complete
=============================
FTP常见的状态码如下:
110重新启动标记答复。
120服务已就绪,在nnn分钟后开始。
125数据连接已打开,正在开始传输。
150文件状态正常,准备打开数据连接。
2xx-肯定的完成答复一项操作已经成功完成。客户端可以执行新命令。
200命令确定。
202未执行命令,站点上的命令过多。
211系统状态,或系统帮助答复。
212目录状态。
213文件状态。
214帮助消息。
215NAME系统类型,其中,NAME是AssignedNumbers文档中所列的正式系统名称。
220服务就绪,可以执行新用户的请求。
221服务关闭控制连接。如果适当,请注销。
225数据连接打开,没有进行中的传输。
226关闭数据连接。请求的文件操作已成功(例如,传输文件或放弃文件)。
227进入被动模式(h1,h2,h3,h4,p1,p2)。
230用户已登录,继续进行。
250请求的文件操作正确,已完成。
257已创建“PATHNAME”。
3xx-肯定的中间答复该命令已成功,但服务器需要更多来自客户端的信息以完成对请求的处理。
331用户名正确,需要密码。
332需要登录帐户。
350请求的文件操作正在等待进一步的信息。
4xx-瞬态否定的完成答复该命令不成功,但错误是暂时的。如果客户端重试命令,可能会执行成功。
421服务不可用,正在关闭控制连接。如果服务确定它必须关闭,将向任何命令发送这一应答。
425无法打开数据连接。 426Connectionclosed;transferaborted.
450未执行请求的文件操作。文件不可用(例如,文件繁忙)。
451请求的操作异常终止:正在处理本地错误。
452未执行请求的操作。系统存储空间不够。
5xx-永久性否定的完成答复该命令不成功,错误是永久性的。如果客户端重试命令,将再次出现同样的错误。
500语法错误,命令无法识别。这可能包括诸如命令行太长之类的错误。
501在参数中有语法错误。
502未执行命令。
503错误的命令序列。
504未执行该参数的命令。
530未登录。
532存储文件需要帐户。
550未执行请求的操作。文件不可用(例如,未找到文件,没有访问权限)。
551请求的操作异常终止:未知的页面类型。
552请求的文件操作异常终止:超出存储分配(对于当前目录或数据集)。
553未执行请求的操作。不允许的文件名。
=============================================================
用C语言实现了一个简单的FTP模块,支持上传和下载文件。
————————————————
#include <stdio.h> #include <stdlib.h> #include <string.h> #include "socket.h" #include "log.h" #include "ftp.h" static int m_socket_cmd; static int m_socket_data; static char m_send_buffer[1024]; static char m_recv_buffer[1024]; //命令端口,发送命令 static int ftp_send_command(char *cmd) { int ret; LOG_INFO("send command: %s\r\n", cmd); ret = socket_send(m_socket_cmd, cmd, (int)strlen(cmd)); if(ret < 0) { LOG_INFO("failed to send command: %s",cmd); return 0; } return 1; } //命令端口,接收应答 static int ftp_recv_respond(char *resp, int len) { int ret; int off; len -= 1; for(off=0; off<len; off+=ret) { ret = socket_recv(m_socket_cmd, &resp[off], 1); if(ret < 0) { LOG_INFO("recv respond error(ret=%d)!\r\n", ret); return 0; } if(resp[off] == '\n') { break; } } resp[off+1] = 0; LOG_INFO("respond:%s", resp); return atoi(resp); } //设置FTP服务器为被动模式,并解析数据端口 static int ftp_enter_pasv(char *ipaddr, int *port) { int ret; char *find; int a,b,c,d; int pa,pb; ret = ftp_send_command("PASV\r\n"); if(ret != 1) { return 0; } ret = ftp_recv_respond(m_recv_buffer, 1024); if(ret != 227) { return 0; } find = strrchr(m_recv_buffer, '('); sscanf(find, "(%d,%d,%d,%d,%d,%d)", &a, &b, &c, &d, &pa, &pb); sprintf(ipaddr, "%d.%d.%d.%d", a, b, c, d); *port = pa * 256 + pb; return 1; } //上传文件 int ftp_upload(char *name, void *buf, int len) { int ret; char ipaddr[32]; int port; //查询数据地址 ret=ftp_enter_pasv(ipaddr, &port); if(ret != 1) { return 0; } ret=socket_connect(m_socket_data, ipaddr, port); if(ret != 1) { return 0; } //准备上传 sprintf(m_send_buffer, "STOR %s\r\n", name); ret = ftp_send_command(m_send_buffer); if(ret != 1) { return 0; } ret = ftp_recv_respond(m_recv_buffer, 1024); if(ret != 150) { socket_close(m_socket_data); return 0; } //开始上传 ret = socket_send(m_socket_data, buf, len); if(ret != len) { LOG_INFO("send data error!\r\n"); socket_close(m_socket_data); return 0; } socket_close(m_socket_data); //上传完成,等待回应 ret = ftp_recv_respond(m_recv_buffer, 1024); return (ret==226); } //下载文件 int ftp_download(char *name, void *buf, int len) { int i; int ret; char ipaddr[32]; int port; //查询数据地址 ret = ftp_enter_pasv(ipaddr, &port); if(ret != 1) { return 0; } //连接数据端口 ret = socket_connect(m_socket_data, ipaddr, port); if(ret != 1) { LOG_INFO("failed to connect data port\r\n"); return 0; } //准备下载 sprintf(m_send_buffer, "RETR %s\r\n", name); ret = ftp_send_command(m_send_buffer); if(ret != 1) { return 0; } ret = ftp_recv_respond(m_recv_buffer, 1024); if(ret != 150) { socket_close(m_socket_data); return 0; } //开始下载,读取完数据后,服务器会自动关闭连接 for(i=0; i<len; i+=ret) { ret = socket_recv(m_socket_data, ((char *)buf) + i, len); LOG_INFO("download %d/%d.\r\n", i + ret, len); if(ret < 0) { LOG_INFO("download was interrupted.\r\n"); break; } } //下载完成 LOG_INFO("download %d/%d bytes complete.\r\n", i, len); socket_close(m_socket_data); ret = ftp_recv_respond(m_recv_buffer, 1024); return (ret==226); } //返回文件大小 int ftp_filesize(char *name) { int ret; int size; sprintf(m_send_buffer,"SIZE %s\r\n",name); ret = ftp_send_command(m_send_buffer); if(ret != 1) { return 0; } ret = ftp_recv_respond(m_recv_buffer, 1024); if(ret != 213) { return 0; } size = atoi(m_recv_buffer + 4); return size; } //登陆服务器 int ftp_login(char *addr, int port, char *username, char *password) { int ret; LOG_INFO("connect...\r\n"); ret = socket_connect(m_socket_cmd, addr, port); if(ret != 1) { LOG_INFO("connect server failed!\r\n"); return 0; } LOG_INFO("connect ok.\r\n"); //等待欢迎信息 ret = ftp_recv_respond(m_recv_buffer, 1024); if(ret != 220) { LOG_INFO("bad server, ret=%d!\r\n", ret); socket_close(m_socket_cmd); return 0; } LOG_INFO("login...\r\n"); //发送USER sprintf(m_send_buffer, "USER %s\r\n", username); ret = ftp_send_command(m_send_buffer); if(ret != 1) { socket_close(m_socket_cmd); return 0; } ret = ftp_recv_respond(m_recv_buffer, 1024); if(ret != 331) { socket_close(m_socket_cmd); return 0; } //发送PASS sprintf(m_send_buffer, "PASS %s\r\n", password); ret = ftp_send_command(m_send_buffer); if(ret != 1) { socket_close(m_socket_cmd); return 0; } ret = ftp_recv_respond(m_recv_buffer, 1024); if(ret != 230) { socket_close(m_socket_cmd); return 0; } LOG_INFO("login success.\r\n"); //设置为二进制模式 ret = ftp_send_command("TYPE I\r\n"); if(ret != 1) { socket_close(m_socket_cmd); return 0; } ret = ftp_recv_respond(m_recv_buffer, 1024); if(ret != 200) { socket_close(m_socket_cmd); return 0; } return 1; } void ftp_quit(void) { ftp_send_command("QUIT\r\n"); socket_close(m_socket_cmd); } void ftp_init(void) { m_socket_cmd = socket_create(); m_socket_data= socket_create(); }
命令和返回码:
C-->S:命令
S-->C:返回码
每一个Ftp发送之后,Ftp服务器都会返回一个字符串,其中包括一个返回代码和一串说明信息。这个返回码主要是用于判断命令是否被成功执行了。除此之 外,还有一个非常重要的命令的返回。当发送PASV之后,返回“227 Entering Passive Mode (127,0,0,1,4,18)”。这意味着在服务器上有一个端口被开放,他将为我们后面接着的数据传输作好准备,但是我们如何知道该端口号呢,就在 (127,0,0,1,4,18)中,前面四位指服务器的地址,关键是最后两位,将最后第二位乘256再加上最后一位的值就是我们的端口号,也就是 4*256+18。取得端口号之后我们就可以用socket连接到这里。这为我们后面的工作作好准备了,因为我们的取得列表,上传,下载文件都要依靠它来 实现。
一个非典型的ftp交互实例:
Response: 220 Gene6 FTP Server v3.10.0 (Build 2) ready...
Request: USER anonymous
Response: 331 Password required for anonymous.
Request: PASS notexist.com
Response: 230 User anonymous logged in.
Request: CWD board
Response: 250 CWD command successful. "/board" is current directory.
Request: TYPE I
Response: 200 Type set to I.
Request: SIZE 4saac062.zip
Response: 213 248288
Request: RETR 4saac062.zip
Response: 150 Data connection accepted from x.x.x.x:2841; transfer starting for /board/4saac062.zip (248288 bytes)
FTP命令:
灰色的命令一般很少使用,所以往往在具体实现中不被支持,所以可能返回的信息是“500 'xx': command not understood”。
命令 | 描述 |
ABOR | 中断数据连接程序 |
ACCT <account> | 系统特权帐号 |
ALLO <bytes> | 为服务器上的文件存储器分配字节 |
APPE <filename> | 添加文件到服务器同名文件 |
CDUP <dir path> | 改变服务器上的父目录 |
CWD <dir path> | 改变服务器上的工作目录 |
DELE <filename> | 删除服务器上的指定文件 |
HELP <command> | 返回指定命令信息 |
LIST <name> | 如果是文件名列出文件信息,如果是目录则列出文件列表 |
MODE <mode> | 传输模式(S=流模式,B=块模式,C=压缩模式) |
MKD <directory> | 在服务器上建立指定目录 |
NLST <directory> | 列出指定目录内容 |
NOOP | 无动作,除了来自服务器上的承认 |
PASS <password> | 系统登录密码 |
PASV | 请求服务器等待数据连接 |
PORT <address> | IP 地址和两字节的端口 ID |
PWD | 显示当前工作目录 |
QUIT | 从 FTP 服务器上退出登录 |
REIN | 重新初始化登录状态连接 |
REST <offset> | 由特定偏移量重启文件传递 |
RETR <filename> | 从服务器上找回(复制)文件 |
RMD <directory> | 在服务器上删除指定目录 |
RNFR <old path> | 对旧路径重命名 |
RNTO <new path> | 对新路径重命名 |
SITE <params> | 由服务器提供的站点特殊参数 |
SIZE <FILENAME> | 文件大小,执行成功返回 213 |
SMNT <pathname> | 挂载指定文件结构 |
STAT <directory> | 在当前程序或目录上返回信息 |
STOR <filename> | 储存(复制)文件到服务器上 |
STOU <filename> | 储存文件到服务器名称上 |
STRU <type> | 数据结构(F=文件,R=记录,P=页面) |
SYST | 返回服务器使用的操作系统 |
TYPE <data type> | 数据类型(A=ASCII,E=EBCDIC,I=binary) |
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 | 文件名不允许 |
=========================================
5. FTP的工作方式
FTP支持两种模式,一种方式叫做Standard (也就是 PORT方式,主动方式),一种是 Passive (也就是PASV,被动方式)。 Standard模式 FTP的客户端发送 PORT 命令到FTP服务器。Passive模式FTP的客户端发送 PASV命令到 FTP Server。
下面介绍一个这两种方式的工作原理:
Port模式FTP 客户端首先和FTP服务器的TCP 21端口建立连接,通过这个通道发送命令,客户端需要接收数据的时候在这个通道上发送PORT命令。 PORT命令包含了客户端用什么端口接收数据。在传送数据的时候,服务器端通过自己的TCP 20端口连接至客户端的指定端口发送数据。 FTP server必须和客户端建立一个新的连接用来传送数据。(可以看到在这种方式下是客户端和服务器建立控制连接,服务器向客户端建立数据连接,其中,客户端的控制连接和数据连接的端口号是大于1024的两个端口号(临时端口),而FTP服务器的数据端口为20,控制端口为21)
Passive模式在建立控制通道的时候和Standard模式类似,但建立连接后发送的不是Port命令,而是Pasv命令。FTP服务器收到Pasv命令后,随机打开一个临时端口(也叫自由端口,端口号大于1023小于65535)并且通知客户端在这个端口上传送数据的请求,客户端连接FTP服务器此端口,然后FTP服务器将通过这个端口进行数据的传送,这个时候FTP server不再需要建立一个新的和客户端之间的连接。(可以看到这种情况下的连接都是由客户端向服务器发起的,与下面所说的“为了解决服务器发起到客户的连接的问题,人们开发了一种不同的FTP连接方式。这就是所谓的被动方式”相对应,而服务器端的数据端口是临时端口,而不是常规的20)
很多防火墙在设置的时候都是不允许接受外部发起的连接的,所以许多位于防火墙后或内网的FTP服务器不支持PASV模式,因为客户端无法穿过防火墙打开FTP服务器的高端端口;而许多内网的客户端不能用PORT模式登陆FTP服务器,因为从服务器的TCP 20无法和内部网络的客户端建立一个新的连接,造成无法工作。
本篇文章链接 地址:https://wmzos.com/?id=130
发表评论
添加新评论