select服務器

unix下,可將I/O分爲五種模型:

  1. 阻塞I/O
  2. 非阻塞I/O
  3. I/O複用(多路轉接:select、(e)poll)
  4. 信號驅動
  5. 異步I/O

其中前四種I/O模型爲同步I/O,最後一個爲異步I/O,而一個I/O操作可分爲兩步:

  1. 等待數據就緒;
  2. 數據的搬移.

而高性能I/O則體現在如何減少等待的時間,即在I/O模型中的I/O複用則是通知底層I/O就緒的高效方法。
系統提供select函數來實現多路複用輸入/輸出模型,select系統調用是用來讓我們的程序監視多個文件句柄的狀態變化的。程序會停在select這裏等待,直到被監視的文件句柄有一個或多個發生了狀態改變則返回(關於文件句柄,即爲文件描述符,最熟悉的句柄是0、1、2三個,0是標準輸入,1是標準輸出,2是標準錯誤輸出。0、1、2是整數表示的,對應的FILE *結構的表示就是stdin、stdout、stderr).

此處要注意select只是負責I/O操作中的等待,其監測多個句柄,若此時一個或多個句柄的讀、寫、異常事件就緒,操作系統通知,select返回,進行讀寫等操作,此時讀寫則不會阻塞。

由於select一次監測多個文件描述符,所以其操作是以文件描述符集fd_set表示,其運用位圖實現。
函數如下:

#include <sys/select.h>
int select(int nfds,fd_set* readfds,fd_set* writefds,fd_set* exceptfds,struct timeval* timeout);

參數如下:
nfds:監測的文件描述符集中最大的文件描述符+1;
readfds/writefds/exceptfds:爲輸入輸出型參數,分別代表讀事件文件描述符集,寫事件文件描述符集,異常事件文件描述符集,以readfds爲例,輸入時,代表其關心的特定fd讀事件,若有一個讀事件就緒,則返回;輸出時,特定fd上的讀事件是否發生,後兩個參數含義相同。
timeout:結構爲timeval,用來設置select()的等待時間,其結構定義如下:

struct timeval
{
    long tv_sec;   //秒
    long tv_usec;  //微秒
}

如果參數timeout設爲:
NULL:則表示select()沒有timeout,select將一直被阻塞,直到某個文件描述符上發生了事件;
0:僅檢測描述符集合的狀態,然後立即返回,並不等待外部事件的發生。
特定的時間值:如果在指定的時間段裏沒有事件發生,select將超時返回。
返回值:
執行成功則返回文件描述詞狀態已改變的個數;
如果返回0代表在描述詞狀態改變前已超過timeout時間,沒有返回;
當有錯誤發生時則返回-1,錯誤原因存於errno,此時參數readfds,writefds,exceptfds和timeout的值變成不可預測。

以下爲操作文件描述符集的宏函數:

FD_CLR(int fd,fd_set* set);⽤//用來清除文件描述符集set中相關fd 的位
FD_ISSET(int fd,fd_set *set);//用來測試文件描述符集set中相關fd的位是否爲真
FD_SET(int fd,fd_set*set);⽤//用來設置文件描述符集set中相關fd的位;
FD_ZERO(fd_set *set);⽤//用來清除文件描述符集set的全部位

select服務器的優點:

  1. 其爲單進程服務器,卻可以處理多個客戶端請求,不需要多進程多線程處理,從而性能高效,cpu調度壓力小,資源佔用少.
  2. 其一次可以檢測多個句柄,只要有一個狀態改變則返回,從而效率高,等待時間少

select服務器的缺點:

  1. 每次調用select,都需要把fd集合從用戶態拷貝到內核態,返回時要將其從內核態切回用戶態,開銷在fd很多時會很大;
  2. 同時每次調用select都需要在內核遍歷傳遞進來的所有fd,這個開銷在fd很多時也很大,性能降低;
  3. select支持的文件描述符數量有上限,linux默認是1024.

代碼如下:
select_server.c:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/select.h>

int fds[sizeof(fd_set)*8];       //定義中轉數組大小 
static void usage(const char* proc)
{
    printf("usage:%s [local_ip] [local_port]\n",proc);
}

int startup(char* ip,int port)    //創建監聽套接字 
{
    int sock=socket(AF_INET,SOCK_STREAM,0);
    if(sock<0)
    {
        perror("socket");
        close(sock);
        exit(2);
    }

    int opt=1;
    setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));      //服務器掛機立即啓動 

    struct sockaddr_in server;
    server.sin_family=AF_INET;
    server.sin_port=htons(port);
    server.sin_addr.s_addr=inet_addr(ip);
    if(bind(sock,(struct sockaddr*)&server,sizeof(server))<0)
    {
        perror("bind");
        close(sock);
        exit(3);
    }

    if(listen(sock,10)<0)
    {
        perror("listen");
        close(sock);
        exit(4);
    }

    return sock;
}

