1. 程式人生 > >Linux網路程式設計--epoll 模型原理詳解以及例項

Linux網路程式設計--epoll 模型原理詳解以及例項

1.簡介

Linux I/O多路複用技術在比較多的TCP網路伺服器中有使用,即比較多的用到select函式。Linux 2.6核心中有提高網路I/O效能的新方法,即epoll 。
epoll是什麼?按照man手冊的說法是為處理大批量控制代碼而作了改進的poll。要使用epoll只需要以下的三個系統函式呼叫: epoll_create(2),epoll_ctl(2),epoll_wait(2)。

2.select模型的缺陷

(1) 在Linux核心中,select所用到的FD_SET是有限的
核心中有個引數__FD_SETSIZE定義了每個FD_SET的控制代碼個數:#define __FD_SETSIZE 1024。也就是說,如果想要同時檢測1025個控制代碼的可讀狀態是不可能用select實現的;或者同時檢測1025個控制代碼的可寫狀態也是不可能的。
(2) 核心中實現select是使用輪詢方法
每次檢測都會遍歷所有FD_SET中的控制代碼,顯然select函式的執行時間與FD_SET中控制代碼的個數有一個比例關係,即select要檢測的控制代碼數越多就會越費時

3.Windows IOCP模型的缺陷

windows完成埠實現的AIO,實際上也只是使用內部用執行緒池實現的,最後的結果是IO有個執行緒池,你的應用程式也需要一個執行緒池。很多文件其實已經指出了這引發的執行緒context-switch所帶來的代價。

4.EPOLL模型的優點

(1) 支援一個程序開啟大數目的socket描述符(FD)
epoll沒有select模型中的限制,它所支援的FD上限是最大可以開啟檔案的數目,這個數字一般遠大於select 所支援的2048。下面是我的小PC機上的顯示:
[email protected]:~$ cat /proc/sys/fs/file-max
6815744
那麼對於伺服器而言,這個數目會更大。
(2) IO效率不隨FD數目增加而線性下降
傳統select/poll的另一個致命弱點就是當你擁有一個很大的socket集合,由於網路得延時,使得任一時間只有部分的socket是”活躍”的,而select/poll每次呼叫都會線性掃描全部的集合,導致效率呈現線性下降。但是epoll不存在這個問題,它只會對”活躍”的socket進行操作:這是因為在核心實現中epoll是根據每個fd上面的callback函式實現的。於是,只有”活躍”的socket才會主動去呼叫callback函式,其他idle狀態的socket則不會。在這點上,epoll實現了一個”偽”AIO”,因為這時候推動力在os核心。在一些 benchmark中,如果所有的socket基本上都是活躍的,比如一個高速LAN環境,epoll也不比select/poll低多少效率,但若過多使用的呼叫epoll_ctl,效率稍微有些下降。然而一旦使用idle connections模擬WAN環境,那麼epoll的效率就遠在select/poll之上了。
(3) 使用mmap加速核心與使用者空間的訊息傳遞
無論是select,poll還是epoll都需要核心把FD訊息通知給使用者空間,如何避免不必要的記憶體拷貝就顯得很重要。在這點上,epoll是通過核心於使用者空間mmap同一塊記憶體實現。

5.EPOLL模型的工作模式

(1) LT模式
LT:level triggered,這是預設的工作方式,同時支援block和no-block socket,在這種模式中,核心告訴你一個檔案描述符是否就緒了,然後你可以對這個就緒的fd進行IO操作。如果你不作任何操作,核心還是會繼續通知你的,所以,這種模式程式設計出錯誤可能性要小一點。傳統的select/poll都是這種模型的代表。
(2) ET模式
LT:edge-triggered,這是高速工作方式,只支援no-block socket。在這種模式下,當描述符從未就緒變為就緒時,核心就通過epoll告訴你,然後它會假設你知道檔案描述符已經就緒,並且不會再為那個檔案描述符傳送更多的就緒通知,直到你做了某些操作而導致那個檔案描述符不再是就緒狀態(比如你在傳送,接收或是接受請求,或者傳送接收的資料少於一定量時導致了一個EWOULDBLOCK 錯誤)。但是請注意,如果一直不對這個fd作IO操作(從而導致它再次變成未就緒),核心就不會發送更多的通知(only once)。不過在TCP協議中,ET模式的加速效用仍需要更多的benchmark確認。

