• C语言select实现高并发服务器


    一、概述

      除了使用多线程或者多进程技术,我们是否还可以使用其他的方法来实现服务端连接多个客户端呢?答案是肯定的,那就是多路IO技术select。

    多路IO技术: select, 同时监听多个文件描述符, 将监控的操作交给内核去处理, 
    
    数据类型fd_set: 文件描述符集合--本质是位图(关于集合可联想一个信号集sigset_t)
    int select(int nfds, fd_set * readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
    函数介绍: 委托内核监控该文件描述符对应的读,写或者错误事件的发生.
    参数说明: 
        nfds: 最大的文件描述符+1
        readfds: 读集合, 是一个传入传出参数
            传入: 指的是告诉内核哪些文件描述符需要监控
            传出: 指的是内核告诉应用程序哪些文件描述符发生了变化
        writefds: 写文件描述符集合(传入传出参数)
        execptfds: 异常文件描述符集合(传入传出参数)
        timeout: 
            NULL--表示永久阻塞, 直到有事件发生
            0 --表示不阻塞, 立刻返回, 不管是否有监控的事件发生
            >0--到指定事件或者有事件发生了就返回
        
    返回值: 成功返回发生变化的文件描述符的个数
            失败返回-1, 并设置errno值.
    
    
    /usr/include/x86_64-linux-gnu/sys/select.h和
    /usr/include/x86_64-linux-gnu/bits/select.h
    从上面的文件中可以看出, 这几个宏本质上还是位操作.
    
    void FD_CLR(int fd, fd_set *set);
    将fd从set集合中清除.
    int FD_ISSET(int fd, fd_set *set);
    功能描述: 判断fd是否在集合中
    返回值: 如果fd在set集合中, 返回1, 否则返回0.
    void FD_SET(int fd, fd_set *set);
    将fd设置到set集合中.
    void FD_ZERO(fd_set *set);
    初始化set集合.
    
    调用select函数其实就是委托内核帮我们去检测哪些文件描述符有可读数据,可写,错误发生;

      案例:使用select技术实现高并发聊天服务

    二、代码示例

    //IO多路复用技术select函数的使用
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <errno.h>
    #include <arpa/inet.h>
    #include <netinet/in.h>
    #include <sys/select.h>
    
    int main(){
        int i;//for循环的初始化
        int n;//读取字节个数
        int lfd;//监听文件描述符
        int cfd;//通讯文件描述符
        int ret;
        int nready;
        int maxfd;//最大的文件描述符
        char buf[FD_SETSIZE];
        socklen_t len;
        int maxi;//有效的文件描述符最大值
        int connfd[FD_SETSIZE];//有效文件描述符数组
        fd_set tmpfds,rdfds;//要监控的文件描述符集
        struct sockaddr_in svraddr,cliaddr;
    
        //创建socket
        lfd = socket(AF_INET,SOCK_STREAM,0);
        if(lfd<0){
            perror("socket error");
            return -1;
        }
        //允许端口复用
        int opt = 1;
        setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(int));
    
        //绑定
        svraddr.sin_family = AF_INET;
        svraddr.sin_port = htons(8888);    
        svraddr.sin_addr.s_addr = htonl(INADDR_ANY);
        ret = bind(lfd,(struct sockaddr *)&svraddr,sizeof(struct sockaddr_in));
        if(ret<0){
            perror("bind error");
            return -1;
        }
        //监听
        ret = listen(lfd,5);
        if(ret<0){
            perror("listen error");
            return -1;
        }
    
        //文件描述符集初始化
        FD_ZERO(&tmpfds);
        FD_ZERO(&rdfds);
    
        //将监听文件描述符加入到监控的读集合中
        FD_SET(lfd,&rdfds);
    
        //初始化有效的文件描述符集,为-1表示可用,该数组不保存lfd
        for(i=0;i<FD_SETSIZE;i++){
            connfd[i] = -1;
        }
        maxfd = lfd;
        len = sizeof(struct sockaddr_in);
        //将监听文件描述符lfd加入到select监控中
        while(1){
            //select为阻塞函数,若没有变化的文件描述符,就一直阻塞,若有事件发生则解除阻塞,函数返回
            //select的第二个参数tmpfds为输入输出参数,调用select完毕后这个节后中保留的是发生变化的文件描述符
            tmpfds = rdfds;
            nready = select(maxfd+1,&tmpfds,NULL,NULL,NULL);
            if(nready>0){//文件描述符集有变化
                //发生变化的文件描述符有两类,一类是监听类的,一类是用于数据通信的。
                //监听文件描述符有变化,有新的连接到来,则accept新的连接
                if(FD_ISSET(lfd,&tmpfds)){
                    cfd = accept(lfd,(struct sockaddr *)&cliaddr,&len);
                    if(cfd<0){
                        if(errno==ECONNABORTED||errno==EINTR){
                            continue;
                        }
                        break;
                    }
                    //先找到位置,然后将新的链接的文件描述符保存到connfd数组中
                    for(i=0;i<FD_SETSIZE;i++){
                        if(connfd[i]==-1){
                            connfd[i] = cfd;
                            break;
                        }
                    }
                    //若连接总数达到的最大值
                    if(i==FD_SETSIZE){
                        close(cfd);
                        printf("too many clients,i==[%d]\n",i);
                        continue;
                    }
                    //确保connfd中maxi保存的是最后一个文件描述符的下标
                    if(i>maxi){
                        maxi = i;
                    }
                    //打印客户端的IP和PORT
                    char sIP[16];
                    memset(sIP,0x00,sizeof(sIP));
                    printf("receive from client ---->IP[%s],PORT=[%d]\n",inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,sIP,sizeof(sIP)),htons(cliaddr.sin_port));
                    //将新的文件描述符加入到select监控的文件描述符中
                    FD_SET(cfd,&rdfds);
                    if(maxfd<cfd){
                        maxfd = cfd;
                    }
                    //如果没有变化的文件描述符,则无需执行后续代码
                    if(--nready<=0){
                        continue;
                    }
                }
                //下面是通信文件描述符有变化的情况
                //只需要循环connfd数组中有效的文件描述符即可,这样可以减少循环次数
                for(i=0;i<=maxi;i++){
                    int sockfd = connfd[i];
                    //数组内的文件描述符如果被释放,有可能变为-1
                    if(sockfd==-1){
                        continue;
                    }
                    if(FD_ISSET(sockfd,&tmpfds)){
                        memset(buf,0x00,sizeof(buf));
                        n = read(sockfd,buf,sizeof(buf));
                        if(n<0){
                            perror("read over");
                            close(sockfd);
                            FD_CLR(sockfd,&rdfds);
                            connfd[i] = -1;//将connfd[0]置为-1,表示位置可用
                        }else if(n==0){
                            printf("client is closed\n");
                            close(sockfd);
                            FD_CLR(sockfd,&rdfds);
                            connfd[i] = -1;//将connfd[0]置为-1,表示位置可用
                        }else{
                            printf("[%d]:[%s]\n",n,buf);
                            write(sockfd,buf,n);
                        }
                        if(--nready<=0){
                            break;//注意这里是break,而不是continue,应该是从最外层的while继续循环
                        }
                    }
                }
            }
        }
        //关闭监听文件描述符
        close(lfd);
        return 0;
    }

      

  • 相关阅读:
    纯前端实现导入导出功能excel
    去除对象有undefined的值
    react组件传值
    获取当前日期的前一天
    uniapp之uni.navigateTo路由跳转传参,参数是对象
    解决uniapp代码在小程序中报错问题[ app.json 文件内容错误] app.json: app.json 未找到]
    css实现块级元素水垂直居中的方法
    JS深拷贝和浅拷贝
    VUE新增属性-数据更新页面不更新
    抄也能抄出不一样的代码,我真是个人才
  • 原文地址:https://www.cnblogs.com/tony-yang-flutter/p/15683241.html
Copyright © 2020-2023  润新知