(推荐!)三张图彻底搞懂select函数

欢迎交流 QQ 2431173627 微信 ccc17862701790

 存在问题

设计思路

工作过程 

第一步

第二步

​第三步

 

特点分析

 程序实例


 存在问题



用socket基本的一套api已经可以构建一个简单的tcp服务器了

思路就是 加一个while循环 在循环里面服务器程序用socket bind listen accept recv send 一条龙

具体可以看我写的这一篇博客 socket网络编程基础篇-------如何写一个简单的TCP服务器

但是这样简单的tcp服务器在只有单个客户端请求的时候是可以应付的,但是在有多个客户端请求的时候是存在很大问题的

以下面的场景作为例子

 

----->进入while循环

--->服务器用socket创建监听套接字(比如该套接字的文件描述符号为4)

-->调用listen函数监听该套接字

-->新来一个客户端调用connect请求连接

--->服务器调用accpet函数接受客户端请求并创建了针对这个客户端的数据传输套接字(比如文件描述符号为5) 

--->在这个套接字5上调用read函数读客户端发过来的数据

-->新来一个客户端调用connect请求连接

--->服务器调用accpet函数接受客户端请求并创建了针对这个客户端的数据传输套接字(比如文件描述符号为6) 

--->在这个套接字6上调用read函数读客户端发过来的数据

-->新来一个客户端调用connect请求连接

--->服务器调用accpet函数接受客户端请求并创建了针对这个客户端的数据传输套接字(比如文件描述符号为7) 

--->在这个套接字7上调用read函数读客户端发过来的数据

........(上述过程在服务器端用while循环 多个客户端请求的时候就不断延续上述过程) 
 上述的这样一个过程存在一个严重的问题 在多个客户端有连接请求或者读写请求的时候

当使用accpet函数的时候发现没有客户端连接时会阻塞,
同样当使用read/write函数的时候发现客户端没有数据传过来时也会阻塞
所以在上述多个客户端请求的过程中,

(1)假如在某个循环中调用read函数阻塞了,这个时候假如又来了一个客户端连接 
由于现在服务器端程序卡在了read这里
 就没办法进入下一次循环到accept那里 ,这样的环这个想建立连接的客户端就一直阻塞在那里

(2)假如在某一个循环调用accept阻塞了,服务端程序由于卡在accept这里不能往下执行到read函数 
故也响应不了前面已经建立连接的客户端套接字发过来的数据,这样根本就不能处理多个客户端的服务请求,
导致客户端的请求大大延迟


那怎么办呢?所以就引入了多路io复用,
他有三种实现机制----select函数,poll函数,epoll函数 这里先介绍select

设计思路


对于select函数工作过程的理解
select函数是怎么工作的呢?我是这么理解的
上面这个服务器客户端交互过程本质上是文件io函数(accept/read)对文件描述符(4,5,6,7)的操作过程.
出现问题的罪魁祸首就是,
accep/read函数"自作主张"就直接去连接/读取文件描述符对应的套接字,
丝毫不管对应文件描述符的套接字是否可以连接/读取
万一对应文件描述符的套接字没有连接/数据请求传输过来,这两个函数自然就会阻塞.


理想的情况应该是请一个"监控员" ,
在执行read/accept之前 先让这个监管员依次检查一番
看哪个文件描述符有连接过来了或者有数据传输过来了 
这时候再调用accept/read函数就不会阻塞了
如果监管员这一轮查找 没看到哪个文件描述符有连接过来了或者有数据传输过来了 
就在进入下一轮的检查 就不调用accept或者read
虽然在这个监管员工作也会耽搁一定的时间(select每次检查都是阻塞查询) 但是比起accept/read函数盲目的就上来读写一个文件描述符 然后卡在那大半天  效率明显来的高 
select就起了这样一个"监管员"的作用,


工作过程 


三步走!


 

select特点分析
 


select缺点: 
(1)每次调用 select(),都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大,
需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。
同时每次调用 select() 都需要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时也很大。 
(2)单个进程能够监视的文件描述符的数量存在最大限制,在 Linux 上一般为 1024,
可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低 
(3)select函数在每次调用之前都要对参数进行重新设定,这样做比较麻烦,而且会降低性能
  (4)对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低。当套接字比较多的时候,
每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。
这会浪费很多CPU时间。如果能给套接字注册某个回调函数,
当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。
(5)后面会讲到 其实select还是同步阻塞的模型 
 在select设定的时间timeval内,进程会阻塞在select那条语句上 
此时内核在遍历select所监管的套接字上 
在此期间只要有一个套接字就就绪了(客户端connect过来 或者客户端send数据过来了 ) 
select就会返回停止阻塞 然后调用read /accept同步读写或者建立连接
 或者在内核遍历期间没有套接字就绪 select会超时返回 
 然后通过while进入下一轮的select遍历 


select优点: 
(1)从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,
甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。
但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。
用户可以注册多个socket,然后不断地调用select读取被激活的socket,
即可达到在同一个线程内同时处理多个IO请求的目的。
而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。

(2)目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点 

 

程序实例
 

#include <stdio.h>
#include <netinet/in.h>   //for souockaddr_in
#include <sys/types.h>     
#include <sys/socket.h>
#include <errno.h>
#include <stdlib.h>
 
#include <arpa/inet.h>
 