6.EPOLL模型的使用方法

epoll用到的所有函式都是在標頭檔案sys/epoll.h中宣告的,下面簡要說明所用到的資料結構和函式:
(1) epoll_data、epoll_data_t、epoll_event
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;

struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};

結構體epoll_event 被用於註冊所感興趣的事件和回傳所發生待處理的事件。epoll_event 結構體的events欄位是表示感興趣的事件和被觸發的事件,可能的取值為:
EPOLLIN: 表示對應的檔案描述符可以讀;
EPOLLOUT: 表示對應的檔案描述符可以寫;
EPOLLPRI: 表示對應的檔案描述符有緊急的資料可讀;
EPOLLERR: 表示對應的檔案描述符發生錯誤;
EPOLLHUP: 表示對應的檔案描述符被結束通話;
EPOLLET: 表示對應的檔案描述符有事件發生;

聯合體epoll_data用來儲存觸發事件的某個檔案描述符相關的資料。例如一個client連線到伺服器,伺服器通過呼叫accept函式可以得到於這個client對應的socket檔案描述符,可以把這檔案描述符賦給epoll_data的fd欄位,以便後面的讀寫操作在這個檔案描述符上進行。

(2)epoll_create
函式宣告:intepoll_create(intsize)
函式說明:該函式生成一個epoll專用的檔案描述符,其中的引數是指定生成描述符的最大範圍。

(3) epoll_ctl函式
函式宣告:intepoll_ctl(int epfd,int op, int fd, struct epoll_event *event)
函式說明:該函式用於控制某個檔案描述符上的事件,可以註冊事件、修改事件、刪除事件。
epfd:由 epoll_create 生成的epoll專用的檔案描述符;
op:要進行的操作,可能的取值EPOLL_CTL_ADD 註冊、EPOLL_CTL_MOD 修改、EPOLL_CTL_DEL 刪除;
fd:關聯的檔案描述符;
event:指向epoll_event的指標;
如果呼叫成功則返回0,不成功則返回-1。

(4) epoll_wait函式
函式宣告:int epoll_wait(int epfd, structepoll_event * events, int maxevents, int timeout)
函式說明:該函式用於輪詢I/O事件的發生。
epfd:由epoll_create 生成的epoll專用的檔案描述符;
epoll_event:用於回傳代處理事件的陣列;
maxevents:每次能處理的事件數;
timeout:等待I/O事件發生的超時值;
返回發生事件數。

7 設計思路及模板

首先通過create_epoll(int maxfds)來建立一個epoll的控制代碼,其中maxfds為你的epoll所支援的最大控制代碼數。這個函式會返回一個新的epoll控制代碼,之後的所有操作都將通過這個控制代碼來進行操作。在用完之後,記得用close()來關閉這個創建出來的epoll控制代碼。
然後在你的網路主迴圈裡面,呼叫epoll_wait(int epfd, epoll_event events, int max_events,int timeout)來查詢所有的網路介面,看哪一個可以讀,哪一個可以寫。基本的語法為:
nfds = epoll_wait(kdpfd, events, maxevents, -1);
其中kdpfd為用epoll_create建立之後的控制代碼,events是一個epoll_event*的指標,當epoll_wait函式操作成功之後,events裡面將儲存所有的讀寫事件。max_events是當前需要監聽的所有socket控制代碼數。最後一個timeout引數指示 epoll_wait的超時條件,為0時表示馬上返回;為-1時表示函式會一直等下去直到有事件返回;為任意正整數時表示等這麼長的時間,如果一直沒有事件,則會返回。一般情況下如果網路主迴圈是單執行緒的話,可以用-1來等待,這樣可以保證一些效率,如果是和主迴圈在同一個執行緒的話,則可以用0來保證主迴圈的效率。epoll_wait返回之後,應該進入一個迴圈,以便遍歷所有的事件。
對epoll 的操作就這麼簡單,總共不過4個API:epoll_create, epoll_ctl,epoll_wait和close。以下是man中的一個例子。

