Socket网络编程--FTP客户端(1)(Windows)

时间:2022-05-27 15:08:29

  已经好久没有写过博客进行分享了。具体原因,在以后说。

  这几天在了解FTP协议,准备任务是写一个FTP客户端程序。直接上干货了。

0.了解FTP作用

  就是一个提供一个文件的共享协议。

1.了解FTP协议

  FTP有指令和响应码。FTP 控制帧即指 TELNET 交换信息,包含 TELNET 命令和选项。然而,大多数 FTP 控制帧是简单的 ASCII 文本,可以分为 FTP 命令或 FTP 消息。 FTP 消息是对 FTP 命令的响应,它由带有解释文本的应答代码构成。

  像这种利用交换信息来进行简单的控制,这种协议,还真的很好玩的说。 命令与响应码部分信息如下

  Socket网络编程--FTP客户端(1)(Windows)

  Socket网络编程--FTP客户端(1)(Windows)

2. 安装一个FTP服务器

  我们先安装一个FTP服务器,用于测试,这里是用FileZilla Server作为FTP服务器。

Socket网络编程--FTP客户端(1)(Windows)

  启动后,增加一个用户user/user

3.FTP客户端源代码讲解

  下面这个是FTPAPI.h文件

 #ifndef FTPAPI_H_INCLUDED
#define FTPAPI_H_INCLUDED #include <stdio.h>
#include <winsock2.h> SOCKET socket_connect(char *host, int port);
SOCKET connect_server(char *host, int port);
int ftp_sendcmd_re(SOCKET sock, char *cmd, char *re_buf, ssize_t *len);
int ftp_sendcmd(SOCKET sock, char *cmd);
int login_server(SOCKET sock, char *user, char *pwd);
void socket_close(int c_sock); /**********可用命令*********/
SOCKET ftp_connect(char *host, int port, char *user, char *pwd); //连接到服务器
int ftp_quit(SOCKET sock); //断开连接
int ftp_type(SOCKET sock, char mode); //设置FTP传输类型
int ftp_cwd(SOCKET sock, char *path); //更改工作目录
int ftp_cdup(SOCKET sock); //回到上级目录
int ftp_mkd(SOCKET sock, char *path); //创建目录
SOCKET ftp_pasv_connect(SOCKET c_sock); //连接到PASV接口
int ftp_list(SOCKET c_sock, char *path, char **data, int *data_len); //列出FTP工作空间的所有目录
int ftp_deletefolder(SOCKET sock, char *path); //删除目录
int ftp_deletefile(SOCKET sock, char *filename); //删除文件
int ftp_renamefile(SOCKET sock, char *s, char *d); //修改文件/目录&移动文件/目录
int ftp_server2local(SOCKET c_sock, char *s, char *d, int * size); //从服务器复制文件到本地 RETR
int ftp_local2server(SOCKET c_sock, char *s, char *d, int * size); //从本地复制文件到服务器 STOR
int ftp_recv(SOCKET sock, char *re_buf, ssize_t *len); //获取响应码 #endif // FTPAPI_H_INCLUDED

  下面这个是FTPResponseCode.h 文件 是对应答码简单的描述

 #ifndef FTPRESPONSECODE_H_INCLUDED
