项目需求
- 客户端能够通过调用“get”指令,来获取服务器的文件
- 客户端能通过“server_ls”指令,来获取服务器路径下的文件列表
- 客户端能通过“server_cd”指令,进入服务器路径下的某文件夹
- 客户端可以通过“upload”指令,上传自己的本地文件到服务器
- 客户端可以通过“client_ls”指令,来查看本地路径下的文件列表
- 客户端可以通过“client_cd”指令,进入本地路径下的某文件夹
预备知识
- strtok函数,用于字符串的分割(详见:C语言 字符串分割_c语言分割字符串_Genven_Liang的博客-CSDN博客)
- sprintf函数,用于字符串的拼接(详见:sprintf()函数简要介绍_sprintf函数_做最完美的自己的博客-CSDN博客)
char s1={'A','B','C'};
sprintf(str,"aaa %.*s",2,s1); //取字符串s1的前2个字符并和“aaa ”拼接
执行完后str字符串为“aaa AB”
- chdir函数。system("cd XXX")并不能成功修改当前程序的路径,因为system本质上是fork了一个子进程,在子进程中执行指令,所以cd是在子进程中执行的,和当前程序无关,想要成功修改当前的路径,所以需要使用chdir(XXX),这样就可以修改当前的目录了(详见:在程序里面执行system(“cd /某个目录“),为什么路径切换不成功?_一口Linux的博客-CSDN博客)
- recv函数,和send函数一组用于网络套接字的收发,和read/write不同,read/write返回的是已读/写的字节数;而send/recv函数返回的是以接受/发送的字节数,所以当网络连接关闭时,recv会返回0,所以recv函数可以用于检测网络连接是否关闭(但是客户端似乎还是要使用进程间通讯来退出)
- find指令,可以用来判断一个文件是否存在,显然也可以直接用system调用(详见:(超详解)Linux系统find命令用法_linux命令find用法详解_是大姚呀的博客-CSDN博客)
- 使用lseek函数计算文件大小
size = lseek(fd_file, 0L, SEEK_END);
实现思路
“get”指令实现思路:
对于客户端:
检测用户是否输入了“get”,若输入之后,就让服务器发送文件过来,并自己创建一个同名文件写入服务器发来的数据
对于服务端:
检测客户端是否发来了“get”,如果是,就检测服务器要哪个文件,并将文件的内容全部读取然后发给客户端
“server_ls”指令实现思路:
对于客户端:
啥也不用干,输入指令后等着接受
对于服务端:
检测客户端是否发来了“server_ls”,如果是,就popen执行“ls -l”并读取运行结果,将结果发给客户端
“server_cd”指令实现思路:
对于客户端:
啥也不用干,输入指令后等着接受
对于服务端:
测客户端是否发来了“server_cd”,如果是,就检测后面cd的地址,然后调用chdir函数执行这个地址,然后popen执行“pwd”并读取运行结果,将结果发给客户端
“upload”指令实现思路:
对于客户端:
检测用户是否输入了“upload”,若输入之后,就检测用户要上传哪个文件,将文件内容全部复制并发送给服务器
对于服务端:
检测客户端是否发来了“upload”,如果是,就检测用户上传的文件名并接受所有文件数据,然后创建一个同名的文件将数据全部写进去
“client_ls”指令实现思路:
对于客户端:
检测用户是否输入了“clien_ls”,若输入之后,就调用system函数执行“ls -l”
对于服务端:
啥也不用干
“client_cd”指令实现思路:
对于客户端:
检测用户是否输入了“clien_cd”,若输入之后,就检测cd之后的地址,然后调用chdir函数执行这个地址,然后调用system函数执行“pwd”
对于服务端:
啥也不用干
具体代码
注意,为了接收到带空格的字符串,我使用了fgets函数,但是fgets函数在长度足够的情况下,会将换行符“\n”也读进字符串,所以strlen也会多计算一位,所以服务器使用fgets得到字符串并发送给客户端,客户端接受之后打印就不需要再换行了;反之亦然,服务器的接收端再接受之后也不需要再加换行符了!
切记切记切记!!以下的写法是完全错误的!!!
char *file_name; char *readbuf = "tyytt";file_name = readbuf; //错错错错错错错错
原因: 字符串常量本质是一个指针,而变量名相当于首地址,这句话相当于地址的赋值!!所以只要写了这句代码,不管readbuf的值变成了什么样,file_name就会跟着变成什么样,所以字符串的赋值一定要用strcpy!!血与泪的教训,自查了好久...
而使用strcpy则要注意,第一个参数不能是字符串常量!而需要是字符串变量!!
- 同时,在代码逻辑中,服务端对于flag_upload的置位和对其的判断都在子进程中,所以可以直接用if判断,但是对于客户端,flag_get的置位在负责写的父进程,对flag_get的判断却在负责读的子进程,正如一直强调的,fork之后的存储空间是独立的,所以父进程将flag置位子进程并不知道,所以不能简单的使用flag_get,需要使用进程间的通信来解决!
- 同样,file_name在服务端可以直接修改,但是客户端也需要使用进程间通信!
在子进程两个while中使用进程间通信就会涉及到上节出现的问题,即负责发数据的父进程写入fifo后,负责接数据的子进程还在阻塞等待服务器的消息,程序无法运行到检测fifo的代码处,解决方法就是让服务器立刻回复一个数据,对于file_name的传输,服务器正好要回复file_name;对于是否要跳转到get_handler,服务器正好要发送文件的内容。所以在这个代码逻辑中,不需要额外再设置服务器回复,如果在日后遇到了同样的情况,服务器又不需要回复什么,那么记得要随便写一条内容来回复。
server.c:
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <linux/in.h>
#include <string.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>int flag_upload = 0;
int flag_exist = 0;
char *file_name=NULL;int cmd_handler(int fd, char readbuf[10000])
{int fd_file; 判断为get命令后,用来存放即将发送的文件的文件标识符int size; //判断为get命令后,用来存放即将发送的文件的大小char *file_buf = NULL; //判断为get命令后,用来存放即将发送的文件的内容int ret;int i = 0;char result[4096] = {0}; //用来存放popen执行指令的结果FILE *fp; //popen返回的文件流标识符char str[10000]; //将读到的数据备份在这里char* stat[5] ={NULL,NULL,NULL,NULL,NULL}; //用来存放拆分后的字符串char cmd[128]; //用来存放加工后的字符串,比如去除“\n”,比如再加上“./”strcpy(str,readbuf); //由于字符串的首地址是字符串的名字,所以此时相当于传入的地址,所有对字符串的操作都会影响它,所以需要进行备份,先备份再对备份的数据进行数据处理就不会影响原数据了char* ptr = strtok(str, " "); //通过空格符号分隔字符串for(; ptr != NULL; ){stat[i] = ptr;//printf("%s\n", stat[i]);i++;ptr = strtok(NULL, " ");}if(strcmp(stat[0],"get")==0){ //如果是get命令sprintf(cmd, "find . -name %.*s",(int)(strlen(stat[1])-1),stat[1]); //remove "\n"fp = popen(cmd,"r"); //运行指令查看文件是否存在fread(&result, sizeof(char), sizeof(result), fp);pclose(fp);if(strlen(result)==0){ret = write(fd,"file not exist\n",20); //不存在就发送给客户端来通知if(ret == -1){perror("write");return 1;}return 1;}else{sprintf(cmd, "./%.*s",(int)(strlen(stat[1])-1),stat[1]); //remove "\n"ret = write(fd,cmd,strlen(cmd)); //如果存在,就先将文件名传给客户端if(ret == -1){perror("write");return 1;}}memset(&result,0,sizeof(result));//sleep(2);sprintf(cmd, "./%.*s",(int)(strlen(stat[1])-1),stat[1]); //remove "\n"fd_file = open(cmd,O_RDWR); //然后打开这个文件,将内容全部复制size = lseek(fd_file, 0L, SEEK_END);printf("file size = %d\n",size);file_buf = (char *)malloc(sizeof(char)*size + 1);lseek(fd_file, 0, SEEK_SET);ret = read(fd_file,file_buf,size);if(ret == -1){perror("read2");return 1;}close(fd_file);ret = write(fd,file_buf,size); //将文件的内容发送给客户端if(ret == -1){perror("write");return 1;}}else if(strcmp(stat[0],"server_cd")==0){ //如果是server_cd命令sprintf(cmd, "%.*s",(int)(strlen(stat[1])-1),stat[1]); //remove "\n"chdir(cmd); //将目录cd到客户端要求的位置fp = popen("pwd","r"); //然后执行pwd,将结果发回给客户端fread(&result, sizeof(char), 1024, fp);pclose(fp);ret = write(fd,&result,strlen(result));if(ret == -1){perror("write");return 1;}memset(&result,0,sizeof(result));}else if(strcmp(stat[0],"upload")==0){ //如果是upload命令flag_upload = 1; //表示准备uploadsprintf(cmd, "./%.*s",(int)(strlen(stat[1])-1),stat[1]); //remove "\n"file_name = cmd; //接收客户端发来的,即将发送过来的文件名}else if(strcmp(stat[0],"exist")==0){flag_exist = 1; //和准备upload的标识符配合使用,只有客户端判断用户输入的upload文件存在时,才会发送exist,此时服务端将文件存在的标识符也置1}else if(strcmp(stat[0],"server_ls\n")==0){ //如果是server_ls命令fp = popen("ls -l","r"); //执行ls-l命令,然后把结果发回客户端fread(&result, sizeof(char), 1024, fp);pclose(fp);ret = write(fd,&result,strlen(result));if(ret == -1){perror("write");return 1;}memset(&result,0,sizeof(result));}else if(strcmp(stat[0],"quit\n")==0){ //如果客户端打出了quitret = write(fd,"Bye\n",4); //立刻回发一个Bye,目的是让客户端取消接收阻塞然后成功从FIFO读取到退出信息if(ret == -1){perror("write");return 1;}}return 0;
}void upload_handler(char readbuf[10000]) //upload指令如果被最终判断为可以执行,则会调用这个函数来通过客户端发来的文件名和文件内容来创建文件
{int fd_file;int n_write;fd_file = open(file_name,O_RDWR|O_CREAT|O_TRUNC,S_IRWXU);n_write = write(fd_file, (char *)readbuf, strlen((char *)readbuf));printf("create new file %s, %d bytes have been written\n",file_name, n_write);close(fd_file);}int main(int argc, char **argv) //main函数
{int conn_num = 0;int flag = 0;int sockfd;int conn_sockfd;int ret;int n_read;int n_write;int len = sizeof(struct sockaddr_in);char readbuf[10000];char msg[10000];pid_t fork_return;pid_t fork_return_1;struct sockaddr_in my_addr;struct sockaddr_in client_addr;memset(&my_addr,0,sizeof(struct sockaddr_in));memset(&client_addr,0,sizeof(struct sockaddr_in));if(argc != 3){printf("param error!\n");return 1;}//socketsockfd = socket(AF_INET,SOCK_STREAM,0);if(sockfd == -1){perror("socket");return 1;}else{printf("socket success, sockfd = %d\n",sockfd);}//bindmy_addr.sin_family = AF_INET;my_addr.sin_port = htons(atoi(argv[2]));//host to net (2 bytes)inet_aton(argv[1],&my_addr.sin_addr); //char* format -> net formatret = bind(sockfd, (struct sockaddr *)&my_addr, len);if(ret == -1){perror("bind");return 1;}else{printf("bind success\n");}//listenret = listen(sockfd,10);if(ret == -1){perror("listen");return 1;}else{printf("listening...\n");}while(1){//acceptconn_sockfd = accept(sockfd,(struct sockaddr *)&client_addr,&len);if(conn_sockfd == -1){perror("accept");return 1;}else{printf("accept success, client IP = %s\n",inet_ntoa(client_addr.sin_addr));}fork_return = fork();if(fork_return > 0){//father keeps waiting for new request//do nothing }else if(fork_return < 0){perror("fork");return 1;}else{//son deals with requestwhile(1){//readmemset(&readbuf,0,sizeof(readbuf));ret = recv(conn_sockfd, &readbuf, sizeof(readbuf), 0);if(ret == 0){ //如果recv函数返回0表示连接已经断开printf("client has quit\n");close(conn_sockfd);break;}else if(ret == -1){perror("recv");return 1;}if(flag_upload == 1 && flag_exist == 1){ //当准备upload的标识位和文件存在的标识位同时置1时,才会进入这段代码flag_exist = 0; //立刻将两个标识位重新置0flag_upload = 0;upload_handler(readbuf); //并执行upload所对应的函数}cmd_handler(conn_sockfd, readbuf); //对客户端发来的消息进行判断的总函数//printf("\nclient: %s",readbuf); //dont need to add"\n"}exit(2);}}return 0;
}
client.c:
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <linux/in.h>
#include <string.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>int flag_quit = 0;int cmd_handler(int fd,char msg[10000])
{int fd_file; //如果判断为upload,则用来存放即将发送的文件的标识符int size; //如果判断为upload,则用来存放即将发送的文件的大小int fd_fifo; //用来存放fifo的标识符char *fifo_msg = "quit";char *fifo_msg_get = "get";char *file_buf = NULL; //如果判断为upload,则用来存放即将发送的文件的内容char result[4096] = {0}; //用来存放popen函数调用指令后的结果FILE *fp; //用来存放popen返回的标识符int ret;int i = 0;char str[10000]; //用来存放客户端读取数据的备份,方便进行数据操作char* stat[5] ={NULL,NULL,NULL,NULL,NULL}; //用来存放分隔后的字符串char cmd[128]; //用来存放进行加工后的字符串strcpy(str,msg); //so split will not affect original data,原因详见server.cchar* ptr = strtok(str, " "); //用空格分隔字符串for(; ptr != NULL; ){stat[i] = ptr;//printf("%s\n", stat[i]);i++;ptr = strtok(NULL, " ");}if(strcmp(stat[0],"client_ls\n")==0){ //如果是client_ls指令system("ls -l"); //执行ls-l}else if(strcmp(stat[0],"client_cd")==0){ //如果是client_cd指令sprintf(cmd, "%.*s",(int)(strlen(stat[1])-1),stat[1]); //remove "\n"chdir(cmd); //cd到用户指定的目录system("pwd"); //然后执行pwd}else if(strcmp(stat[0],"get")==0){ //如果是get指令fd_fifo = open("./fifo",O_WRONLY); //阻塞的只写打开fifo,阻塞直到fifo被只读打开write(fd_fifo,fifo_msg_get,strlen(fifo_msg_get)); //向fifo中写信息,告诉负责读的子进程:用户调用了get指令close(fd_fifo);}else if(strcmp(stat[0],"quit\n")==0){ //如果是quit指令//printf("quit detected!\n");fd_fifo = open("./fifo",O_WRONLY); //阻塞的只写打开fifo,阻塞直到fifo被只读打开write(fd_fifo,fifo_msg,strlen(fifo_msg)); //向fifo中写信息,告诉负责读的子进程:用户调用了quit指令close(fd_fifo);close(fd); //关闭套接字wait(NULL); //等待子进程退出flag_quit = 1; //将退出标识符置1}else if(strcmp(stat[0],"upload")==0){ //如果是upload指令sprintf(cmd, "find . -name %.*s",(int)(strlen(stat[1])-1),stat[1]); //remove "\n"fp = popen(cmd,"r"); //通过调用find查看要上传的文件是否存在fread(&result, sizeof(char), sizeof(result), fp);pclose(fp);if(strlen(result)==0){printf("file not exit!\n");return 1;}else{ret = write(fd,"exist",20); //只有文件存在,才向服务器发送existif(ret == -1){perror("write");return 1;}}memset(&result,0,sizeof(result));sprintf(cmd, "./%.*s",(int)(strlen(stat[1])-1),stat[1]); //remove "\n"fd_file = open(cmd,O_RDWR); //打开要发送的文件,将内容全部复制,发送给服务器size = lseek(fd_file, 0L, SEEK_END);printf("file size = %d\n",size);file_buf = (char *)malloc(sizeof(char)*size + 1);lseek(fd_file, 0, SEEK_SET);ret = read(fd_file,file_buf,size);if(ret == -1){perror("read2");return 1;}close(fd_file);sleep(2); //ensure the "flag_upload" is set to 1 in serverret = write(fd,file_buf,size);if(ret == -1){perror("write");return 1;}}return 0;
}void get_handler(char *file_name, char readbuf[10000]) //当确定要执行get命令时,会调用这个函数,通过服务器发来的文件名和文件内容创建文件
{int fd_file;int n_write;fd_file = open(file_name,O_RDWR|O_CREAT|O_TRUNC,S_IRWXU);n_write = write(fd_file, (char *)readbuf, strlen((char *)readbuf));printf("\ncreate new file %s, %d bytes have been written\n",file_name, n_write);close(fd_file);}int main(int argc, char **argv) //main函数
{int cnt = 0;char file_name[20]={0};int no_print = 0;int sockfd;int ret;int n_read;int n_write;char readbuf[10000];char msg[10000];int fd; //fifochar fifo_readbuf[20] = {0};int fd_get;pid_t fork_return;if(argc != 3){printf("param error!\n");return 1;}struct sockaddr_in server_addr;memset(&server_addr,0,sizeof(struct sockaddr_in));//socketsockfd = socket(AF_INET,SOCK_STREAM,0);if(sockfd == -1){perror("socket");return 1;}else{printf("socket success, sockfd = %d\n",sockfd);}//connectserver_addr.sin_family = AF_INET;server_addr.sin_port = htons(atoi(argv[2]));//host to net (2 bytes)inet_aton(argv[1],&server_addr.sin_addr); ret = connect(sockfd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr_in));if(ret == -1){perror("connect");return 1;}else{printf("connect success!\n");}//fifoif(mkfifo("./fifo",S_IRWXU) == -1 && errno != EEXIST){perror("fifo");}//forkfork_return = fork();if(fork_return > 0){//father keeps writing msgwhile(1){//writememset(&msg,0,sizeof(msg));fgets(msg,sizeof(msg),stdin);n_write = write(sockfd,&msg,strlen(msg));cmd_handler(sockfd,msg); //负责处理并判断用户输入的总函数if(flag_quit == 1){ //如果退出标识符被置位flag_quit = 0;break; //则父进程退出循环,然后退出}if(n_write == -1){perror("write");return 1;}else{//printf("%d bytes msg sent\n",n_write);}}}else if(fork_return < 0){perror("fork");return 1;}else{//son keeps reading while(1){fd = open("./fifo",O_RDONLY|O_NONBLOCK); //非阻塞的只读打开FIFOlseek(fd, 0, SEEK_SET); //光标移到最前read(fd,&fifo_readbuf,20); //从FIFO读取数据if(strcmp(fifo_readbuf,"quit")==0){ //如果FIFO中是quitexit(1); //则子进程立刻退出}else if(cnt == 1){ //如果判断为get指令,服务器将发送两次消息,第一次为文件名,第二次为文件内容,使用cnt和fifo消息的读取来配合,第一次将读来的值赋值给文件名,第二次将读来的值作为文件内容传入get_handler用来创建文件get_handler(file_name, readbuf);cnt = 0;no_print = 0; //将静止打印标识符重新归0}else if(strcmp(fifo_readbuf,"get")==0){ //如上一个else if的注释所言,能进入这个循环,说明是get指令后服务器发送的第一次消息strcpy(file_name,readbuf); //重要!字符串的赋值要用strcpy!!if(strcmp(file_name,"file not exist\n")==0){ //如果服务器第一次发来的消息是文件不存在,则啥都不用干,就当这次get没发生//do nothing}else{ //此时,将cnt++,使得服务器下一次发送的消息会被准确的判断为文件的内容cnt++;no_print = 1; //并且使得静止打印的标识符为1,防止在界面中打印文件内容}}memset(&fifo_readbuf,0,sizeof(fifo_readbuf)); //重要,不要忘记//readmemset(&readbuf,0,sizeof(readbuf));n_read = read(sockfd,&readbuf,sizeof(readbuf));if(ret == -1){perror("read1");return 1;}else if(ret != -1 && no_print == 0){ //只有静止打印标识符为0时,才打印服务器发来的消息,为了防止当get指令生效时,将服务器发来的大量文件内容打在屏幕上影响观感printf("\nserver: %s",readbuf); //dont need to add"\n"}}}return 0;
}
对于客户端中使用FIFO配合服务器的回复实现get指令的详细说明
实现效果
编译并运行服务端和客户端,建立连接:
“server_ls”指令演示
观察server的目录来验证:
“server_cd”指令演示
“client_ls”指令演示
观察client的目录来验证:
“client_cd”指令演示
“upload”指令演示
客户端:
服务端:
再看服务端的目录,可见文件已经被成功上传:
也可以用“server_ls” 来验证:
“get”指令演示
客户端:
服务端:
再看客户端的目录,可见文件已经被成功获取:
也可以用“client_ls” 来验证:
“quit”指令演示:
客户端:
服务端: