linux下模拟FTP服务器(笔记)
要在linux下做一个模仿ftp的小型服务器,后来在百度文库中找到一份算是比较完整的实现,就在原代码一些重要部分上备注自己的理解(可能有误,千万不要轻易相信)。
客户端:
客户端要从服务器端中读取数据,然后将read到的数据存在rcv_buf数组中,再使用atoi函数将rcv_buf中的数字字符提取出来(atoi提取数字时有着自己的规则),这样客户端解可以根据reply_code这个返回值来判断服务器要告诉自己什么。
1 2 //从套接字描述sock_fd中获取服务器的回复// 3 int resp_from_server(int sock_fd) 4 { 5 static int reply_code=0,count=0; 6 char rcv_buf[512]; 7 count=read(sock_fd, rcv_buf, 510); 8 if(count>0) //如果从服务器端读取数据成功,那么就向服务器反馈reply_code=atoi(rcv_buf);然后服务器端用recv_client_info(client_sock);来接收反馈信息 9 { 10 reply_code=atoi(rcv_buf); //atoi函数把字符串转化成整型数,该函数返回转换后的长整数,如果没有执行有效的转换,则返回零。 11 //该函数要求被转换的字符串是按十进制数理解的。 12 } 13 else 14 return 0; 15 while(1) 16 { 17 if(count<=0) 18 break; 19 rcv_buf[count]='\0'; 20 printf("%s",rcv_buf); 21 count=read(sock_fd, rcv_buf, 510); 22 } 23 return reply_code; 24 } 25 /*详解一下这个resp_from_server()函数,在此之前先了解一下read函数的大致用法及其定义; 26 参考:https://blog.csdn.net/hhhlizhao/article/details/71552588 27 28 函数定义:ssize_t read(int fd, void * buf, size_t count); 29 30 函数说明:read()会把参数fd所指的文件传送count 个字节到buf 指针所指的内存中。 31 32 返回值:返回值为实际读取到的字节数, 如果返回0, 表示已到达文件尾或是无可读取的数据。若参数count 为0, 则read()不会有作用并返回0。 33 34 注意:read时fd中的数据如果小于要读取的数据,就会引起阻塞。 35 36 好的,read函数就点到为止;意犹未尽者可以自行请教搜索引擎; 37 38 简单总结一下,read函数就是把用第一个参数描述的文件的第三个参数的数量的字节写入到第二个参数中 39 那么,怎么会突然就read起来了呢?我怎么知道第一个参数第对哪个文件的描述? 40 请君息怒,您冷静分析,是不是 大部分函数在调用resp_from_server()函数之前都会调用send_to_server()或者其他的可以从服务器端得到数据的函数 41 或者你可以看一看服务器端的send_to_client()函数,是不是有很多个send_to_client()函数的第二个参数都是从以下的字符数组中传入的 42 char serverInfo220[]="220 登录服务器"; 43 char serverInfo230[]="230 root用户"; 44 char serverInfo331[]="331 请输入用户密码"; 45 char serverInfo221[]="221 "; 46 char serverInfo150[]="150 "; 47 char serverInfo226[]="226 关闭数据连接"; 48 char serverInfo200[]="200 "; 49 char serverInfo215[]="215 "; 50 char serverInfo213[]="213 "; 51 char serverInfo211[]="211 "; 52 char serverInfo350[]="350 "; 53 char serverInfo530[]="530 登录失败"; 54 char serverInfo531[]="531 匿名用户"; 55 char serverInfo123[]="123 wu"; 56 char serverInfo[]="202"; 57 58 这时我们要回到客户端了,看看这个函数count=read(sock_fd, rcv_buf, 510);如果read成功的话,就会返回第二个参数所读入的字节数 59 否则返回-1(至于返回0的情况就自行百度了) 60 61 这时我们再看int resp_from_server(int sock_fd)函数如果调用成功后的返回值是什么,没错就是reply_code; 62 那么reply_code是从何而来的? 63 请看 reply_code=atoi(rcv_buf); 64 这里有牵引出一个atoi函数了,至于它的作用就不详细介绍了 65 大意就是将一个字符串中的数字字符串转化成int型的,当然它并不是无条件转换的,atoi遇到空格会直接无视掉它; 66 “它们都从字符串开始寻找数字或者正负号或者小数点,然后遇到非法字符终止,不会报异常:”“” 67 看到了吧,所谓的非法字符就是从左到右遇到的第一个非数字或者正负号或者小数点的字符; 68 比如,atoi处理 "226 关闭数据连接";这个字符串,返回值是226; 69 再看,atoi处理 " 331 请输入用户密码"; 的返回值是331;【注意前面的空格】 70 这样atoi函数处理完后使得 resp_from_server(int sock_fd) 函数返回的reply_code是一个从字符串提取出来的int型; 71 好像可以理解为啥 char serverInfo[]="202";存储的字符串是几个数字了; 72 73 这就是为啥你会看到一堆的 如什么 226,227,331,220这样的数了; 74 75 当然,我的理解是,为啥服务器端要以这样的形式来告诉客户端呢? 76 你去看看服务器端的 do_client_work()函数中,人家服务器是不是每发送一次send_to_client(client_sock, serverInfo221, strlen(serverInfo221)); 77 这样的信息前面都会调用相关的函数来处理客户端发起的请求了,之所以要这样反馈我个人觉得有以下两个原因 78 一是客户端和服务器端进行的不是一次性处理事件,需要进行交互 79 二是服务器端为了提醒客户端,喂,我出来完你的请求了,你那边还要干什么,自己看着办吧 80 不然没有通知的话,我们人类就不能明显地发现二者之间什么时候完成处理,我们下一步要客户端做什么这样的事情了 81 以上是我理解 82 对于atoi函数的理解:https://www.cnblogs.com/wzxwhd/p/6030083.html 83 对于read函数的理解:http://c.biancheng.net/cpp/html/3037.html 84 */
客户端要给服务器端发送数据,下面定义的函数并不是简单地将某个参数直接发送过去,而是将两个参数strcat起来后再发过去。
1 //客户端发送数据到服务器 2 /*send_to_server(const char *s1, const char *s2, int sock_fd)函数的具体作用是 3 将参数的两个字符拼接起来放在“ char send_buf[256];”中,然后一并发送给服务器端 4 */ 5 int send_to_server(const char *s1, const char *s2, int sock_fd) 6 { 7 char send_buf[256]; 8 int send_err, len; 9 if(s1) 10 { 11 strcpy(send_buf,s1); 12 if(s2) 13 { //strcat函数用来将两个char类型链接,结果存放在第一个参数中 14 strcat(send_buf, s2); 15 strcat(send_buf,"\r\n"); 16 len = strlen(send_buf); 17 send_err = send(sock_fd, send_buf, len, 0); 18 } 19 else 20 { 21 strcat(send_buf,"\r\n"); 22 len = strlen(send_buf); 23 send_err = send(sock_fd, send_buf, len, 0); 24 } 25 } 26 if(send_err < 0) 27 printf("send() error!\n"); 28 return send_err; 29 }
服务器:
1 //服务器端给客户端发送信息 2 /*send函数的参数: 3 第一个参数:指定发送端套接字描述符; 4 第二个参数:指明一个存放应用程序要发送数据的缓冲区; 5 第三个参数:指明实际要发送的数据的字节数; 6 第四个参数:一般为0; 7 */ 8 9 void send_to_client(int client_sock,char* info,int length) 10 { 11 int len; 12 if((len = send(client_sock, info, length,0))<0) 13 { 14 perror("信息发送失败"); 15 return; 16 } 17 }
1 /*服务器接收客户端的信息 2 函数中使用了一个recv函数来接收客户端发来的数据,该函数的第一个参数client_sock指定接收端套接字描述符; 3 第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据,第三个参数指明第二个餐数中缓冲区的长度; 4 第四个参数一般为0; 因此,执行这个函数后,client_Control_Info里的内容就是从客户端接收到的数据; 5 参考:https://blog.csdn.net/ly0303521/article/details/52290217 6 */ 7 //这个client_Control_Info数组是一个全局变量,在下面函数中用来存在接收客户端发送给服务器端的控制连接的相关数据 8 int recv_from_client(int client_sock) 9 { 10 int num; 11 if((num=recv(client_sock,client_Control_Info,MAX_INFO,0))<0) 12 { 13 perror("信息接收失败"); 14 return; 15 } 16 client_Control_Info[num]='\0'; 17 printf("Client %d Message:%s\n",pthread_self(),client_Control_Info); 18 //获得线程自身的ID。pthread_t的类型为unsigned long int; 19 //参考:https://blog.csdn.net/liangzhao_jay/article/details/79746794 20 if(strncmp("USER", client_Control_Info, 4) == 0||strncmp("user", client_Control_Info, 4) == 0) 21 return 2; 22 return 1; 23 } 24 /*strncmp函数的用法: 25 原型:int strncmp (const char *s1, const char *s2, size_t size) 26 strncmp函数指定比较size个字符。也就是说,如果字符串s1与s2的前size个字符相同, 27 函数返回值为0。 28 为什么呢? 29 你可以去看看服务器端的 “int send_to_server(const char *s1, const char *s2, int sock_fd)” 30 函数的实现,它将两个参数s1和s2拼接起来放在send_buf[256]数组中,然后使用 send(sock_fd, send_buf, len, 0); 31 将数组发送给客户端,所以此时只需比较收到的前四为是否我USER即可; 32 33 当然,服务器使用client_Control_Info数组来接收上述从客户端发送过来的数据 34 */
服务器是如何识别客户端发送给自己的数据所要实现的是什么功能?
服务器使用了一个strncmp函数来处理从客户端得到的字符命令,先处理前n位,得出是什么类型的命令,后续再处理命令后面的其他信息。
对accept函数的一丢丢理解
1 int main(int argc,char *argv[]) 2 { 3 pthread_t thread; 4 struct ARG arg; 5 struct sockaddr_in server; 6 7 if((ftp_server_sock=socket(AF_INET,SOCK_STREAM,0))==-1) 8 { 9 perror("套接字创建失败"); 10 exit(1); 11 } 12 13 int opt=SO_REUSEADDR; 14 setsockopt(ftp_server_sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt)); 15 /* 获取或者设置与某个套接字关联的选 项。选项可能存在于多层协议中,它们总会出现在最上面的套接字层。 16 当操作套接字选项时,选项位于的层和选项的名称必须给出。为了操作套接字层的选项, 17 应该将层的值指定为SOL_SOCKET。为了操作其它层的选项,控制选项的合适协议号必须给出。 18 */ 19 20 bzero(&server,sizeof(server)); 21 server.sin_family=AF_INET; 22 server.sin_port=htons(FTP_SERVER_PORT); 23 server.sin_addr.s_addr=htonl(INADDR_ANY); 24 //服务器端要用 bind() 函数将套接字与特定的IP地址和端口绑定起来,只有这样,流经该IP地址和端口的数据才能交给套接字处理; 25 //而客户端要用 connect() 函数建立连接。 26 27 if(bind(ftp_server_sock,(struct sockaddr *)&server,sizeof(struct sockaddr))==-1) 28 { 29 perror("套接字绑定错误"); 30 exit(1); 31 } 32 //bind起来之后,服务器端就可以进入listen()监听的状态; 33 if(listen(ftp_server_sock,LISTEN_QENU)==-1) //listen函数进入监听的状态,第二个参数是监听队列的长度 34 { 35 perror("监听错误"); 36 exit(1); 37 } 38 // 接收客户端请求 39 int ftp_client_sock; //即将在accept()函数中产生的新套接字的描述 40 struct sockaddr_in client; 41 int sin_size=sizeof(struct sockaddr_in); //accept函数中的第三个参数 42 while(1) //使用一个while循环主要是因为对面的等待处理是个对列来的 43 { //accept函数提取出所监听套接字的等待连接队列中第一个连接请求,创建一个新的套接字,并返回指向该套接字的文件描述符 44 //也就是这里的ftp_client_sock是accept后新创建的套接字的描述符 45 //新建立的套接字准备发送send和接收数据recv(),所以下面你会看到arg.client_sock=ftp_client_sock;赋值给client_sock的操作 46 //然后这个client_sock作为一个全局变量就开始在服务器和客户端的send和recv之间开始忙碌地奔波起来 47 //accept函数中的第一个参数是服务器一开始创建的套接字描述符 48 //accept函数中的第二个参数保存了客户端的IP地址和端口号 49 if((ftp_client_sock=accept(ftp_server_sock,(struct sockaddr *)&client,&sin_size))==-1) 50 { 51 perror("接收错误"); 52 exit(1); 53 } 54 55 arg.client_sock=ftp_client_sock; 56 memcpy((void*)&arg.client,&client,sizeof(client)); 57 58 if(pthread_create(&thread,NULL,Handle_Client_Request,(void*)&arg)) 59 { 60 perror("多线程创建错误"); 61 exit(1); 62 } 63 } 64 close(ftp_server_sock); 65 }
备注:以上都是我自己的理解,可能有些地方有误,如果你发现有错的地方,希望你能在评论区指出来,大家一起学习,感谢!
文档原文链接:https://wenku.baidu.com/view/3e9d8a98856a561253d36f2e.html?from=search