如何在linux下实现简单的FTP,这是我在这个暑假完成的最主要的学习任务。实现简单的服务器与客户端间的上传与下载功能,我们需要知道什么是c/s架构以及套接字。关于套接字socket,这篇博客里讲的很详细,链接奉上
http://blog.csdn.net/sim_szm/article/details/9569607#comments
那么什么又是c/s架构呢?Client和Server常常分别处在相距很远的两台计算机上,Client程序的任务是将用户的要求提交给Server程序,再将Server程序返回的结果以特定的形式显示给用户;Server程序的任务是接收客户程序提出的服务请求,进行相应的处理,再将结果返回给客户程序。
上图很清楚的画出了一个面向连接的c/s模型,中间需要用到针对socket的一些操作函数,还有read等文件函数。
接下来,就是我画的服务器端与客户端的流程图了。(画的比较粗糙,大家凑合着看)
server端首先用socket函数创建了一个套接字,接下来用setsocket函数设置了套接字可以重新绑定端口,当把setsocket中的第三个参数设为SO_REUSEADDR时,如果该socket绑定了一个端口,那么该socket正常关闭或异常退出后的一段时间内,其他程序依然可以绑定该端口。接着初始化服务器端的地址结构,将struct sockaddr_in中的sin_addr设置为INADDR_ANY,可以保证其绑定到任意网络接口(网卡)。然后用bind函数将一个套接字和某个端口绑定在一起。用listen函数把套接字转化为被动监听,等待来自客户端的连接请求。
代码如下:
//创建套接字 sock_fd = socket(AF_INET, SOCK_STREAM, 0); if (sock_fd < 0) { my_err("socket", __LINE__); } //设置套接字可以重新绑定端口 optval = 1; if ((setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(int))) < 0) { my_err("setsockopt" , __LINE__); } //初始化s端地址结构 memset(&serv_addr, 0, sizeof(struct sockaddr_in) ); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(SERV_PORT); serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);//绑定到任意网络接口 //绑定 len = sizeof(struct sockaddr); if(bind(sock_fd, (struct sockaddr *)&serv_addr, len) < 0) { my_err("bind", __LINE__); } //监听 if(listen(sock_fd, LISTENQ) < 0) { my_err("listen", __LINE__); }
这时候,服务器端就已经做好准备等待客户端的连接了。接下来,就来看看客户端。相对于服务器端,客户端就会简单很多。
client端需要用户从键盘输入所要连接的服务端的ip及端口号,接着用socket函数创建一个TCP套接字,再使用connect函数在将刚创建好的套接字上创建一个连接,即主动请求连接到刚刚输入ip的服务器。代码如下:
serv_addr.sin_family = AF_INET; printf("ip:"); //接收ip scanf("%s", addr); getchar(); if((inet_aton(addr, &serv_addr.sin_addr)) == 0) { my_err("inet_aton", __LINE__); } printf("sin_port:"); //接收端口号 scanf("%d", &serv_port); if(serv_port < 0 || serv_port > 65535) { printf("invalid serv_addr.sin_port\n"); } if((serv_addr.sin_port = htons(serv_port)) == 0) { my_err("htons", __LINE__); } //创建套接字 conn_fd = socket(AF_INET, SOCK_STREAM, 0); if (conn_fd < 0) { my_err("socket", __LINE__); } if (connect(conn_fd, (struct sockaddr*)&serv_addr, sizeof(struct sockaddr_in)) < 0) { my_err("connect", __LINE__); }
服务器端要接受这个客户端发来的连接,就要用到accept函数了,当然,如果有多个客户端同时连接上来的话,我们就fork一个子进程来处理。也就是说,主进程负责accept()连接,fork的子进程负责处理连接。
直到这时,客户端与服务端才真正的连接上了,可以通过套接字进行数据间的交流了。//接受客户端的连接 cli_len = sizeof(struct sockaddr_in); while(1) { conn_fd = accept(sock_fd, (struct sockaddr *)&cli_addr, &cli_len); f_lock(inet_ntoa(cli_addr.sin_addr), "connect us", "\0"); find_file("/home/songrunyu/ftptest"); if(conn_fd < 0) { my_err("accept", __LINE__); } printf("accept a new client,ip:%s\n", inet_ntoa(cli_addr.sin_addr)); if((pid = fork()) == 0) { deal_command(conn_fd); } }
1.ls(查询)
我实现的第一个功能是类似于ls -R的功能,当客户端选择了ls功能时,服务器端会将一个指定的目录下的所有目录名及文件名存到一个数组里发送给客户端。首先用opendir将指定目录打开,用readdir读出该目录下的文件,判断若为文件,则将文件名存入指定数组,若为目录,则递归调用,打开该目录,继续遍历。直到找出所有文件名为止。然后用send函数将该数组发送到服务器端。
这时客户端只需要用recv函数接收这个数组,然后输出到显示屏即可。void open_dir(char path[256], char filename[4096]) { DIR *dir; //目录文件描述符 struct dirent *pdr; //目录文件的结构体 struct stat buf; dir = opendir(path); if(dir == NULL) { my_err("opendir", __LINE__); } chdir(path); //切换到当前目录中去 while((pdr = readdir(dir)) != NULL) { if(pdr->d_name[0] == '.') //若为隐藏文件则跳过 { continue; } lstat(pdr->d_name, &buf) < 0; //获取下一级成员属性 if(S_IFDIR & buf.st_mode) //是目录则递归调用,继续打开 { strcat(filename, "\n"); strcat(filename, pdr->d_name); strcat(filename, ":\n"); open_dir(pdr->d_name, filename); } else //是文件则存入filename { strcat(filename, pdr->d_name); strcat(filename, "\n"); } } chdir(".."); //回到上一级目录 closedir(dir); } void choice_ls(int conn_fd) { DIR *dir; int len; char filename[256]; char workpath[256] = "/home/songrunyu/ftptest"; memset(filename, '\0', 256); open_dir(workpath, filename); len = send(conn_fd, (void *)&filename, sizeof(filename), 0); if(len == -1) { my_err("send", __LINE__); } printf("文件名已传送完毕!\n"); f_lock(inet_ntoa(cli_addr.sin_addr), "ls", "\0"); deal_command(conn_fd); }
void command_ls(int conn_fd) { int len; int ret; char name[256]; char choice; recv_dir(conn_fd); } void recv_dir(int conn_fd) { char filename[4096]; int len; int ret; memset((void *)&filename, '\0', sizeof(filename)); len = recv(conn_fd, (void *)&filename, sizeof(filename), 0 ); if(len == -1) { printf("接收文件错误\n"); my_err("recv", __LINE__); } printf("%s", filename); printf("\n接收完毕!\n\n\n"); printf("按任意键返回主菜单:\n"); getchar(); getchar(); system("reset"); choice_input(conn_fd); }
2.get(下载)
下载功能是从服务器端指定的目录中(即刚刚ls)选取一个文件下载到客户端,为了方便服务器端寻找文件,不用每次下载都遍历一遍所有的文件,我将该目录下所有的文件及其绝对路径存入一个结构体数组中,每次下载只需strcmp需要下载的文件名和结构体数组中的文件名即可。注意结构体数组要定义为全局变量。
//将文件名存入结构体数组 void find_file(char *dir) { DIR *dp; struct dirent *entry; struct stat statbuff; if(!(dp = opendir(dir))) { perror("scan_dir :can't open dir !\n"); return; } chdir(dir); while((entry = readdir(dp)) != NULL) { lstat(entry->d_name, &statbuff); if(S_IFDIR & statbuff.st_mode) { if(strcmp(".", entry->d_name) == 0 || strcmp("..", entry->d_name) == 0) continue; find_file(entry->d_name); } else { char path_buff[256]; strcpy(name[a].filename, entry->d_name); memset(path_buff, '\0', 256); getcwd(path_buff, 256); strcat(path_buff, "/"); strcat(path_buff, entry->d_name); strcpy(name[a].path, path_buff); count += statbuff.st_size; a++; } } chdir(".."); //回到上一级目录 closedir(dp); }
在客户端,当用户输入绝对路径时,客户端会解析出文件名,发送到服务器端的只有文件名(不包含路径),然后客户端会在当前工作目录下创建一个文件夹,再在该文件夹下创建一个同名文件, 当服务器端找到文件发送过来时,客户端会先用recv函数接收,然后再用write函数将recv接到的数据写入刚刚创建的同名文件。注意写入文件的时候写入数据的大小最好与文件大小本身大小相等,否则传过来的文件会多出许多“\0”。对应的服务器端发送文件的代码:void command_get(int conn_fd) { int ret; int i = 0; int j = 0; int recv_len = 1; int re_len; int write_len; char newfilename[256]; char filename[256]; char temp[256]; char path[256]; char getpath[256]; char *file; char buf[4096]; int fd; //文件描述符 printf("\n\n\t请输入要下载的文件名:\n"); getchar(); gets(filename); ret = send(conn_fd, filename, sizeof(filename), 0); if(ret == -1) { my_err("send", __LINE__); } if(filename[0] == '/') { memset((void *)&newfilename, '\0', sizeof(filename)); //从路径中解析出文件名 for(i = 0, j = 0; i < strlen(filename); i++) { if(filename[i] == '/') { j = 0; continue; } newfilename[j] = filename[i]; j++; } newfilename[j] = '\0'; } else { strcpy(newfilename, filename); } recv(conn_fd, &re_len, sizeof(re_len), 0); //接收文件大小 printf("该文件大小为:%d字节\n", re_len); getcwd(path, 256); //获取当前目录 if(path == NULL) { my_err("getcwd", __LINE__); } mkdir("ftp_get", S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH); //创建接收文件的目录ftp_get memset(getpath, '\0', 256); strcat(getpath, path); strcat(getpath, "/"); strcat(getpath, "ftp_get/"); strcat(getpath, newfilename); //文件的绝对路径 fd = open(getpath, O_CREAT | O_RDWR, 0666); //创建newfilename文件 if(fd == -1) { my_err("open", __LINE__); } while(re_len != 0) { if(re_len > sizeof(buf)) { memset((void *)&buf, '\0', 4096); recv_len = recv(conn_fd, buf, 4096, 0); //将文件写入缓存 if(recv_len == -1) { my_err("recv", __LINE__); } printf("已接收%d字节\n", recv_len); write_len = write(fd, buf, recv_len); if(write_len == -1) { my_err("write", __LINE__); } } else //最后一次接收 { memset((void *)&buf, '\0', 4096); recv_len = recv(conn_fd, buf, re_len, 0); //将文件写入缓存 if(recv_len == -1) { my_err("recv", __LINE__); } printf("已接收%d字节\n", recv_len); write_len = write(fd, buf, recv_len); if(write_len == -1) { my_err("write", __LINE__); } } re_len = re_len - recv_len; } printf("\n文件接收完毕!\n\n"); close(fd); printf("按任意键返回主菜单:\n"); getchar(); system("reset"); choice_input(conn_fd); }
void choice_get(int conn_fd) { int ret; int read_len = 1; int send_len; int re_len; int len; int fd; int k = 0; int block = 0; char *p, *q; char filename1[256]; char path[256]; char workpath[30] = "/home/songrunyu/ftptest"; char buff[4096]; struct stat buf; struct file name[100]; memset(filename1, '\0', sizeof(filename1)); memset(path, '\0', 256); ret = recv(conn_fd, filename1, sizeof(filename1), 0); //接收到文件名 if(ret == -1) { my_err("recv", __LINE__); } if(filename1[0] != '/') //若没有路径,就获取绝对路径,存入path { printf("%s\n", filename1); search_file(k, path, filename1); } else //有路径则存入path中 { strcpy(path, filename1); } printf("%s\n", path); if((lstat(path, &buf)) == -1) { my_err("lstat", __LINE__); } re_len = buf.st_size; //获取文件属性(大小) send(conn_fd, &re_len, sizeof(re_len), 0); fd = open(path, O_RDWR, 0666); if(fd == -1) { my_err("open", __LINE__); } while((block = read(fd, buff, sizeof(buff))) > 0) { if(send(conn_fd, buff, sizeof(buff), 0) < 0) { my_err("send",__LINE__); } memset(buff, '\0', 4096); } printf("发送完毕\n"); close(fd); f_lock(inet_ntoa(cli_addr.sin_addr), "get", filename1); deal_command(conn_fd); } void search_file(int k, char *path, char *filename1) { k = 0; printf("\nnext print\n"); while(k < 50) { printf("[ %d: %s . %s ]\n", k, name[k].filename, name[k].path); if(strcmp(filename1, name[k].filename) == 0) { printf("文件已找到\n"); strcpy(path, name[k].path); break; } k++; } }
3.put(上传)
上传与下载是完全相反的两个过程,当客户端输入要上传的文件,服务器端在当前目录下创建一个同名文件,用recv函数接收,再用write函数写入文件即可。
4.quit(退出)
当用户想要退出该程序时,只需要用close函数关闭连接的套接字描述符即可。
5.watch(查看目录文件的总大小)
这个功能其实不是单独实现的,在之前的find_file函数中,大家可以看到还有一个全局变量count,在find_file函数执行的时候,就已经将每个文件的大小存入count,当客户端选择watch功能的时候,服务器端只需要将count的值转换为以GB为单位的大小,再用send函数发送给客户端即可。
void choice_watch(int conn_fd) { int len; float size; char workpath[256] = "/home/songrunyu/ftptest"; size = (count*1.0)/(1024*1024*1024); printf("size:%.4f",size); len = send(conn_fd, &size, sizeof(float), 0); if(len == -1) { my_err("send",__LINE__); } printf("该目录下总文件大小为%.4fGB\n",size); f_lock(inet_ntoa(cli_addr.sin_addr), "choice_watch", "\0"); deal_command(conn_fd); }
6.sort_get(分类下载)
这个功能实现的是当客户端输入“.”+“后缀名”时,服务器端会将所有的该后缀名的文件下载到客户端。当“.+后缀名”发送过来时,服务器端会将类型名转化为倒序(eg:.mp3转化为3pm.),同时,也将结构体数组中的文件名转化为倒序的,再用strncmp比较后缀名与文件名,将后缀名相同的文件序号记录到一个新的数组中,最后将这些记录下来文件序号的文件一一发送到客户端即可。
void choice_sortget(int conn_fd) { int m,n; int k = 0, kk; int i = 0; int fd; int ret; int block; int re_len; int len1,len2; char filename[256]; char type[10]; char newtype[10]; char buff[4096]; struct stat buf; char path[10]; ret = recv(conn_fd, type, sizeof(type), 0); if(ret == -1) { my_err("recv",__LINE__); } len2 = strlen(type); memset(newtype, '\0', 256); for(m=len2-1,n=0; m >= 0; m--) { newtype[n] = type[m]; n++; } printf("转换后后缀名为:%s\n",newtype); while(i<50) { memset(filename, '\0', 256); len1 = strlen(name[i].filename); for(m=len1-1,n=0; m>=0; m--) { filename[n] = name[i].filename[m]; n++; } if(strncmp(filename, newtype, len2) == 0) { path[k] = i; i++; k++; } else { i++; } } printf("共找出文件%d个\n",k); ret = send(conn_fd, &k, sizeof(k), 0); if(ret == -1) { my_err("send",__LINE__); } kk = k; for(k=0; k<kk; k++) { ret = send(conn_fd, name[(path[k])].filename, sizeof(name[(path[k])].filename), 0); if(ret == -1) { my_err("send",__LINE__); } printf("%s %s\n",name[(path[k])].filename, name[(path[k])].path); if((lstat(name[(path[k])].path, &buf)) == -1) { my_err("lstat", __LINE__); } re_len = buf.st_size; //获取文件属性(大小) send(conn_fd, &re_len, sizeof(re_len), 0); printf("%d\n",re_len); fd = open(name[(path[k])].path, O_RDWR, 0666); if(fd == -1) { my_err("open", __LINE__); } while((block = read(fd, buff, sizeof(buff))) > 0) { if(send(conn_fd, buff, block, 0) < 0) { my_err("send",__LINE__); memset(buff, '\0', 4096); } } printf("发送完毕\n"); close(fd); } printf("所有文件已发送完毕\n"); f_lock(inet_ntoa(cli_addr.sin_addr), "sortget", type); deal_command(conn_fd); }
7.系统日志
系统日志分为system_log.txt和error_log.txt。system_log记录的是连接上来的客户端的日期、时间、ip、操作、文件名;error_log记录的连接上来的客户端的日期、时间、ip、出错函数以及该函数所在的行数。为了防止多个客户端同时对日志文件进行写操作,我们加入了文件锁,同一时间只允许一个进程对日志文件进行写操作。(获取时间用time函数)
以下是system_log的代码:
int lock_set(int fd, struct flock *lock) { if(fcntl(fd, F_SETLK, lock) == 0) { if(lock->l_type == F_RDLCK) { printf("set read lock, pid:%d\n",getpid()); } else if(lock->l_type == F_WRLCK) { printf("set write lock,pid:%d\n",getpid()); } else if(lock->l_type == F_UNLCK) { printf("release lock, pid:%d\n",getpid()); } } else { my_err("fcntl",__LINE__); } return 0; } int lock_test(int fd, struct flock *lock) { if(fcntl(fd, F_GETLK, lock) == 0) { if(lock->l_type == F_UNLCK) { printf("lock can be set in fd\n"); return 0; } else { if(lock->l_type == F_RDLCK) { printf("can't set lock, read lock has been set by:%d\n",lock->l_pid); } else if(lock->l_type == F_WRLCK) { printf("can't set lock, write lock has been set by:%d\n",lock->l_pid); } return -2; } } else { my_err("fcntl",__LINE__); } } int f_lock(char ip[20], char command[10], char filename[20]) { int fd; int ret; char buf[256]; char buf_time[32]; time_t *ptm; struct flock lock; if((fd = open("/home/songrunyu/my_ftp/log/system_log.txt", O_CREAT|O_RDWR|O_APPEND, 0666)) == -1) { my_err("open",__LINE__); } memset(&lock, 0, sizeof(struct flock)); lock.l_start = SEEK_SET; lock.l_whence = 0; lock.l_len = 0; lock.l_type = F_WRLCK; if(lock_test(fd, &lock) == 0) { lock.l_type = F_WRLCK; lock_set(fd, &lock); } ptm = (time_t *)malloc(sizeof(time_t)); time(ptm); memset(buf, 0, 256); strcpy(buf_time, ctime(ptm)); buf_time [strlen (buf_time) - 1] = '\0'; sprintf(buf, "%s %-20s %-20s %-20s\n",buf_time, ip,command,filename); write(fd, buf, strlen(buf)); lock.l_type = F_UNLCK; lock_set(fd, &lock); close(fd); return 0; }
以上就是一个简单的ftp的实现,用到了套接字,进程,文件、目录操作,文件锁,以及c语言的基本操作等,若大家发现有问题或者有需要改进的地方,欢迎提出。