//for select
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/select.h>
 
#include <strings.h>   //for bzero
#include <string.h>
 
#define BUFF_SIZE 1024
#define backlog 7
#define ser_port 8000
#define CLI_NUM 3
 
 
int client_fds[CLI_NUM];
 
int main(int agrc,char **argv)
{
    int ser_souck_fd;
    int i;  
    char input_message[BUFF_SIZE];
    char resv_message[BUFF_SIZE];
 
 
    struct sockaddr_in ser_addr;
    ser_addr.sin_family= AF_INET;    //IPV4
    ser_addr.sin_port = htons(ser_port);
    ser_addr.sin_addr.s_addr = INADDR_ANY;  //指定的是所有地址
 
    //creat socket
    if( (ser_souck_fd = socket(AF_INET,SOCK_STREAM,0)) < 0 )
    {
        perror("creat failure");
        return -1;
    }
 
    //bind soucket
    if(bind(ser_souck_fd, (const struct sockaddr *)&ser_addr,sizeof(ser_addr)) < 0)
    {
        perror("bind failure");
        return -1;
    }
 
    //listen
    if(listen(ser_souck_fd, backlog) < 0)
    {
        perror("listen failure");
        return -1;
    }
 
 
    //fd_set
    fd_set ser_fdset;
    int max_fd=1;
    struct timeval mytime;
    printf("wait for client connnect!\n");
 
    while(1)
    {
        mytime.tv_sec=27;
        mytime.tv_usec=0;
 
        FD_ZERO(&ser_fdset);
 
        //add standard input
        FD_SET(0,&ser_fdset);
        if(max_fd < 0)
        {
            max_fd=0;
        }
 
        //add serverce
        FD_SET(ser_souck_fd,&ser_fdset);
        if(max_fd < ser_souck_fd)
        {
            max_fd = ser_souck_fd;
        }
 
        //add client
        for(i=0;i<CLI_NUM;i++)  //用数组定义多个客户端fd
        {
            if(client_fds[i]!=0)
            {
                FD_SET(client_fds[i],&ser_fdset);
                if(max_fd < client_fds[i])
                {
                    max_fd = client_fds[i];
                }
            }
        }
 
        //select多路复用
        int ret = select(max_fd + 1, &ser_fdset, NULL, NULL, &mytime);
 
        if(ret < 0)   
        {   
            perror("select failure\n");   
            continue;   
        }   
 
        else if(ret == 0)
        {
            printf("time out!");
            continue;
        }
 
        else
        {
            if(FD_ISSET(0,&ser_fdset)) //标准输入是否存在于ser_fdset集合中(也就是说,检测到输入时,做如下事情)
            {
                printf("send message to");
                bzero(input_message,BUFF_SIZE);
                fgets(input_message,BUFF_SIZE,stdin);
 
                for(i=0;i<CLI_NUM;i++)
                {
                    if(client_fds[i] != 0)
                    {
                        printf("client_fds[%d]=%d\n", i, client_fds[i]);
                        send(client_fds[i], input_message, BUFF_SIZE, 0);
                    }
                }
            }
 
            if(FD_ISSET(ser_souck_fd, &ser_fdset))
            {
                struct sockaddr_in client_address;
                socklen_t address_len;
                int client_sock_fd = accept(ser_souck_fd,(struct sockaddr *)&client_address, &address_len);
                if(client_sock_fd > 0)
                {
                    int flags=-1;
                    //一个客户端到来分配一个fd,CLI_NUM=3,则最多只能有三个客户端,超过4以后跳出for循环,flags重新被赋值为-1
                    for(i=0;i<CLI_NUM;i++)
                    {
                        if(client_fds[i] == 0)
                        {
                            flags=i;
                            client_fds[i] = client_sock_fd;
                            break;
                        }
                    }
 
 
                    if (flags >= 0)
                    {
                        printf("new user client[%d] add sucessfully!\n",flags);
 
                    }
 
                    else //flags=-1
                    {  
                        char full_message[]="the client is full!can't join!\n";
                        bzero(input_message,BUFF_SIZE);
                        strncpy(input_message, full_message,100);
                        send(client_sock_fd, input_message, BUFF_SIZE, 0);
 
                    }
                }   
            }
 
        }
 
        //deal with the message
 
        for(i=0; i<CLI_NUM; i++)
        {
            if(client_fds[i] != 0)
            {
                if(FD_ISSET(client_fds[i],&ser_fdset))
                {
                    bzero(resv_message,BUFF_SIZE);
                    int byte_num=read(client_fds[i],resv_message,BUFF_SIZE);
                    if(byte_num > 0)
                    {
                        printf("message form client[%d]:%s\n", i, resv_message);
                    }
                    else if(byte_num < 0)
                    {
                        printf("rescessed error!");
                    }
 
                    //某个客户端退出
                    else  //cancel fdset and set fd=0
                    {
                        printf("clien[%d] exit!\n",i);
                        FD_CLR(client_fds[i], &ser_fdset);
                        client_fds[i] = 0;
                       // printf("clien[%d] exit!\n",i);
                        continue;  //这里如果用break的话一个客户端退出会造成服务器也退出。 
                    }
                }
            }
        }   
    }
    return 0;
}
发布了61 篇原创文章 · 获赞 17 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/vjhghjghj/article/details/100938255