select服务器---I/O多路连接之select

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/fern_girl/article/details/73928672

现如今,提高网络服务器的性能,提高I/O的性能,我们知道I/O系统其实做了两件事,一是等待数据就绪,二是数据的搬移;尤其是远距离网络传输“等”状态尤其明显,所以提高I/O性能本质是减少等的比重,其越趋近于0,效率就越高;I/O模型中提供一种多路复用模型,一次等待多个文件描述符,只要存在就绪文件,就可进行读写,大大提高了I/O的性能;
多路链接中存在三个特殊的函数select、poll、epoll,他们只负责I/O系统中的等,一次等待多个文件描述符,所以一旦函数返回,就一定存在就绪事件,因此它们又叫I/O事件的通知机制

今天我们先来介绍select服务器

一、select的原理
利用select函数,判断套接字上是否存在数据,或者能否向一个套接字写入数据。目的是防止应用程序在套接字处于锁定模式时,调用recv(或send)从没有数据的套接字上接收数据,被迫进入阻塞状态。

1.select函数
这里写图片描述

参数:
nfds:表示需要监视的最大文件描述符+1,这里监视的文件描述符既包括需要监视的读事件,又包括需要监视的写事件。
fd_set :fd_set是一个文件描述符的集合,其低层用位图实现,用比特位0、1表示是否关注该事件的读写或则该事件的读写是否就绪。
readfds/writefds/exceptfds:分别表示读事件的文件描述符集、写事件文件描述符集、错误事件文件描述符集。它们是输入输出型参数,输入输出的含义完全不同;作为输入参数,例如readfds,readfds表示要关心的读事件,一旦中间有一个读事件就绪,则立即返回;作为输出参数,readfds表示已就绪的读事件集合(此时可以对就绪事件直接读写);同理writefds和exceptfds也一样。
timeout表示设置的等待时间,它是一个结构体,成员一时秒,成员二是毫秒;设置为0表示非阻塞式等待,设置为NULL表示阻塞式等待。
这里写图片描述

返回值:
成功返回三个文件描述符集合已就绪文件描述符的数量,0表示等待超时,此时没有就绪fd,-1表示失败。

对位图集合操作的函数:
FD_CLR( s, *set) 从队列set删除句柄s;
FD_ISSET( s, *set) 检查句柄s是否存在与队列set中;
FD_SET( s, *set )把句柄s添加到队列set中;
FD_ZERO( *set ) 把set队列初始化成空队列.

2.Select工作流程

  • 用FD_ZERO宏来初始化我们感兴趣的fd_set。
    也就是select函数的第二三四个参数
  • 用FD_SET宏来将套接字句柄分配给相应的fd_set。
    如果想要检查一个套接字是否有数据需要接收,可以用FD_SET宏把套接接字句柄加入可读性检查队列列
  • 调用select函数。
    如果该套接字没有数据需要接收,select函数会把该套接字从可读性检查队列中删除掉,
  • 用FD_ISSET对套接字句柄进行检查。
    如果我们所关注的那个套接字句柄仍然在开始分配的那个fd_set里,那么说明马上可以进行相应的IO操 作。比如一个分配给select第一个参数的套接字句柄在select返回后仍然在select第一个参数的fd_set里,那么说明当前数据已经来了, 马上可以读取成功而不会被阻塞。

3.select服务器编写的实现

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

//打印输入标识
static void usage(const char* port)
{
    printf("usage:%s [local_ip] [local_port]\n",port);
}

int startup(char* ip,int port)
{
    //创建套接字
    int sock = socket(AF_INET,SOCK_STREAM,0);
    if(sock<0){
        perror("socket");
        exit(2);
    }

    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_port = htons(port);
    local.sin_addr.s_addr = inet_addr(ip);

     // 绑定套接字与服务器的ip和端口号
    if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0){
        perror("bind");
        exit(3);
    }

   //监听
    if(listen(sock,10)<0){
        perror("listen");
        exit(4);
    }
    return sock;
}

