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模式下,数据存在丢失的可能,为了在只提式一次的情况下,将所有的数据全部都出来,因此需要进行以下优化。
-
描述符设置成非阻塞
-
循环处理
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 }