struct epoll_event ev, *events;
for(;;) 
{
   nfds = epoll_wait(kdpfd, events, maxevents, -1);    //等待IO事件
   for(n = 0; n < nfds; ++n)
   {
   //如果是主socket的事件,則表示有新連線進入,需要進行新連線的處理。
      if(events[n].data.fd == listener)
      {
         client = accept(listener, (struct sockaddr *) &local,  &addrlen);
if(client < 0)
         {
            perror("accept error");
            continue;
         }
         // 將新連線置於非阻塞模式
         setnonblocking(client);
         ev.events = EPOLLIN | EPOLLET; 
         //注意這裡的引數EPOLLIN | EPOLLET並沒有設定對寫socket的監聽,
         //如果有寫操作的話,這個時候epoll是不會返回事件的,
         //如果要對寫操作也監聽的話,應該是EPOLLIN | EPOLLOUT | EPOLLET。
         // 並且將新連線也加入EPOLL的監聽佇列
         ev.data.fd = client;
         // 設定好event之後,將這個新的event通過epoll_ctl
         if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, client, &ev) < 0)
         {
            //加入到epoll的監聽佇列裡,這裡用EPOLL_CTL_ADD
            //來加一個新的 epoll事件。可以通過EPOLL_CTL_DEL來減少
            //一個epoll事件,通過EPOLL_CTL_MOD來改變一個事件的監聽方式。
            fprintf(stderr, "epoll set insertion error: fd=%d"0, client);
            return -1;
         }
      }
      else
      // 如果不是主socket的事件的話,則代表這是一個使用者的socket的事件,
      // 則用來處理這個使用者的socket的事情是,比如說read(fd,xxx)之類,或者一些其他的處理。
         do_use_fd(events[n].data.fd);
   }
}

8 EPOLL模型的簡單例項

#include <iostream>
#include <sys/socket.h> 
#include <sys/epoll.h>
#include <netinet/in.h> 
#include <arpa/inet.h>
#include <fcntl.h> 
#include <unistd.h> 
#include <stdio.h>

#define MAXLINE 10 
#define OPEN_MAX 100
#define LISTENQ 20
#define SERV_PORT 5555 
#define INFTIM 1000

void setnonblocking(int sock)
{
   int opts;
   opts = fcntl(sock, F_GETFL);
   if(opts < 0)
   {
      perror("fcntl(sock, GETFL)");
      exit(1);
   }
   opts = opts | O_NONBLOCK;
   if(fcntl(sock, F_SETFL, opts) < 0)
   {
      perror("fcntl(sock,SETFL,opts)");
      exit(1);
   }
}

int main()
{
   int i, maxi, listenfd, connfd, sockfd, epfd, nfds; 
   ssize_t n; 
   char line[MAXLINE];
   socklen_t clilen;
   //宣告epoll_event結構體的變數, ev用於註冊事件, events陣列用於回傳要處理的事件
   struct epoll_event ev,events[20];
   //生成用於處理accept的epoll專用的檔案描述符, 指定生成描述符的最大範圍為256 
   epfd = epoll_create(256);
   struct sockaddr_in clientaddr; 
   struct sockaddr_in serveraddr;
   listenfd = socket(AF_INET, SOCK_STREAM, 0);

   setnonblocking(listenfd);       //把用於監聽的socket設定為非阻塞方式
   ev.data.fd = listenfd;          //設定與要處理的事件相關的檔案描述符
   ev.events = EPOLLIN | EPOLLET;  //設定要處理的事件型別
   epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);     //註冊epoll事件
   bzero(&serveraddr, sizeof(serveraddr)); 
   serveraddr.sin_family = AF_INET;
   char *local_addr = "200.200.200.204";
   inet_aton(local_addr, &(serveraddr.sin_addr));
   serveraddr.sin_port = htons(SERV_PORT);  //或者htons(SERV_PORT);
   bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));
   listen(listenfd, LISTENQ);

   maxi = 0;
   for( ; ; )
   { 
      nfds = epoll_wait(epfd, events, 20, 500); //等待epoll事件的發生
      for(i = 0; i < nfds; ++i)                 //處理所發生的所有事件
      {
         if(events[i].data.fd == listenfd)      //監聽事件
         {
            connfd = accept(listenfd, (sockaddr *)&clientaddr, &clilen); 
            if(connfd < 0)
            {
               perror("connfd<0");
               exit(1);
            }
            setnonblocking(connfd);           //把客戶端的socket設定為非阻塞方式
            char *str = inet_ntoa(clientaddr.sin_addr);
            std::cout << "connect from " << str  <<std::endl;
            ev.data.fd=connfd;                //設定用於讀操作的檔案描述符
            ev.events=EPOLLIN | EPOLLET;      //設定用於注測的讀操作事件
            epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
            //註冊ev事件
         }
         else if(events[i].events&EPOLLIN)      //讀事件
         {
            if ( (sockfd = events[i].data.fd) < 0)
            {
               continue;
            }
            if ( (n = read(sockfd, line, MAXLINE)) < 0) // 這裡和IOCP不同
            {
               if (errno == ECONNRESET)
               {
                  close(sockfd);
                  events[i].data.fd = -1; 
               }
               else
               {
                  std::cout<<"readline error"<<std::endl;
               }
            }
            else if (n == 0)
            {
               close(sockfd); 
               events[i].data.fd = -1; 
            }
            ev.data.fd=sockfd;              //設定用於寫操作的檔案描述符
            ev.events=EPOLLOUT | EPOLLET;   //設定用於注測的寫操作事件 
            //修改sockfd上要處理的事件為EPOLLOUT
            epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev);
         } 
         else if(events[i].events&EPOLLOUT)//寫事件
         {
            sockfd = events[i].data.fd;
            write(sockfd, line, n);
            ev.data.fd = sockfd;               //設定用於讀操作的檔案描述符
            ev.events = EPOLLIN | EPOLLET;     //設定用於註冊的讀操作事件
            //修改sockfd上要處理的事件為EPOLIN
            epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev);
         } 
      }
   }
}

