IO多路转接模型-----select及select版本的TCP服务器的实现

本文深入探讨了IO多路转接模型在数据通信中的应用,详细解析了Select函数的工作原理及其在TCP服务器中的实现。通过实例,阐述了Select如何通过监控多个文件描述符的读、写和异常事件,提升数据传输效率。
摘要由CSDN通过智能技术生成

一.关于IO多路转接模型的作用:

作用:在数据传输过程中提高传输效率;

我们知道在数据通信的过程中通常分为两个部分:
1.等待数据到达内核;
2.将数据从内核中拷贝到用户区;

在实际的应用中,等待的时间往往比拷贝的时间多,因此我们要想提高效率,必然要将等待时间减少(在一定的时间内减少等待比重);

IO多路转接就是解决这个问题的:一次监视多个文件描述符;
在IO多路转接中,由于一次等待多个文件描述符,在单位时内就绪事件发生的概率就越大,所以等的比重就会越小;

监视的文件描述符返回条件是什么?

  1. 监视的文件描述符都有自己要关注的事件(读/写/异常事件);
  2. 返回条件就是:我们所监视(关心)的文件描述符的事件至少一个已经就绪;
  3. IO多路转接的实现方式:
    1. select
    2. poll
    3. epoll

select函数的实现及原理:

1.select函数模型:
#include <sys/select.h>

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
           
2.select函数参数:
1. nfds :表示的是等待的文件描述符中的最大的那个+1;  
         用于限定操作系统遍历的区间(只关心的那部分,其他的就不会去遍历,减少了开销) 

2. fd_set:该结构实际是一个位图;
          因为文件描述符其实是数组下标,也就是从0开始的整数;
          所以对于位图的每一个比特位表示的是一个文件描述符的状态;
          如果是1表示关心该文件描述符上的事件,是0,表示不关心该文件描述符上的事件。
          而具体关心文件描述符上的什么事件,则由中间三个参数决定。

3. readfds :表示的是需要等待的读事件的文件描述符集;
4. writefds :表示的是需要等待的写事件的文件描述符集;
5. exceptfds :表示的是需要等待的异常事件的文件描述符集; 

   注意:以上三个位图参数都是输入输出型参数 :
        1.作为输入参数:告诉系统我要关心的文件描述符的哪些事件
        2.作为输出参数:关心的文件描述符中哪些事件就绪
6. timeout:用于设置select阻塞等待的时间。
   取值如下:
   1. NULL:表示select阻塞等待,关心的多个文件描述符上没有时间发生时,
            进程会一直阻塞在select的函数调用处。
            如果至少有一个文件描述符上有事件发生,则select返回。 

            eg:
             select(max_fd+1,&rfds,NULL,NULL,NULL) 
            // 表示只监视该文件描述符的读事件 

   2. 0:表示select非阻塞等待,只是用于检测等待事件的状态。  
         当调用该函数时,不管有无事件发生,该函数都会立即返回,
         进程不会挂起等待事件的发生。 

        eg :
          // 设置select()的等待事件
          struct timeval timeout = {0,0};
          // 开始调用select等待所关心的文件描述符集
          select(max_fd+1,&rfds,NULL,NULL,&timeout)
          // 表示只监视该文件描述符的读事件

   3.特定的时间值:表示select只会阻塞等待一定的时间;
                 在该时间段内,如果有事件发生,则select返回,进程结束阻塞。 
                 如果达到规定的时间,还没有事件发生,此时select将会超时返回。  

          eg :
          // 设置select()的等待事件
          struct timeval timeout = {5,0};
          // 开始调用select等待所关心的文件描述符集
          select(max_fd+1,&rfds,NULL,NULL,&timeout)
          // 表示只监视该文件描述符的读事件
timeval的结构如下:
struct timeval {
               long    tv_sec;         /* seconds :秒*/
               long    tv_usec;        /* microseconds:微秒 */
           };
//头文件<sys/time.h>

3.返回值含义:
1. > 0 :满足就绪条件的事件个数
2. 0 : 在规定的时间内没有事件发生(超出timeout设置的时间)
3. -1 :错误 
       原因由errno标识;此时中间三个参数的值变得不可预测。
       
4.select模型----理解select的执行过程:
关键在于理解fd_set

假设fd_set长度为1字节,即8个bit位,一个bit位可以表示一个文件描述符;
则1字节长的fd_set最大可以对应8个文件描述符(fd)

步骤:
(1)执行fd_set set; FD_ZERO(&set);则set用位表示是0000,0000。

(2)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)

(3)若再加入fd=2,fd=1,则set变为0001,0011

(4)执⾏行select(6,&set,0,0,0)阻塞等待

