Linux網絡編程3、 IO操做

  當從一個文件描述符進行讀寫操做時,accept、read、write這些函數會阻塞I/O。在這種會阻塞I/O的操做好處是不會佔用cpu寶貴的時間片,可是若是須要對多個描述符操做時,阻塞會使同一時刻只能處理一個操做,從而使程序的執行效率大大下降。一種解決辦法是使用多線程或多進程操做,可是這浪費大量的資源。另外一種解決辦法是採用非阻塞、忙輪詢,這種辦法提升了程序的執行效率,缺點是須要佔用更多的cpu和系統資源。因此,最終的解決辦法是採用IO多路轉接技術。linux

  IO多路轉接是先構造一個關於文件描述符的列表,將要監聽的描述符添加到這個列表中。而後調用一個阻塞函數用來監聽這個表中的文件描述符,直到這個表中有描述符要進行IO操做時,這個函數返回給進程有哪些描述符要進行操做。從而使一個進程能完成對多個描述符的操做。而函數對描述符的檢測操做都是由系統內核完成的。數組

  linux下經常使用的IO轉接技術有:select、poll和epoll。服務器

select:多線程

  頭文件:#include <sys/select.h>、#include <sys/time.h>、#include <sys/types.h>、#include <unistd.h>socket

  函數:函數

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

      nfds:要檢測的文件描述符中最大的fd+1,nfds最大值爲1024。select最多隻能檢測1024個文件描述符。spa

      readfds:讀集合。讀緩衝區中有數據時,readfds寫入數據。fd_set文件描述符集類型,具體實現見下面。線程

      writefds:寫集合。一般設爲NULL。code

      exceptfds:異常集合。一般設爲NULL。

      timeout:設置超時返回。爲NULL時只有檢測到fd變化時返回。struct timeval a; a.tv_sec=10; a.tv_usec=0;

      返回值:成功返回要操做的描述符個數,超時返回0,失敗返回-1。

      select最多隻能檢測1024個文件描述符,是因爲fd_set在內核代碼中的設置所限制

1 //部分fd_set的內核代碼
2 
3 #define __FDSET_LONGS     (__FD_SETSIZE/__NFDBITS)
4 #define __FD_SETSIZE        1024
5 #define __NFDBITS             (8 * sizeof(unsigned long))
6 typedef __kernel_fd_set       fd_set;
7 typedef struct    {
8         unsigned long fds_bits    [__FDSET_LONGS];
9 }    __kernel_fd_set;

    void FD_CLR(int fd, fd_set *set);     從set集合中刪除文件描述符fd。

    int  FD_ISSET(int fd, fd_set *set);   判斷文件描述符fd是否在set集合中。

    void FD_SET(int fd, fd_set *set);    將fd添加到set集合中。

    void FD_ZERO(fd_set *set);           清空set集合。

 1 #include <stdio.h>                                                                                                                                                  
 2 #include <sys/types.h>
 3 #include <sys/stat.h>
 4 #include <sys/socket.h>
 5 #include <arpa/inet.h>
 6 #include <string.h>
 7 #include <unistd.h>
 8 #include <sys/select.h>
 9 #include <sys/time.h>