#define FTPRESPONSECODE_H_INCLUDED #define FTP_SUCCESS 200 //成功
#define FTP_SERVICE_READY 220 //服务器就绪
#define FTP_LOGIN_SUCCESS 230 //登录因特网服务器
#define FTP_FILE_ACTION_COMPLETE 250 //文件行为完成
#define FTP_FILE_CREATED 257 //文件创建成功
#define FTP_PASSWORD_REQUIREd 331 //要求密码
#define FTP_LOGIN_PASSWORD_INCORRECT 530 //用户密码错误 #endif // FTPRESPONSECODE_H_INCLUDED

  下面这些是FTPAPI.cpp文件的函数代码

  创建一个socket连接并返回socket套接字 socket_connect

 /**
* 作用: 创建一个Socket并返回.
* 参数: IP或域名, 端口
* 返回值: Socket套接字
* */
SOCKET socket_connect(char *host, int port)
{
int i=;
//初始化 Socket dll
WSADATA wsaData;
WORD socketVersion = MAKEWORD(,);
if(WSAStartup(socketVersion, &wsaData))
{
printf("Init socket dll error!");
exit();
} struct hostent * server = gethostbyname(host);
if(!server)
return -;
unsigned char ch[];
char ip[];
//一个hostname 可以对应多个ip
while(server->h_addr_list[i]!=NULL)
{
memcpy(&ch,server->h_addr_list[i],);
sprintf(ip,"%d.%d.%d.%d",ch[],ch[],ch[],ch[]);
//printf("%s\n",ip);
i++;
} //创建Socket
SOCKET s = socket(AF_INET, SOCK_STREAM, ); //TCP socket
if(SOCKET_ERROR == s)
{
printf("Create Socket Error!");
exit();
}
//设置超时连接
int timeout = ; //复杂的网络环境要设置超时判断
int ret = setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, (char *)&timeout, sizeof(timeout));
ret = setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout,sizeof(timeout));
//指定服务器地址
struct sockaddr_in address;
address.sin_family = AF_INET;
address.sin_addr.S_un.S_addr = inet_addr(ip);
address.sin_port = htons((unsigned short)port);
//连接
if(SOCKET_ERROR == connect(s,(LPSOCKADDR)&address,sizeof(address)))
{
printf("Can Not Connect To Server IP!\n");
exit();
}
return s;
}

  连接到一个ftp服务器 connect_server

 /**
* 作用: 连接到一个FTP服务器,返回socket
* 参数: IP或域名, 端口
* 返回值: Socket套接字
* */
SOCKET connect_server(char *host, int port)
{
SOCKET ctrl_sock;
char buf[BUFSIZE];
int result;
ssize_t len; ctrl_sock = socket_connect(host,port);
if(- == ctrl_sock)
{
return -;
}
while((len = recv(ctrl_sock, buf, BUFSIZE, )) > )
{
//len = recv(ctrl_sock, buf, BUFSIZE, 0);
buf[len]=;
printf("%s\n",buf); //220-FileZilla Server version 0.9.43 beta
}
sscanf(buf, "%d", &result); if(FTP_SERVICE_READY != result)
{
printf("FTP Not ready, Close the socet.");
closesocket(ctrl_sock); //关闭Socket
return -;
}
return ctrl_sock;
}

  send发送命令,并返回recv结果 ftp_sendcmd_re

 /**
* 作用: send发送命令,并返回recv结果
* 参数: SOCKET,命令,命令返回码-命令返回描述,命令返回字节数
* 返回值: 0 表示发送成功 -1表示发送失败
* */
int ftp_sendcmd_re(SOCKET sock, char *cmd, char *re_buf, ssize_t *len)
{
char buf[BUFSIZE];
ssize_t r_len;
if(send(sock, cmd, strlen(cmd), ) == -)
{
return -;
}
r_len = recv(sock, buf, BUFSIZE, );
if(r_len < )
return -;
buf[r_len]=;
if(NULL != len)
*len = r_len;
if(NULL != re_buf)
sprintf(re_buf, "%s", buf);
return ;
}

  send发送命令 ftp_sendcmd

 /**
* 作用: send发送命令
* 参数: SOCKET,命令
* 返回值: FTP响应码
* */
int ftp_sendcmd(SOCKET sock, char *cmd)
{
char buf[BUFSIZE];
int result;
ssize_t len;
printf("FTP Client: %s", cmd);
result = ftp_sendcmd_re(sock, cmd, buf, &len);
printf("FTP Server: %s", buf);
if( == result)
{
sscanf(buf, "%d", &result);
}
return result;
}

  登录FTP服务器 login_server

 /**
* 作用: 登录FTP服务器
* 参数: SOCKET套接字,明文用户名,明文密码
* 返回值: 0 表示登录成功 -1 表示登录失败
* */
int login_server(SOCKET sock, char *user, char *pwd)
{
char buf[BUFSIZE];
int result;
sprintf(buf, "USER %s\r\n", user);
//这里要对socket进行阻塞
int timeout=;
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout,sizeof(timeout));
result = ftp_sendcmd(sock, buf);
if(FTP_LOGIN_SUCCESS == result) //直接登录
return ;
else if(FTP_PASSWORD_REQUIREd == result) //需要密码
{
sprintf(buf, "PASS %s\r\n", pwd);
result = ftp_sendcmd(sock, buf);
if(FTP_LOGIN_SUCCESS == result)
{
return ;
}
else //530 密码错误
{
return -;
}
}
else
{
return -;
}
}

  winsock使用后,要调用WSACleanup函数关闭网络设备 socket_close

 /**
* 作用: winsock使用后,要调用WSACleanup函数关闭网络设备,以便释放其占用的资源
* 参数: SOCKET
* 返回值: 无
* */
void socket_close(int c_sock)
{
WSACleanup();
}

  连接到FTP服务器 ftp_connect

 /**
* 作用: 连接到FTP服务器
* 参数: hostname或IP,端口,用户名,密码
* 返回值: 已连接到FTP服务器的SOCKET -1 表示登录失败
* */
SOCKET ftp_connect(char *host, int port, char *user, char *pwd)
{
SOCKET sock;
sock = connect_server(host, port);
if(- == sock)
{
return -;
}
if(- == login_server(sock, user, pwd))
{
closesocket(sock);
return -;
}
return sock;
}

  断开FTP服务器 ftp_quit

 /**
* 作用: 断开FTP服务器
* 参数: SOCKET
* 返回值: 成功断开状态码
* */
int ftp_quit(SOCKET sock)
{
int result = ;
result = ftp_sendcmd(sock, "QUIT\r\n");
closesocket(sock);
socket_close(sock);
return result;
}

  设置FTP传输类型 A:ascii I:Binary  ftp_type

 /**
* 作用: 设置FTP传输类型 A:ascii I:Binary
* 参数: SOCkET,类型
* 返回值: 0 表示成功 -1 表示失败
* */
int ftp_type(SOCKET sock, char mode)
{
char buf[BUFSIZ];
sprintf(buf,"TYPE %c\r\n", mode);
if(FTP_SUCCESS != ftp_sendcmd(sock, buf))
return -;
else
return ;
}

  更改工作目录 ftp_cwd

 /**
* 作用: 更改工作目录
* 参数: SOCKET,工作目录
* 返回值: 0 表示成功 -1 表示失败
* */
int ftp_cwd(SOCKET sock, char *path)
{
char buf[BUFSIZE];
int result;
sprintf(buf, "CWD %s\r\n", path);
result = ftp_sendcmd(sock, buf);
if(FTP_FILE_ACTION_COMPLETE != result) //250 文件行为完成
return -;
else
return ;
}

  回到上级目录 ftp_cdup

 /**
* 作用: 回到上级目录
* 参数: SOCKET
* 返回值: 0 正常操作返回 result 服务器返回响应码
* */
int ftp_cdup(SOCKET sock)
{
int result;
result = ftp_sendcmd(sock, "CDUP\r\n");
if(FTP_FILE_ACTION_COMPLETE == result || FTP_SUCCESS == result)
return ;
else
return result;
}

  创建目录 ftp_mkd

 /**
* 作用: 创建目录
* 参数: SOCKET,文件目录路径(可相对路径,绝对路径)
* 返回值: 0 正常操作返回 result 服务器返回响应码
* */
int ftp_mkd(SOCKET sock, char *path)
{
char buf[BUFSIZE];
int result;
sprintf(buf, "MKD %s\r\n", path);
result = ftp_sendcmd(sock, buf);
if(FTP_FILE_CREATED != result) //257 路径名建立
return result; //550 目录已存在
else
return ;
}

  连接到PASV接口 ftp_pasv_connect

 /**
* 作用: 连接到PASV接口
* PASV(被动)方式的连接过程是:
* 客户端向服务器的FTP端口(默认是21)发送连接请求,
* 服务器接受连接,建立一条命令链路。
* 参数: 命令链路SOCKET cmd-socket
* 返回值: 数据链路SOCKET raw-socket -1 表示创建失败
* */
SOCKET ftp_pasv_connect(SOCKET c_sock)
{
SOCKET r_sock;
int send_result;
ssize_t len;
int addr[]; //IP*4+Port*2
char buf[BUFSIZE];
char result_buf[BUFSIZE]; //设置PASV被动模式
memset(buf,sizeof(buf),);
sprintf(buf, "PASV\r\n");
send_result = ftp_sendcmd_re(c_sock, buf, result_buf, &len);
if(send_result == )
{
sscanf(result_buf, "%*[^(](%d,%d,%d,%d,%d,%d)",
&addr[],&addr[],&addr[],&addr[],
&addr[],&addr[]);
} //连接PASV端口
memset(buf, sizeof(buf), );
sprintf(buf, "%d.%d.%d.%d",addr[],addr[],addr[],addr[]);
r_sock = socket_connect(buf,addr[]*+addr[]);
if(- == r_sock)
return -;
return r_sock;
}

  列出FTP工作空间的所有目录 ftp_list

 /**
* 作用: 列出FTP工作空间的所有目录
* 参数: 命令链路SOCKET,工作空间,列表信息,列表信息大小
* 返回值: 0 表示列表成功 result>0 表示其他错误响应码 -1 表示创建pasv错误
* */
int ftp_list(SOCKET c_sock, char *path, char **data, int *data_len)
{
SOCKET r_sock;
char buf[BUFSIZE];
int send_re;
int result;
ssize_t len,buf_len,total_len; //连接到PASV接口
r_sock = ftp_pasv_connect(c_sock);
if(- == r_sock)
{
return -;
}
//发送LIST命令
memset(buf,sizeof(buf),);
sprintf(buf, "LIST %s\r\n", path);
send_re = ftp_sendcmd(c_sock, buf);
if(send_re >= || send_re == )
return send_re;
len=total_len=;
buf_len=BUFSIZE;
char *re_buf = (char *)malloc(buf_len);
while( (len = recv(r_sock,buf,BUFSIZE,)) > )
{
if(total_len+len > buf_len)
{
buf_len *= ;
char *re_buf_n = (char *)malloc(buf_len);
memcpy(re_buf_n, re_buf, total_len);
free(re_buf);
re_buf = re_buf_n;
}
memcpy(re_buf+total_len, buf, len);
total_len += len;
}
closesocket(r_sock); //向服务器接收返回值
memset(buf, sizeof(buf), );
len = recv(c_sock, buf, BUFSIZE, );
buf[len] = ;
sscanf(buf, "%d", &result);
if(result != )
{
free(re_buf);
return result;
}
*data = re_buf;
*data_len = total_len;
return ;
}

  删除目录 ftp_deletefolder

 /**
* 作用: 删除目录
* 参数: 命令链路SOCKET,路径目录
* 返回值: 0 表示列表成功 result>0 表示其他错误响应码
* */
int ftp_deletefolder(SOCKET sock, char *path)
{
char buf[BUFSIZE];
int result;
sprintf(buf,"RMD %s\r\n", path);
result = ftp_sendcmd(sock, buf);
if(FTP_FILE_ACTION_COMPLETE != result)
{
//550 Directory not empty.
//550 Directory not found.
return result;
}
return ;
}

  删除文件 ftp_deletefile

 /**
* 作用: 删除文件
* 参数: 命令链路SOCKET,路径文件(相对/绝对)
* 返回值: 0 表示列表成功 result>0 表示其他错误响应码
* */
int ftp_deletefile(SOCKET sock, char *filename)
{
char buf[BUFSIZE];
int result;
sprintf(buf, "DELE %s\r\n", filename);
result = ftp_sendcmd(sock, buf);
if(FTP_FILE_ACTION_COMPLETE != ) //250 File deleted successfully
{
//550 File not found.
return result;
}
return ;
}

  修改文件名&移动目录 ftp_renamefile

 /**
* 作用: 修改文件名&移动目录
* 参数: 命令链路SOCKET,源地址,目的地址
* 返回值: 0 表示列表成功 result>0 表示其他错误响应码
* */
int ftp_renamefile(SOCKET sock, char *s, char *d)
{
char buf[BUFSIZE];
int result;
sprintf(buf, "RNFR %s\r\n", s);
result = ftp_sendcmd(sock, buf);
if( != result) //350 文件行为暂停,因为要进行移动操作
return result;
sprintf(buf, "RNTO %s\r\n", d);
result = ftp_sendcmd(sock, buf);
if(FTP_FILE_ACTION_COMPLETE != result)
{
return result;
}
return ;
}

  从服务器复制文件到本地 RETR  ftp_server2local

 /**
* 作用: 从服务器复制文件到本地 RETR
* 参数: SOCKET,源地址,目的地址,文件大小
* 返回值: 0 表示列表成功 result>0 表示其他错误响应码
* -1:文件创建失败 -2 pasv接口错误
* */
int ftp_server2local(SOCKET c_sock, char *s, char *d, int * size)
{
SOCKET d_sock;
ssize_t len,write_len;
char buf[BUFSIZ];
int result;
*size=;
//打开本地文件
FILE * fp = fopen(d, "wb");
if(NULL == fp)
{
printf("Can't Open the file.\n");
return -;
}
//设置传输模式
ftp_type(c_sock,'I'); //连接到PASV接口 用于传输文件
d_sock = ftp_pasv_connect(c_sock);
if(- == d_sock)
{
fclose(fp); //关闭文件
return -;
} //发送RETR命令
memset(buf, sizeof(buf), );
sprintf(buf, "RETR %s\r\n", s);
result = ftp_sendcmd(c_sock, buf);
// 150 Opening data channel for file download from server of "xxxx"
if(result >= || result == ) //失败可能是没有权限什么的,具体看响应码
{
fclose(fp);
return result;
} //开始向PASV读取数据(下载)
memset(buf, sizeof(buf), );
while((len = recv(d_sock, buf, BUFSIZE, )) > )
{
write_len = fwrite(&buf, len, , fp);
if(write_len != ) //写入文件不完整
{
closesocket(d_sock); //关闭套接字
fclose(fp); //关闭文件
return -;
}
if(NULL != size)
{
*size += write_len;
}
}
//下载完成
closesocket(d_sock);
fclose(fp); //向服务器接收返回值
memset(buf, sizeof(buf), );
len = recv(c_sock, buf, BUFSIZE, );
buf[len] = ;
printf("%s\n",buf);
sscanf(buf, "%d", &result);
if(result >= )
{
return result;
}
//226 Successfully transferred "xxxx"
return ;
}

  从本地复制文件到服务器 STOR ftp_local2server

 /**
* 作用: 从本地复制文件到服务器 STOR
* 参数: SOCKET,源地址,目的地址,文件大小
* 返回值: 0 表示列表成功 result>0 表示其他错误响应码
* -1:文件创建失败 -2 pasv接口错误
* */
int ftp_local2server(SOCKET c_sock, char *s, char *d, int * size)
{
SOCKET d_sock;
ssize_t len,send_len;
char buf[BUFSIZE];
FILE * fp;
int send_re;
int result;
//打开本地文件
fp = fopen(s, "rb");
if(NULL == fp)
{
printf("Can't Not Open the file.\n");
return -;
}
//设置传输模式
ftp_type(c_sock, 'I');
//连接到PASV接口
d_sock = ftp_pasv_connect(c_sock);
if(d_sock == -)
{
fclose(fp);
return -;
} //发送STOR命令
memset(buf, sizeof(buf), );
sprintf(buf, "STOR %s\r\n", d);
send_re = ftp_sendcmd(c_sock, buf);
if(send_re >= || send_re == )
{
fclose(fp);
return send_re;
} //开始向PASV通道写数据
memset(buf, sizeof(buf), );
while( (len = fread(buf, , BUFSIZE, fp)) > )
{
send_len = send(d_sock, buf, len, );
if(send_len != len)
{
closesocket(d_sock);
fclose(fp);
return -;
}
if(NULL != size)
{
*size += send_len;
}
}
//完成上传
closesocket(d_sock);
fclose(fp); //向服务器接收响应码
memset(buf, sizeof(buf), );
len = recv(c_sock, buf, BUFSIZE, );
buf[len] = ;
sscanf(buf, "%d", &result);
if(result >= )
{
return result;
}
return ;
}

  获取一行响应码 ftp_recv

 /**
* 作用: 获取一行响应码
* 参数:
* 返回值:
* */
int ftp_recv(SOCKET sock, char *re_buf, ssize_t *len)
{
char buf[BUFSIZE];
ssize_t r_len;
int timeout = ;
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout, sizeof(timeout));
r_len = recv(sock, buf, BUFSIZE, );
timeout = ;
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout, sizeof(timeout));
if(r_len < )
return -;
buf[r_len]=;
if(NULL != len)
*len = r_len;
if(NULL != re_buf)
sprintf(re_buf, "%s", buf);
return ;
}

