1. 程式人生 > >I/O多路轉接之select

I/O多路轉接之select

在完成I/O操作時,程式中完成真正I/O的時間可能只有少的一部分,而大部分時間都處於一個等的時間。比如,此時需要從一個套接字中讀取資料read(socket, buf, BUFSIZE); 這個操作可能會一直阻塞,直到有資料從網路的另一端傳送過來。等的時間過於長,這是I/O效率低下的真正原因。可能有人會提出讓程式碼不要阻塞的等,可以進行非阻塞的等待,比如當read一個socket發現沒有資料時,就不在等待,而去read其他的socket,進行一種輪詢式的read。可是這種模式還是會進行read的這個操作,不過這時進行操作時不成功的話就去read其他socket,效率還是低下的。 為了解決上述問題,提出了I/O多路轉接
。它的做法是這樣的,一次等多個檔案描述符,當有一個或者多個檔案描述符就緒,可以進行I/O操作時,便返回通知有哪些那些檔案描述符可以I/O。
系統提供了select函式實現多路複用輸入/輸出模型,select系統呼叫可以監視多個檔案描述符的狀態變化。程式會停在select這裡等待,直到被監視的檔案描述符至少有一個的狀態發生了變化。 函式的定義:

引數描述:
  • nfds:要關心的檔案描述符
  • readfds:表示要監視檔案描述符集中,所有檔案描述符的讀狀態
  • writefds:表示要監視檔案描述符集中,所有檔案描述符的寫狀態
  • exceptfds:表示要監視檔案描述符集中,所有檔案描述符的異常狀態
  • timeout:監視多長時間,
  1. 當timeout被設定為0,表示以非阻塞方式等待;
  2. 當timeout被設定為大於0的數字,則表示其等待時間,有秒和毫秒的區分
                     struct timeval                     {                          long tv_sec;//秒數                          long tv_usec;//微秒數                     }
  1. 當timeout設定為NULL時,表示已阻塞方式等待。 
返回值:
  1. 當監視的檔案描述符集中有檔案描述符就緒,則會返回一個大於0的數
  2. 當監視的檔案描述符沒有任何一個就緒時,並且指定的時間已到,返回0
  3. 當函式調用出錯時,返回-1        
fd_set: select()機制中提供一fd_set的資料結構,可以理解為一個集合,實際上是一個位圖,每一個特定位來標誌相應大小檔案描述符,這個集合中存放的是檔案描述符,即就是檔案控制代碼(不管是socket控制代碼,還是其他檔案或命名管道或裝置控制代碼)建立聯絡,建立聯絡的工作由程式設計師完成。 系統提供了四個巨集函式對fd_set進行操作 FD_CLR用於將fd_set中fd對應的位關閉。 FD_ISSET判斷fd是否在fd_set中 FD_SET將fd新增進fd_set中 FD_ZERO將fd_set清空 基於select實現的網路伺服器和客戶端 server:
#include<stdio.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<sys/select.h>
#include<error.h>
#include<unistd.h>
#include<sys/types.h>
#define NUMS 1024
static void Usage(char* proc)
{
    printf("Usage: %s [local_ip] [local_port]\n", proc);
}

int startup(char* ip, int port)
{
    //建立檔案特性
    //AF_INET ipv4,SOCK_STREAM,基於位元組流服務
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0)
    {
        perror("socket");
        return 2;
    }
    printf("sock =  %d\n", sock);

    struct sockaddr_in local;
    //確定地址協議型別
    local.sin_family = AF_INET;
    //繫結埠
    local.sin_port = htons(port);
    //繫結ip
    local.sin_addr.s_addr = inet_addr(ip);
    //繫結網路特性
    if (bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
    {
        perror("bind");
        return 3;
    }
    //監聽套接字
    if (listen(sock, 10) < 0)
    {
        perror("listen");
        return 4;
    }
    return sock;
}

//初始化檔案描述符陣列 
void Init(int *fds)
{
    int i = 0;
    for (; i < NUMS; i++)
    {
        fds[i] = -1;
    }
}

//將檔案描述符陣列所儲存的檔案描述符設定進檔案描述符集
int Addfd(int *fds, fd_set *set)
{
    int i = 0;
    int maxfd = fds[0];
    for (; i < NUMS; i++)
    {
        if (fds[i] != -1)
        {
            FD_SET(fds[i], set);
            if (maxfd < fds[i])
            {
                maxfd = fds[i];
            }
        }
    }
    //返回最大的檔案描述符
    return maxfd;
}
int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        return 5;
    }

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

    //建立一個輔助的空間(一個數組)用於儲存 檔案描述符資訊
    //fds中儲存的檔案描述符將會被監聽讀狀態
    int fds[NUMS];
    //wfd中儲存的檔案描述符將會被監聽寫狀態
    int wfd[NUMS];
    //建立檔案描述符集
    fd_set set;
    fd_set wset;
    //將陣列初始化
    Init(fds);
    Init(wfd);
    //監聽listen_sock新增進陣列
    fds[0] = listen_sock;
    printf("listen_sock:%d\n", fds[0]);

    while (1)
    {
        //初始化檔案描述符集
        FD_ZERO(&set);
        FD_ZERO(&wset);
        int maxrfd = -1;
        int maxwfd = -1;
        //將存於陣列中的檔案描述符新增至檔案描述符集
        maxrfd = Addfd(fds, &set);
        maxwfd = Addfd(wfd, &wset);
        struct timeval timeread = { 2, 0 };
        struct timeval timewrite = { 0, 0 };
        if (maxrfd < maxwfd)
            maxrfd = maxwfd;
        //監視set中的讀狀態資訊,監視wset中的寫狀態資訊
        int rres = select(maxrfd + 1, &set, &wset, NULL, &timeread);
        switch (rres)
        {
            case -1:
            {
                   perror("selete");
                   break;
            }
            case 0:
            {
                  printf("Timeout...\n");
                  break;
            }
            default:
            {//至少有一個檔案描述符狀態就緒
                   int i = 0;
                   for (; i < NUMS; i++)
                   {
                       //此條件滿足,說明客戶端的連線已經就緒
                       if (i == 0 && fds[i] != -1 && \
                           FD_ISSET(fds[i], &set))
                       {
                           struct sockaddr_in client;
                           socklen_t len = sizeof(client);
                           //接受監聽到的套接字
                           int new_sock = accept(listen_sock, \
                               (struct sockaddr*)&client, &len);

                           printf("client [%s] [%d]\n", \
                               inet_ntoa(client.sin_addr), \
                               ntohs(client.sin_port));
                           if (new_sock < 0)
                           {
                               perror("accept");
                               continue;
                           }
                           //連線套接字後,將其新增到陣列中,他將被監聽讀狀態就緒
                           for (i; i < NUMS; i++)
                           {
                               if (fds[i] == -1)
                               {
                                   fds[i] = new_sock;
                                   break;
                               }
                           }
                       }
                       //監視到有檔案描述符的讀狀態就緒,就可以執行讀操作
                       else if (i != 0 && fds[i] != -1 && FD_ISSET(fds[i], &set))
                       {
                           char buf[1024];
                           //從套接字讀取資訊到buf中
                           ssize_t s = read(fds[i], buf, sizeof(buf)-1);
                           if (s > 0)
                           {
                               buf[s] = 0;
                               printf("client#  %s\n", buf);
                               int j = 1;
                               //從這個檔案描述符中讀取資料後,就可以監視它的寫狀態
                               for (; j < NUMS; j++)
                               {
                                   if (wfd[j] == -1)
                                   {
                                       wfd[j] = fds[i];
                                       fds[i] = -1;
                                       break;
                                   }
                               }
                           }
                           else if (s == 0)
                           {
                               close(fds[i]);
                               printf("客戶端已經退出!\n");
                               fds[i] = -1;
                           }
                           else
                           {
                               perror("read");
                               close(fds[i]);
                               fds[i] = -1;
                           }
                       }
                       //監視到有檔案描述符的寫狀態就緒,就可以進行寫操作
                       if (wfd[i] != -1 && FD_ISSET(wfd[i], &wset))
                       {
                           char buf[1024];
                           printf("Please Enter# ");
                           fflush(stdout);
                           //從鍵盤輸入資訊到buf中
                           ssize_t _s = read(0, buf, sizeof(buf)-1);
                           if (_s > 0)
                           {
                               buf[_s - 1] = 0;
                               //傳送資訊到套接字
                               write(wfd[i], buf, strlen(buf));
                               //寫完之後,就可等待其回覆,監視其讀狀態
                               int j = 0;
                               for (; j < NUMS; j++)
                               {
                                   if (fds[j] == -1)
                                   {
                                       fds[j] = wfd[i];
                                       wfd[i] = -1;
                                       break;
                                   }
                               }
                           }
                           else
                           {
                               perror("read");
                               close(wfd[i]);
                               wfd[i] = -1;
                               break;
                           }

                       }
                   }
            }
        }
    }
    return 0;
}
client:
#include<stdio.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<unistd.h>
#include<sys/types.h>
#define NUMS 1024
static void Usage(char *proc)
{
    printf("Usage %s [server_ip] [server_port]\n", proc);
}