9.epoll進階思考

9.1. 問題來源
最近學習EPOLL模型,介紹中說將EPOLL與Windows IOCP模型進行比較,說其的優勢在於解決了IOCP模型大量執行緒上下文切換的開銷,於是可以看出,EPOLL模型不需要多執行緒,即單執行緒中可以處理EPOLL邏輯。如果引入多執行緒反而會引起一些問題。但是EPOLL模型的伺服器端到底可以不可以用多執行緒技術,如果可以,改怎麼取捨,這成了困擾我的問題。上網查了一下,有這樣幾種聲音:
(1) “要麼事件驅動(如epoll),要麼多執行緒,要麼多程序,把這幾個綜合起來使用,感覺更加麻煩。”;
(2) “單執行緒使用epoll,但是不能發揮多核;多執行緒不用epoll。”;
(3) “主通訊執行緒使用epoll所有需要監控的FD,有事件交給多執行緒去處理”;
(4) “既然用了epoll, 那麼執行緒就不應該看到fd, 而只看到的是一個一個的業務請求/響應; epoll將網路資料組裝成業務資料後, 轉交給業務執行緒進行處理。這就是常說的半同步半非同步”。
我比較贊同上述(3)、(4)中的觀點
EPOLLOUT只有在緩衝區已經滿了,不可以傳送了,過了一會兒緩衝區中有空間了,就會觸發EPOLLOUT,而且只觸發一次。如果你編寫的程式的網路IO不大,一次寫入的資料不多的時候,通常都是epoll_wait立刻就會觸發 EPOLLOUT;如果你不呼叫 epoll,直接寫 socket,那麼情況就取決於這個socket的緩衝區是不是足夠了。如果緩衝區足夠,那麼寫就成功。如果緩衝區不足,那麼取決你的socket是不是阻塞的,要麼阻塞到寫完成,要麼出錯返回。所以EPOLLOUT事件具有較大的隨機性,ET模式一般只用於EPOLLIN, 很少用於EPOLLOUT。
9.2. 具體做法
(1) 主通訊執行緒使用epoll所有需要監控的FD,負責監控listenfd和connfd,這裡只監聽EPOLLIN事件,不監聽EPOLLOUT事件;
(2) 一旦從Client收到了資料以後,將其構造成一個訊息,放入訊息佇列中;
(3) 若干工作執行緒競爭,從訊息佇列中取出訊息並進行處理,然後把處理結果傳送給客戶端。傳送客戶端的操作由工作執行緒完成。直接進行write。write到EAGAIN或EWOULDBLOCK後,執行緒迴圈continue等待緩衝區佇列
傳送函式程式碼如下:

bool send_data(int connfd, char *pbuffer, unsigned int &len,int flag)
{
   if ((connfd < 0) || (0  == pbuffer))
   {
      return false;
   }

   int result = 0;
   int remain_size = (int) len;
   int send_size = 0;
   const char *p = pbuffer; 

   time_t start_time = time(NULL);
   int time_out = 3;

   do
   {
      if (time(NULL) > start + time_out)
      {
         return false;
      }

      send_size = send(connfd, p, remain_size, flag);
      if (nSentSize < 0)
      {
         if ((errno == EAGAIN) || (errno == EWOULDBLOCK) || (errno == EINTR))
         {
            continue;
         }
         else
         {
            len -= remain_size;
            return false;
         }
      }

      p += send_size;
      remain_size -= send_size;
   }while(remain_size > 0);

   return true;
}

10 epoll 實現伺服器和客戶端例子

最後我們用C++實現一個簡單的客戶端回射,所用到的程式碼檔案是

net.h  server.cpp   client.cpp

伺服器端:epoll實現的,幹兩件事分別為:1.等待客戶端的連結,2.接收來自客戶端的資料並且回射;

客戶端:select實現,幹兩件事為:1.等待鍵盤輸入,2.傳送資料到伺服器端並且接收伺服器端回射的資料;

/***********
net.h
***********/
#include <stdio.h>

#ifndef _NET_H
#define _NET_H

#include <iostream>
#include <vector>
#include <algorithm>

#include <stdio.h>
#include <sys/types.h>
#include <sys/epoll.h>  //epoll ways file
#include <sys/socket.h>
#include <fcntl.h>    //block and noblock

#include <stdlib.h>
#include <error.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <signal.h>

using namespace std;


#define hand_error(msg) do{perror(msg); exit(EXIT_FAILURE);}while(0)
#endif
/***********
server.c
***********/
#include "net.h"
#define MAX_EVENTS 10000

int setblock(int sock)
{
    int ret =  fcntl(sock, F_SETFL, 0);
    if (ret < 0 )
        hand_error("setblock");
    return 0;
}
int setnoblock(int sock)  //設定非阻塞模式
{
    int ret = fcntl(sock,  F_SETFL, O_NONBLOCK );
    if(ret < 0)
        hand_error("setnoblock");
    return 0;
}

int main()
{
    signal(SIGPIPE,SIG_IGN);
  int listenfd;
    listenfd = socket( AF_INET, SOCK_STREAM,0 );   //create a socket stream
    if( listenfd < 0 )
        hand_error( "socket_create");
    setnoblock(listenfd);
    int on = 1;
    if( setsockopt( listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))< 0)
        hand_error("setsockopt");

    struct sockaddr_in my_addr;
    memset(&my_addr, 0, sizeof(my_addr));
    my_addr.sin_family = AF_INET;
    my_addr.sin_port = htons(18000);   //here is host  sequeue
    my_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if( bind( listenfd, (struct sockaddr *)&my_addr, sizeof(my_addr)) < 0)
        hand_error("bind");

    int lisId = listen(listenfd, SOMAXCONN);
    if( lisId < 0)   //LISTEN
        hand_error("listen");

    struct sockaddr_in peer_addr;   //用來 save client addr
    socklen_t peerlen;  
    //下面是一些初始化,都是關於epoll的。
    vector<int> clients;
    int count = 0;
    int cli_sock = 0;
    int epfd = 0;  //epoll 的檔案描述符
    int ret_events;  //epoll_wait()的返回值
  struct epoll_event ev_remov, ev, events[MAX_EVENTS];  //events 用來存放從核心讀取的的事件
    ev.events = EPOLLET | EPOLLIN;   //邊緣方式觸發
    ev.data.fd = listenfd;

    epfd = epoll_create(MAX_EVENTS);   //create epoll,返回值為epoll的檔案描述符
    //epfd = epoll_create(EPOLL_CLOEXEC);  //新版寫法
    if(epfd < 0)
        hand_error("epoll_create");
    int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);   //新增時間
    if(ret < 0)
        hand_error("epoll_ctl");


    while(1)
    {
        ret_events = epoll_wait(epfd, events, MAX_EVENTS, -1);   //類似於select函式,這裡是等待事件的到來。
        if(ret_events == -1)
        {
            cout<<"ret_events = "<<ret_events<<endl;
            hand_error("epoll_wait");
        }

        if( ret_events == 0)
        {
            cout<<"ret_events = "<<ret_events<<endl;
            continue;
        }

//      cout<<"ret_events = "<<ret_events<<endl;
        for( int num = 0; num < ret_events; num ++)
        {
            cout<<"num = "<<num<<endl;
            cout<<"events[num].data.fd = "<<events[num].data.fd<<endl;
            if(events[num].data.fd == listenfd) //client connect
            {
                cout<<"listen sucess and listenfd = "<<listenfd<<endl;
                cli_sock = accept(listenfd, (struct sockaddr*)&peer_addr, &peerlen);
                if(cli_sock < 0)
                    hand_error("accept");
                cout<<"count = "<<count++;
                printf("ip=%s,port = %d\n", inet_ntoa(peer_addr.sin_addr),peer_addr.sin_port);
                clients.push_back(cli_sock);
                setnoblock(cli_sock);   //設定為非阻塞模式
                ev.data.fd = cli_sock;// 將新連線也加入EPOLL的監聽佇列
                ev.events = EPOLLIN | EPOLLET ;
                if(epoll_ctl(epfd, EPOLL_CTL_ADD, cli_sock, &ev)< 0)
                    hand_error("epoll_ctl");
            }

            else if( events[num].events & EPOLLIN)
            {
                cli_sock = events[num].data.fd;
                if(cli_sock < 0)
                    hand_error("cli_sock");
                char recvbuf[1024];
                memset(recvbuf, 0 , sizeof(recvbuf));
                int num = read( cli_sock, recvbuf, sizeof(recvbuf));
                if(num == -1)
                    hand_error("read have some problem:");
                if( num == 0 )  //stand of client have exit
                {
                    cout<<"client have exit"<<endl;
                    close(cli_sock);
                    ev_remov = events[num];
                    epoll_ctl(epfd, EPOLL_CTL_DEL, cli_sock, &ev_remov);
                    clients.erase(remove(clients.begin(), clients.end(), cli_sock),clients.end());
                }
                fputs(recvbuf,stdout);
                write(cli_sock, recvbuf, strlen(recvbuf));
            }
        }
    }

    return 0;
}