4.测试ftp客户端

  下载文件到本地

     SOCKET s = ftp_connect("127.0.0.1",,"user","user"); //登录到FTP服务器
int ret = ftp_server2local(s,"user/user.zip","bin.zip",&size); //在FTP服务器获取文件
ftp_quit(s); //退出FTP服务器

  上传文件到服务器

     SOCKET s = ftp_connect("127.0.0.1",,"user","user"); //登录到FTP服务器
int ret = ftp_local2server(s,"user/user.zip","bin.zip",&size); //发送文件到FTP服务器
ftp_quit(s); //退出FTP服务器

  下面这个是服务器的日志信息

  Socket网络编程--FTP客户端(1)(Windows)

  下面这个是程序打印的调试信息

  Socket网络编程--FTP客户端(1)(Windows)

5.后话

  到这里这个简单的ftp库就可以实现绝大部分的客户端功能了,但是这里面有一个问题,就是ftp是明文传输用户名/密码的,如果ftp上的文件比较重要的话,那么就有点问题了。当然这个不是本次的关注点,本次主要是了解ftp协议,还有从代码中了解这种交换控制命令的方法是一种很不错的技术手段,虽然这种方法已经是好多年前的,不安全,也过时了。但还是有可学的地方。

6.附录

  下面这个附录是利用wireshark进行本地网络抓包测试。