int main(int argc,char* argv[])
{
    if(argc!=3){
        usage(argv[0]);
        return 1;
    }

    //创建套接字(大函数)
    int listen_sock = startup(argv[1],atoi(argv[2]));

    fd_set rsdf;//读事件的文件描述符集
    fd_set wsdf;//写事件的文件描述符集
    int maxfd = -1;//为select第一参数准备
    //初始化两个集合
    FD_ZERO(&rsdf);
    FD_ZERO(&wsdf);

    //由于参数二三是输入输出型参数(参数改变),所以每次都要重新设置输入参数,我们用一个数组保存初始输入状态的事件文件描述符。(select缺点一)
    int fds_array[sizeof(rsdf)*8];
    int wfds_array[sizeof(fd_set)*8];
    int nums = sizeof(fds_array)/sizeof(fds_array[0]);
    int count = sizeof(wfds_array)/sizeof(wfds_array[0]);
    int i = 0;
    //初始化数组,-1表示非法状态
    for(; i<nums; ++i){//read_array init
        fds_array[i] = -1;
    }
    for(i=0;i<count;++i){//write_array init
        wfds_array[i] = -1;
    }

    //将listen_sock加入到读事件集合(服务器最先关心的就是客户端的连接请求)
    fds_array[0] = listen_sock;

    //轮询(select缺点二)
   while(1)
   {
       //设置读事件集合
       for(i=0;i<nums;++i){//notice read events
           if(fds_array[i]<0)
               continue;
           else{
               FD_SET(fds_array[i],&rsdf);//相应时间位图置1
               if(maxfd < fds_array[i])
                   maxfd = fds_array[i];//得到最大的文件描述符
           }
       }
       //写时间的文件描述符集同上
       for(i=0;i<count;++i){//notice write events
           if(wfds_array[i]<0)
               continue;
           else{
               FD_SET(wfds_array[i],&wsdf);
               if(maxfd < wfds_array[i])
                   maxfd = wfds_array[i];
           }
       }

       struct timeval timeout;
       timeout.tv_sec = 10;
       //select等待
       int s = select(maxfd+1,&rsdf,&wsdf,0,&timeout);
       if(s<0){//失败
           perror("select");
           exit(5);
       }else if(s == 0){//超时
           printf("timeout...\n");
       }else{//at last have one fd ready
           for(i=0;i<nums;++i){//遍历找到就绪文件
               if(fds_array[i]<0){
                   continue; 
               }else if(i==0 && FD_ISSET(listen_sock,&rsdf)){//客户端的请求事件
                   int msg[1024];
                   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");
                       exit(6);
                   }
                   //和客户端成功建立连接,打印连接客户端的信息
                   printf("get a client:[%s:%d]\n",inet_ntoa(client.sin_addr),\
                           ntohs(client.sin_port));

                   int j=0;
                   for(j=1;j<nums;++j){
                       if(fds_array[j] < 0){
                               break;
                       }
                   }
                   //将连接后客户端的fd加入读写文件集合中
                   if(j==nums){
                       printf("fd_set is full\n");
                       close(fds_array[i]);
                   }else{
                       fds_array[j] = new_sock;

                      for(j=0;j<=count;++j){
                          if(wfds_array[j]<0){
                              break;
                          }
                      }
                          if(j == count){
                              printf("write fd_set id full\n");
                          }else{
                              wfds_array[j] = new_sock;
                          }

                   }
               }else if(i!=0 && FD_ISSET(fds_array[i],&rsdf)){//若是普通文件,表明该文件读已就绪,则对文件进行读
                   char buf[1024];
                   ssize_t s = read(fds_array[i],buf,sizeof(buf)-1);//读
                   if(s<0){
                       perror("read");
                       close(fds_array[i]);
                       fds_array[i] = -1;
                       exit(7);
                   }else if(s==0){
                       printf("client is quit\n");
                       close(fds_array[i]);
                       fds_array[i] = -1;
                   }else{//读成功
                       buf[s] = 0;
                       printf("client# %s",buf);
                       int j=0;
                       for(;j<count;++j){
                           if(FD_ISSET(fds_array[i],&wsdf)){//读完后进行写,此时写就绪
                               break;
                           }
                       }
                               printf("Please Server Enter#: ");
                               fflush(stdout);
                               int s = read(0,buf,sizeof(buf)-1);
                               if(s<0){
                                   perror("read");
                                   exit(8);
                               }else{
                                   write(fds_array[i],buf,strlen(buf));//写
                                   printf("service echo#:%s",buf);
                           }
                   }
               }else{}//
           }
       }
   }
   return 0;
}

4.客户端代码编写的实现
客户端我们采用标准输出重定向的方法,来代替write,提高效率;
这里写图片描述

利用函数dup ,我们可以复制一个文件描述符。传给该函数一个既有的文件描述符,它会返回一个新的文件描述符,这个文件描述符是传给它文件描述符的拷贝。这意味着这两个文件描述符共享同一个数据结构。
dup2函数与dup类似,它有两个参数newfd和oldfd,函数的含义是newfd是oldfd的一份写时拷贝,此时newfd指向oldfd指向的结构体;

客户端实现重定向的方法:

  • 先保存标准输出的结构体内容,int ret = dup(1),保存1到ret,ret一般为3(因为默认打开0、1、2),此时ret指向标准输出;
  • 进行文件描述符的重定向,函数dup(sock,1),将标准输出重定向到网络sock,此时输出到显示屏上额内容直接输入到sock中,进行数据网络传输
  • 恢复标准输出,dup2(1,ret);

    代码实现:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<string.h>
#include<sys/stat.h>
#include<fcntl.h>


static void usage(const char* proc)
{
    printf("usage:%s [local_ip] [local_proc]\n",proc);
}

int main(int argc,char* argv[])
{
    if(argc!=3){
        usage(argv[0]);
        exit(1);
    }

    int sock = socket(AF_INET,SOCK_STREAM,0);   

    struct sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_port = htons(atoi(argv[2]));
    server.sin_addr.s_addr = inet_addr(argv[1]);

    if(connect(sock,(struct sockaddr*)&server,sizeof(server))<0){
        perror("connect");
        exit(2);
    }

    char buf[1024];
    while(1){
        printf("please enter#");
        fflush(stdout);

        int sfd = dup(1);
        close(1);
        dup2(sock,1);
        ssize_t s = read(0,buf,sizeof(buf)-1);  
        printf("%s",buf);
        dup2(sfd,STDOUT_FILENO);

        s = read(sock,buf,sizeof(buf)-1);
        buf[s] = 0;
        printf("server# %s",buf);

    //  ssize_t s = read(0,buf,sizeof(buf)-1);
    //  if(s<0){
    //      perror("read");
    //      exit(3);
    //  }
    //  write(sock,buf,strlen(buf));
    }
    return 0;
}

结果图:
这里写图片描述

5.select服务器的优缺点
优点:

  • select()的可移植性更好,在某些Unix系统上不支持poll()
  • select() 对于超时值提供了更好的精度:微秒,而poll是毫秒

缺点:

  • 单个进程可监视的fd数量被限制。
  • 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。
  • 对fd进行扫描时是线性扫描。fd剧增后,IO效率较低,因为每次调用都对fd进行线性扫描遍历,所以随着fd的增加会造成遍历速度慢的性能问题
  • select() 函数的超时参数在返回时也是未定义的,考虑到可移植性,每次在超时之后在下一次进入到select之前都需要重新设置超时参数。
展开阅读全文

没有更多推荐了,返回首页