我们在之前的文章中,曾经对FTP文件传输协议做过详细的介绍。本章,我们对如何用C语言实现FTP服务器做一个简单的介绍。
![06ea188444fe0beccf55715179f41d40.png](https://i-blog.csdnimg.cn/blog_migrate/757e97b34a4726709e5cf27d07520c72.jpeg)
概述
FTP文件传输协议,是因特网上使用得最广泛的文件传输协议。FTP提供交互式的访问,允许客户指明文件的格式与类型,并允许文件具有存储权限。FTP屏蔽了不同操作系统之前的细节,因此适合在异构网络中任意计算机之间传送文件。
FTP的基本工作原理
FTP使用C/S方式,一个FTP服务器可以为多个客户进程提供服务,FTP服务器进程由两大部分组成:一个主进程,负责接收新的请求;另外有若干个从属进程,负责处理单个请求。
主进程的工作步骤如下:
- 打开端口号(一般为21),使客户端能通过此端口号访问;
- 等待客户端发出连接请求;
- 启动从属进程来处理客户进程发来的请求。从属进程对客户进程的请求处理完后即终止,从属进程在运行期间可能会根据需要另外创建其他一些进程。
- 回到等待状态,继续等待其他客户进程发来的连接请求。主进程和从属进程是并发进行的。
在进行文件传输时,FTP的客户和服务器之间要建立两个并行的TCP连接:“控制连接”和“数据连接”。控制连接在整个会话期间一直保持打开,FTP客户所发出的传送请求,通过控制连接发送给服务器端的控制进程,但是控制连接并不会用于传输数据。实际传输文件的是“数据连接”。服务器端的控制进程在接收到FTP客户发送来的文件传输请求后,就会创建“数据传送进程”和“数据连接”,用来连接客户端和服务器端的数据传送进程。由于FTP使用了一个分离的控制连接,因此FTP的控制信息是带外控制的。
当客户进程向服务器进程发出建立连接请求时,通过服务器端口号21请求连接,同时会告诉服务器进程自己用于建立数据传送连接的另一个端口号。服务器一般使用端口号20同客户进程建立数据连接,由于FTP使用两个不同的端口号,所以数据连接和控制连接不会发生混乱。
![82d06df09f2d04f3d0be70d705251038.png](https://i-blog.csdnimg.cn/blog_migrate/9b3e1e01a81811966097a8839754be0d.jpeg)
综上所述,我们可以画出基本的算法流程图
![70ac5267f83294f0bbdcdbb48ee74ab8.png](https://i-blog.csdnimg.cn/blog_migrate/ddf2593333bd719aa8c9432538b0b41a.jpeg)
代码实现:
- 首先是基本的定义
/* Commands enumeration */typedef enum cmdlist { ABOR, CWD, DELE, LIST, MDTM, MKD, NLST, PASS, PASV, PORT, PWD, QUIT, RETR, RMD, RNFR, RNTO, SITE, SIZE, STOR, TYPE, USER, NOOP} cmdlist;/* String mappings for cmdlist */static const char *cmdlist_str[] = { "ABOR", "CWD", "DELE", "LIST", "MDTM", "MKD", "NLST", "PASS", "PASV", "PORT", "PWD", "QUIT", "RETR", "RMD", "RNFR", "RNTO", "SITE", "SIZE", "STOR", "TYPE", "USER", "NOOP" };
控制端口的定义
/* define FTP control port */#define CONTROLPORT 21
主函数
/** * Sets up server and handles incoming connections * @param port Server port */int main(){ int sock = create_socket(CONTROLPORT ); struct sockaddr_in client_address; int len = sizeof(client_address); int connection, pid, bytes_read; while(1){ connection = accept(sock, (struct sockaddr*) &client_address,&len); char buffer[BSIZE]; Command *cmd = malloc(sizeof(Command)); State *state = malloc(sizeof(State)); pid = fork(); memset(buffer,0,BSIZE); if(pid<0){ fprintf(stderr, "Cannot create child process."); exit(EXIT_FAILURE); } if(pid==0){ close(sock); char welcome[BSIZE] = "220 "; if(strlen(welcome_message)BSIZE)){ /* TODO: output this to log */ buffer[BSIZE-1] = '0'; printf("User %s sent command: %s",(state->username==0)?"unknown":state->username,buffer); parse_command(buffer,cmd); state->connection = connection; /* Ignore non-ascii char. Ignores telnet command */ if(buffer[0]<=127 || buffer[0]>=0){ response(cmd,state); } memset(buffer,0,BSIZE); memset(cmd,0,sizeof(cmd)); } else{ /* Read error */ perror("server:read"); } } printf("Client disconnected."); exit(0); }else{ printf("closing... :("); close(connection); } }}
- 功能的实现
/** * Handle USER command * @param cmd Command with args * @param state Current client connection state */void ftp_user(Command *cmd, State *state){ const int total_usernames = sizeof(usernames)/sizeof(char *); if(lookup(cmd->arg,usernames,total_usernames)>=0){ state->username = malloc(32); memset(state->username,0,32); strcpy(state->username,cmd->arg); state->username_ok = 1; state->message = "331 User name okay, need password"; }else{ state->message = "530 Invalid username"; } write_state(state);}/** PASS command */void ftp_pass(Command *cmd, State *state){ if(state->username_ok==1){ state->logged_in = 1; state->message = "230 Login successful"; }else{ state->message = "500 Invalid username or password"; } write_state(state);}/** PASV command */void ftp_pasv(Command *cmd, State *state){ if(state->logged_in){ int ip[4]; char buff[255]; char *response = "227 Entering Passive Mode (%d,%d,%d,%d,%d,%d)"; Port *port = malloc(sizeof(Port)); gen_port(port); getip(state->connection,ip); /* Close previous passive socket? */ close(state->sock_pasv); /* Start listening here, but don't accept the connection */ state->sock_pasv = create_socket((256*port->p1)+port->p2); printf("port: %d",256*port->p1+port->p2); sprintf(buff,response,ip[0],ip[1],ip[2],ip[3],port->p1,port->p2); state->message = buff; state->mode = SERVER; puts(state->message); }else{ state->message = "530 Please login with USER and PASS."; printf("%s",state->message); } write_state(state);}/** LIST command */void ftp_list(Command *cmd, State *state){ if(state->logged_in==1){ struct dirent *entry; struct stat statbuf; struct tm *time; char timebuff[80], current_dir[BSIZE]; int connection; time_t rawtime; /* TODO: dynamic buffering maybe? */ char cwd[BSIZE], cwd_orig[BSIZE]; memset(cwd,0,BSIZE); memset(cwd_orig,0,BSIZE); /* Later we want to go to the original path */ getcwd(cwd_orig,BSIZE); /* Just chdir to specified path */ if(strlen(cmd->arg)>0&&cmd->arg[0]!='-'){ chdir(cmd->arg); } getcwd(cwd,BSIZE); DIR *dp = opendir(cwd); if(!dp){ state->message = "550 Failed to open directory."; }else{ if(state->mode == SERVER){ connection = accept_connection(state->sock_pasv); state->message = "150 Here comes the directory listing."; puts(state->message); while(entry=readdir(dp)){ if(stat(entry->d_name,&statbuf)==-1){ fprintf(stderr, "FTP: Error reading file stats..."); }else{ char *perms = malloc(9); memset(perms,0,9); /* Convert time_t to tm struct */ rawtime = statbuf.st_mtime; time = localtime(&rawtime); strftime(timebuff,80,"%b %d %H:%M",time); str_perm((statbuf.st_mode & ALLPERMS), perms); dprintf(connection, "%c%s %5d %4d %4d %8d %s %s", (entry->d_type==DT_DIR)?'d':'-', perms,statbuf.st_nlink, statbuf.st_uid, statbuf.st_gid, statbuf.st_size, timebuff, entry->d_name); } } write_state(state); state->message = "226 Directory send OK."; state->mode = NORMAL; close(connection); close(state->sock_pasv); }else if(state->mode == CLIENT){ state->message = "502 Command not implemented."; }else{ state->message = "425 Use PASV or PORT first."; } } closedir(dp); chdir(cwd_orig); }else{ state->message = "530 Please login with USER and PASS."; } state->mode = NORMAL; write_state(state);}/** QUIT command */void ftp_quit(State *state){ state->message = "221 Goodbye, friend. I never thought I'd die like this."; write_state(state); close(state->connection); exit(0);}/** PWD command */void ftp_pwd(Command *cmd, State *state){ if(state->logged_in){ char cwd[BSIZE]; char result[BSIZE]; memset(result, 0, BSIZE); if(getcwd(cwd,BSIZE)!=NULL){ strcat(result,"257 ""); strcat(result,cwd); strcat(result,"""); state->message = result; }else{ state->message = "550 Failed to get pwd."; } write_state(state); }}/** CWD command */void ftp_cwd(Command *cmd, State *state){ if(state->logged_in){ if(chdir(cmd->arg)==0){ state->message = "250 Directory successfully changed."; }else{ state->message = "550 Failed to change directory."; } }else{ state->message = "500 Login with USER and PASS."; } write_state(state);}/** * MKD command * TODO: full path directory creation */void ftp_mkd(Command *cmd, State *state){ if(state->logged_in){ char cwd[BSIZE]; char res[BSIZE]; memset(cwd,0,BSIZE); memset(res,0,BSIZE); getcwd(cwd,BSIZE); /* TODO: check if directory already exists with chdir? */ /* Absolute path */ if(cmd->arg[0]=='/'){ if(mkdir(cmd->arg,S_IRWXU)==0){ strcat(res,"257 ""); strcat(res,cmd->arg); strcat(res,"" new directory created."); state->message = res; }else{ state->message = "550 Failed to create directory. Check path or permissions."; } } /* Relative path */ else{ if(mkdir(cmd->arg,S_IRWXU)==0){ sprintf(res,"257 "%s/%s" new directory created.",cwd,cmd->arg); state->message = res; }else{ state->message = "550 Failed to create directory."; } } }else{ state->message = "500 Good news, everyone! There's a report on TV with some very bad news!"; } write_state(state);}/** RETR command */void ftp_retr(Command *cmd, State *state){ if(fork()==0){ int connection; int fd; struct stat stat_buf; off_t offset = 0; int sent_total = 0; if(state->logged_in){ /* Passive mode */ if(state->mode == SERVER){ if(access(cmd->arg,R_OK)==0 && (fd = open(cmd->arg,O_RDONLY))){ fstat(fd,&stat_buf); state->message = "150 Opening BINARY mode data connection."; write_state(state); connection = accept_connection(state->sock_pasv); close(state->sock_pasv); if(sent_total = sendfile(connection, fd, &offset, stat_buf.st_size)){ if(sent_total != stat_buf.st_size){ perror("ftp_retr:sendfile"); exit(EXIT_SUCCESS); } state->message = "226 File send OK."; }else{ state->message = "550 Failed to read file."; } }else{ state->message = "550 Failed to get file"; } }else{ state->message = "550 Please use PASV instead of PORT."; } }else{ state->message = "530 Please login with USER and PASS."; } close(fd); close(connection); write_state(state); exit(EXIT_SUCCESS); } state->mode = NORMAL; close(state->sock_pasv);}/** Handle STOR command. TODO: check permissions. */void ftp_stor(Command *cmd, State *state){ if(fork()==0){ int connection, fd; off_t offset = 0; int pipefd[2]; int res = 1; const int buff_size = 8192; FILE *fp = fopen(cmd->arg,"w"); if(fp==NULL){ /* TODO: write status message here! */ perror("ftp_stor:fopen"); }else if(state->logged_in){ if(!(state->mode==SERVER)){ state->message = "550 Please use PASV instead of PORT."; } /* Passive mode */ else{ fd = fileno(fp); connection = accept_connection(state->sock_pasv); close(state->sock_pasv); if(pipe(pipefd)==-1)perror("ftp_stor: pipe"); state->message = "125 Data connection already open; transfer starting."; write_state(state); /* Using splice function for file receiving. * The splice() system call first appeared in Linux 2.6.17. */ while ((res = splice(connection, 0, pipefd[1], NULL, buff_size, SPLICE_F_MORE | SPLICE_F_MOVE))>0){ splice(pipefd[0], NULL, fd, 0, buff_size, SPLICE_F_MORE | SPLICE_F_MOVE); } /* TODO: signal with ABOR command to exit */ /* Internal error */ if(res==-1){ perror("ftp_stor: splice"); exit(EXIT_SUCCESS); }else{ state->message = "226 File send OK."; } close(connection); close(fd); } }else{ state->message = "530 Please login with USER and PASS."; } close(connection); write_state(state); exit(EXIT_SUCCESS); } state->mode = NORMAL; close(state->sock_pasv);}/** ABOR command */void ftp_abor(State *state){ if(state->logged_in){ state->message = "226 Closing data connection."; state->message = "225 Data connection open; no transfer in progress."; }else{ state->message = "530 Please login with USER and PASS."; } write_state(state);}/** * Handle TYPE command. * BINARY only at the moment. */void ftp_type(Command *cmd,State *state){ if(state->logged_in){ if(cmd->arg[0]=='I'){ state->message = "200 Switching to Binary mode."; }else if(cmd->arg[0]=='A'){ /* Type A must be always accepted according to RFC */ state->message = "200 Switching to ASCII mode."; }else{ state->message = "504 Command not implemented for that parameter."; } }else{ state->message = "530 Please login with USER and PASS."; } write_state(state);}/** Handle DELE command */void ftp_dele(Command *cmd,State *state){ if(state->logged_in){ if(unlink(cmd->arg)==-1){ state->message = "550 File unavailable."; }else{ state->message = "250 Requested file action okay, completed."; } }else{ state->message = "530 Please login with USER and PASS."; } write_state(state);}/** Handle RMD */void ftp_rmd(Command *cmd, State *state){ if(!state->logged_in){ state->message = "530 Please login first."; }else{ if(rmdir(cmd->arg)==0){ state->message = "250 Requested file action okay, completed."; }else{ state->message = "550 Cannot delete directory."; } } write_state(state);}/** Handle SIZE (RFC 3659) */void ftp_size(Command *cmd, State *state){ if(state->logged_in){ struct stat statbuf; char filesize[128]; memset(filesize,0,128); /* Success */ if(stat(cmd->arg,&statbuf)==0){ sprintf(filesize, "213 %d", statbuf.st_size); state->message = filesize; }else{ state->message = "550 Could not get file size."; } }else{ state->message = "530 Please login with USER and PASS."; } write_state(state);}