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;
}

 

  

posted on 2021-12-13 14:32  飘杨......  阅读(1076)  评论(0编辑  收藏  举报