Linux下的socket編程實踐(七)I/O多工技術之select模型

來源:互聯網
上載者:User

Linux下的socket編程實踐(七)I/O多工技術之select模型

在進入今天的select模型的主題之前,我們先來簡單瞭解一下五種I/O模型:

(1)阻塞I/O(預設採用這種方式)

在服務端socket編程中,我們常見的accpet函數、recv函數都是採取的阻塞形式。以recv為例: 當上層應用App調用recv系統調用時,如果對等方沒有發送資料(Linux核心緩衝區中沒有資料),上層應用Application1將阻塞;當對等方發送了資料,Linux核心recv端緩衝區資料到達,核心會把資料copy給使用者空間。然後上層應用App解除阻塞,執行下一步操作。

(2)非阻塞I/O(不推薦)

 

 

上層應用如果應用非阻塞模式, 會迴圈調用recv函數,接受資料。若緩衝區沒有資料,上層應用不會阻塞,recv傳回值為-1,錯誤碼是EWOULDBLOCK(圖中的標記有誤)。上層應用程式不斷輪詢有沒有資料到來。造成上層應用忙等待。大量消耗CPU。因此非阻塞模式很少直接用。應用範圍小,一般和IO複用配合使用。

(3)訊號驅動I/O模型(不經常使用)

 

上層應用建立SIGIO訊號處理常式。當緩衝區有資料到來,核心會發送訊號告訴上層應用App; 當上層應用App接收到訊號後,調用recv函數,因緩衝區有資料,recv函數一般不會阻塞。但是這種用於模型用的比較少,屬於典型的“拉模式(上層應用被動的去Linux核心空間中拉資料)”。即:上層應用App,需要調用recv函數把資料拉進來,會有時間延遲,我們無法避免在延遲時,又有新的訊號的產生,這也是他的缺陷。

(4)非同步I/O(不常用)

上層應用調用aio_read函數,同時提交一個應用程式層的緩衝區buf;調用完畢後,不會阻塞。上層應用程式App可以繼續其他任務; 當TCP/IP協議緩衝區有資料時,Linux主動的把核心資料copy到使用者空間。然後再給上層應用App發送訊號;告訴App資料到來,需要處理!

非同步IO屬於典型的“推模式”, 是效率最高的一種模式,上層應用程式App有非同步處理的能力(在Linux核心的支援下,處理其他任務的同時,也可支援IO通訊, 與Windows平台下的完成連接埠作用類似IOCP)。

(5)I/O複用的select模型(本篇的重點)

 

試想如果你遇到下面的問題會怎麼處理?

1)server除了要對外響應client的服務外,還要能夠接受標準輸入的命令來進行管理。

假如使用上述阻塞方式,在單線程中,accept調用和read調用必定有先後順序,而它們都是阻塞的。比如先調用accept,後調用 read,那麼如果沒有客戶請求時,伺服器會一直阻塞在accept,沒有機會調用read,也就不能響應標準輸入的命令。

2) server要對外提供大量的client請求服務。

假如使用阻塞方式,在單線程中,由於accept和recev都是阻塞式的,那麼當一個client被伺服器accept後,它可能在send發送訊息時阻塞,因此伺服器就會阻塞在recev調用。即時此時有其他的client進行connect,也無法進行響應。

 

這時就需要select來解決啦!select實現的是一個管理者的功能: 用select來管理多個IO, 一旦其中的一個IO或者多個IO檢測到我們所感興趣的事件, select就返回, 傳回值就是偵測到的事件個數, 並且由第2~4個參數返回那些IO發送了事件, 這樣我們就可以遍曆這些事件, 進而處理這些事件。

有人說,我用多線程不就可以了嗎?但是在UNIX平台下多進程模型擅長處理並髮長串連,但卻不適用於串連頻繁產生和關閉的情形。當然select並不是最高效的,有著O(N)的時間複雜度,關於更高效的epoll我將在後面的部落格中繼續講解,歡迎大家關注,╰( ̄▽ ̄)╮

 

#include   #include   #include   #include   int select(int nfds, fd_set *readfds, fd_set *writefds,             fd_set *exceptfds, struct timeval *timeout);

nfds: is the highest-numbered file descriptor in any of the three sets,plus 1[讀,寫,異常集合中的最大檔案描述符+1].

fd_set[四個宏用來對fd_set進行操作]

FD_CLR(int fd, fd_set *set);

FD_ISSET(int fd, fd_set *set);

FD_SET(int fd, fd_set *set);

FD_ZERO(fd_set *set);

timeout[從調用開始到select返回前,會經曆的最大等待時間, 注意此處是指的是相對時間]

/timeval結構:  struct timeval  {      long    tv_sec;     /* seconds */      long    tv_usec;    /* microseconds */  };  //一些調用使用3個空的set, n為0, 一個非空的timeout來達到較為精確的sleep.

Linux中, select函數改變了timeout值,用來指示還剩下的時間,但很多實現並不改timeout。

為了較好的可移植性,timeout在迴圈中一般常被重新賦初值。

Timeout取值:

timeout== NULL

