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

 

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM