select&epoll讲解(例:实现FTP文件上传下载)

一.为什么会出现select

1在默认的阻塞模式下的套接字里,recv会阻塞在那里,直到套接字连接上有数据可读,把数据读到buffer里后recv函数才会返回,不然就会一直阻塞在那里。在单线程的程序里出现这种情况会导致主线程(单线程程序里只有一个默认的主线程)被阻塞,这样整个程序被锁死在这里,如果永 远没数据发送过来,那么程序就会被永远锁死。这个问题可以用多线程解决,但是在有多个套接字连接的情况下,这不是一个好的选择,扩展性很差

2)在非阻塞模式的套接字里,recv的调用不管套接字连接上有没有数据可以接收都会马上返回但在没有数据的情况下recv确实是马上返回了,但是也返回了一个错误:WSAEWOULDBLOCK,即请求的操作没有成功完成

  若重复调用recv并检查返回值,直到成功为止,但是这样做效率很成问题,开销太大

 select就是为了解决上述两个问题,能够实现在一个线程内完成并发操作,即I/O多路复用:多个描述符的I/O操作都能在一个线程内并发交替地完成,随着研究的进展,poll/epoll方法(select的改善)也相继出现,其功能和select类似,可以同时监视多个描述符的读写就绪状况,即在一个线程内同时处理多个socket的I/O请求