無限等待,被訊號打斷時返回-1, errno 設定成 EINTR

timeout->tv_sec == 0 && tvptr->tv_usec == 0

不等待立即返回

timeout->tv_sec != 0 || tvptr->tv_usec != 0

等待特定時間長度, 逾時返回0

註:關於select用來設定逾時時間的用法可以參考我的另外一篇部落格 http://blog.csdn.net/nk_test/article/details/49050379

傳回值:

如果成功,返回所有sets中描述符的個數;如果逾時,返回0;如果出錯,返回-1.


下面是使用select改進伺服器端和用戶端的程式,解決了上述提出的兩個問題:

伺服器端:

 

/*樣本1: 用select來改進echo回聲伺服器的client端的echoClient函數 使得可以在單進程的情況下同時監聽多個檔案描述符; */  void echoClient(int sockfd)  {      char buf[512];      fd_set rset;      //確保標準輸入不會被重新導向      int fd_stdin = fileno(stdin);      int maxfd = fd_stdin > sockfd ? fd_stdin : sockfd;      while (true)      {          FD_ZERO(&rset);//監視兩個I/O          FD_SET(fd_stdin, &rset);          FD_SET(sockfd, &rset);          int nReady = select(maxfd+1, &rset, NULL, NULL, NULL);  //不需要的置NULL         if (nReady == -1)              err_exit("select error");          else if (nReady == 0)              continue;            /** nReady > 0: 檢測到了可讀事件 **/            if (FD_ISSET(fd_stdin, &rset))          {              memset(buf, 0, sizeof(buf));              if (fgets(buf, sizeof(buf), stdin) == NULL)                  break;              if (writen(sockfd, buf, strlen(buf)) == -1)                  err_exit("write socket error");          }          if (FD_ISSET(sockfd, &rset))          {              memset(buf, 0, sizeof(buf));              int readBytes = readline(sockfd, buf, sizeof(buf));              if (readBytes == 0)              {                  cerr << "server connect closed..." << endl;                  exit(EXIT_FAILURE);              }              else if (readBytes == -1)                  err_exit("read-line socket error");                cout << buf;          }      }  } 

用戶端:

 

 

/*樣本2: 用select來改進echo回射伺服器的server端的接受串連與處理串連部分的代碼: 使得可以在單進程的情況下處理多客戶串連, 對於單核的CPU來說, 單進程使用select處理串連與監聽通訊端其效率不一定就會比多進程/多線程效能差; */      struct sockaddr_in clientAddr;      socklen_t addrLen;      int maxfd = listenfd;      fd_set rset;      fd_set allset;      FD_ZERO(&rset);      FD_ZERO(&allset);      FD_SET(listenfd, &allset);         int client[FD_SETSIZE];  //用於儲存已串連的用戶端通訊端     for (int i = 0; i < FD_SETSIZE; ++i)          client[i] = -1;      int maxi = 0;   //用於儲存最大的不閒置位置, 用於select返回之後遍曆數組        while (true)      {          rset = allset;          int nReady = select(maxfd+1, &rset, NULL, NULL, NULL);          if (nReady == -1)          {              if (errno == EINTR)                  continue;              err_exit("select error");          }          //nReady == 0表示逾時, 但是此處是一定不會發生的          else if (nReady == 0)              continue;            if (FD_ISSET(listenfd, &rset))          {              addrLen = sizeof(clientAddr);              int connfd = accept(listenfd, (struct sockaddr *)&clientAddr, &addrLen);              if (connfd == -1)                  err_exit("accept error");                int i;              for (i = 0; i < FD_SETSIZE; ++i)              {                  if (client[i] < 0)                  {                      client[i] = connfd;                      if (i > maxi)                          maxi = i;                      break;                  }              }              if (i == FD_SETSIZE)              {                  cerr << "too many clients" << endl;                  exit(EXIT_FAILURE);              }              //列印客戶IP地址與連接埠號碼              cout << "Client information: " << inet_ntoa(clientAddr.sin_addr)                   << ", " << ntohs(clientAddr.sin_port) << endl;              //將串連套介面放入allset, 並更新maxfd              FD_SET(connfd, &allset);              if (connfd > maxfd)                  maxfd = connfd;                if (--nReady <= 0)                  continue;          }            /**如果是已串連套介面發生了可讀事件**/          for (int i = 0; i <= maxi; ++i)              if ((client[i] != -1) && FD_ISSET(client[i], &rset))              {                  char buf[512] = {0};                  int readBytes = readline(client[i], buf, sizeof(buf));                  if (readBytes == -1)                      err_exit("readline error");                  else if (readBytes == 0)                  {                      cerr << "client connect closed..." << endl;                      FD_CLR(client[i], &allset);                      close(client[i]);                      client[i] = -1;                  }                  //注意此處: Server從Client擷取資料之後並沒有立即回射回去,                  //        而是等待四秒鐘之後再進行回射                  sleep(4);                  cout << buf;                  if (writen(client[i], buf, readBytes) == -1)                      err_exit("writen error");                    if (--nReady <= 0)                      break;              }      } 

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.