用C语言实现FTP
1关于ftp
FTP协议包括两个组成部分,其一为FTP服务器,其二为FTP客户端。其中FTP服务器用来存储文件,用户可以使用FTP客户端通过FTP协议访问位于FTP服务器上的资源。
默认情况下FTP协议使用TCP端口中的 20和21这两个端口,其中20用于传输数据,21用于传输控制信息。
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必须和客户端建立一个新的连接用来传送数据。
Passive
在建立控制通道的时候和Standard模式类似,但建立连接后发送的不是Port命令,而是Pasv命令。FTP服务器收到Pasv命令后,随机打开一个高端端口(端口号大于1024)并且通知客户端在这个端口上传送数据的请求,客户端连接FTP服务器此端口,然后FTP服务器将通过这个端口进行数据的传送,这个时候FTP server不再需要建立一个新的和客户端之间的连接。
很多防火墙在设置的时候都是不允许接受外部发起的连接的,所以许多位于防火墙后或内网的FTP服务器不支持PASV模式,因为客户端无法穿过防火墙打开FTP服务器的高端端口;而许多内网的客户端不能用PORT模式登陆FTP服务器,因为从服务器的TCP 20无法和内部网络的客户端建立一个新的连接,造成无法工作。
我这次实现简化了FTP协议过程,原本ftp使用两个端口分别传输数据和控制信息,我只用了一个端口同时完成这两个功能。实现了一些简单命令,如ls、download、upload。
2算法流程
client.c:
server.c:
3代码及注释
client.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include <fcntl.h> #define N 256 void commd_help(); void commd_exit(); void commd_ls(struct sockaddr_in, char *); void commd_get(struct sockaddr_in , char *); void commd_put(struct sockaddr_in , char *); int main(int argc, char *argv[]) { char commd[N]; struct sockaddr_in addr; int len; bzero(&addr, sizeof(addr)); //将&addr中的前sizeof(addr)字节置为0,包括'\0' addr.sin_family = AF_INET; //AF_INET代表TCP/IP协议 addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //将点间隔地址转换为网络字节顺序 addr.sin_port = htons(8989); //转换为网络字节顺序 len = sizeof(addr); while(1) { printf("ftp>"); bzero(commd,N); //fgets函数从stdin流中读取N-1个字符放入commd中 if(fgets(commd,N,stdin) == NULL) { printf("Fgets Error!\n"); return -1; } commd[strlen(commd)-1]='\0'; //fgets函数读取的最后一个字符为换行符,此处将其替换为'\0' printf("Input Command Is [ %s ]\n",commd); if(strncmp(commd,"help",4) == 0) //比较两个字符串前4个字节,若相等则返回0 { commd_help(); }else if(strncmp(commd, "exit",4) == 0) { commd_exit(); exit(0); //结束进程 }else if(strncmp(commd, "ls" , 2) == 0) { commd_ls(addr, commd); }else if(strncmp(commd, "get" , 3) == 0) { commd_get(addr, commd); }else if(strncmp(commd, "put", 3) ==0 ) { commd_put(addr, commd); }else { printf("Command Is Error!Please Try Again!\n"); } } return 0; } /* **帮助信息 */ void commd_help() { printf("\n=---------------------欢迎使用FTP--------------------------|\n"); printf("| |\n"); printf("| help:显示所有FTP服务器命令 |\n"); printf("| |\n"); printf("| exit:离开FTP服务器 |\n"); printf("| |\n"); printf("| ls : 显示FTP服务器的文件列表 |\n"); printf("| |\n"); printf("| get <file>:从FTP服务器下载文件 |\n"); printf("| |\n"); printf("| put <file>:上传文件到FTP服务器 |\n"); printf("| |\n"); printf("|-----------------------------------------------------------|\n"); return ; } /* **退出FTP服务器 */ void commd_exit() { printf("Bye!\n"); } /* **显示文件列表 */ void commd_ls(struct sockaddr_in addr, char *commd) { int sockfd; //创建套接字 if((sockfd=socket(AF_INET, SOCK_STREAM, 0)) < 0) { printf("Socket Error!\n"); exit(1); } if(connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { printf("Connect Error!\n"); exit(1); } //将commd指向的内容写入到sockfd所指的文件中,此处即指套接字 if(write(sockfd, commd, N) < 0) { printf("Write Error!\n"); exit(1); } while(read(sockfd, commd, N) > 0) //从sockfd中读取N字节内容放入commd中, { //返回值为读取的字节数 printf(" %s ",commd); } printf("\n"); close(sockfd); return ; } /* **实现文件的下载 */ void commd_get(struct sockaddr_in addr, char *commd) { int fd; int sockfd; char buffer[N]; int nbytes; //创建套接字,并进行错误检测 if((sockfd=socket(AF_INET, SOCK_STREAM, 0)) < 0) { printf("Socket Error!\n"); exit(1); } //connect函数用于实现客户端与服务端的连接,此处还进行了错误检测 if(connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { printf("Connect Error!\n"); exit(1); } //通过write函数向服务端发送数据 if(write(sockfd, commd, N) < 0) { printf("Write Error!At commd_get 1\n"); exit(1); } //利用read函数来接受服务器发来的数据 if(read(sockfd, buffer, N) < 0) { printf("Read Error!At commd_get 1\n"); exit(1); } //用于检测服务器端文件是否打开成功 if(buffer[0] =='N') { close(sockfd); printf("Can't Open The File!\n"); return ; } //open函数创建一个文件,文件地址为(commd+4),该地址从命令行输入获取 if((fd=open(commd+4, O_WRONLY|O_CREAT|O_TRUNC, 0644)) < 0) { printf("Open Error!\n"); exit(1); } //read函数从套接字中获取N字节数据放入buffer中,返回值为读取的字节数 while((nbytes=read(sockfd, buffer, N)) > 0) { //write函数将buffer中的内容读取出来写入fd所指向的文件,返回值为实际写入的字节数 if(write(fd, buffer, nbytes) < 0) { printf("Write Error!At commd_get 2"); } } close(fd); close(sockfd); return ; } /* **实现文件的上传 */ void commd_put(struct sockaddr_in addr, char *commd) { int fd; int sockfd; char buffer[N]; int nbytes; //创建套接字 if((sockfd=socket(AF_INET, SOCK_STREAM, 0)) < 0) { printf("Socket Error!\n"); exit(1); } //客户端与服务端连接 if(connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { printf("Connect Error!\n"); exit(1); } //从commd中读取N字节数据,写入套接字中 if(write(sockfd, commd, N)<0) { printf("Wrtie Error!At commd_put 1\n"); exit(1); } //open函数从(commd+4)中,读取文件路径,以只读的方式打开 if((fd=open(commd+4, O_RDONLY)) < 0) { printf("Open Error!\n"); exit(1); } //从fd指向的文件中读取N个字节数据 while((nbytes=read(fd, buffer, N)) > 0) { //从buffer中读取nbytes字节数据,写入套接字中 if(write(sockfd, buffer, nbytes) < 0) { printf("Write Error!At commd_put 2"); } } close(fd); close(sockfd); return ; }
server.c
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <dirent.h> #include <fcntl.h> #define N 256//文件名和命令名最长为256字节 void commd_ls(int); void commd_get(int, char *); void commd_put(int, char *); int main(int arg, char *argv[]) { int ser_sockfd,cli_sockfd; struct sockaddr_in ser_addr,cli_addr; int ser_len, cli_len; char commd [N]; bzero(commd,N);//将commd所指向的字符串的前N个字节置为0,包括'\0' if((ser_sockfd=socket(AF_INET, SOCK_STREAM, 0) ) < 0) { printf("Sokcet Error!\n"); return -1; } bzero(&ser_addr,sizeof(ser_addr)); ser_addr.sin_family = AF_INET; ser_addr.sin_addr.s_addr = htonl(INADDR_ANY);//本地ip地址 ser_addr.sin_port = htons (8989);//转换成网络字节 ser_len = sizeof(ser_addr); //将ip地址与套接字绑定 if((bind(ser_sockfd, (struct sockaddr *)&ser_addr, ser_len)) < 0) { printf("Bind Error!\n"); return -1; } //服务器端监听 if(listen(ser_sockfd, 5) < 0) { printf("Linsten Error!\n"); return -1; } bzero(&cli_addr, sizeof(cli_addr)); ser_len = sizeof(cli_addr); while(1) { printf("server>"); //服务器端接受来自客户端的连接,返回一个套接字,此套接字为新建的一个,并将客户端的地址等信息存入cli_addr中 //原来的套接字仍处于监听中 if((cli_sockfd=accept(ser_sockfd, (struct sockaddr *)&cli_addr, &cli_len)) < 0) { printf("Accept Error!\n"); exit(1); } //由套接字接收数据时,套接字把接收的数据放在套接字缓冲区,再由用户程序把它们复制到用户缓冲区,然后由read函数读取 //write函数同理 if(read(cli_sockfd, commd, N) < 0) //read函数从cli_sockfd中读取N个字节数据放入commd中 { printf("Read Error!\n"); exit(1); } printf("recvd [ %s ]\n",commd); if(strncmp(commd,"ls",2) == 0) { commd_ls(cli_sockfd); }else if(strncmp(commd,"get", 3) == 0 ) { commd_get(cli_sockfd, commd+4); }else if(strncmp(commd, "put", 3) == 0) { commd_put(cli_sockfd, commd+4); }else { printf("Error!Command Error!\n"); } } return 0; } /* **显示文件列表 */ void commd_ls(int sockfd) { DIR * mydir =NULL; struct dirent *myitem = NULL; char commd[N] ; bzero(commd, N); //opendir为用来打开参数name 指定的目录, 并返回DIR*形态的目录流 //mydir中存有相关目录的信息 if((mydir=opendir(".")) == NULL) { printf("OpenDir Error!\n"); exit(1); } while((myitem = readdir(mydir)) != NULL)//用来读取目录,返回是dirent结构体指针 { if(sprintf(commd, myitem->d_name, N) < 0)//把文件名写入commd指向的缓冲区 { printf("Sprintf Error!\n"); exit(1); } if(write(sockfd, commd, N) < 0 )//将commd缓冲区的内容发送会client { printf("Write Error!\n"); exit(1); } } closedir(mydir);//关闭目录流 close(sockfd); return ; } /* **实现文件的下载 */ void commd_get(int sockfd, char *filename) { int fd, nbytes; char buffer[N]; bzero(buffer, N); printf("get filename : [ %s ]\n",filename); if((fd=open(filename, O_RDONLY)) < 0)//以只读的方式打开client要下载的文件 { printf("Open file Error!\n"); buffer[0]='N'; if(write(sockfd, buffer, N) <0) { printf("Write Error!At commd_get 1\n"); exit(1); } return ; } buffer[0] = 'Y'; //此处标示出文件读取成功 if(write(sockfd, buffer, N) <0) { printf("Write Error! At commd_get 2!\n"); close(fd); exit(1); } while((nbytes=read(fd, buffer, N)) > 0)//将文件内容读到buffer中 { if(write(sockfd, buffer, nbytes) < 0)//将buffer发送回client { printf("Write Error! At commd_get 3!\n"); close(fd); exit(1); } } close(fd); close(sockfd); return ; } /* **实现文件的上传 */ void commd_put(int sockfd, char *filename) { int fd, nbytes; char buffer[N]; bzero(buffer, N); printf("get filename : [ %s ]\n",filename); if((fd=open(filename, O_WRONLY|O_CREAT|O_TRUNC, 0644)) < 0)//以只写的方式打开文件,若文件存在则清空,若文件不存在则新建文件 { printf("Open file Error!\n"); return ; } while((nbytes=read(sockfd, buffer, N)) > 0)//将client发送的文件写入buffer { if(write(fd, buffer, nbytes) < 0)//将buffer中的内容写到文件中 { printf("Write Error! At commd_put 1!\n"); close(fd); exit(1); } } close(fd); close(sockfd); return ; }
效果:
server:
client:
因为我在代码中将读取文件的大小设置为不超过256字节,当上传(或下载)文件大小超过256字节,后面内容则为空。
所以上穿(或下载)一个超过256字节的文件后,查看文件,发现文件部分内容丢失,则可以证明FTP实现成功,文件是从服务器端下载的。