IO复用函数

I/O复用技术使得程序能够同时监听多个文件描述符,这对于提高程序的性能至关重要。

  • TCP 服务器同时要处理监听套接字和连接套接字。

  • 服务器要同时处理 TCP 请求和 UDP 请求。

  • 程序要同时处理多个套接字。

  • 客户端程序要同时处理用户输入和网络连接。

  • 服务器要同时监听多个端口。

需要指出的是,I/O 复用虽然能同时监听多个文件描述符,但它本身是阻塞的。并且当 多个文件描述符同时就绪时,如果不采取额外的措施,程序就只能按顺序依处理其中的每一 个文件描述符,这使得服务器看起来好像是串行工作的。如果要提高并发处理的能力,可以 配合使用多线程或多进程等编程方法。

select

select 系统调用的用途是:在一段指定时间内,监听用户感兴趣的文件描述符的可读、 可写和异常等事件。

select接口

1. #include <sys/select.h>
2.
3. int select(int maxfd, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
4. /*
5. select 成功时返回就绪(可读、可写和异常)文件描述符的总数。
6.如果在超时时间内没有任何文件描述符就绪,select 将返回 0。
7.select 失败是返回-1.如果在 select 等待期间,程序接收到信号,则 select 立即返回-1,并设置 errno 为 EINTR。
​
7. maxfd 参数指定的被监听的文件描述符的总数。它通常被设置为 select 监听的所有文件描述符中的最大值+1
​
8. readfds、writefds 和 exceptfds 参数分别指向可读、可写和异常等事件对应的文件描述符集合。应用程序调用 select 函数时,通过这 3 个参数传入自己感兴趣的文件描述符。select 返回时,内核将修改它们来通知应用程序哪些文件描述符已经就绪
​
fd_set 结构如下:
通过下列宏可以访问 fd_set 结构中的位:
23. FD_ZERO(fd_set *fdset); // 清除 fdset 的所有位
24. FD_SET(int fd, fd_set *fdset); // 设置 fdset 的位 fd
25. FD_CLR(int fd, fd_set *fdset); // 清除 fdset 的位 fd
26. int FD_ISSET(int fd, fd_set *fdset);// 测试 fdset 的位 fd 是否被设置
​
27. timeout 参数用来设置 select 函数的超时时间。它是一个 timeval 结构类型的指针,采用指针参数是因为内核将修改它以告诉应用程序 select 等待了多久。timeval结构的定义如下:
28. struct timeval
29. {
30. long tv_sec; //秒数
31. long tv_usec; // 微秒数
32. };
33. 如果给 timeout 的两个成员都是 0,则 select 将立即返回。如果 timeout 传递
NULL,则 select 将一直阻塞,直到某个文件描述符就绪
​

select的应用

判断键盘是否输入数据

lcx@lcx-virtual-machine:~/mycode/2.2$ vi main.c
​
​
​
  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<string.h>
  4 #include<unistd.h>
  5 #include<sys/select.h>
  6 
  7 #define STDIN 0
  8 
  9 int main()
 10 {
 11     int fd=STDIN;
 12     fd_set fdset;  //定义集合 1024个位
 13     while(1)
 14     {
 15         FD_ZERO(&fdset);
 16         FD_SET(fd,&fdset);
 17 
 18         struct timeval tv={5,0};
 19 
 20         int n=select(fd+1,&fdset,NULL,NULL,&tv);
 21 
 22         if(n==-1)
 23         {
 24             printf("select err\n");
 25         }
 26         else if(n==0)
 27         {
 28             printf("timeval out\n");
 29         }
 30         else
 31         {
 32             if(FD_ISSET(fd,&fdset))
 33             {
 34                 char buff[128]={0};
 35                 read(fd,buff,128);
 36                 printf("read:%s\n",buff);
 37             }
 38         }
 39     }
 40 }
 41 
 42 
~      
​
lcx@lcx-virtual-machine:~/mycode/2.2$ ./main
timeval out
timeval out
abv
read:abv
​
lc
read:lc
​
lcx
read:lcx
​
awdodec
read:awdodec
​
^C
​
​

在TCP的服务器端使用select

因为select处理的是一个集和,我们需要构建一个数组用来储存相应的信息

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<string.h>
  4 #include<unistd.h>
  5 #include<sys/select.h>
  6 #include<assert.h>
  7 #include<sys/socket.h>
  8 #include<arpa/inet.h>
  9 #include<netinet/in.h>
 10 #define MAX 10   //数组的最大空间
 11 
 12 void fds_init(int fds[])  //对数组进行初始化为-1,因为文件描述符>=0,用-1做区别
 13 {
 14     int i=0;
 15     for(;i<MAX;i++)
 16     {
 17         fds[i]= -1;
 18     }
 19 
 20 }
 21 void fds_add(int fd,int fds[]) //在数组中加入一个文件描述符元素
 22 {
 23     if(fd<0)//判断文件描述符的合法性                 
 24     {
 25         return ;
 26     }
 27     for(int i=0;i<MAX;i++) //遍历一遍
 28     {
 29         if(fds[i]==-1)//为-1 表示空闲空间,可以存
 30         {
 31             fds[i]=fd;//将文件描述符存入,并退出循环
 32             break;
 33         }
 34     }
 35 }
 36 void fds_del(int fd,int fds[])  //在数组中删除一个文件描述符元素
 37 {
 38     for(int i=0;i<MAX;i++)  //遍历一遍
 39     {
 40 
 41         if(fds[i]==fd)      //找到该文件描述符置为-1
 42         {
 43             fds[i]=-1;
 44             break;
 45         }
 46     }
 47 }
 48 
 49 int socket_init();   //设置监听套接字的声明
 50 int main()
 51 {
 52 
 53     int sokfd=socket_init();  //定义一个监听套接字
 54     assert(sockfd!=-1);        //判断是否成功
 55 
 56     int fds[MAX];              //定义一个数组,用来存储文件描述符
 57     fds_init(fds);             //初始化数组
 58     fds_add(sockfd,fds);       //往数组中加入监听套接字
 59 
 60     fd_set fdset;              //定义一个集合 大小1024个位
 61     while(1)
 62     {
 63         FD_ZERO(&fdset);       //将集合每个位置为0
 64         int maxfd=-1;          //用来保存数组中文件描述符的最大值
 65         for(int i=0;i<MAX;i++) //遍历一遍 将有效文件描述符写入集合 并找到最大值
 66         {
 67             if(fds[i]==-1)     //数组中该空间如果为空闲 直接进入下一次循环
 68             {
 69                 continue;
 70             }
 71             FD_SET(fds[i],&fdset); //不为空闲,便写入集合
 72             if(maxfd<fds[i])  //保存最大值
 73             {
 74                 maxfd=fds[i];
 75             }
 76         }
 77         struct timeval tv={5,0}; //设置等待时间
 78         int n=select(maxfd+1,&fdset,NULL,NULL,&tv);//阻塞,等待读事件 ,返回值为几个描述符产生了读事件
 79 
 80         if(n<0)
 81         {
 82             printf("select err\n"); //错误
​
 83         }
 84         else if(n==0)   //表示没有读行为且等待超时
 85         {
 86             printf("time out\n");
 87         }
 88         else  //有读行为,此时需要找出是哪个文件描述符有数据了
 89         {
 90             for(int i=0;i<MAX;i++) //将数组遍历一遍
 91             {
 92                 if(fds[i]==-1)    //空闲空间直接进入下一个循环
 93                 {
 94                     continue;
 95                 }
 96                 if(FD_ISSET(fds[i],&fdset)) //如果不是空闲资源,且该文件描述符在集合中为1(即发生了读事件)
 97                 {
 98                     if(fds[i]==sockfd)  //判断该文件描述符是否是监字,是监听套接字则需要建立连接
 99                     {
100                         struct sockaddr_in caddr; //定义客户端结构体
101                         int len=sizeof(caddr); //客户端长度
102                         int c=accept(sockfd,(struct sockaddr*)&caddr,&len); //接受链接 ,返回连接套接字
103                         if(c<0) //无效的连接套接字直接进入下一次循环
104                         {
105                             continue;
106                         }
107                         printf("accept c=%d\n",c); //打印 连接套接字
108                         fds_add(c,fds); //将连接套接字加入 存储文件描述符的 数组中 
109                     }
110                     else //该文件描述符 为连接套接字
111                     {
112                         char buff[128]={0}; 
113                         int num=recv(fds[i],buff,127,0); //定义空间接收数据
114                         if(num <=0)  //如果数据大小<=0,即客户端关闭连接
115                         {
​
116                             printf("client close\n");//打印客户端关闭
117                             close(fds[i]); //关闭该连接套接字
118                             fds_del(fds[i],fds); //将该套接字 移除资源数组
119                         }
120                         else //有数据,即打印数据,并发送 ok
121                         {
122                             printf("recv(%d)=%s\n",fds[i],buff); 
123                             send(fds[i],"ok",2,0);
124                         }
125                     }
126                 }
127             }
128         }
129 
130     }
131 
132 }
133 int socket_init()//建立监听套接字
134 {
135     int sockfd=socket(AF_INET,SOCK_STREAM,0); //建立套接字
136     if(sockfd==-1)
137     {
138         return -1;
139     }
140     struct sockaddr_in saddr;   //设置服务器结构体
141     memset(&saddr,0,sizeof(saddr));
142     saddr.sin_family=AF_INET;
143     saddr.sin_port=htons(6000);
144     saddr.sin_addr.s_addr=inet_addr("127.0.0.1");
145 
146     int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//将套接字与地址绑定
147     if(res==-1)
148     {
149         return -1;
150     }
151 
152     res=listen(sockfd,5);  //设置监听队列
153     if(res==-1)
154     {
155         return -1;
156     }
157     return sockfd;
158 }
​
lcx@lcx-virtual-machine:~/mycode/2.2$ vi select.c
​
运行结果:可以同时接收多个 客户端的消息 

poll

poll 系统调用和 select 类似,也是在指定时间内轮询一定数量的文件描述符,以测试其 中是否有就绪者。

poll接口

1. #include <poll.h>
2.
3. int poll(struct pollfd *fds, nfds_t nfds, int timeout);
4.
5. /*
6. poll 系统调用成功返回就绪文件描述符的总数,超时返回 0,失败返回-1
​
8. nfds 参数指定被监听事件集合 fds 的大小。
9. timeout 参数指定 poll 的超时值,单位是毫秒,timeout 为-1 时,poll 调用将永久阻塞,直到某个事件发生,timeout 为 0 时,poll 调用将立即返回。
10.
11. fds 参数是一个 struct pollfd 结构类型的数组,它指定所有用户感兴趣的文件描述
符上发生的可读、可写和异常等事件。pollfd 结构体定义如下:
12. struct pollfd
13. {
14. int fd; // 文件描述符
15. short events; // 注册的关注事件类型
16. short revents; // 实际发生的事件类型,由内核填充
17. };
18. 其中,fd 成员指定文件描述符,events 成员告诉 poll 监听 fd 上的哪些事件类型,通过对16字节进行偏移将0改为1。它是一系列事件的按位或,
revents 成员则有内核修改,通知应用程序 fd 上实际发生了哪些事件。poll 支持的事件类型如下
19. */

应用:

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<string.h>
  4 #include<unistd.h>
  5 #include<sys/poll.h>
  6 #include<assert.h>
  7 #include<sys/socket.h>
  8 #include<arpa/inet.h>
  9 #include<netinet/in.h>
 10 #define MAX 10
 11 int socket_init();
 12 
 13 void poll_fds_init(struct pollfd fds//封装 对poll结构数组的初始化
 14 {
 15     for(int i=0;i<MAX;i++)
 16     {
 17         fds[i].fd=-1;    //文件描述符全部置为无效-1
 18         fds[i].events=0; //关注事件初始化为0
 19         fds[i].revents=0;//实际发生事件初始化为0
 20     }
 21 }
 22 void poll_fds_add(int fd,struct pollfd fds[])//封装 对 poll 结构数组中增加一个文件描述符
 23 {
 24     for(int i=0;i<MAX;i++)
 25     {
 26        if( fds[i].fd==-1) //找到空的空间来存储
 27        {
 28            fds[i].fd=fd;//设置文件描述符
 29            fds[i].events=POLLIN; //设置关注事件-- 读
 30            fds[i].revents=0; //实际发生事件初始化为 0
 31            break;
 32        }
 33     }
 34 }
 35 void poll_fds_del(int fd,struct pollfd fds[])//封装 对 poll 结构数组中减去一个文件描述符
 36 {
 37     for(int i=0;i<MAX;i++)
 38     {
 39         if(fds[i].fd==fd)//找到该文件描述符
 40         {
 41             fds[i].fd=-1; //置为空闲
 42             fds[i].events=0; //关注事件 置为0
 43             fds[i].revents=0;//实际发生 置为0
 44             break;
 45         }
 46     }
 47 }
 48 int main()
 49 {
 50 
 51     int sockfd=socket_init();//设置监听套接字
 52     assert(sockfd!=-1);
 53     struct pollfd poll_fds[MAX]; //创建 poll 结构体数组 存放文件描述符等
 54     poll_fds_init(poll_fds);//初始化结构体数组
 55     poll_fds_add(sockfd,poll_fds);//将监听套接字加入结构体数组
 56     while(1)
 57     {
 58         int n=poll(poll_fds,MAX,5000); //调用poll  阻塞,等待读事件 ,返回值为几个描述符产生了读事件
 59         if(n==0) //超时无读事件
 60         {
 61             printf("time out\n");
 62         }
 63         else if(n<0)
 64         {
 65             printf("poll error\n");
 66         }
 67         else//有读事件
 68         {
 69             for(int i=0;i<MAX;i++)//找到哪个文件描述符发生读事件
 70             {
 71                 if(poll_fds[i].fd==-1)//无效文件描述符,直接下一个
 72                 {
 73                     continue;
 74                 }
 75                 if(poll_fds[i].revents & POLLIN)//进行位与 如果不为0 说明有该事件发生
 76                 {
 77                     if(poll_fds[i].fd==sockfd)//判断该文件描述符是不是监听套接字
 78                     {
 79                         struct sockaddr_in caddr;//定义套接字详细信息结构体
 80                         int len=sizeof(caddr);//计算结构体长度
 81                         int c=accept(sockfd,(struct sockaddr*)&caddr,&len);//接受链接
 82                         if(c<0)
 83                         {
 84                             continue;
 85                         }
 86                         printf("accept:%d\n",c);//打印连接信息
 87                         poll_fds_add(c,poll_fds);// 对 poll 结构数组中增加该文件描述符
 88                     }
 89                     else//如果为连接套接字
 90                     {
 91                         char buff[128]={0};//创建空间存储数据
 92                         int num=recv(poll_fds[i].fd,buff,127,0);//读取数据
 93                         if(num<=0)//小于0 说明客户端已经关闭,注:客户端关闭会产生读事件,但不发送数据
 94                         {
 95                             close(poll_fds[i].fd);//关闭连接套接字
 96                             poll_fds_del(poll_fds[i].fd,poll_fds);//从poll数组中移除还描述符
 97                             printf("client close\n");
 98 
 99                         }
100                         else//有数据
101                         {
102                             printf("recv(%d)=%s\n",poll_fds[i].fd,buff);//打印数据
103                             send(poll_fds[i].fd,"ok",2,0);//回复ok
104                         }
105                     }
106 
107                 }
108             }
109         }
110 
111     }
112 }
113 int socket_init()
114 {
115     int sockfd=socket(AF_INET,SOCK_STREAM,0);
116     if(sockfd==-1)
117     {
118         return -1;
119     }
120     struct sockaddr_in saddr;
121     memset(&saddr,0,sizeof(saddr));
122     saddr.sin_family=AF_INET;
123     saddr.sin_port=htons(6000);
124     saddr.sin_addr.s_addr=inet_addr("127.0.0.1");
125 
126     int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
127     if(res==-1)
128     {
129         return -1;
130     }
131 
132     res=listen(sockfd,5);
133     if(res==-1)
134     {
135         return -1;
136     }
137     return sockfd;
138 }
      

select/poll性能总结

  • 在用户空间创建集合或数组 来存放描述符及事件 ,每一次调用select 或 poll 时 ,需要将数组和集合当作参数传进去,每循环调用一次意味着从用户空间往内核空间拷贝一次数据结构;

  • 内核实现 :在内核中 以 轮询 的方式实现,因此在内核中检测是否有事件就绪的时间复杂度为O(n);

  • 函数调用完成返回后,无法确定具体哪个描述符有事件就绪,仅知道有个描述符上有事件就绪,此时便需要用户再次进行遍历查找具体是哪个描述符有事件,此时时间复杂度也为O(n);

结论:无法应对大量描述符。

epoll

epoll 的接口

epoll 是 Linux 特有的 I/O 复用函数。它在实现和使用上与 select、poll 有很大差异。为了解决大量文件描述符的问题。

  • 首 先,epoll 使用一组函数来完成任务,而不是单个函数。

  • 其次,epoll 把用户关心的文件描述 符上的事件放在内核里的一个事件表中。从而无需像 select 和 poll 那样每次调用都要重复传 入文件描述符或事件集。

  • 但 epoll 需要使用一个额外的文件描述符,来唯一标识内核中的这 个事件表。epoll 相关的函数如下:

  • 内核实现:注册回调函数的方式实现。事件复杂度O(1)

  • 返回方式:将就绪个数及具体的就绪的文件描述符信息全部返回 。时间复杂度O(1)

1. #include <sys/epoll.h>
2.
3. int epoll_create(int size);  //创建内核事件表,存放描述符及事件,数据结构:红黑树
​
4. /*
5. epoll_create()成功返回内核事件表的文件描述符,失败返回-1
6. size 参数现在并不起作用,只是给内核一个提示,告诉它事件表需要多大。
7. */
8.
9. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);//用于操作内核事件表,每个描述符只添加一次
10. /*
11. epoll_ctl()成功返回 0,失败返回-1
12. epfd 参数指定要操作的内核事件表的文件描述符
13. fd 参数指定要操作的文件描述符
14. op 参数指定操作类型:
15. EPOLL_CTL_ADD 往内核事件表中注册 fd 上的事件
16. EPOLL_CTL_MOD 修改 fd 上的注册事件
17. EPOLL_CTL_DEL 删除 fd 上的注册事件
18. event 参数指定事件,它是 epoll_event 结构指针类型,epoll_event 的定义如下:
19. struct epoll_event
20. {
21. _uint32_t events; // epoll 事件
22. epoll_data_t data; // 用户数据
23. };
24. 其中,events 成员描述事件类型,epoll 支持的事件类型与 poll 基本相同,表示
epoll 事件的宏是在 poll 对应的宏前加上‘E’,比如 epoll 的数据可读事件是
EPOLLIN。但是 epoll 有两个额外的事件类型--EPOLLET 和 EPOLLONESHOT。
data 成员用于存储用户数据,是一个联合体,其定义如下:
25. typedef union epoll_data
26. {
27. void *ptr;
28. int fd;
29. uint32_t u32;
30. uint64_t u64;
31. }epoll_data_t;
32. 其中 fd 成员使用的最多,它指定事件所从属的目标文件描述符。
33. */
34.
35. int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);//用于在一段超时时间内等待一组文件描述符上的事件,返回就绪的描述符的个数及具体的描述符信息
36.
37. /*
38. epoll_wait()成功返回就绪的文件描述符的个数,失败返回-1,超时返回 0
39. epfd 参数指定要操作的内核事件表的文件描述符
40. events 参数是一个用户数组,这个数组仅仅在 epoll_wait 返回时保存内核检测到
的所有就绪事件,而不像 select 和 poll 的数组参数那样既用于传入用户注册的事
件,又用于输出内核检测到的就绪事件。这就极大地提高了应用程序索引就绪文件
描述符的效率。
41. maxevents 参数指定用户数组的大小,即指定最多监听多少个事件,它必须大于
0
42. timeout 参数指定超时时间,单位为毫秒,如果 timeout 为 0,则 epoll_wait 会立即
返回,如果 timeout 为-1,则 epoll_wait 会一直阻塞,直到有事件就绪。
43. */

