select 多路复用socket

select多路复用

1.1 select实现原理

int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);

在初学socket编程的时候,经常就是流程化的写socket,bind,listen,accept,其中server端在创建好文件描述符之后就阻塞在accept等待客户端的连接,函数不能立即返回。可是使用Select就可以完成非阻塞(所谓非阻塞方式non-block,就是进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高)方式工作的程序,它能够监视我们需要监视的文件描述符的变化情况——读写或是异常。
基于select的I/O复用模型的是单进程执行可以为多个客户端服务,这样可以减少创建线程或进程所需要的CPU时间片或内存资源的开销;此外几乎所有的平台上都支持select(),其良好跨平台支持是它的另一个优点select函数实现I/O端口的复用,传递给 select函数的参数会告诉内核:

  • 所需要关系的文件描述符
  • 所希望监控文件描述符的属性变化,包括readfds(文件描述符有数据到来可读),writefds(可写),exceptfds(异常)。调用后select函数会阻塞,直到有描述符就绪(有数据可读、可写、或者有错误异常),或者超时( timeout 指定等待时间)发生函数才返回。当select()函数返回后,可以通过遍历 fdset,来找到究竟是哪些文件描述符就绪。
  • 带三个参数是select的超时时间,设置NULL则永不超时。

第一个参数表示最大文件描述符加一(maxfd+1)Linux内核从0开始到max_fd-1扫描文件描述
符,如果有数据出现事件(读、写、异常)将会返回;假设需要监测的文件描述符是8,9,10,那么Linux内核实际也要监测0~7,此时真正测试的文件描述符为0-10总共11个,即max(8,9,10)+1,所以第一个参数是所有要监听的文件描述符中最大的+1.

中间的三个集合fd_set实际上可以理解成特定的位来标识文件描述符,对于集合可以使用四个函数来操作。

  • FD_ZERO(fd_set* fds) //清空集合
  • FD_SET(int fd, fd_set* fds) //将给定的描述符加入集合
  • FD_ISSET(int fd, fd_set* fds) //判断指定描述符是否在集合中
  • FD_CLR(int fd, fd_set* fds) //将给定的描述符从文件中删除

理解select函数:
取fd_set长度为1个字节,fd_set中的每一个bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。

  • 执行FD_ZERO(&fd_set);表示把集合清0,用位表示位0000 0000
  • 执行FD_SET(fd,&fd_set);表示吧fd加入到需要监听的集合中,如果有三个集合分别为fd=1,fd=3,fd=5.则fd_set用位表示变成0001 0101。
  • select(6,&set,0,0,0)阻塞等待
  • 如果5没有发生事件,只有1和3发生事件,则select返回后,fd_set变成0000 0101。未发生事件的fd被清空。由于会把之前加入集合但是没有发生时间的fd清空所以,在select之前会使用一个fds_arry数组来保存fd,每次开始再把fds_arry中的数据加入fd_set。

第三个参数设置select的超时事件NULL表示永不超时时间设置是通过结构体来实现
struct timeval
{
long tv_sec; //seconds
long tv_usec; //microseconds
};

函数返回:

  • 当监视的相应的文件描述符集中满足条件时,比如说读文件描述符集中有数据到来时,内核(I/O)根据状态修改文件描述符集,并返回 发生事件的文件描述符。
  • 当超时后则返回0。
  • 发生错误返回负值。

select的缺点:

  • 单个进程能够监视的文件描述符的数量存在最大限制,在Linux内核有个参数__FD_SETSIZE定义了每个FD_SET的句柄个数中通常是1024,可以通过setrlimit()、修改宏定义等方法提示限制,但由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差。
  • 内核 / 用户空间内存拷贝问题,select需要复制大量的句柄数据结构,产生巨大的开销;
  • select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件,假设数组中只有7、8、9三个文件描述符发生事件但是select还是回访问前面的0-7个文件描述符;

1.2 select流程图

在这里插入图片描述

1.3 select编写socket服务器程序