/***********
client.c
***********/

#include "net.h"

int main()
{
    signal(SIGPIPE,SIG_IGN);
  int sock;
    sock = socket( AF_INET, SOCK_STREAM,0 );   //create a socket stream
    if( sock< 0 )
        hand_error( "socket_create");

    struct sockaddr_in my_addr;

    //memset my_addr;
    memset(&my_addr, 0, sizeof(my_addr));
    my_addr.sin_family = AF_INET;
    my_addr.sin_port = htons(18000);   //here is host sequeue
//  my_addr.sin_addr.s_addr = htonl( INADDR_ANY );
    my_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    int conn = connect(sock, (struct sockaddr *)&my_addr, sizeof(my_addr)) ;
    if(conn != 0)
        hand_error("connect");

    char recvbuf[1024] = {0};
    char sendbuf[1024] = {0};
    fd_set rset;
    FD_ZERO(&rset);     

    int nready = 0;
    int maxfd;
    int stdinof = fileno(stdin);
    if( stdinof > sock)
        maxfd = stdinof;
    else
        maxfd = sock;
    while(1)
    {
        //select返回後把原來待檢測的但是仍沒就緒的描述字清0了。所以每次呼叫select前都要重新設定一下待檢測的描述字
        FD_SET(sock, &rset);  
        FD_SET(stdinof, &rset);
        nready = select(maxfd+1, &rset, NULL, NULL, NULL); 
        cout<<"nready = "<<nready<<"  "<<"maxfd = "<<maxfd<<endl;
        if(nready == -1 )
            break;
        else if( nready == 0)
            continue;
        else
        {
            if( FD_ISSET(sock, &rset) )  //檢測sock是否已經在集合rset裡面。
            {
                int ret = read( sock, recvbuf, sizeof(recvbuf));  //讀資料
                if( ret == -1)
                    hand_error("read");
                else if( ret == 0)
                {
                    cout<<"sever have close"<<endl;
                    close(sock);
                    break;
                }
                else
                {
                    fputs(recvbuf,stdout);    //輸出資料
                    memset(recvbuf, 0, strlen(recvbuf));
                }   
            }

            if( FD_ISSET(stdinof, &rset))   //檢測stdin的檔案描述符是否在集合裡面
            {   
                if(fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
                {
                    int num = write(sock, sendbuf, strlen(sendbuf));   //寫資料
                    cout<<"sent num = "<<num<<endl;
                    memset(sendbuf, 0, sizeof(sendbuf));
                }
            }
        }
    }
    return 0;
}