10 #include <stdlib.h>
11 int main()
12 {
13     int fd=socket(AF_INET,SOCK_STREAM,0);
14     struct sockaddr_in serv;
15     memset(&serv,0,sizeof(serv));
16     serv.sin_addr.s_addr=htonl(INADDR_ANY);
17     serv.sin_port=htons(8888);
18     serv.sin_family=AF_INET;
19     bind(fd,(struct sockaddr*)&serv,sizeof(serv));
20 
21     listen(fd,20);
22 
23     struct sockaddr_in client;
24     socklen_t cli_len=sizeof(client);
25     int maxfd=fd;
26     fd_set reads, temp;
27     FD_ZERO(&reads);
28     FD_SET(fd,&reads);
29     while(1)
30 {
31         temp=reads;
32         int ret=select(maxfd+1,&temp,NULL,NULL,NULL);
33         if(-1==ret)
34         {
35             perror("select error");
36             exit(1);
37         }
38         //客戶端發起鏈接
39         if(FD_ISSET(fd,&temp))
40         {
41             //接受鏈接
42             int cfd=accept(fd,(struct sockaddr*)&client,&cli_len);
43             if(cfd==-1)
44             {
45                 perror("accept error");
46                 exit(1);
47             }
48             FD_SET(cfd,&reads);
49             //更新最大文件描述符
50             maxfd=maxfd<cfd?cfd:maxfd;
51             
52         }
53         for(int i=fd+1;i<=maxfd;++i)
54         {
55             if(FD_ISSET(i,&temp))
56             {
57                 char buf[1024]={0};
58                 int len=recv(i,buf,sizeof(buf),0);
59                 if(len==-1)
60                 {
61                     perror("recv error");
62                     exit(1);
63 
64                 }
65                 else if(len==0)
66                 {
67                     printf("客戶端斷開鏈接\n");
68                     close(i);
69 
70                     FD_CLR(i,&reads);
71                 }
72                else
73                {                                                                                                                                                    
74                    printf("recv buf: %s\n",buf);
75                    send(i,buf,strlen(buf)+1,0);
76                }
77             }
78         }
79     }
80     close(fd);
81     return 0;
82 }

 poll:

  頭文件:#include <poll.h>

  函數:

    int poll(struct pollfd *fds, nfds_t nfds, int timeout);

      fds:數組地址。內核檢測fds中的文件描述符。

      nfds:數組的最大長度,數組中最後有效元素的下標+1。

      timeout:超時返回,-1永久阻塞,0不阻塞調用後當即返回,>0等待的時長,單位毫秒。

      返回值:成功返回要操做的個數,失敗返回-1。

struct pollfd {
    int fd;     /*文件描述符*/
    short events;           /*等待的事件*/
    short revents;          /*實際發生的事件,內核給的反饋*/
}