epoll应用

实现服务器端:

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<string.h>
  4 #include<unistd.h>
  5 #include<sys/epoll.h>
  6 #include<assert.h>
  7 #include<sys/socket.h>
  8 #include<arpa/inet.h>
  9 #include<netinet/in.h>
 10 #define MAX 10
 11 int socket_init();               //设置套接字
 12 void epoll_add(int epfd,int fd)  //封装一个往内核事件表添加描述符的函数
 13 {
 14     struct epoll_event ev;  //定义事件结构体
 15     ev.data.fd=fd;          //写入文件描述符
 16     ev.events=EPOLLIN;      //写入关注事件 :读
 17     if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev)==-1) //往内核时间表中加入该文件描述符信息
 18     {
 19         printf("epoll ctl erro\n");  
 20     }
 21 }
 22 void epoll_del(int epfd,int fd)  //封装一个从内核事件表删除描述符的函数
 23 {
 24     if(epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL)==-1) //从内核事件表中删除该文件描述符
 25     {
 26         perror("epoll ctl del err\n");
 27     }
 28 }
 29 int main()
 30 {
 31     int sockfd=socket_init();  //创建监听套接字
 32     assert(sockfd!=-1);
 33 
 34     int epfd=epoll_create(MAX); //创建内核事件表 大小为MAX
 35     assert(epfd!=-1);
 36 
 37     epoll_add(epfd,sockfd); //加入监听套接字
 38 
 39     struct epoll_event evs[MAX]; //创建事件结构存储所有就绪的事件
 40 
 41     while(1)
 42     {
 43         int n=epoll_wait(epfd,evs,MAX,5000); //等待有事件就绪,可能会阻塞
 44         if(n==-1)
 45         {
 46             printf("epoll wait err\n"); 
 47         }
 48         else if(n==0)
 49         {
 50             printf("time out\n");
 51         }
 52         else
 53         {
 54             for(int i=0;i<n;i++)  //从就绪的事件中找想要关注的事件
 55             {
 56                 int fd=evs[i].data.fd;      //保存文件描述符
 57                 if(evs[i].events & EPOLLIN) //判断是不是读事件
 58                 {
 59                     if(fd==sockfd)      //判断是不是 监听套接字 有读事件产生
 60                     {
 61                         struct sockaddr_in caddr;  //定义 连接套接字
 62                         int len=sizeof(caddr);  
 63                         int c=accept(sockfd,(struct sockaddr*)&caddr,&len); //接受连接
 64                         if(c<0)
 65                         {
 66                             continue;
 67                         }
 68                         printf("accept c=%d\n ",c); // 打印套接字大小
 69                         epoll_add(epfd,c); //将连接套接字加入内核事件表
 70                     }
 71                     else //连接套接字有读事件产生
 72                     {
 73                         char buff[128]={0}; //创建空间存储数据
 74                         int num=recv(fd,buff,127,0); //从接收缓冲区读数据
 75                         if(num<=0) //客户端关闭
 76                         {
 77                             epoll_del(epfd,fd);  //注:由于要用到文件描述符,因此先将文件描述符移出内核事件表,在进行关闭
 78                             close(fd);
 79                             printf("client close\n");
 80                         }
 81                         else
 82                         {
 83                             printf("recv(%d)=%s\n",fd,buff);//打印读的数据
 84                             send(fd,"ok",2,0); //
 85                         }
 86 
 87                     }
 88                 }
 89             }
 90         }
 91     }
 92 
 93 }
 94 int socket_init() //创建并封装监听套接字
 95 {
 96     int sockfd=socket(AF_INET,SOCK_STREAM,0); //创建监听套接字
 97     if(sockfd==-1)
 98     {
 99         return -1;
100     }
101     struct sockaddr_in saddr;  //创建结构体存储套接字信息
102     memset(&saddr,0,sizeof(saddr)); //置0
103     saddr.sin_family=AF_INET;  
104     saddr.sin_port=htons(6000); //端口
105     saddr.sin_addr.s_addr=inet_addr("127.0.0.1"); //IP
106 
107     int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//绑定套接字及信息
108     if(res==-1)
109     {
110         return -1;
111     }
112 
113     res=listen(sockfd,5); //设置监听队列
114     if(res==-1)
115     {
116         return -1;
117     }
118     return sockfd; 
119 }
​
                                              
