Linux网络编程三、 IO操作

  当从一个文件描述符进行读写操作时,accept、read、write这些函数会阻塞I/O。在这种会阻塞I/O的操作好处是不会占用cpu宝贵的时间片,但是如果需要对多个描述符操作时,阻塞会使同一时刻只能处理一个操作,从而使程序的执行效率大大降低。一种解决办法是使用多线程或多进程操作,但是这浪费大量的资源。另一种解决办法是采用非阻塞、忙轮询,这种办法提高了程序的执行效率,缺点是需要占用更多的cpu和系统资源。所以,最终的解决办法是采用IO多路转接技术。

  IO多路转接是先构造一个关于文件描述符的列表,将要监听的描述符添加到这个列表中。然后调用一个阻塞函数用来监听这个表中的文件描述符,直到这个表中有描述符要进行IO操作时,这个函数返回给进程有哪些描述符要进行操作。从而使一个进程能完成对多个描述符的操作。而函数对描述符的检测操作都是由系统内核完成的。

  linux下常用的IO转接技术有:select、poll和epoll。

select:

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

  函数:

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

      nfds:要检测的文件描述符中最大的fd+1,nfds最大值为1024。select最多只能检测1024个文件描述符。

      readfds:读集合。读缓冲区中有数据时,readfds写入数据。fd_set文件描述符集类型,具体实现见下面。

      writefds:写集合。通常设为NULL。

      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()函数)。效率最高,可以将缓冲区数据完全读完。

posted @ 2019-10-13 17:01  qetuo[  阅读(335)  评论(0编辑  收藏  举报