一、FTP 概述
文件传输协议(英文:File Transfer Protocol,缩写:FTP)是用于在网络上进行文件传输的一套标准协议,使用客户/服务器模式。它属于网络传输协议的应用层。FTP的传输有两种方式:ASCII、二进制。
二、传输方式
ASCII传输方式
假定用户正在拷贝的文件包含的简单ASCII码文本,如果在远程机器上运行的不是UNIX,当文件传输时ftp通常会自动地调整文件的内容以便于把文件解释成另外那台计算机存储文本文件的格式。
但是常常有这样的情况,用户正在传输的文件包含的不是文本文件,它们可能是程序,数据库,字处理文件或者压缩文件。在拷贝任何非文本文件之前,用binary 命令告诉ftp逐字拷贝。
二进制传输模式
在二进制传输中,保存文件的位序,以便原始和拷贝的是逐位一一对应的。即使目的地机器上包含位序列的文件是没意义的。例如,macintosh以二进制方式传送可执行文件到Windows系统,在对方系统上,此文件不能执行。
如在ASCII方式下传输二进制文件,即使不需要也仍会转译。这会损坏数据。(ASCII方式一般假设每一字符的第一有效位无意义,因为ASCII字符组合不使用它。如果传输二进制文件,所有的位都是重要的。)。
三、支持模式
FTP支持两种模式:Standard (PORT方式,主动方式),Passive (PASV,被动方式)。
Port模式
FTP 客户端首先和服务器的TCP 21端口建立连接,用来发送命令,客户端需要接收数据的时候在这个通道上发送PORT命令。PORT命令包含了客户端用什么端口接收数据。在传送数据的时候,服务器端通过自己的TCP 20端口连接至客户端的指定端口发送数据。FTP server必须和客户端建立一个新的连接用来传送数据。
Passive模式
建立控制通道和Standard模式类似,但建立连接后发送Pasv命令。服务器收到Pasv命令后,打开一个临时端口(端口号大于1023小于65535)并且通知客户端在这个端口上传送数据的请求,客户端连接FTP服务器此端口,然后FTP服务器将通过这个端口传送数据。
四、FTP 命令
命令 |
描述 |
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> |
由服务器提供的站点特殊参数 |
SMNT <pathname> |
挂载指定文件结构 |
STAT <directory> |
在当前程序或目录上返回信息 |
STOR <filename> |
储存(复制)文件到服务器上 |
STOU <filename> |
储存文件到服务器名称上 |
STRU <type> |
数据结构(F=文件,R=记录,P=页面) |
SYST |
返回服务器使用的操作系统 |
TYPE <data type> |
数据类型(A=ASCII,E=EBCDIC,I=binary) |
USER <username>> |
系统登录的用户名 |
标准 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 |
文件名不允许 |
五、Linux c ftp客户端编写
ftp客户端代码:
/*********************************************************************************
* Copyright: (C) 2019 Wu Yujun<[email protected]>
* All rights reserved.
*
* Filename: ftp_client.c
* Description: This file is ftp client
*
* Version: 1.0.0(2019年03月16日)
* Author: Wu Yujun <[email protected]>
* ChangeLog: 1, Release initial version on "2019年03月16日 15时29分29秒"
*
********************************************************************************/
#include "ftp_client.h"
int g_stop = 0 ;
void print_usage(const char *program_name)
{
printf("\n%s -- (2019.3.16)\n", program_name);
printf(" Usage: %s -i <server_ip>/<ftp_server_hostname> -p <ftp_server_port> [-h <use_help>]\n", program_name);
printf(" -p --port the port of the ftp server you want to connect\n") ;
printf(" -i --ip the ip address or hostname of the ftp server you want to connect\n") ;
printf(" -h --help the ftp_client how to use\n");
printf("After Loin OK:\n") ;
ftp_usage_print() ;
return ;
}
void sighandler(int sig_num)
{
if(sig_num == SIGUSR1)
{
g_stop = 1 ;
}
}
int main(int argc, char **argv)
{
char *program_name ;
int port = 0 ;
int opt = -1 ;
char *ip = NULL;
struct hostent * hostnp ;
int no_ipport ;
char buf[BUF_SIZE] ;
int sock_fd = -1 ;
int rv = 0 ;
char cmd[32] ;
program_name = basename(argv[0]) ;
const char *short_opts = "i:n:p:h";
const struct option long_opts[] = {
{"help", no_argument, NULL, 'h'},
{"ip", required_argument, NULL, 'i'},
{ "port", required_argument, NULL, 'p'},
{0, 0, 0, 0}
};
while ((opt= getopt_long(argc, argv, short_opts, long_opts,NULL)) != -1)
{
switch (opt)
{
case 'i':
ip = optarg ;
break ;
case 'p':
port = atoi(optarg) ;
break ;
case 'h':
print_usage(program_name) ;
return 0 ;
}
}
no_ipport = ( (!ip) || (!port) ) ; //port or ip is NULL, no_ipprot = 1 ;
if( no_ipport )
{
print_usage(program_name);
return 0;
}
/* connect server get host by name */
if ( inet_addr(ip)== INADDR_NONE )
{
if( (hostnp = gethostbyname(ip) ) == NULL )
{
printf("get host by name failure: %s\n", strerror(h_errno)) ;
return -1 ;
}
ip = inet_ntoa( * (struct in_addr *)hostnp->h_addr );
}
signal(SIGUSR1, sighandler);
if( (sock_fd=connect_server(ip, port)) < 0 )
{
printf("connect_server() failed\n") ;
return -2 ;
}
memset(buf, 0, sizeof(buf)) ;
if( read(sock_fd, buf, sizeof(buf)) <= 0)
{
printf("Read information from ftp_server failed:%s\n", strerror(errno)) ;
goto cleanup ;
}
printf("ftp Information: %s\n", buf) ;
while(rv<=0)
{
rv = login(sock_fd) ;
}
while(!g_stop)
{
memset(cmd, 0, sizeof(cmd)) ;
printf("\nFTP: ") ;
scanf("%s", cmd) ;
if(!strcmp(cmd,"ls"))
{
if( ftp_list(sock_fd) == ERROR )
continue ;
}
else if(!strcmp(cmd,"get"))
{
if( ftp_get(sock_fd) == ERROR)
continue ;
}
else if(!strcmp(cmd,"put"))
{
if( ftp_put(sock_fd) ==ERROR)
{
continue ;
}
read(sock_fd, buf, sizeof(buf));
printf("Read from ftp:%s\n",buf) ;
}
else if(!strcmp(cmd,"quit"))
{
write(sock_fd, "quit\r\n", strlen("quit\r\n")) ;
read(sock_fd, buf, sizeof(buf)) ;
printf("Read from ftp: %s",buf) ;
goto cleanup ;
}
else if(!strcmp(cmd,"cd"))
{
ftp_cwd(sock_fd) ;
}
else{
ftp_usage_print() ;
continue ;
}
}
cleanup:
printf("program %s is stop\n", program_name) ;
close(sock_fd) ;
return 0 ;
/* End Of main */
}
登录代码
给ftp server控制端发送“USER (username)\r\n”,如果用户名正确会接收到”331 Please specify the password.\r\n”接着就可以发送密码进行登录,”PASS (password)\r\n”,如果登录成功会接收到”230 Login successful.\r\n“,getpass()函数输入不回显;
#include "ftp_client.h"
int login(int sock_fd)
{
int rv = 0 ;
char buf[256] ;
char scanf_buf[256] ;
char *password ;
printf("\n/************ START LOGIN *************/\n") ;
/* Input ftp username */
printf("Please input User_name:") ;
memset(scanf_buf, 0, sizeof(scanf_buf)) ;
scanf("%s",scanf_buf) ;
memset(buf, 0 ,sizeof(buf)) ;
snprintf(buf, sizeof(buf),"USER %s\r\n",scanf_buf) ;
if(write(sock_fd, buf, strlen(buf))< 0 )
{
printf("write to ftp server failed:%s\n",strerror(errno)) ;
return ERROR ;
}
memset(buf, 0, sizeof(buf)) ;
if( (rv =read(sock_fd, buf, sizeof(buf)) ) <= 0)
{
printf("Read from ftp_server failed:%s\n", strerror(errno)) ;
return ERROR ;
}
//printf("Read %d from ftp: %s\n",rv, buf) ;
if(strcmp(buf,"331 Please specify the password.\r\n")!= 0)
{
printf("Invaild user name\n") ;
return ERROR ;
}
/* input ftp user passwd */
password = getpass("Please input Password:");
memset(buf, 0,sizeof(buf)) ;
snprintf(buf,sizeof(buf),"PASS %s\r\n",password);
if(write(sock_fd, buf, strlen(buf))< 0 )
{
printf("write to ftp server failed:%s\n",strerror(errno)) ;
return ERROR ;
}
if( (rv = read(sock_fd, buf, sizeof(buf))) <= 0)
{
printf("Read from ftp_server failed:%s\n", strerror(errno)) ;
return ERROR ;
}
//printf("Read %d byte from ftp: %s\n",rv, buf) ;
if(strcmp(buf,"230 Login successful.\r\n")== 0)
{
printf("You are Welcome!\n") ;
printf("/************ FINISH LOGIN *************/\n") ;
return TRUE ;
}
else
{
printf("Invaild username or password, Please try again\n") ;
return ERROR ;
}
}
主动模式(port)需要客户端有公网ip,port模式提供的ip地址和port端口ftp服务器才能连接得上,被动模式(pasv)ftp服务器开一个临时端口并发送ip地址和port端口,客户端连接上就能进行数据传输。通常客户端不会有公网ip,所以我的代码都是用被动模式(pasv)实现的,如果ftp服务器也没有公网ip那么需要在局域网下面进行操作,被动模式发来的 (h1,h2,h3,h4,p1,p2),h1.h2.h3.h4是ip地址,p1*256+p2是port端口。
被动模式代码
#include "ftp_client.h"
int ftp_pasv(int sock_fd)
{
char pasv_ip[64] ;
char ip0[4],ip1[4],ip2[4],ip3[4] ;
char port0[4], port1[4] ;
int port ;
char ip[32] ;
int pasv_fd = -1 ;
char *ip_start = NULL, *ip_end =NULL ;
if( write(sock_fd, "PASV\r\n", strlen("PASV\r\n")) <= 0 ) //Send PASV to tell ftp server start PASV mode
{
printf("write to ftp failed: %s\n", strerror(errno)) ;
return ERROR ;
}
memset(pasv_ip, 0, sizeof(pasv_ip)) ;
read(sock_fd, pasv_ip, sizeof(pasv_ip)) ;//ftp will send “227 Entering Passive Mode (xxx,xxx,xxx,xxx,xxx,xxx).”
//printf("%s\n", pasv_ip) ;
ip_start = strstr(pasv_ip, "(") ;
ip_start ++ ;
ip_end = strstr(ip_start, ",") ;
memset(ip0, 0, sizeof(ip0)) ;
strncpy(ip0, ip_start, (ip_end - ip_start)) ;
ip_start = ip_end + 1 ;
ip_end = strstr(ip_start, ",") ;
memset(ip1, 0, sizeof(ip1)) ;
strncpy(ip1, ip_start, (ip_end-ip_start));
ip_start = ip_end + 1 ;
ip_end = strstr(ip_start, ",") ;
memset(ip2, 0, sizeof(ip2)) ;
strncpy(ip2, ip_start, (ip_end-ip_start)) ;
ip_start = ip_end + 1 ;
ip_end = strstr(ip_start, ",") ;
memset(ip3, 0, sizeof(ip3)) ;
strncpy(ip3, ip_start, (ip_end-ip_start)) ;
snprintf(ip, sizeof(ip),"%s.%s.%s.%s\n", ip0,ip1,ip2,ip3) ;//analysis ip address
ip_start = ip_end + 1 ;
ip_end = strstr(ip_start, ",") ;
memset(port0, 0, sizeof(port0)) ;
strncpy(port0, ip_start, (ip_end-ip_start)) ;
ip_start = ip_end+1 ;
ip_end = strstr(ip_start, ")") ;
memset(port1, 0, sizeof(port1)) ;
strncpy(port1, ip_start, ip_end- ip_start) ;
port = atoi(port0)*256+atoi(port1) ;//analysis port
pasv_fd = connect_server(ip, port) ;//connect ftp data port
if(pasv_fd < 0)
{
printf("connect ftp pasv fail!\n") ;
return ERROR ;
}
return pasv_fd ;
}
接下来的其他功能都是登录上之后,控制端(sock_fd)给ftp服务器发送PASV,创建一个数据端(pasv_fd)的连接,接着控制端(sock_fd)发送你想要的操作,数据端(pasv_fd)负责接收就完成了,源代码:https://gitee.com/wyj98/ftp_client/。
使用命令:./ftp -i ftp服务器的ip地址或域名 -p ftp服务器的端口
一开始需要登录,登录成功后输入help可以查看ftp_client功能
ls列出ftp服务器的文件
输入get下载,接着输入需要下载的文件名,下载成功后quit退出
输入put进行上传,然后输入需要上传的文件名字
上传成功
六、编写过程中出现的问题
给ftp服务器写控制命令之后,如果不及时将ftp服务器发回来的消息读出来,大概是不及时将数据从receive buffer中取出,最终导致receive buffer填满,接收端会阻止发送端向其发送数据。这些控制皆发生在TCP/IP栈中,对应用程序是透明的,应用程序继续发送数据,最终导致send buffer填满,write调用阻塞。会出现段错误。
调试过程中发现对图片上传和下载会出错,一直以为是传输模式的问题,我调用的write最后一个参数传的是strlen(buf),strlen所作的仅仅是一个计数器的工作,它从内存的某个位置(可以是字符串开头,中间某个位置,甚至是某个不确定的内存区域)开始扫描,直到碰到第一个字符串结束符'\0'为止,最后解决方法是rv=read(), write()最后一个参数传的rv,read返回值为实际读取到的字节数,如果返回0,表示已到达文件尾或无可读取的数据。
参考:https://www.ibm.com/developerworks/cn/linux/l-cn-socketftp/index.html#FTP