lcx@lcx-virtual-machine:~/mycode/2.12$ gcc -o epoll epoll.c
​
​
    
    
accept c=5
time out
time out
time out
time out
time out
time out
time out
accept c=6
time out
recv(6)=asd
​
recv(5)=wad
​
recv(6)=qwd
​
recv(5)=qwsde
​
client close
client close
​

客户端

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<unistd.h>
  4 #include<assert.h>
  5 #include<string.h>
  6 #include<netinet/in.h>
  7 #include<arpa/inet.h>
  8 
  9 int main()
 10 {
 11     int sockfd=socket(AF_INET,SOCK_STREAM,0);
 12     assert(sockfd!=-1);
 13 
 14     struct sockaddr_in saddr;
 15     saddr.sin_family=AF_INET;
 16     saddr.sin_port=htons(6000);
 17     saddr.sin_addr.s_addr=inet_addr("127.0.0.1");
 18 
 19 
 20     int res=connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
 21 
 22     assert(res!=-1);
 23     while(1)
 24     {
 25     char buff[128]={0};
 26 
 27     printf("intput:\n");
 28     fgets(buff,128,stdin);
 29     if(strncmp(buff,"end",3)==0)
 30     {
 31         break;
 32     }
 33     send(sockfd,buff,strlen(buff),0);
 34     memset(buff,0,128);
 35     recv(sockfd,buff,127,0);
 36     printf("buff=%s\n",buff);
 37     }
 38     close(sockfd);
 39 
 40 }
