为了从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未执行请求的操作。不允许的文件名。
下面的函数实现了ftp_download和ftp_upload两个接口,在需要使用FTP功能的地方调用者两个接口就可以实现FTP通信;
参考程序:
APP_FTP.c
#include "APP_FTP.h"
#define COMMAND_LEN 1024
static char m_send_buffer[1024];
static char m_recv_buffer[1024];
pthread_mutex_t mutex_cmd = PTHREAD_MUTEX_INITIALIZER;/*初始化互斥锁*/
struct ftp_property_t {
char *download_src; //下载源URL
char *download_desc; //下载目的URL
char *upload_src; //上传源URL
char *upload_desc; //上传目的URL
char *ip_addr; //ftp服务端ip
int port; //ftp服务端端口
char *username; //username
char *password; //password;
};
static struct ftp_property_t ftp_property;
static int m_socket_cmd;
static int m_socket_data;
int socket_create(void)
{
char buf[512];
int sockfd;
//打开套接字,得到套接字描述符
sockfd = socket(AF_INET, SOCK_STREAM,0);
if(sockfd<0){
perror("socket error\n");
exit(EXIT_FAILURE);
}
return sockfd;
}
int socket_connect(int sock, const char *addr, int port)
{
struct sockaddr_in server_addr = {0};
int ret;
//调用connect链接远端服务器
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port);
inet_pton(AF_INET,addr,&server_addr.sin_addr); //将IP地址从字符串格式转换成网络地址格式
//链接
ret = connect(sock,(struct sockaddr *)&server_addr,sizeof(server_addr));
if(ret < 0){
perror("connect error");
//close(sock);
return ret;
}
printf("[FTP]服务器链接成功!\n\n");
}
void socket_close(int sock)
{
pthread_mutex_lock(&mutex_cmd);
close(sock);
pthread_mutex_unlock(&mutex_cmd);
}
int socket_send(int sock, void *data, int len)
{
int ret;
pthread_mutex_lock(&mutex_cmd);
ret = send(sock,data,len,0);
pthread_mutex_unlock(&mutex_cmd);
if(ret<0){
perror("send error");
}
return ret;
}
int socket_recv(int sock, void *data, int len)
{
int ret;
//读取数据
pthread_mutex_lock(&mutex_cmd);
ret = recv(sock,data,len,0);
pthread_mutex_unlock(&mutex_cmd);
if(ret<0){
perror("recv error\n");
close(sock);
}
return ret;
}
//命令端口,发送命令
static int ftp_send_command(char *cmd)
{
int ret;
char buf[1024];
memset(buf,'\0',1024);
memcpy(buf,cmd,strlen(cmd));
printf("[FTP]send command: %s\r\n", buf);
ret = socket_send(m_socket_cmd, buf, (int)strlen(buf));
if(ret < 0)
{
printf("[FTP]failed to send command: %s",buf);
return 0;
}
return 1;
}
//命令端口,接收应答
static int ftp_recv_respond(char *resp, int len)
{
int ret;
int off;
len -= 1;
memset(resp,'\0',len);
for(off=0; off<len; )
{
printf("%d ",off);
ret = socket_recv(m_socket_cmd, &resp[off], 1);
if(ret < 0)
{
printf("[FTP]recv respond error(ret=%d)!\r\n", ret);
return 0;
}
off+=ret;
if(resp[off-1] == '\n')
{
break;
}
}
resp[off+1] = 0;
printf("[FTP]respond:%s", resp);
return atoi(resp);
}
//设置FTP服务器为被动模式,并解析数据端口
static int ftp_enter_pasv(char *ipaddr, int *port)
{
int ret;
char *find;
int a=0,b=0,c=0,d=0;
int pa=0,pb=0;
ret = ftp_send_command("PASV\r\n");
if(ret != 1)
{
return 0;
}
ret = ftp_recv_respond(m_recv_buffer, COMMAND_LEN);
if(ret != 227)
{
return 0;
}
//memcpy(m_recv_buffer,"12345(6)",sizeof("12345(6)"));
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 *src, char *des)
{
int ret,i;
char ipaddr[32];
int port;
char buf[102400] = {0};
FILE *fp = NULL;
//查询数据地址
ret=ftp_enter_pasv(ipaddr, &port);
if(ret != 1)
{
return 0;
}
ret=socket_connect(m_socket_data, ipaddr, port);
if(ret < 0)
{
return 0;
}
//准备上传
sprintf(m_send_buffer, "STOR %s\r\n", des);
ret = ftp_send_command(m_send_buffer);
if(ret != 1)
{
return 0;
}
ret = ftp_recv_respond(m_recv_buffer, COMMAND_LEN);
if(ret != 150)
{
socket_close(m_socket_data);
return 0;
}
//读取文件
fp = fopen(src,"ab+");
if(fp == NULL){
perror("Open error");
return 0;
}
//获取文件大小
struct stat statbuf;
stat(src, &statbuf);
int len = statbuf.st_size;
for(i=0; i<len; )
{
//开始上传
memset(buf,'\0', sizeof(buf));
//ret = fread(buf,1,10240,fp);
ret = fread(buf,1,sizeof(buf),fp);
ret = socket_send(m_socket_data, buf, ret);
if(ret < 0)
{
printf("[FTP]send data error:%d!\r\n",ret);
socket_close(m_socket_data);
return 0;
}
i+=ret;
printf("[FTP]upload: %d/%d.\r\n",i,len);
if(feof(fp)){
break;
}
}
socket_close(m_socket_data);
fclose(fp);
//上传完成,等待回应
ret = ftp_recv_respond(m_recv_buffer, COMMAND_LEN);
if(ret == 226){
printf("[FTP]upload complete,save to%s!\r\n",des);
}
return (ret==226);
}
//下载文件
int _ftp_download(char *src,char *des ,int len)
{
int i;
int ret;
char ipaddr[32];
int port;
char buf[102400];
FILE *fp = NULL;
//查询数据地址
ret = ftp_enter_pasv(ipaddr, &port);
if(ret != 1)
{
return 0;
}
//连接数据端口
ret = socket_connect(m_socket_data, ipaddr, port);
if(ret < 0)
{
printf("[FTP]failed to connect data port\r\n");
return 0;
}
//准备下载
sprintf(m_send_buffer, "RETR %s\r\n", src);
ret = ftp_send_command(m_send_buffer);
if(ret != 1)
{
return 0;
}
ret = ftp_recv_respond(m_recv_buffer, COMMAND_LEN);
if(ret != 150)
{
socket_close(m_socket_data);
return 0;
}
fp = fopen(des,"ab+");
if(fp == NULL){
perror("Open error");
return 0;
}
//开始下载,读取完数据后,服务器会自动关闭连接
for(i=0; i<len; i+=ret)
{
memset(buf,'\0',sizeof(buf));
//ret = socket_recv(m_socket_data, ((char *)buf) + i, len);
ret = socket_recv(m_socket_data, buf, sizeof(buf));
printf("[FTP]download %d/%d.\r\n", i + ret, len);
if(ret < 0)
{
printf("[FTP]download was interrupted.\r\n");
break;
}
if(fwrite(buf,1,ret,fp) < ret){
printf("[FTP]fwrite error.\r\n");
fclose(fp);
return 0;
};
}
socket_close(m_socket_data);
fclose(fp);
ret = ftp_recv_respond(m_recv_buffer, COMMAND_LEN);
if(ret == 226){
//下载完成
printf("[FTP]download %d/%d bytes complete,Save to %s.\r\n", i, len,des);
}else{
printf("[FTP]download falied.\r\n");
return 0;
}
return (ret==226);
}
//返回文件大小
int ftp_filesize(char *name)
{
int ret;
int size;
memset(m_send_buffer, '\0', sizeof(m_send_buffer));
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, COMMAND_LEN);
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,i;
printf("[FTP]connect...\r\n");
//3次链接服务器不成功就取消连接
for ( i = 0; i < 3; i++)
{
ret = socket_connect(m_socket_cmd, addr, port);
if(ret >= 0){
break;
}else if(ret < 0 && i < 3 ){
sleep(3);
printf("[FTP]Trying connect again...\r\n");
}else{
printf("[FTP]connect server failed!\r\n");
return 0;
}
}
printf("[FTP]connect ok.\r\n");
//等待欢迎信息
ret = ftp_recv_respond(m_recv_buffer, COMMAND_LEN);//1024
if(ret != 220)
{
printf("[FTP]bad server, ret=%d!\r\n", ret);
socket_close(m_socket_cmd);
return 0;
}
printf("[FTP]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, COMMAND_LEN);
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, 24);
if(ret != 230)
{
socket_close(m_socket_cmd);
return 0;
}
printf("[FTP]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, COMMAND_LEN);
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();
}
/**
* @brief Function:API,从ftp服务器下载文件
* @param src:FTP服务器URL
* @param des:本地的保存位置-
* @param Host:FTP服务器IP地址 *
* @param Port:FTP服务器端口
* @param username:FTP服务器账户
* @param password:FTP服务器密码
*/
int ftp_download(char *src, char *des,char *Host,int Port,char *username,char *password)
{
memset(&ftp_property,0,sizeof(ftp_property));
ftp_property.download_src = src;
ftp_property.download_desc = des;
ftp_property.ip_addr = Host;
ftp_property.username = username;
ftp_property.password = password;
ftp_property.port = Port;
printf("[FTP]ftp_download:\r\n[src:%s]\r\n[des:%s]\r\n[IPAddr:%s]\r\n[Port:%u]\r\n[username:%s]\r\n[password:%s]\r\n", \
ftp_property.download_src,ftp_property.download_desc,ftp_property.ip_addr,ftp_property.port,ftp_property.username,ftp_property.password);
int ret = 0;
ftp_init();
ret = ftp_login(ftp_property.ip_addr, ftp_property.port, ftp_property.username,ftp_property.password);
if(ret == 0){
ftp_quit();
return;
}
int size = ftp_filesize(ftp_property.download_src);
if(ret == 0){
ftp_quit();
return;
}
ret = _ftp_download(ftp_property.download_src,ftp_property.download_desc, size);
if(ret == 0){
ftp_quit();
return;
}
ftp_quit();
/*
if(pthread_create(&Ftp_download_thread, NULL, ftp_download_thread, NULL) != 0){
perror("线程\"socket_net_dila\"创建失败!\r\n");
}
*/
}
/**
* @brief Function:API,向ftp服务器上传文件
* @param src:本地URL
* @param des:服务器保存位置
* @param Host:FTP服务器IP地址 *
* @param Port:FTP服务器端口
* @param username:FTP服务器账户
* @param password:FTP服务器密码
*/
int ftp_upload(char *src, char *des,char *Host,int Port,char *username,char *password)
{
int ret = 0;
memset(&ftp_property,0,sizeof(ftp_property));
ftp_property.upload_src = src;
ftp_property.upload_desc = des;
ftp_property.ip_addr = Host;
ftp_property.username = username;
ftp_property.password = password;
ftp_property.port = Port;
printf("[FTP]ftp_upload:\r\n[src:%s]\r\n[des:%s]\r\n[IPAddr:%s]\r\n[Port:%u]\r\n[username:%s]\r\n[password:%s]\r\n", \
ftp_property.upload_src,ftp_property.upload_desc,ftp_property.ip_addr,ftp_property.port,ftp_property.username,ftp_property.password);
ftp_init();
ret = ftp_login(ftp_property.ip_addr, ftp_property.port, ftp_property.username,ftp_property.password);
if(ret == 0)
return;
ret = _ftp_upload(ftp_property.upload_src,ftp_property.upload_desc);
if(ret == 0)
return;
ftp_quit();
//if(pthread_create(&Ftp_upload_thread, NULL, ftp_upload_thread, NULL) != 0){
// perror("线程\"socket_net_dila\"创建失败!\r\n");
//}
}
void main(void)
{
return 0;
}
APP_FTP.h
#ifndef __APP_THREAD_FTP__
#define __APP_THREAD_FTP__
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <sys/socket.h>
#include <linux/if.h>
#include <fcntl.h>
#include <arpa/inet.h>
int ftp_download(char *src, char *des,char *Host,int Port,char *username,char *password);
int ftp_upload(char *src, char *des,char *Host,int Port,char *username,char *password);
#endif // !__APP_THREAD_FTP__
文章参考了(60条消息) C语言实现的简易FTP客户端_星沉地动的博客-优快云博客_c语言实现ftp
在他的基础上对接口进行了进一步封装和优化