/*********************************************************************************
 *      Copyright:  (C) 2021 jiaoer237
 *                  All rights reserved.
 *
 *       Filename:  socket_server_select.c
 *    Description:  This file 
 *                 
 *        Version:  1.0.0(11/23/2021)
 *         Author:  yanp <2405204881@qq.com>
 *      ChangeLog:  1, Release initial version on "11/23/2021 04:35:39 PM"
 *                 
 ********************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <ctype.h>
#include <time.h>
#include <pthread.h>
#include <getopt.h>
#include <libgen.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#define MSG_STR "Hello yanp\n"
#define BACKLOG 13

void printf_usage(char *program);
int socket_init(char *listen_ip,int listen_port);

int main(int argc,char **argv)
{
    int daemon_run=0;
    char *program;
    int serv_port=0;
    int listen_fd;
    int clifd=-1;
    int rv=-2;
    int opt;
    fd_set rdset;
    int fds_array[1024];
    int i,maxfd=0;
    struct timeval timeout={300,0};/*设置超时时间为300s*/
    char buf[1024];
    struct sockaddr_in cliaddr;
    socklen_t cliaddr_len;

    struct option long_options[] =
    {
        {"daemon", no_argument, NULL, 'b'},
        {"port", required_argument, NULL, 'p'},
        {"help", no_argument, NULL, 'h'},
        {NULL, 0, NULL, 0}
    };

    program=argv[0];
    while((opt = getopt_long(argc, argv, "bp:h", long_options, NULL)) != -1)
    {
        switch(opt)
        {
            case 'b':
                daemon_run=1;
                break;
            case 'p':
                serv_port = atoi(optarg);/*将字符串转换成整型*/
                break;
            case 'h':
                printf_usage(program);
                break;
            default:
                break;
        }
    }
    if(!serv_port)
    {
        printf_usage(argv[0]);
        return -1;
    }
    if((listen_fd=socket_init(NULL,serv_port))<0)/*socket初始化*/
    {
        printf("socket_init failure error:%s",strerror(errno));
        return -2;
    }
    if(daemon_run)
    {
        daemon(0,0);
    }
    for(i=0;i<1024;i++)/*创建一个存放文件描述符的数组并赋值为-1*/
    {
        fds_array[i]=-1;
    }
    fds_array[0]=listen_fd;/*将listen_fd作为第一个需要监听的文件描述符*/

    while(1)
    {
        printf("start accept new client income or client send message...\n");
        FD_ZERO(&rdset);/*将rdset清0*/
        for(i=0;i<1024;i++)/*更新fd_set和maxfd,将数组fds_arry中的fd加入fd_set集合中*/
        {
            if(fds_array[i]<0)
            continue;

            maxfd=fds_array[i]>maxfd?fds_array[i]:maxfd;
            FD_SET(fds_array[i],&rdset);
        }

        rv=select(maxfd+1,&rdset,NULL,NULL,&timeout);/*这里只关心rdset,每当有fd发生事件则返回*/
        if(rv<0)
        {
            printf("select failure:%s\n",strerror(errno));
            continue;
        }
        else if(0==rv)
        {
            printf("select timeout\n");
            continue;
        }
        else
        {
            if(FD_ISSET(listen_fd, &rdset))/*listen_fd发生事件,新的客户端连接*/
            {
                if((clifd=accept(listen_fd,(struct sockaddr *)&cliaddr,&cliaddr_len))<0)
                {
                    printf("listen_fd accept new client failure:%s",strerror(errno));
                    return -5;
                }
                int found=0;
                for(i=0;i<1024;i++)/*只要数组元素为-1则表示数组的这个位可用,则将数组的元素设置为fd的值,保存在数组中,用于更新select的fd_set,且select的个数不能大于1024个*/
                {
                    if(fds_array[i]<0)
                    {
                        printf("accept new client[%d] and add it to arry\n",clifd);
                        fds_array[i]=clifd;
                        found=1;
                        break;
                    }
                }
                if(!found)
                {
                    printf("accept new client[%d],but arry full\n",clifd);
                    close(clifd);
                }
            }
            else/*已经连接的客户端发生反应则发生读写操作*/
            {
                for(i=0;i<1024;i++)
                {
                    if(fds_array[i]<0||!FD_ISSET(fds_array[i],&rdset))
                        continue;
                    if((rv=read(fds_array[i],buf,sizeof(buf)))<=0)
                    {
                        printf("read from client[%d] failure\n",fds_array[i]);
                        close(fds_array[i]);
                        fds_array[i]=-1;
                    }
                    else
                    {
                        int j;
                        printf("socket[%d] read get %d bytes data\n", fds_array[i], rv);
                        for(j=0; j<rv; j++)
                        {
                            buf[j]=toupper(buf[j]);
                        }
                        if( write(fds_array[i], buf, rv) < 0 )
                        {
                            printf("socket[%d] write failure: %s\n", fds_array[i], strerror(errno));
                            close(fds_array[i]);
                            fds_array[i] = -1;
                        }
                    }
                }
            }
        }
    }
    


   
}