~       

LT 和 ET 模式

对文件描述符有两种操作模式:LT(Level Trigger,电平触发)模式和 ET(Edge Trigger,边沿触发)模式。

LT模式 :

概念:对于 LT 模式操作的文件描述符,当 检测到其上有事件发生并将此事件通知 应用程序后,应用程序可以不处理完该事件。这样,当应用程序下一次调用检测时, 还会再次向应用程序通告此事件,直到该事件被处理完。即对于数据的处理不需要一次性必须处理完。

注:select / poll / epoll 都具有这种模式。

ET 模式:

注:只有epoll 具有这种模式。

当往 epoll 内核事件表中注册一个文 件描述符上的 EPOLLET 事件时,epoll 将以高效的 ET 模式来操作该文件描述符。

概念:对于 ET 模式操作的文件描述符,当 epoll_wait 检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的 epoll_wait 调用将不再向应用程序 通知这一事件。所以 ET 模式在很大程度上降低了同一个 epoll 事件被重复触发的次数,因 此效率比 LT 模式高。

应用:

将用epoll实现的服务器端从LT模式改成ET模式

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<string.h>
  4 #include<unistd.h>
  5 #include<sys/epoll.h>
  6 #include<assert.h>
  7 #include<sys/socket.h>
  8 #include<arpa/inet.h>
  9 #include<netinet/in.h>
 10 #define MAX 10
 11 int socket_init();
 12 void epoll_add(int epfd,int fd)
 13 {
 14     struct epoll_event ev;
 15     ev.data.fd=fd;
 16     ev.events=EPOLLIN|EPOLLET;  // 设置 ET 模式 :按位或 ET模式
 17     if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev)==-1)
 18     {
 19         printf("epoll ctl erro\n");
 20     }
 21 }
 22 void epoll_del(int epfd,int fd)
 23 {
 24     if(epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL)==-1)
 25     {
 26         perror("epoll ctl del err\n");
 27     }
 28 }
 29 int main()
 30 {
 31     int sockfd=socket_init();
 32     assert(sockfd!=-1);
 33 
 34     int epfd=epoll_create(MAX);
 35     assert(epfd!=-1);
 36 
 37     epoll_add(epfd,sockfd);
 38 
 39     struct epoll_event evs[MAX];
 40 
 41     while(1)
 42     {
 43         int n=epoll_wait(epfd,evs,MAX,5000);
 44         if(n==-1)
 45         {
 46             printf("epoll wait err\n");
 47         }
 48         else if(n==0)
 49         {
 50             printf("time out\n");
 51         }
 52         else
 53         {
 54             for(int i=0;i<n;i++)
 55             {
 56                 int fd=evs[i].data.fd;
 57                 if(evs[i].events & EPOLLIN)
 58                 {
 59                     if(fd==sockfd)
 60                     {
 61                         struct sockaddr_in caddr;
 62                         int len=sizeof(caddr);
 63                         int c=accept(sockfd,(struct sockaddr*)&caddr,&len);
 64                         if(c<0)
 65                         {
 66                             continue;
 67                         }
 68                         printf("accept c=%d\n ",c);
 69                         epoll_add(epfd,c);
 70                     }
 71                     else
 72                     {
 73                         char buff[128]={0};
 74                         int num=recv(fd,buff,1,0);  //一次只读一个数据
 75                         if(num<=0)
 76                         {
 77                             epoll_del(epfd,fd);
 78                             close(fd);
 79                             printf("client close\n");
 80                         }
 81                         else
 82                         {
 83                             printf("recv(%d)=%s\n",fd,buff);
 84                             send(fd,"ok",2,0);
 85                         }
 86 
 87                     }
 88                 }
 89             }
 90         }
 91     }
 92 
 93 }
 94 int socket_init()
 95 {
 96     int sockfd=socket(AF_INET,SOCK_STREAM,0);
 97     if(sockfd==-1)
 98     {
 99         return -1;
100     }
101     struct sockaddr_in saddr;
102     memset(&saddr,0,sizeof(saddr));
103     saddr.sin_family=AF_INET;
104     saddr.sin_port=htons(6000);
105     saddr.sin_addr.s_addr=inet_addr("127.0.0.1");
106 
107     int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
108     if(res==-1)
109     {
110         return -1;
111     }
112 
113     res=listen(sockfd,5);
114     if(res==-1)
115     {
116         return -1;
117     }
118     return sockfd;
119 }
         