(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。
    注意:没有事件发生的fd=5被清空

1. 所以,在select之前需要将关心的文件描述符集合保存起来。这样便可设置下一次需要关心的
文件描述符集,同时select返回时也可由此判断哪些文件描述符上的事件就绪了。这里,可以
提供一个数组fdArray[]来保存关心的文件描述符。将文件描述符作为数组元素保存在数组中。

也就是说,fdArray[]数组来表示连接的客户端,该数组大小表示最多可以连接的数量

int fdArray[sizeof(fd_set)*8];//第三方数组 
sizeof(fd_set)*8 就是最多能够连接的数量

2. fd_set:该结构实际是一个位图;
          因为文件描述符其实是数组下标,也就是从0开始的整数;
          所以对于位图的每一个比特位表示的是一个文件描述符的状态;
          如果是1表示关心该文件描述符上的事件,是0,表示不关心该文件描述符上的事件。
          
5.select的就绪条件:
一. 读就绪:

    1. 当对内核中的数据进行读取时,如果接收缓冲区中的字节数,大于等于低水位
       SO_RCVLOWAT,此时就说明读就绪,在对该文件描述符调用read等进行读取时,
       不会阻塞,且返回值大于0;
    2. 在TCP通信中,如果服务器的socket上有新的连接到达时,客户端会发送SYN数据包
       给服务器,说明读就绪。此时在调用accept接收新连接时,不会阻塞。
       而且会返回新的文件描述符与客户端进行通信;
    3. TCP通信中,如果对端关闭连接,此时会发送FIN数据包。也说明读就绪,
       此时在调用read对文件描述符进行读取时,会返回0;
    4. 当socket上有未处理的错误时,也说明读就绪,
       在对文件描述符进行read读取时,会返回-1;

二.  写就绪:
    1. 在socket内核中,如果发送缓冲区中的可用字节数大于等于低水位 
       标记SO_SNDLOWAT时,说明写就绪,此时调用write进行写操作时,
       不会阻塞,且返回值大于0;
    2. 当一方要进行写操作(即关心的是写事件),而对端将文件描述符关闭,
       此时写就绪。调用write时会触发SIGPIPE信号;
    3. 当socket使用非阻塞connect连接成功或失败之后,写就绪;
    4. 当socket上有未读取的错误时,写就绪,此时write进行写时,会返回-1;

 三. 异常就绪:当socket上收到带外数据时,异常事件就绪。
 
6.select的优缺点:
优点 :
1. 不需要fork或者pthread_create就可以实现一对多的通信,简化了进程线程的使用,也就是没有多进程和多线程的一些缺点;
2. 同时等待多个文件描述符,效率相对较高;
3. 遵循POSIX标准,可以跨平台进行监控,监控的时间可以精确到微秒;

缺点 :
4. 代码编写复杂,维护起来较麻烦
2.select每次监控的时候都会修改监控集合中的值,都需要重新向内核拷贝监控描述符集合;
5. select实现的监控原理是在内核中轮循遍历状态,随着描述符增多而性能下降;
6. 所有能够接收的文件描述符有上限,select支持的文件描述符数量过小,默认是1024,取决于FD_SETSIZE的大小;
7. select要监控的集合中的描述符每次都要向内核拷贝数据;

7.实现select版本的TCP服务器:

流程:
1)定义一个描述符集合:fd_set(位图,最大值为1024)
2)将集合拷贝到内核中进行监控,监控的原理就是对所有描述符轮询编译状态
3)当有描述符就绪的时候,在返回前,将没有就绪的描述符剔除掉
4)用户操作,对所有描述符进行遍历,看哪一个还在集合中,那么这个就和就是已经就绪的
5)重新fd_set拷贝到内核中进行监控
注意:监控的fd_set,但是我们不能让他传递到内核中去,不然传递回来的时候已经改变,所以我们需要定义一个中间变量拷贝到内核中,当传递回来的时候中间变量中就是已经准备好的文件描述符,然而我们监控的文件描述符就可以继续定义变量监控;

MakeFile:

select_server:select_server.c
        gcc $^ -o $@

select_server.c

#include <stdio.h>                                                                                                                                                                             
#include <unistd.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int fdArray[sizeof(fd_set)*8];//第三方数组