pollfd經常使用事件:讀事件,POLLIN;寫事件,POLLOUT;錯誤事件,POLLERR(不能做爲events的值);

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <stdlib.h>
  4 #include <sys/types.h>
  5 #include <string.h>
  6 #include <sys/socket.h>
  7 #include <arpa/inet.h>
  8 #include <ctype.h>
  9 #include <poll.h>
 10 
 11 #define SERV_PORT 8989
 12 
 13 int main(int argc, const char* argv[])
 14 {
 15     int lfd, cfd;
 16     struct sockaddr_in serv_addr, clien_addr;
 17     int serv_len, clien_len;
 18 
 19     // 建立套接字
 20     lfd = socket(AF_INET, SOCK_STREAM, 0);
 21     // 初始化服務器 sockaddr_in 
 22     memset(&serv_addr, 0, sizeof(serv_addr));
 23     serv_addr.sin_family = AF_INET;                   // 地址族 
 24     serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);    // 監聽本機全部的IP
 25     serv_addr.sin_port = htons(SERV_PORT);            // 設置端口 
 26     serv_len = sizeof(serv_addr);
 27     // 綁定IP和端口
 28     bind(lfd, (struct sockaddr*)&serv_addr, serv_len);
 29 
 30     // 設置同時監聽的最大個數
 31     listen(lfd, 36);
 32     printf("Start accept ......\n");
 33 
 34     // poll結構體
 35     struct pollfd allfd[1024];
 36     int max_index = 0;
 37     // init
 38     for(int i=0; i<1024; ++i)
 39     {
 40         allfd[i].fd = -1;
 41     }
 42     allfd[0].fd = lfd;
 43     allfd[0].events = POLLIN;
 44 
 45     while(1)
 46     {
 47         int i = 0;
 48         int ret = poll(allfd, max_index+1, -1); 
 49         if(ret == -1)
 50         {
 51             perror("poll error");
 52             exit(1);
 53         }
 54 
 55         // 判斷是否有鏈接請求
 56         if(allfd[0].revents & POLLIN)
 57         {
 58             clien_len = sizeof(clien_addr);
 59             // 接受鏈接請求
 60             int cfd = accept(lfd, (struct sockaddr*)&clien_addr, &clien_len);
 61             printf("============\n");
 62 
 63             // cfd添加到poll數組
 64             for(i=0; i<1024; ++i)
 65             {
 66                 if(allfd[i].fd == -1)
 67                 {
 68                     allfd[i].fd = cfd;
 69                     break;
 70                 }
 71             }
 72             // 更新最後一個元素的下標
 73             max_index = max_index < i ? i : max_index;
 74         }
 75 
 76         // 遍歷數組
 77         for(i=1; i<=max_index; ++i)
 78         {
 79             int fd = allfd[i].fd;
 80             if(fd == -1)
 81             {
 82                 continue;
 83             }
 84             if(allfd[i].revents & POLLIN)
 85             {
 86                 // 接受數據
 87                 char buf[1024] = {0};
 88                 int len = recv(fd, buf, sizeof(buf), 0);
 89                 if(len == -1)
 90                 {
 91                     perror("recv error");
 92                     exit(1);
 93                 }
 94                 else if(len == 0)
 95                 {
 96                     allfd[i].fd = -1;
 97                     close(fd);
 98                     printf("客戶端已經主動斷開鏈接。。。\n");
 99                 }
100                 else
101                 {
102                     printf("recv buf = %s\n", buf);
103                     for(int k=0; k<len; ++k)
104                     {
105                         buf[k] = toupper(buf[k]);
106                     }
107                     printf("buf toupper: %s\n", buf);
108                     send(fd, buf, strlen(buf)+1, 0);
109                 }
110 
111             }
112 
113         }
114     }
115 
116     close(lfd);
117     return 0;
118 }

   select和poll雖然沒有前面幾種方法的缺點,可是select和poll只返回個數,不會告訴進程具體是哪幾個描述符要操做, 並且select和poll最多隻能檢測1024個。select每次調用時,都須要把fd集合從用戶態和內核態之間相互拷貝,這在fd不少時會消耗大量資源。

  epoll檢測的個數沒有限制,它在內部構造維護了紅黑樹,減小了資源的消耗。

