Linux系统编程--网络Socket编程 之 I/O多路复用(select)服务器编程

I/O 复用模型

调用select或poll,在这两个系统调用中的某一个上阻塞,而不是阻塞于真正I/O系统调用。 阻塞于select调用,等待数据报套接口可读。当select返回套接口可读条件时,调用recevfrom将数据报拷贝到应用缓冲区中。
在这里插入图片描述

这是五种I/O模型中的一种,也正是select函数的模型,
相比于多线程和多进程,I/O多路复用是在单一进程的上下文中的,当有多个并发连接请求时,多线程或者多进程模型需要为每个连接创建一个线程或者进程,而这些进程或者线程中大部分是被阻塞起来的。由于CPU的核数一般都不大,比如4个核要跑1000个线程,那么每个线程的时间槽非常短,而线程切换非常频繁。这样是有问题的。而使用I/O多路复用时,处理多个连接只需要1个线程监控就绪状态,对就绪的每个连接开一个线程处理(由线程池支持)就可以了,这样需要的线阿程数大大减少,减少了内存开销和上下文切换的CPU开销。

其他几种模型就不赘述了,下面细讲select服务器编程;

select 函数

该函数允许进程指示内核等待多个事件中的一个或多个,并在指定的时间后才唤醒它。可能还不太明白,下面是该函数的定义:

#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
返回: 若有就绪描述符则为其数目,超时为 0 ,出错为 -1


nfds : 参数指定待测试的描述符个数,其值为待测试的最大描述符 + 1,

中间3个参数readfds,writefds和exceptfds指定我们要让内核测试读,写和异常条件的描述符,如果我们对某一个条件的条件不感兴趣,可以将其设置为空指针

timeout :它告知内核等待所指定的文件描述符中的任何一个就绪可花多少时间的秒数和微妙数。