int startup( int port )
{   
    // 1. 创建套接字
    int sock = socket(AF_INET,SOCK_STREAM,0);//这里第二个参数表示TCP
    if( sock < 0 )
    {   
        perror("socket fail...\n");
        exit(2);
    }

    // 2. 解决TIME_WAIT时,服务器不能重启问题;使服务器可以立即重启
    int opt = 1;
    setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));

    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_addr.s_addr = htonl(INADDR_ANY);// 地址为任意类型
    local.sin_port = htons(port);// 这里的端口号也可以直接指定8080

    // 3. 绑定端口号
    if( bind(sock,(struct sockaddr *)&local,sizeof(local)) < 0 )
    {
        perror("bind fail...\n");
        exit(3);
    }   

    // 4. 获得监听套接字
    if( listen(sock,5) < 0 )
    {
        perror("listen fail...\n");
        exit(4);
    }   
    return sock;
}   

int main(int argc,char* argv[] )
{
    if( argc != 2 )
    {
        printf("Usage:%s port\n ",argv[0]);
        return 1;
    }

    // 1. 获得监听套接字
    int listen_sock = startup(atoi(argv[1]));//端口号传入的时候是以字符串的形式传入的,需要将其转为整型

    // 2. 初始化数组
    fdArray[0] = listen_sock ; 
    int num = sizeof(fdArray)/sizeof(fdArray[0]);// num表示最多能描述的文件的描述符
    printf("%d\n",num);//我们可以把它的值打印出来看一下,这里就是可以监视的文件描述符的上限
                        // 另外根据不同的操作系统,这里的值也不同
    int i = 1;
    for( ; i < num; i++  )
    {
        fdArray[i] = -1;//为什么置为1?因为文件描述符的大小是从0开始递增的小整数;所以初始化时,将其全置为-1
    }

    while(1)
    {
        //3. 根据数组向文件描述符集中添加文件描述符

        fd_set rfds;// 定义一个只读文件描述符集
        FD_ZERO(&rfds);//清空文件描述符集
        int max_fd = fdArray[0];

        int i = 0; 
        for( ; i < num; i++ )
        {
            if( fdArray[i] >= 0 )//遍历数组,遇到一个不是-1的数组元素,将该元素表示的文件描述符添加进文件描述符集中
            {
                FD_SET(fdArray[i],&rfds);
                if( fdArray[i] > max_fd )
                {
                    max_fd = fdArray[i];// 不断的更新,找到所关心的最大的描述符,是为了填写select的第一个参数
                }
            }
        }

        // 4. 设置select()的等待事件

       struct timeval timeout = {5,0};

       //5. 开始调用select等待所关心的文件描述符集
       switch( select(max_fd+1,&rfds,NULL,NULL,&timeout) )// 表示只监视该文件描述符的读事件
       {
           case 0:// 表示词状态改变前已经超过了timeout的时间
               printf("timeout...\n");
           case -1:// 失败了
               printf("select fail...\n");
           default: // 成功了
               {
                   // 6. 根据数组中记录的所关心的文件描述符集先判断哪个文件描述符就绪
                   //    如果是监听文件描述符,则调用accept接受新连接
                   //   如果是普通文件描述符,则调用read读取数据
                   int i = 0;
                   for( ;i < num; i++ )
                   {
                       if( fdArray[i] == -1 )
                       {
                           continue;
                       }
                       if( fdArray[i] == listen_sock && FD_ISSET( fdArray[i],&rfds ) )
                       {
                           // 1. 如果监听套接字上读就绪,此时提供接受连接服务
                           struct sockaddr_in client;
                           socklen_t len = sizeof(client);
                           int new_sock = accept(listen_sock,(struct sockaddr *)&client,&len);
                           if(new_sock < 0)
                           {
                               perror("accept fail...\n ");
                               continue;
                           }
                           //获得新的文件描述符之后,将该文件描述符添加进数组中,以供下一次关心该文件描述符
                           int i = 0;
                           for( ; i < num; i++ )
                           {
                               if( fdArray[i] == -1 )//放到数组中第一个值为-1的位置
                                   break;
                           }
                           if( i < num )
                           {
                               fdArray[i] = new_sock;
                           }
                           else
                           {
                               close(new_sock);
                           }
                           printf("get a new link!,[%s:%d]\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
                           continue;
                       }

                        //2. 此时关心的是普通文件描述符
                        //   此时提供读取数据的服务
                        if( FD_ISSET( fdArray[i],&rfds ) )
                        {
                            char buf[1024];
                            ssize_t s = read(fdArray[i],buf,sizeof(buf)-1);
                            if( s < 0 )
                            {
                                printf("read fail...\n");
                                close(fdArray[i]);
                                fdArray[i] = -1;
                            }
                            else if( s == 0 )
                            {
                                printf("client quit...\n");
                                close(fdArray[i]);
                                fdArray[i] = -1;
                            }
                            else
                            {
                                buf[s] = 0;
                                printf("client# %s\n",buf);
                            }
                        }
                   }
               }
               break;
       }
    }
    return 0;
}                   



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值