1、抓包,要看部署点,在路由器、交换机等设备上做端口镜像、或分光口,或是接HUB、TAP等设备就可以直接获得通过这些口的报文。
2、抓包,也可在以局域网部署相关的网管软件或黑客工具(比如cain),可以用arp骗方式,让你的数据先发送到监控机上,然后再转发走。。这样你的数据就。。

建议:
1、建议在电脑上打开ARP防护功能
2、在使用中尽量使用加密传输的工具,比如SSH、SSL、QQ一类的东西。可避免一些危害.

  注意wireshark是不能抓取本地回环地址的数据包的,所以我以远程ftp服务器进行测试

   Socket网络编程--FTP客户端(1)(Windows)

  这里是通过浏览器进行连接的。wireshark 1.12.4 从上面可以看到的信息 29-44这些表示了,浏览器一开始使用匿名进行登录,发现登录不上,所以请求用户名登录在81 82 84 85这4行中我们可以分析到,我是输入用户名user 密码user进行登录的,第106行表示用户名/密码错误。 如果是230 Login in 就表示成功登录了。如果我们捉到了这些信息,那么我们就可以进行登录了。这样就不安全了。既然ftp这么不安全为什么那么多地方用到ftp共享文件。这个就要说到ftp的作用了,ftp作用本来就是共享文件,所以安全性就不是很重要了。 至于加密方式以后再讲。

  (开发环境mignw 编译的时候要加入libws2_32.a 这个库, 编译命令 g++ ftpapi.cpp -c -o ftpapi.o -lws2_32)

  参考资料

  TanHao的 THFTPAPI.c 文件 http://www.tanhao.me

  文件下载 ftpapi.zip http://files.cnblogs.com/files/wunaozai/ftpapi20150512.zip