epoll:

  頭文件:#include <sys/epoll.h>

  函數:

    int epoll_create(int size);     生成epoll專用的文件描述符,size:epoll上能關注的最大描述符個數。

    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

      epfd:epoll_create生成的文件描述符。

      op:選項,EPOLL_CTL_ADD  註冊,EPOLL_CTL_MOD  修改,EPOLL_CTL_DEL   刪除。

      fd:關聯的文件描述符。

      event:告訴內核要監聽的事件

      返回值:成功返回0,失敗返回-1。

    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);   等待IO事件發生,能夠設置阻塞。

      epfd:要檢測的句柄。

      events:回傳待處理的數組。

      maxevents:events的大小。

      timeout:超時返回。-1永久阻塞;0當即返回;>0超時時間。

 1 typedef union epoll_data {
 2                void    *ptr;
 3                int      fd;
 4                uint32_t u32;
 5                uint64_t u64;
 6            } epoll_data_t;
 7 
 8 struct epoll_event {
 9             uint32_t     events;    /* Epoll events */
10             epoll_data_t data;      /* User data variable */
11 };
  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <stdlib.h>
  4 #include <sys/types.h>
  5 #include <string.h>
  6 #include <sys/socket.h>
  7 #include <arpa/inet.h>
  8 #include <ctype.h>
  9 #include <sys/epoll.h>
 10 
 11 
 12 int main(int argc, const char* argv[])
 13 {
 14     if(argc < 2)
 15     {
 16         printf("eg: ./a.out port\n");
 17         exit(1);
 18     }
 19     struct sockaddr_in serv_addr;
 20     socklen_t serv_len = sizeof(serv_addr);
 21     int port = atoi(argv[1]);
 22 
 23     // 建立套接字
 24     int lfd = socket(AF_INET, SOCK_STREAM, 0);
 25     // 初始化服務器 sockaddr_in 
 26     memset(&serv_addr, 0, serv_len);
 27     serv_addr.sin_family = AF_INET;                   // 地址族 
 28     serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);    // 監聽本機全部的IP
 29     serv_addr.sin_port = htons(port);            // 設置端口 
 30     // 綁定IP和端口
 31     bind(lfd, (struct sockaddr*)&serv_addr, serv_len);
 32 
 33     // 設置同時監聽的最大個數
 34     listen(lfd, 36);
 35     printf("Start accept ......\n");
 36 
 37     struct sockaddr_in client_addr;
 38     socklen_t cli_len = sizeof(client_addr);
 39 
 40     // 建立epoll樹根節點
 41     int epfd = epoll_create(2000);
 42     // 初始化epoll樹
 43     struct epoll_event ev;
 44     ev.events = EPOLLIN;
 45     ev.data.fd = lfd;
 46     epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
 47 
 48     struct epoll_event all[2000];
 49     while(1)
 50     {
 51         // 使用epoll通知內核fd 文件IO檢測
 52         int ret = epoll_wait(epfd, all, sizeof(all)/sizeof(all[0]), -1);
 53 
 54         // 遍歷all數組中的前ret個元素
 55         for(int i=0; i<ret; ++i)
 56         {
 57             int fd = all[i].data.fd;
 58             // 判斷是否有新鏈接
 59             if(fd == lfd)
 60             {
 61                 // 接受鏈接請求
 62                 int cfd = accept(lfd, (struct sockaddr*)&client_addr, &cli_len);
 63                 if(cfd == -1)
 64                 {
 65                     perror("accept error");
 66                     exit(1);
 67                 }
 68                 // 將新獲得的cfd掛到樹上
 69                 struct epoll_event temp;
 70                 temp.events = EPOLLIN;
 71                 temp.data.fd = cfd;
 72                 epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &temp);
 73                 
 74                 // 打印客戶端信息
 75                 char ip[64] = {0};
 76                 printf("New Client IP: %s, Port: %d\n",
 77                     inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip, sizeof(ip)),
 78                     ntohs(client_addr.sin_port));
 79                 
 80             }
 81             else
 82             {
 83                 // 處理已經鏈接的客戶端發送過來的數據
 84                 if(!all[i].events & EPOLLIN) 
 85                 {
 86                     continue;
 87                 }
 88 
 89                 // 讀數據
 90                 char buf[1024] = {0};
 91                 int len = recv(fd, buf, sizeof(buf), 0);
 92                 if(len == -1)
 93                 {
 94                     perror("recv error");
 95                     exit(1);
 96                 }
 97                 else if(len == 0)
 98                 {
 99                     printf("client disconnected ....\n");
100                     // fd從epoll樹上刪除
101                     ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
102                     if(ret == -1)
103                     {
104                         perror("epoll_ctl - del error");
105                         exit(1);
106                     }
107                     close(fd);
108                     
109                 }
110                 else
111                 {
112                     printf(" recv buf: %s\n", buf);
113                     write(fd, buf, len);
114                 }
115             }
116         }
117     }
118 
119     close(lfd);
120     return 0;
121 }

   epoll三種工做模式:

    水平觸發:epoll默認工做模式,只要fd對應的緩衝區有數據,epoll_wait就會返回。epoll_wait調用次數越多,系統開銷越大。

    邊沿觸發:fd默認是阻塞的,客戶端發送一次數據epoll_wait就返回一次,無論數據是否讀完。若是要讀完數據,能夠循環讀取,可是recv會阻塞,解決方法是將fd設置爲非阻塞。

    邊沿非阻塞觸發:將fd設置爲非阻塞(open下設置O_NONBLOCK,或者利用fcntl()函數)。效率最高,能夠將緩衝區數據徹底讀完。

相關文章
相關標籤/搜索