多路IO转接服务器实现方法一:select()函数

采用多进程与多线程的方法来实现并发服务器时,监听的工作由server应用程序自身通过accept函数不断去监听。当客户端连接较多时,这种方法会大大降低程序执行效率,消耗CPU资源(CPU需要在不同进/线程中切换执行)。
多进程与多线程实现并发服务器方法可以参考以下两篇文章:

因为以上两种方法的局限性所以出现了采用多路IO转接的方式来设计服务器,该类服务器实现的思想为应用程序本身不在监听客户端连接,转而由内核来代替监听。主要使用的方法有三种,其一为 select()函数。

select函数

  1. select能监听的文件描述符个数受限于FD_SETSIZE,一般为1024,单纯改变进程打开的文件描述符个数不能改变select监听文件个数
  2. 解决1024以下个客户端时使用select十分合适,但是如果链接客户端过多,select采用的轮询模型,会大大降低服务器响应效率,应采用其他方法。

函数原型

int select(int nfds,fd_set* readfds,fd_set* writefds,fd_set* exceptfds,struct timeval* timeout);

  • 返回值
    • 成功:返回所监听的所有监听集合中满足条件的总数。
    • 失败:返回-1,并设置errno。
  • 参数
    • nfds:监控的文件描述符集里最大文件描述符加1,此参数会告诉内核检测前多少个文件描述符的状态
    • readfds:监控有读数据到达的文件描述符集合。传入传出参数
    • writefds:监控有写数据到达的文件描述符集合。传入传出参数
    • exceptfds:监控异常发生到达文件描述符集合。传入传出参数
    • timeout:定时阻塞监控时间,3种情况
    1. NULL,永远等下去
    2. 设置timeval,等待固定时间
    3. 设置timeval里时间均为0,检查描述字后立即返回,轮询
    • struct timeval结构体
      struct timeval{
      	long tv_sec;//分
      	long tv_usec;//微秒
      }
      

四个工具函数

想要看的更详细,请参考man手册

//fd_set类型为位图集合
void FD_CLR(int fd, fd_set *set);//将fd从set集合中清除出去,相当于将对应位置为0
int  FD_ISSET(int fd, fd_set *set);//判断fd是否在set集合中,返回1为在集合中,0为假,即不在集合中
void FD_SET(int fd, fd_set *set);//将fd设置到set集合中,相当于将对应位置为1
void FD_ZERO(fd_set *set);//将文件描述符集合清空,位图清空相当于全部置为0

例子

服务端代码实现
特别要举例说的是rset参数,在调用select函数前调用了rset = allset语句,假如此时赋值后rset中有fd1、fd2、fd3三个需要被监听的文件描述符,但是这三个文件描述符中只有fd2文件描述符满足了读的监听条件,那么在调用完select函数后,select函数会将fd1、fd3文件描述符从rset集合中剔除,rset集合中只会存在满足读监听条件的文件描述符。
同样地,readfdswritefdsexceptfds三个传入传出参数皆是如此。

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<string.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<sys/select.h>
#include<sys/types.h>
#include<sys/time.h>
#include<ctype.h>
#define PORT 8888
#define IP "127.0.0.1"
int main(){
    //1、建立套接字
    int sfd = socket(AF_INET,SOCK_STREAM,0);

    //2、绑定
    struct sockaddr_in serv_addr;
    bzero(&serv_addr,sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);
    inet_pton(AF_INET,IP,&serv_addr.sin_addr.s_addr);
    socklen_t serv_len = sizeof(serv_addr);
    int ret = bind(sfd,(struct sockaddr*)&serv_addr,serv_len);
    if(ret != 0){
        printf("bind err:%s\n",strerror(ret));
        exit(1);
    }

    //3、监听
    listen(sfd,128);

    //定义一个客户端数组,用来存放监听文件描述符,以免select到时候循坏0~1023个文件描述符
    int client[FD_SETSIZE];

    //将数组内值全部初始化为-1
    for(int i = 0; i < FD_SETSIZE;i++){
        client[i] = -1;
    }

    fd_set rset,allset;//rset 用来保存满足读监听条件的文件描述符集合,allset用来保存要监听的文件描述符集合
    FD_ZERO(&allset);
    FD_SET(sfd,&allset);
    int nready,i;
    int index = -1;//定义数组下标
    int maxfd = sfd;//定义一个最大文件描述符,select第一个参数:监听的文件描述符集里最大的文件描述符+1
    while(1){
        //rset中存放监听条件为读的文件描述符
        rset = allset;

        //只监听读集合,写集合与异常集合暂时不监听,timeout参数传NULL表示永不超时
        /*select调用后会更改rset集中文件描述符集,只保留符合监听条件的文件描述符*/
        nready = select(maxfd+1,&rset,NULL,NULL,NULL);
        if(nready < 0){
            printf("监听的集合中没有满足条件的文件描述符\n");
        }

        int sockfd;
        //判断sfd是否存在于满足监听条件的集合中
        if(FD_ISSET(sfd,&rset)){
            //如果监听到了有满足条件的文件描述符,开始accept()
            struct sockaddr_in clie_addr;
            socklen_t clie_len = sizeof(clie_addr);
            int cfd = accept(sfd,(struct sockaddr*)&clie_addr,&clie_len);//返回一个新的客户端的文件描述符
            if(cfd == -1){
                perror("accept error");
                exit(2);
            }
            char buf[BUFSIZ];
            printf("%sconnected...;port:%d\n",inet_ntop(AF_INET,&clie_addr.sin_addr.s_addr,buf,sizeof(buf)),ntohs(clie_addr.sin_port));//打印谁连接了上来

            for(i = 0; i < FD_SETSIZE;i++){
                if(client[i] < 0){
                    client[i] = cfd;
                    break;
                }
            }
            //由于select()函数只能有1024个文件描述符,所以进行判断,如果超过了就应该退出
            if(i == FD_SETSIZE){
                printf("too many connecting...\n");
                exit(1);
            }
            FD_SET(cfd,&allset);
            //更新文件描述符集中最大的文件描述符
            if(cfd > maxfd){
                maxfd = cfd;
            }

            if(i > index){
                index = i;
            }

            if(--nready == 0){
                continue;
            }
        }
        //遍历client[i]数组,查看数组中是否需要监听的文件描述符
        int len;
        char rwbuf[BUFSIZ];

        for(i = 0; i <= index;i++)
        {
            if((sockfd=client[i]) < 0){
                //满足此语句说明client[]中i位置是-1,并将client[i]的值赋给sockfd
                continue;
            }
            //如果sockfd文件描述符在rset读文件描述符集中,说明是要监听的
            if(FD_ISSET(sockfd,&rset)){
                len = read(sockfd,rwbuf,sizeof(rwbuf));
                if(len==0){
                    //说明客户端关闭了
                    //将这个文件描述符从allset中清除,在对应的client[]中i位置改回-1
                    printf("------%d--disconnected\n",i);
                    close(sockfd);
                    FD_CLR(sockfd,&allset);
                    client[i] = -1;
                }else if(len > 0){
                    for(int j = 0; j < len;j++){
                        rwbuf[j] = toupper(rwbuf[j]);
                    }
                    write(sockfd,rwbuf,len);
                }
                if(--nready == 0){
                    break;
                }
            }
        }

    }
    close(sfd);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值