void printf_usage(char *program)
{
    printf("使用方法:%s【选项】 \n", program);
    printf(" %s是一个服务器程序,用来等待客户端的连接\n",program);
    printf("\n传入参数\n");
    printf(" -b[daemon]设置程序在后台运行\n");
    printf(" -p[port ] 指定连接的端口号\n");
    printf(" -h[help ] 打印帮助信息\n");
    printf("\n例如: %s -b -p 8900\n", program);
    return;

}
int socket_init(char *listen_ip,int listen_port)
{
    int listenfd;
    struct sockaddr_in servaddr;
                

    if((listenfd=socket(AF_INET,SOCK_STREAM,0))<0)
    {
        printf("socket_server to create a TCP socket fd failure:[%s]\n",strerror(errno));
        return -1;
    }
    printf("create a tcp socket fd[%d] success\n",listenfd);

    int on=1;  
    if((setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)))<0)  
    {
        printf("setsockopt failure:%s",strerror(errno));
        return -2;
    }

    memset(&servaddr,0,sizeof(servaddr));
    servaddr.sin_family=AF_INET;
    servaddr.sin_port=htons(listen_port);
    if(!listen_ip)
    {
        servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
    }
    else                         
    { 
        servaddr.sin_addr.s_addr=htonl(listen_port);
    }                                               
    if(bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr))<0)
    { 
        printf("socket[%d] bind on port[%d] for ip address failure:%s\n",listenfd,listen_port,strerror(errno));
        return -3;
    }
    printf("socket[%d] bind on port[%d] for ip address success\n",listenfd,listen_port);
    listen(listenfd,BACKLOG);
    printf("start listen on port[%d]\n",listen_port);
    return listenfd;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
select多路复用是一种IO模型,可以在一个线程内处理多个socket的IO请求。它通过将需要进行IO操作的socket加入到select中,并阻塞等待select()系统调用返回。当有数据到达时,socket被激活,select函数返回,用户线程可以发起read请求来读取数据并继续执行。\[1\] 使用select的优势在于可以使用单个进程接收多个客户端的请求,并且具有良好的跨平台支持。但select也有一些缺点,比如调用select会将文件描述符从用户态拷贝到内核态,并且内核需要遍历所有的文件描述符,文件描述符越多,系统开销越大。此外,单个进程能够监视的文件描述符数量也存在限制,通常为1024。\[2\] 在程序中使用select函数来实现I/O多路复用时,可以使用fd_set类型来存储多个文件描述符。当某个文件描述符上有可读事件时,select会返回,然后程序可以对所有处于读就绪状态的文件描述符进行处理。\[3\] #### 引用[.reference_title] - *1* *2* [多路复用select](https://blog.csdn.net/weixin_43001046/article/details/97109602)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [select函数实现多路io复用](https://blog.csdn.net/weixin_42352787/article/details/130146859)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值