二.select原理和示例

  • select原理

        在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常事件,帮助调用者寻找当前就绪的设备

        原理图如下所示,select首先进行系统调用,当数据准备好时返回socket接口,然后使用recv接受socket发送过来的数据,并进行处理。   select&epoll讲解(例:实现FTP文件上传下载)_第1张图片

  •  select示例

        select伪代码如下:依次顺序遍历fd,等待数据准备好。select&epoll讲解(例:实现FTP文件上传下载)_第2张图片

  •  select缺点

        1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大;
        (2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大;
        (3select支持的文件描述符数量太小了,默认是1024;

三.epoll原理和示例

  • 为什么使用epoll?

        每次调用Select都需要将进程加入到所有监视Socket等待队列,每次唤醒都需要从每个队列中移除。这里涉及了两次遍历,而且每次都要将整个FDS列表传递给内核,有一定的开销。所以为了减少遍历次数,并保存就绪的socket(fd),研究出现了epoll方法,它是select和poll的增强版本。

  • epoll示例

        epoll伪代码如下,将所有socket加入到等待队列中,当有数据到达时,可以直接找到对应的socket,无需遍历;

select&epoll讲解(例:实现FTP文件上传下载)_第3张图片

  • select和epoll区别

        使用快递来进行类比,select:需要对快递编号依次遍历,看看有没有快递员到达,也就是当快递到达时没法直接知道快递编号,需要依次遍历进行获取;epoll:当有快递到达时,可以直接得知快递编号。select&epoll讲解(例:实现FTP文件上传下载)_第4张图片

三.分别使用select和epoll实现一个FTP文件上传下载

  • select     

        server.c

//server.c
#include  
#include  
#include  
#include  
#include  
#include  
#include  
#include 
#include 

int main() 
{ 
    int server_sockfd, client_sockfd; 
    int server_len, client_len; 
    struct sockaddr_in server_address; 
    struct sockaddr_in client_address; 
    int ret; 
    fd_set testfds,readfds; 

    //1.建立服务器端socket,用于描述IP地址和端口,通过socket向网络发请求或应答网络请求
    server_sockfd = socket(AF_INET, SOCK_STREAM, 0);//建立服务器端socket

    //2.绑定侦听的IP地址和端口
    server_address.sin_family = AF_INET;  
    server_address.sin_addr.s_addr = htonl(INADDR_ANY);  //INADDR_ANY=0,表示侦听全部IP地址
    server_address.sin_port = htons(8888);  //端口
    server_len = sizeof(server_address); 
    bind(server_sockfd, (struct sockaddr *)&server_address, server_len);  //将套接字套接字绑定到地址server上。

    //3.侦听
    listen(server_sockfd, 7); //监听队列最多容纳7个 

    FD_ZERO(&readfds); 
    FD_SET(server_sockfd, &readfds);//可读集合初始化为[server_sockfd,]

    while(1) 
    {
        char ch; 
        int nread; 
        printf("server waiting......\n"); 

        /*无限期阻塞,并测试文件描述符变动 */
        testfds = readfds; //将需要监视的描述符集copy到select查询队列中,select会对其修改,所以一定要分开使用变量 
        ret = select(FD_SETSIZE, &testfds, (fd_set *)0,(fd_set *)0, (struct timeval *) 0); //数据准备就绪,则文件描述符会发生变化
        /* select:寻找当前准备就绪的设备,当可读、可写、异常任一文件描述符准备就绪,则select返回,每次结束后,select重新赋值
         * 当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位(变成ready),使得进程可以获得这些文件描述符从而进行后续的读写操作
         * readfds:监视的socket,testfds:监视的socket中数据就绪的socket
         * select(int maxfdp, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);
         * maxfdp(FD_SETSIZE):被监听文件描述符总数;readfds、writefds、exceptset:分别指向可读、可写和异常事件
         * timeout: 等于NULL(0)表示无限等待
         */
        //testfds :初始化的可读集合里面哪一个准备就绪了,返回的testfds就等于几,FD_SETSIZE等于初始化可读集合总数+1
        //testfds初始是监听的集合,返回值是准备好的集合
        if(ret < 1)  //ret:0表示超时,-1表示失败
        { 
            perror("select error"); 
            exit(1); 
        } 

        /*扫描所有的文件描述符*/
        for(int fd = 0; fd < FD_SETSIZE; fd++) /*循环遍历*/
        {
            if(FD_ISSET(fd,&testfds)) //有数据准备就绪,fd为进来的请求,定位置位
            {
                // printf("fd:%d\n",fd);
                if(fd == server_sockfd)  /*判断是否为服务器套接字(3),是则表示为客户请求连接。*/
                { 
                    client_len = sizeof(client_address); 
                    client_sockfd = accept(server_sockfd,(struct sockaddr *)&client_address, &client_len);  //接收客户端socket
                    FD_SET(client_sockfd, &readfds);//将客户端socket加入到集合中(可读集合扩充了)
                    printf("adding client on fd %d......\n", client_sockfd); 
                } 
                else  /*客户端socket中有数据请求时*/
                { 
                    char cmd[256];
                    ret = recv(fd, cmd, sizeof(cmd), 0); //接受数据(命令)
                    // printf("cmd:%s",cmd);

                    if(ret < 1 || strncmp(cmd, "exit", 4) ==0)/*客户数据请求完毕,关闭套接字,从集合中清除相应描述符 */
                    {
                        close(fd); 
                        FD_CLR(fd, &readfds); //去掉关闭的fd
                        printf("client on fd %d closed......\n", fd); 
                    }
                    else /*对数据进行处理*/
                    {                        
                        /*处理客户数据请求*/
                        if(strncmp(cmd, "put", 3) ==0) //上传文件
                        {
                            char filename[256] = "";
                            char filepath[256] = "./data/";  //上传的文件存放在服务器的位置
                            for(int i=4;cmd[i]!='\0';i++)
                            {
                                filename[i-4]=cmd[i];
                            }
                            strcat(filepath,filename);

                            // printf("recv cmd on fd %d: %s\n", fd,cmd);
                            char a[2] = "ok";
                            ret = send(fd, a,sizeof(a), 0); //通知客户端收到,防止粘包

                            char buffer[1024];
                            FILE * fp = fopen(filepath,"w"); //保存客户端发送的文件内容
                            bzero(buffer,1024);
                            int length = 0;
                            if((length = recv(fd,buffer,1024,0))>0)       //接收并写到文件
                            {
                                if(length < 0)
                                {
                                    printf("Recieve Data Failed!\n");
                                    break;
                                }
                                int write_length = fwrite(buffer,sizeof(char),length,fp);
                                if (write_length0)
                                {
                                    if(send(fd,buffer,file_length,0)<0) //向客户端发送文件内容
                                    {
                                        printf("Send File:\t%s Failed\n", filepath);
                                        break;
                                    }
                                    bzero(buffer, 1024);
                                    printf("put all file(file_length = %d) to fd %d...\n",file_length,fd);
                                }                                                                
                                fclose(fp);
                            }
                            else
                            {
                                char a[2] = "on";
                                ret = send(fd, a,sizeof(a), 0); //通知客户端文件不存在
                                printf("get file is not valid\n");
                            }                
                        }                        
                    }       
                } 
            }
        }
    } 

    return 0;
}

         client.c

//client.c
#include  
#include  
#include  
#include  
#include  
#include  
#include 
#include 
#include 


int main() 
{ 
    int client_sockfd,len,ret;  
    struct sockaddr_in address;//服务器端网络地址结构体 
    char cmd[256]; 

    //1.建立客户端socket
    client_sockfd = socket(AF_INET, SOCK_STREAM, 0); 
    //2.连接服务端
    address.sin_family = AF_INET; 
    address.sin_addr.s_addr = inet_addr("127.0.0.1");
    address.sin_port = htons(8888); 
    len = sizeof(address); 
    ret = connect(client_sockfd, (struct sockaddr *)&address, len); 
    if(ret == -1) 
    { 
         perror("connect error"); 
         exit(1); 
    } 

    printf("------Welcome!!-----\n");
    printf("                    (helps:information you need)\n");
    
    while(1)
    {
        printf(">>>");
        bzero(cmd,256);
        if(fgets(cmd,256,stdin) == NULL)
        {
            printf("input Error!\n");
            return -1;
        }
 
        cmd[strlen(cmd)-1]='\0';    //fgets函数读取的最后一个字符为换行符,此处将其替换为'\0'
        
        if(strncmp(cmd, "put", 3) ==0 ) //上传文件(空格是为了标准化格式)
        {
            if(cmd[3]=='\0'||cmd[4]=='\0') //直接回车
            {
                printf("No filename follows after put cmd!\n");
                continue;
            }

            char filename[256] = "";
            char filepath[256] = "./data/";  //上传文件在本地的位置
            for(int i=4;cmd[i]!='\0';i++)
            {
                filename[i-4]=cmd[i];
            }
            strcat(filepath,filename);
            if((access(filepath,F_OK))!=-1) //文件存在
            {        
                ret = send(client_sockfd, cmd,sizeof(cmd), 0); //发送命令

                char a[2];
                ret = recv(client_sockfd, a, sizeof(a), 0); //防止粘包
                if(strncmp(a, "ok" , 2) == 0) //收到服务器的回应
                {
                    char buffer[1024];
                    FILE * fp = fopen(filepath,"r");
                    bzero(buffer, 1024);
                    int file_length = 0;
                    while( (file_length = fread(buffer,sizeof(char),1024,fp))>0)
                    {
                        //上传文件内容
                        if(send(client_sockfd,buffer,file_length,0)<0)
                        {
                            printf("Send File:\t%s Failed\n", filepath);
                            break;
                        }
                        bzero(buffer, 1024);
                        printf("put all file(file_length = %d)...\n",file_length);
                    }                                                                 //这段代码是循环读取文件的一段数据,在循环调用send,发送到客户端,这里强调一点的TCP每次接受最多是1024字节,多了就会分片,因此每次发送时尽量不要超过1024字节。
                    fclose(fp);
                }
            }
            else{
                printf("File is not valid\n");
                continue;
            }            
        }
        else if(strncmp(cmd, "get" , 3) == 0) //下载文件
        {
            if(cmd[3]=='\0'||cmd[4]=='\0') //直接回车的情况
            {
                printf("No filename follows after get cmd!\n");
                continue;
            }

            char filename[256] = "";
            char filepath[256] = "./data/";  //下载的文件在服务器的位置
            for(int i=4;cmd[i]!='\0';i++)
            {
                filename[i-4]=cmd[i];
            }
            strcat(filepath,filename);
 
            ret = send(client_sockfd,cmd,sizeof(cmd), 0); //发送命令

            char a[2];
            ret = recv(client_sockfd, a, sizeof(a), 0); //防止粘包
            if(strncmp(a, "ok" , 2) == 0) //收到服务器的回应,文件存在
            {
                char buffer[1024];
                FILE * fp = fopen(filepath,"w"); //保存服务器发送的文件内容
                bzero(buffer,1024);
                int length = 0;
                if((length = recv(client_sockfd,buffer,1024,0))>0)     //接收并写到文件
                {
                    if(length < 0)
                    {
                        printf("Recieve Data Failed!\n");
                        break;
                    }
                    int write_length = fwrite(buffer,sizeof(char),length,fp);
                    if (write_length

          操作说明:

#原始文件#
--server
  --/data
    --5.txt 6.txt  服务器中的文件
--client
  --/data
    --1.txt 2.txt  客户端中的文件

1.打开服务器
cd server
gcc server.c -o server
./server

2.打开客户端1
cd client
gcc client.c -o client
./client

#向服务器上传文件1.txt,成功后会出现/server/data/1.txt
put 1.txt

#从服务器中下载文件5.txt,成功后会出现/client/data/5.txt
get 5.txt

3.打开客户端2
cd client
gcc client.c -o client
./client

#向服务器上传文件2.txt,成功后会出现/server/data/2.txt
put 2.txt

#从服务器中下载文件6.txt,成功后会出现/client/data/6.txt
get 6.txt

  • epoll

        server.c

//server.c
#include  
#include  
#include  
#include  
#include  
#include  
#include  
#include 
#include 
#include 

int main() 
{ 
    int server_sockfd, client_sockfd; 
    int server_len, client_len; 
    struct sockaddr_in server_address; 
    struct sockaddr_in client_address; 
    int ret;  

    //1.建立服务器端socket,用于描述IP地址和端口,通过socket向网络发请求或应答网络请求
    server_sockfd = socket(AF_INET, SOCK_STREAM, 0);//建立服务器端socket

    //2.绑定侦听的IP地址和端口
    server_address.sin_family = AF_INET;  
    server_address.sin_addr.s_addr = htonl(INADDR_ANY);  //INADDR_ANY=0,表示侦听全部IP地址
    server_address.sin_port = htons(8787);  //端口
    server_len = sizeof(server_address); 
    bind(server_sockfd, (struct sockaddr *)&server_address, server_len);  //将套接字套接字绑定到地址server上。

    //3.侦听
    listen(server_sockfd, 7); //监听队列最多容纳7个 

    int epollfd = epoll_create(10); //创建文件描述符(10表示epollfd上能关注的最大fd数)

    struct epoll_event tmp,epevents[5];  //创建epoll对象
    tmp.events = EPOLLIN; //表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
    tmp.data.fd = server_sockfd;
    epoll_ctl(epollfd,EPOLL_CTL_ADD,server_sockfd,&tmp);//将需要监视的socket加入到epollfd中(关注了server_sockfd),等待队列
    /*(epoll_create()返回值,动作,需要监听的socket,需要监听的内容(可读))*/

    while(1) 
    {
        char ch; 
        int nread; 
        printf("server waiting......\n"); 

        //获取准备好的描述符事件   
        int num = epoll_wait(epollfd,epevents,5,-1); //等待epevents对象,可读时通知,读到后就改为写时通知,epollfd为需要监视的socket集合

        /*select扫描所有的fd,然后准备就绪的fd放在等待队列进行数据处理
         *epoll 不用,其等待队列已经存放于events中(即epevents[i].data.fd),只需要判断它的fd是不是可读
         *select每次都需要添加可读等待队列(fd),然后阻塞,遍历所有的fd-size。
         *但是epoll不用,其等待队列(fd)已经存放于events中,一直阻塞,只需要遍历epevents事件判断其fd是否可读
         */

        /*扫描所有的文件描述符*/
        for(int i = 0; i < num; i++) /*num为等待到的连接数(1)*/
        {
            if(!(epevents[i].events & EPOLLIN)) //如果不是读事件,直接跳过 [&:两个都运行,有一个为假就不成立]
            {
                continue;
            }
            if(epevents[i].data.fd == server_sockfd) /*判断是否为服务器套接字(3),是则表示为客户请求连接。*/
            {
                client_len = sizeof(client_address); 
                client_sockfd = accept(server_sockfd,(struct sockaddr *)&client_address, &client_len);  //接收客户端socket
                tmp.events = EPOLLIN;
                tmp.data.fd = client_sockfd;
                ret = epoll_ctl(epollfd,EPOLL_CTL_ADD,client_sockfd, &tmp); //将客户端socket加入到epollfd中
                printf("adding client on fd %d......\n", client_sockfd); 
            }
            else /*客户端socket中有数据请求时*/
            {
                char cmd[256];
                ret = recv(epevents[i].data.fd, cmd, sizeof(cmd), 0); //接受数据(命令)

                if(ret < 1 || strncmp(cmd, "exit", 4) ==0)/*客户数据请求完毕,关闭套接字,从集合中清除相应描述符 */
                {
                    close(epevents[i].data.fd);
                    epoll_ctl(epollfd,EPOLL_CTL_DEL,epevents[i].data.fd,NULL);//删除已经注册的fd
                    printf("client on fd %d closed......\n", epevents[i].data.fd); 
                }
                else /*对数据进行处理*/
                {                        
                    /*处理客户数据请求*/
                    if(strncmp(cmd, "put", 3) ==0) //上传文件
                    {
                        char filename[256] = "";
                        char filepath[256] = "./data/";  //上传的文件存放在服务器的位置
                        for(int i=4;cmd[i]!='\0';i++)
                        {
                            filename[i-4]=cmd[i];
                        }
                        strcat(filepath,filename);

                        // printf("recv cmd on fd %d: %s\n", fd,cmd);
                        char a[2] = "ok";
                        ret = send(epevents[i].data.fd, a,sizeof(a), 0); //通知客户端收到,防止粘包

                        char buffer[1024];
                        FILE * fp = fopen(filepath,"w"); //保存客户端发送的文件内容
                        bzero(buffer,1024);
                        int length = 0;
                        if((length = recv(epevents[i].data.fd,buffer,1024,0))>0)       //接收并写到文件
                        {
                            if(length < 0)
                            {
                                printf("Recieve Data Failed!\n");
                                break;
                            }
                            int write_length = fwrite(buffer,sizeof(char),length,fp);
                            if (write_length0)
                            {
                                if(send(epevents[i].data.fd,buffer,file_length,0)<0) //向客户端发送文件内容
                                {
                                    printf("Send File:\t%s Failed\n", filepath);
                                    break;
                                }
                                bzero(buffer, 1024);
                                printf("put all file(file_length = %d) to fd %d...\n",file_length,epevents[i].data.fd);
                            }                                                                
                            fclose(fp);
                        }
                        else
                        {
                            char a[2] = "on";
                            ret = send(epevents[i].data.fd, a,sizeof(a), 0); //通知客户端文件不存在
                            printf("get file is not valid\n");
                        }                
                    }                        
                }       
            }
        }
    } 
    
    close(epollfd);

    return 0;
}

        client.c

//client.c
#include  
#include  
#include  
#include  
#include  
#include  
#include 
#include 
#include 


int main() 
{ 
    int client_sockfd,len,ret;  
    struct sockaddr_in address;//服务器端网络地址结构体 
    char cmd[256]; 

    //1.建立客户端socket
    client_sockfd = socket(AF_INET, SOCK_STREAM, 0); 
    //2.连接服务端
    address.sin_family = AF_INET; 
    address.sin_addr.s_addr = inet_addr("127.0.0.1");
    address.sin_port = htons(8787); 
    len = sizeof(address); 
    ret = connect(client_sockfd, (struct sockaddr *)&address, len); 
    if(ret == -1) 
    { 
         perror("connect error"); 
         exit(1); 
    } 

    printf("------Welcome!!-----\n");
    printf("                    (helps:information you need)\n");
    
    while(1)
    {
        printf(">>>");
        bzero(cmd,256);
        if(fgets(cmd,256,stdin) == NULL)
        {
            printf("input Error!\n");
            return -1;
        }
 
        cmd[strlen(cmd)-1]='\0';    //fgets函数读取的最后一个字符为换行符,此处将其替换为'\0'
        
        if(strncmp(cmd, "put", 3) ==0 ) //上传文件(空格是为了标准化格式)
        {
            if(cmd[3]=='\0'||cmd[4]=='\0') //直接回车
            {
                printf("No filename follows after put cmd!\n");
                continue;
            }

            char filename[256] = "";
            char filepath[256] = "./data/";  //上传文件在本地的位置
            for(int i=4;cmd[i]!='\0';i++)
            {
                filename[i-4]=cmd[i];
            }
            strcat(filepath,filename);
            if((access(filepath,F_OK))!=-1) //文件存在
            {     
                ret = send(client_sockfd, cmd,sizeof(cmd), 0); //发送命令

                char a[2];
                ret = recv(client_sockfd, a, sizeof(a), 0); //防止粘包
                if(strncmp(a, "ok" , 2) == 0) //收到服务器的回应
                {
                    char buffer[1024];
                    FILE * fp = fopen(filepath,"r");
                    bzero(buffer, 1024);
                    int file_length = 0;
                    while( (file_length = fread(buffer,sizeof(char),1024,fp))>0)
                    {
                        //上传文件内容
                        if(send(client_sockfd,buffer,file_length,0)<0)
                        {
                            printf("Send File:\t%s Failed\n", filepath);
                            break;
                        }
                        bzero(buffer, 1024);
                        printf("put all file(file_length = %d)...\n",file_length);
                    }                                                                 //这段代码是循环读取文件的一段数据,在循环调用send,发送到客户端,这里强调一点的TCP每次接受最多是1024字节,多了就会分片,因此每次发送时尽量不要超过1024字节。
                    fclose(fp);
                }
            }
            else{
                printf("File is not valid\n");
                continue;
            }            
        }
        else if(strncmp(cmd, "get" , 3) == 0) //下载文件
        {
            if(cmd[3]=='\0'||cmd[4]=='\0') //直接回车的情况
            {
                printf("No filename follows after get cmd!\n");
                continue;
            }

            char filename[256] = "";
            char filepath[256] = "./data/";  //下载的文件在服务器的位置
            for(int i=4;cmd[i]!='\0';i++)
            {
                filename[i-4]=cmd[i];
            }
            strcat(filepath,filename);
 
            ret = send(client_sockfd,cmd,sizeof(cmd), 0); //发送命令

            char a[2];
            ret = recv(client_sockfd, a, sizeof(a), 0); //防止粘包
            if(strncmp(a, "ok" , 2) == 0) //收到服务器的回应,文件存在
            {
                char buffer[1024];
                FILE * fp = fopen(filepath,"w"); //保存服务器发送的文件内容
                bzero(buffer,1024);
                int length = 0;
                if((length = recv(client_sockfd,buffer,1024,0))>0)     //接收并写到文件
                {
                    if(length < 0)
                    {
                        printf("Recieve Data Failed!\n");
                        break;
                    }
                    int write_length = fwrite(buffer,sizeof(char),length,fp);
                    if (write_length

        操作说明:

#原始文件#
--server
  --/data
    --5.txt 6.txt  服务器中的文件
--client
  --/data
    --1.txt 2.txt  客户端中的文件

1.打开服务器
cd server
gcc server.c -o server
./server

2.打开客户端1
cd client
gcc client.c -o client
./client

#向服务器上传文件1.txt,成功后会出现/server/data/1.txt
put 1.txt

#从服务器中下载文件5.txt,成功后会出现/client/data/5.txt
get 5.txt

3.打开客户端2
cd client
gcc client.c -o client
./client

#向服务器上传文件2.txt,成功后会出现/server/data/2.txt
put 2.txt

#从服务器中下载文件6.txt,成功后会出现/client/data/6.txt
get 6.txt

参考博客:

https://www.cnblogs.com/skyfsm/p/7079458.html
https://www.cnblogs.com/-zyj/p/5719923.html

你可能感兴趣的:(模糊测试,C++,c语言)