int main(int argc,char* argv[])
{
    if(argc != 3)    //命令行使用方法 
    {
        usage(argv[0]);
        return 1;
    }

    int nums=sizeof(fds)/sizeof(fds[0]);           //數組大小 
    int i=0;
    for( ;i<nums;++i)
    {
        fds[i]=-1;   //初始化爲-1 
    }

    int listen_sock=startup(argv[1],atoi(argv[2]));        //監聽套接字 

    fds[0]=listen_sock;
    fd_set rfds;     //讀文件描述符集 
    fd_set wfds;    //寫事件描述符集 

    printf("fd_set# %d\n",sizeof(fd_set)*8);
    while(1)
    {
        FD_ZERO(&rfds);         //清0 
        FD_ZERO(&wfds);

        int maxfd=-1;
        int i=0;
        for(;i<nums;++i)
        {
            if(fds[i]==-1)
            {
                continue;
            }

            FD_SET(fds[i],&rfds);       //將數組中的文件描述符加入讀文件描述符集中 

            if(maxfd<fds[i])
            {
                maxfd=fds[i];    //取得最大文件描述符 
            }
        }

        struct timeval timeout={2,0};
        switch(select(maxfd+1,&rfds,&wfds,NULL,&timeout))    //select函數I/O等待rfds裏讀事件與wfds的寫事件 
        {
            case -1:
                perror("select");
                break;
            case 0:
                printf("timeout\n");
                break;
            default:        //有一個fd等待成功,返回 
                {
                    i=0;
                    for(;i<nums;++i)
                    {
                        struct sockaddr_in client;
                        int len=sizeof(client);
                        if(i==0 && FD_ISSET(fds[i],&rfds))    //若爲監聽套接字則創建連接 
                        {
                            int new_sock=accept(listen_sock,(struct sockaddr*)&client,&len);

                            if(new_sock<0)
                            {
                                perror("accept");
                                close(new_sock);

                            }
                            else
                            {
                                printf("get a new client:[%s:%d]\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));

                                int j=1;
                                for(;j<nums;++j)
                                {
                                    if(fds[j]==-1)
                                        break;
                                }
                                if(j==nums)
                                {
                                    close(new_sock);
                                    continue;
                                }
                                fds[j]=new_sock;            //將新的套接字寫入數組,下次更新到文件描述符集中rfds 
                            }

                        }
                        else if(i !=0 &&FD_ISSET(fds[i],&rfds))    //正常文件描述符集就緒則讀 
                        {
                            char buf[1024];
                            ssize_t s=read(fds[i],buf,sizeof(buf)-1);
                            if(s>0)                  //讀成功 
                            {
                                buf[s]=0;
                                printf("client# %s\n",buf);

                                FD_SET(fds[i],&wfds);    //則此fd要關心寫事件,加入寫文件描述符集中 
                            }
                            else if(s==0)
                            {
                                printf("client close!!!\n");
                                close(fds[i]);
                                fds[i]=-1; 
                            }
                            else
                            {
                                perror("read");
                                continue; 
                            }
                        }
                        if(i != 0 && FD_ISSET(fds[i],&wfds))        //若寫事件就緒則寫 
                        {
                            const char* msg="I am s erver!!!\n";
                            ssize_t s=write(fds[i],msg,strlen(msg));
                            if(s<0)
                            {
                                perror("write"); 
                                close(fds[i]);
                                fds[i]=-1;
                                continue;
                            }

                            FD_CLR(fds[i],&wfds);    //寫完將此fd清除於wfds(即讀完一個增加一個,寫完一個清除一個) 
                        }

                    }
                }
                break;
        }
    }
    close(listen_sock);
    return 0;
}

select_client.c:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/select.h>

static void usage(const char* porc)
{
    printf("usage:%s [server_ip] [server_port]\n",porc);
}
int main(int argc,char* argv[])
{
    if(argc != 3)   //用法 
    {
        usage(argv[0]);
    }

    int sock=socket(AF_INET,SOCK_STREAM,0);
    if(sock<0)
    {
        perror("socket");
        exit(2);
    }

    struct sockaddr_in server;
    server.sin_family=AF_INET;
    server.sin_port=htons(atoi(argv[2]));
    server.sin_addr.s_addr=inet_addr(argv[1]);
    if(connect(sock,(struct sockaddr*) &server,sizeof(server))<0)    //連接 
    {
        perror("connect");
        exit(3);
    }

    char buf[1024];
    while(1)
    {
        printf("please Enter# ");
        fflush(stdout);

        int fd=dup(1);    //先使fd也指向標準輸出(1)stdout 

        ssize_t s=read(0,buf,sizeof(buf)-1);
        if(s>0)
        {
            buf[s-1]=0;
            //write(sock,buf,strlen(buf));

            //dup2
            close(1);
            dup2(sock,1);    // 改變1內容指針,同指向sock 
            printf("%s",buf);   //則此時打印到標準輸出的內容將打印到網絡sock中 
            fflush(stdout);

        }
        else
        {
            perror("read");
            exit(4);
        }

        dup2(fd,1);     //使1再次指向標準輸出,恢復 
        ssize_t _s=read(sock,buf,sizeof(buf)-1);
        if(_s>0)
        {
            buf[s]=0;
            printf("server# %s\n",buf);
        }

    }
    close(fd);
    close(sock);
    return 0;
}

結果如下:

這裏寫圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章