​
​
lcx@lcx-virtual-machine:~/mycode/2.12$ ./epoll_et
accept c=5
recv(5)=h
time out
time out
​
//客户端输入的是 hello 
//服务器端只收到一次提示有数据读到h,其余数据在节后缓冲区,epoll不会提醒有数据需要读,直到有新的数据在来,
 //epoll提示,此时会读到 e

优化:

由于ET模式下,数据存在丢失的可能,为了在只提式一次的情况下,将所有的数据全部都出来,因此需要进行以下优化。

  1. 描述符设置成非阻塞

  2. 循环处理

 01 #include<fcntl.h>  //描述符设置成非阻塞
 02 #include<errno.h>  //循环处理
​
 11 int socket_init();
 12 void setnonblock(int fd)//1. 封装函数 将文件描述符设置成非阻塞模式
 13 {
 14    int oldfl=fcntl(fd,F_GRTFL); //获取之前文件描述符属性
 15    int newfl=oldfl|O_NONBLOCK;  //增添非阻塞模式属性 ,当文件没有数据读阻塞时会返回-1
 16    if(fcntl(fd,F_SETFL,newfl)==-1)//给文件描述符设置增添后的新属性
 15    {
 14         printf("fcntl error\n");
 15    }
 15 }
​
 12 void epoll_add(int epfd,int fd)
 13 {
 14     struct epoll_event ev;
 15     ev.data.fd=fd;
 16     ev.events=EPOLLIN|EPOLLET;  // 设置 ET 模式 :按位或 ET模式
 17     if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev)==-1)
 18     {
 19         printf("epoll ctl erro\n");
 20     }
 22     setnonblock(fd); //调用函数 给文件描述符 设置非阻塞形式
 21 }
 22 void epoll_del(int epfd,int fd)
 23 {
 24     if(epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL)==-1)
 25     {
 26         perror("epoll ctl del err\n");
 27     }
 28 }
 29 int main()
 30 {
 31     int sockfd=socket_init();
 32     assert(sockfd!=-1);
 33 
 34     int epfd=epoll_create(MAX);
 35     assert(epfd!=-1);
 36 
 37     epoll_add(epfd,sockfd);
 38 
 39     struct epoll_event evs[MAX];
 40 
 41     while(1)
 42     {
 43         int n=epoll_wait(epfd,evs,MAX,5000);
 44         if(n==-1)
 45         {
 46             printf("epoll wait err\n");
 47         }
 48         else if(n==0)
 49         {
 50             printf("time out\n");
 51         }
 52         else
 53         {
 54             for(int i=0;i<n;i++)
 55             {
 56                 int fd=evs[i].data.fd;
 57                 if(evs[i].events & EPOLLIN)
 58                 {
 59                     if(fd==sockfd)
 60                     {
 61                         struct sockaddr_in caddr;
 62                         int len=sizeof(caddr);
 63                         int c=accept(sockfd,(struct sockaddr*)&caddr,&len);
 64                         if(c<0)
 65                         {
 66                             continue;
 67                         }
 68                         printf("accept c=%d\n ",c);
 69                         epoll_add(epfd,c);
 70                     }
 71                     else
 72                     {
 76                       while(1)  // 2. 循环处理数据
 74                       {
 73                             char buff[128]={0};
 74                             int num=recv(fd,buff,1,0);  //一次只读一个数据    
 81                             if(num==-1) //-1 可能是出错或者 或者无数据读阻塞
 82                             {
 81                                 if(errno==EAGAIN || errno==EWOULDBLOCK) //是阻塞 ,即数据读完了
 82                                 {
 83                                     send(fd,"ok",2,0); // 回复 ok
 85                                 }
 81                                 else//出错
 82                                 {
 83                                     printf("recv error\n"); 
 85                                 }
 81                                 break;//退出循环
 85                             }
 75                             else if(num==0) //客户端关闭
 76                             {
 77                                 epoll_del(epfd,fd); //移出内核事件表
 78                                 close(fd); //关闭文件描述符
 79                                 printf("client close\n");
 81                                 break;     //退出循环
 80                             }
 81                             else//有数据
 82                             {
 83                                 printf("recv(%d)=%s\n",fd,buff); //打印读到数据
 85                             }
 76                         }
 87                     }
 88                 }
 89             }
 90         }
 91     }
 92 
 93 }
 94 int socket_init()
 95 {
 96     int sockfd=socket(AF_INET,SOCK_STREAM,0);
 97     if(sockfd==-1)
 98     {
 99         return -1;
100     }
101     struct sockaddr_in saddr;
102     memset(&saddr,0,sizeof(saddr));
103     saddr.sin_family=AF_INET;
104     saddr.sin_port=htons(6000);
105     saddr.sin_addr.s_addr=inet_addr("127.0.0.1");
106 
107     int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
108     if(res==-1)
109     {
110         return -1;
111     }
112 
113     res=listen(sockfd,5);
114     if(res==-1)
115     {
116         return -1;
117     }
118     return sockfd;
119 }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值