void Init(int *fds)
{
    int i = 0;
    for (; i < NUMS; i++)
    {
        fds[i] = -1;
    }
}

int Addfd(int *fds, fd_set *set)
{
    int i = 0;
    int maxfd = fds[0];
    for (; i < NUMS; i++)
    {
        if (fds[i] != -1)
        {
            FD_SET(fds[i], set);
            if (maxfd < fds[i])
            {
                maxfd = fds[i];
            }
        }
    }
    return maxfd;
}
int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        return 1;
    }
    //建立套接字
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0)
    {
        perror("socket");
        return 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("connect111");
        return 3;
    }
    //建立一個輔助的空間(一個數組)用於儲存 檔案描述符資訊
    //wfd中儲存的檔案描述符將會被監聽寫狀態
    int wfd[NUMS];
    //rfd中儲存的檔案描述符將會被監聽讀狀態
    int rfd[NUMS];
    //初始化陣列
    Init(wfd);
    Init(rfd);
    //客戶端先寫,所以將sock新增進wfd中
    wfd[0] = sock;
    //建立檔案描述符集
    fd_set rset;
    fd_set wset;
    char buf[1024];
    while (1)
    {
        //清空檔案檔案描述符集
        FD_ZERO(&rset);
        FD_ZERO(&wset);
        int maxrfd = -1;
        int maxwfd = -1;
        //將存於陣列中的檔案描述符新增至檔案描述符集
        maxrfd = Addfd(rfd, &rset);
        maxwfd = Addfd(wfd, &wset);
        struct timeval timeread = { 2, 0 };
        struct timeval timewrite = { 0, 0 };
        if (maxrfd < maxwfd)
            maxrfd = maxwfd;
        
        //監視rset中的讀狀態資訊,監聽wset中的寫狀態資訊
        int res = select(maxrfd + 1, &rset, &wset, NULL, &timeread);
        switch (res)
        {
        case -1:
        {
                   perror("selete");
                   break;
        }
        case 0:
        {
                  printf("Timeout...\n");
                  break;
        }
        default:
        {
                   int i = 0;
                   for (; i < NUMS; i++)
                   {
                       //寫狀態就緒
                       if (wfd[i] != -1 && FD_ISSET(wfd[i], &wset))
                       {
                           printf("Please Enter# ");
                           fflush(stdout);
                           //從鍵盤寫入內容到緩衝區
                           ssize_t s = read(0, buf, sizeof(buf)-1);
                           if (s >0)
                           {
                               buf[s - 1] = 0;
                               printf("server# ");
                               fflush(stdout);
                               //將緩衝區內容通過套接字傳送到伺服器
                               write(wfd[i], buf, strlen(buf));
                               int j = 0;
                               //寫操作完成,應該進行讀操作,監視其讀操作
                               for (; j < NUMS; j++)
                               {
                                   if (rfd[j] == -1)
                                   {
                                       rfd[j] = wfd[i];
                                       wfd[i] = -1;
                                       break;
                                   }
                               }
                           }
                           else
                           {
                               FD_CLR(wfd[i], &wset);
                               close(wfd[i]);
                               return 5;
                           }
                       }
                       if (rfd[i] != -1 && FD_ISSET(rfd[i], &rset))
                       {

                           //在從套接字中讀取伺服器的迴應資訊
                           ssize_t _s = read(sock, buf, sizeof(buf)-1);
                           if (_s > 0)
                           {
                               buf[_s] = 0;
                               printf("%s\n", buf);
                               int j = 0;
                               //讀操作完成,應該進行寫操作,監視其寫操作
                               for (; j < NUMS; j++)
                               {
                                   if (wfd[j] == -1)
                                   {
                                       wfd[j] = rfd[i];
                                       rfd[i] = -1;
                                       break;
                                   }
                               }
                           }
                           if (_s < 0)
                           {
                               FD_CLR(rfd[i], &rset);
                               close(rfd[i]);
                               perror("read");
                               return 4;
                           }
                       }
                   }
        }
        }
    }
    return 0;
}