struct timeval {
  long   tv_sec;      /* seconds */
  long   tv_usec;     /* microseconds */  

这个参数有三种可能:
1.永远等待下去:仅在有一个文件描述符准备好 I/O 时才返回,为此,将该参数设置为 空指针;
2.等待一段时间:在有一个描述符准备好 I/O 时返回,但是不超过由该参数所指向的timeval结构体中指定的秒数和微秒数相加;
3.根本不等待:检查描述符后立刻返回,这称为 轮询(polling)。为此,该函数必须指向一个timeval结构,而且其中的定时器(秒数,微秒数)必须为0.

操作描述符集

selcet使用描述符集,通常是一个整数数组,其中每个整数中的的每一位对应一个描述符。

void FD_ZERO(fd_set *fd_set);
void FD_SET(int fd,fd_set *fdset);
void FD_CLR(int fd,fd_set *fdset);
int FD_ISSET(int fd,fd_set *fdset);

但我们在使用 fd_set 定义一个描述符集后,需要使用 FD_ZERO() 初始化;
FD_SET() 将感兴趣的文件描述符添加到集合中;
FD_CLR() 移除;
FD_ISSET()判断指定的集合中有哪些文件描述符准备就绪,在服务器代码中,这些集合可能是新的客户端请求连接(listenfd),也可能是已连接的客户端发来数据,通过这个函数,可以让服务器知道现在是该接收新客户端(accept)还是读(read);

select函数返回

因为,selcet 函数返回后,会将描述符集中未就绪(未发生响应的)的文件描述符对应的位清零,再用 FD_ISSET() 查询是描述符已就绪(新客户端请求连接,或是已连接客户端发来数据请求);
因为每次返回都会清 0,所以,我们需要用到一个数组,在保存那些已经建立连接的描述符和用来监听的 listenfd,在么一次调用 select 阻塞之前,都要将数组中的值一一赋值到字符集中;

图解
服务器初态

在使用select创建了服务器程序之后,还没有客户端连接服务器状态:
在这里插入图片描述
这个黑点就是socket创建的listenfd;
服务器只维护一个读描述符集,那么,描述符0,1,2位置将会分别被置为标准输入,标准输出和标准出错;监听的描述符紧跟在后
在这里插入图片描述
此时,只有用于监听的描述符创建,我们把这个描述符一般为3存入数组中,再循环使用FD_SET函数,将数组中存在的文件描述符,添加到字符集中,字符集中的变现是该描述符大小的对应的位上为1,程序会阻塞在select处;

第一个客户端连接

当第一个客户端与服务器建立连接后,监听的描述符变为可读,select返回将除开本位的其他位清零,程序调用accept函数返回一个新的文件描述符,并把该描述符加入数组中,第一个连接的客户端通常是 4;
在这里插入图片描述
此时,数组和描述符集是这样的:
在这里插入图片描述
从现在起,程序必须用数组记录每个已连接的新描述符,并把它加到描述符集中,就是 fd4

第二个客户端连接

再有客户端连接,则数组与字符集:
在这里插入图片描述
程序必须将新连接的客户端记录,即在首个为-1处的项填入描述符大小,再将他添加到字符集中,fd5 ,即,对应的位将会设置为1;

假设此时,已连接的第一个客户端发来数据之后断开连接,阻塞的select函数将会返回,并处理发来的数据,再将已退出的描述符的数组为设置为-1(清理房间,等待其他人入住);
selcet返回时:
在这里插入图片描述
此时,调用FD_ISSET()找出是哪一个描述符准备就绪,找到之后就可以进行相关读写了!当然,也可能多个服务器同时发来数据,也可能是是新的客户端连接,同理,响应的位为1,为响应的位置0,因为我们每一个描述符都保存在数组中,所以并不担心置0,下一次阻塞前再一 一赋值即可;
客户端退出后的数组:
在这里插入图片描述
下一次select阻塞前,就会将这个数组的内容在赋给rset;
select就继续监听描述符集中的多有描述符,兵来将挡,水来土掩;

select服务器代码
/*********************************************************************************
 *      Copyright:  (C) 2020 Xiao yang System Studio
 *                  All rights reserved.
 *
 *       Filename:  select_server.c
 *    Description:  This file 
 *                 
 *        Version:  1.0.0(03/09/2020)
 *         Author:  Lu Xiaoyang <920916829@qq.com>
 *      ChangeLog:  1, Release initial version on "03/09/2020 06:29:03 PM"
 *                 
 ********************************************************************************/
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <getopt.h>
#include <stdlib.h>
#include <libgen.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <unistd.h>

#define ARRAY_SIZE(x)   (sizeof(x)/sizeof(x[0]))   //用来计算数组大小(多少个项)

static inline void print_usage(char *progname);   //打印帮助信息
int socket_Be_ready(char *listen_ip,int listen_port);   //将服务器的socket(),bind(),listen() 包装成一个函数;

int main(int argc,char *argv[])
{
    char              buf[1024];
    char              *progname;
    int               listenfd,clifd;
    int               maxfd = 0;
    int               serv_port;
    int               rv;
    int               daemon_run = 0;   //与程序后台运行有关
    fd_set            allset;          //添加感兴趣的文件描述符的集合位;
    int               fds_array[1024];   //用来存放文件描述符的数组
    int               found = 0;
    int               i;
    int               opt = 0;
    struct option     opts[] = {
        {"port",required_argument,NULL,'p'},
        {"daemon",no_argument,NULL,'d'},
        {"help",no_argument,NULL,'h'},
        {NULL,0,NULL,0}
    };                                  //上下这一块用来接收命令行参数执行对应的赋值或者打印相关信息
    
    progname = basename(argv[0]);
    while((opt = getopt_long(argc,argv,"p:dh",opts,NULL)) != -1)
    {
        switch(opt)
            {
                case 'p':
                    serv_port = atoi(optarg);
                    break;

                case 'h':
                    print_usage(progname);
                    break;

                case 'd':
                    daemon_run = 1;
                    break;

                default:
                    break;
            }
    }

    if(!serv_port)   //未接收到用户传的端点;
    {
        print_usage(progname);
        return -1;
    }

    if((listenfd = socket_Be_ready(NULL,serv_port)) < 0)   //创建服务器;
    {
        perror("Socket create failure");
        return -2;
    }

    printf(" server start listen on port[%d]\n",serv_port);

    if(daemon_run)
    {
        daemon(0,0);   //用户加入了 -d 选项后,服务器将会在后台运行,不会占用窗口;
    }

    for(i = 0;i < ARRAY_SIZE(fds_array);i++)
    {
        fds_array[i] = -1;   //最小的文件描述符为0;将数组全部置-1,相当于把房间清空,等待人进来居住;可使用循环判断 fds_arry[i] < 0 来判断该位置是否空;
    }

    fds_array[0] = listenfd;   //将用来监听的文件描述符存放在数组的第一个位置;
   
    for( ; ; )
    {
        FD_ZERO(&allset);
        for(i = 0;i < ARRAY_SIZE(fds_array);i++)
        {
            if(fds_array[i] < 0)
                continue;
            maxfd = fds_array[i] > maxfd ? fds_array[i] : maxfd;
            FD_SET(fds_array[i],&allset);
        }                                        //selcet在返回后会清零未就绪的描述符对应位,所以每一次使用都要赋值,找到最大位;

        rv = select(maxfd+1,&allset,NULL,NULL,NULL);   //程序阻塞在此,等待字符集中的响应;
        if(rv < 0)
        {
            perror("select failure");
            break;
        }

        else if(rv == 0)
        {
            printf("Time Out\n");
            break;
        }

        if(FD_ISSET(listenfd,&allset))   //新的客户端发来连接请求,则使用 accept() 函数处理;
        {
           if((clifd = accept(listenfd,(struct sockaddr  *)NULL,NULL)) < 0)
           {
               perror("Accept new client failure");
               continue;
           }
           for(i = 1;i < ARRAY_SIZE(fds_array);i++)   //找到一个为 -1 的项,说明这个位置未被占用;
           {
               if(fds_array[i] < 0)
               {
                   printf("Put new clifd[%d] into fds_array[%d]\n",clifd,i);
                   fds_array[i] = clifd;   //将新接收的客户端的文件描述符放入文件描述符数组中;
                   found = 1;
                   break;
               }
           }

           if(!found)
           {
               printf("Put new client into array failure:full\n");
               close (clifd);
           }
        }

        else   //已连接的客户端发来数可读;
        {
            for(i = 1;i < ARRAY_SIZE(fds_array);i++)
            {
               if(fds_array[i] < 0 || !FD_ISSET(fds_array[i],&allset))   //往响应的客户端读写;
                   continue;
            
               rv = read(fds_array[i],buf,sizeof(buf));
               if(rv <= 0)
               {
                   printf("Read from client[%d] failure:%s\n",fds_array[i],strerror(errno));
                   close(fds_array[i]);
                   fds_array[i] = -1;
               }  
               else
               {
                   printf("Read %d bytes data from client[%d] : %s\n",rv,fds_array[i],buf);
                   int j = 0;
                   for(j = 0;j < rv;j++)
                   {
                       buf[j] = toupper(buf[j]);
                   }

                   rv = write(fds_array[i],buf,rv);
                   if(rv <= 0)
                   {
                       printf("Write to client[%d] failure:%s\n",fds_array[i],strerror(errno));
                       close(fds_array[i]);
                       fds_array[i] = -1;
                   }
                }
               
            }
        }
    }

    return 0;
}

void print_usage(char *progname)
{
    printf("progname usage:\n");
    printf("-p(--port) for port you will bind\n");
    printf("-d(--deamon) the programe will run at background\n");
    printf("-h(--help) print help massage\n");

    return;
}

int socket_Be_ready(char *listen_ip,int serv_port)
{
    int                    listenfd;
    int                    on = 1;
    struct sockaddr_in     servaddr;
    socklen_t              addrlen = sizeof(servaddr);

    if((listenfd = socket(AF_INET,SOCK_STREAM,0)) < 0)
    {
        perror("socket() failure");
        return -1;
    }
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

    bzero(&servaddr,sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(serv_port);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

    if(bind(listenfd,(struct sockaddr *)&servaddr,addrlen) < 0)
    {
        printf("Bind failure:%s\n",strerror(errno));
        return -1;
    }

    listen(listenfd,13);

    return listenfd;

}

        
    
Select服务器的缺点

1.每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大 ;
2.同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大 ;
3.select支持的文件描述符数量太小了,默认是1024。 但相对于TCP单进程版本、TCP多线程版本和TCP多进程版本,select服务器效率高于以上三个;

  • 10
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值