第十七章:优于select的epoll(含epoll使用特性、触发方式与select差异以及回声服务器客户端实现)
之前我们在第十二章中介绍了select函数。传统的I/O复用的方法有select函数和epoll函数。但是其性能存在缺点(这点在下面会说到)。因此有了不同操作系统下的I/O复用技术,Linux下的epoll、BSD的kqueue、Win的IOCP等。
17.1 epoll理解及应用
select函数的复用方法,无论如何优化程序性能也无法实现接入上百个客户端。这种方式并不适合当前以Web服务器端开发的主流环境。因此我们这里学习Linux下的epoll
17.1.1 基于select的I/O复用技术为什么速度慢?
12章 基于select的IO复用服务器端,最主要有两点设计不合理(如果忘记代码上方有12章链接):
- 调用select函数后常见的针对所有文件描述符的循环语句
- 每次调用select函数时都需要向该函数传递监视对象信息
调用select函数后并非把所有发生变化的文件描述符单独集中到一起,而是通过观察作为监视对象的fd_set结构变量的变化,找出发生变化的文件描述符。
同时,调用select函数前要对监控对象的fd_set变量进行保存复制,因为变量会发生变化~
并且需要再每次调用select函数时传递新的监视对象信息。
在这些过程中,性能提高的主要障碍是:每次传递监视对象信息。
每次调用select函数时向操作系统传递监视对象信息
这里的关键字是操作系统
应用程序将数据传递给操作系统有很大负担,但是为什么要把监视对象的信息传递给操作系统呢?
因为套接字是由操作系统管理的,而我们需要监视套接字的变化,需要借助操作系统的才能完成
优化的方式就是:仅向操作系统传递一次监视对象,监视范围或内容发生变化时只通知发生变化的事项
这样就不需要每次调用select函数都要向操作系统传递监视对象信息了,这在linux下就是epoll
17.1.2 select的特点(优点)
下面情况下应该考虑使用select函数
- 服务器端接入者少
- 程序需要高兼容性
17.1.3 实现epoll时必要的函数和结构体
epoll的特点:
- 无需编写用来监视状态变化,针对所有文件描述符的循环语句
- 调用对用于select函数的epoll_wait函数时无需每次传递监视对象信息
下面说下使用epoll服务器的3个函数,结合epoll的优点理解下面函数功能:
- epoll_create:创建保存epoll文件描述符的空间。
- epoll_ctl:向空间注册并注销文件描述符
- epoll_wait:以select函数类似,等待文件描述符发生变化
下面解释一下:
- select方式中为了保存监视对象文件描述符,直接声明了fd_set变量。epoll方式下由操作系统负责保存监视对象文件描述符,因此需要向操作系统请求创建保存文件描述符的空间,此时使用的函数就是
epoll_create
。 - 为了添加和删除监视对象文件描述符,select方式中使用FD_SET FD_CLR函数。但在epoll函数中,通过
epoll_ctl
函数请求操作系统完成。 - select方式下调用select函数等待文件描述符变化,epoll中调用
epoll_wait
函数。还有select方式中通过fd_set变量查看监视对象的状态变化(事件发生与否),epoll中通过结构体epoll_event
将发生变化的(发生事件的)文件描述符单独集中到一起。(如下所示)
struct epoll_event
{
__uint32_t events;
epoll_data_t data;
}
typedef union epoll_data
{
void * prt;
int fd;
__uint32_t u32;
__uint64_t u64;
}epoll_data_t;
声明足够大的epoll_event
结构体数组后,传递给epoll_wait函数时,发生变化的文件描述符,将被填入该数组。因此无需像select函数那样针对所有文件描述符进行循环。
下面对函数进行详细说明
17.1.4 epoll_create
从Linux内核从2.5.44开始引入了epoll
,输入cat /proc/sys/kernel/osrelease
查看自己的Linux版本。
epoll_create函数
调用epoll_create
函数时创建的文件描述符保存空间称为epoll例程有些时候名称不同,需要稍加注意。
通过参数size传递的值决定epoll例程的大小,但该值只是向操作系统提的建议,换言之,size并非用来决定epoll例程的大小,仅供操作系统参考。
#include <sys/epoll.h>
int epoll_create(int size);
->成功时返回epoll文件描述符,失败时返回-1
size:epoll实例的大小。
(在Linux2.6.8之后,操作系统将完全忽略传入epoll_create
函数的参数size,内核会根据情况调整epoll例程的大小。)
epoll_create函数创建的资源与套接字相同,也由操作系统管理。因此该函数和创建套接字的情况相同,也会返回文件描述符。
也就是说,该函数的文件描述符主要用于区分epoll例程。需要终止时,与其他文件描述符相同,也要调用close函数。
17.1.5 epoll_ctl
生成epoll例程后,应在其内部注册监视对象文件描述符,此时使用epoll_ctl函数。
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
->成功时返回0,失败时返回-1.
epfd:用于注册监视对象的epoll例程的文件描述符。
op:用于指定监视对象的添加、删除或更改等操作。
fd:需要注册的监视对象文件描述符
event:监视对象的事件类型
理解一下:
epoll_ctl(A, EPOLL_CTL_ADD, B, C);
“epoll例程A中注册文件描述符B,主要目的是监视参数C中保存的事件”
epoll_ctl(A, EPOLL_CTL_DEL, B, NULL);
“从epoll例程A中删除文件描述符B”
从监视对象中删除时,不需要监视类型(事件信息),因此向第四个参数传递NULL。
接下来介绍epoll_ctl第二个参数传递的常量及含义。
- EPOLL_CTL_ADD:将文件描述符注册到epoll例程
- EPOLL_CTL_DEL:从epoll例程中删除文件描述符
- EPOLL_CTL_MOD:更改注册的文件描述符的关注事件发生情况
(Linux2.6.9之前的内核不允许传递NULL,向epoll_ctl第二个参数传递EPOLL_CTL_DEL时,应同时向第四个参数传递NULL,所以正常传递epoll_event结构体变量的地址值就行了。)
epoll_ctl的第四个参数:epoll_event event
“这里是用于保存发生变化(发生事件)的文件描述符。但其作用远不止保存一个文件描述符而已”
struct epoll_event
{
__uint32_t events;
epoll_data_t data;
}
typedef union epoll_data
{
void * prt;
int fd;
__uint32_t u32;
__uint64_t u64;
}epoll_data_t;
我们从例子中去理解这个结构体里面乱七八糟的东西
struct epoll_event event;
...
event.events = EPOLLIN; // 发生需要 读取数据 的事件时
event.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event);
...
上面代码:将sockfd注册到epoll例程epfd中,并在需要读取数据时产生相应事件;
下面给出epoll_event的成员events中可以保存的常量及所指的事件类型
- EPOLLIN:需要读取数据的情况
- EPOLLOUT:输出缓冲为空,可以理解发送数据的情况
- EPOLLPRI:收到OOB数据的情况
- EPOLLRDHUP:断开连接或半关闭的情况(在边缘触发方式下非常有用)
- EPOLLERR:发生错误的情况
- EPOLLET:以边缘触发的方式得到事件通知
- EPOLLONESHOT:发生一次事件后,相应文件描述符不再收到事件通知。因此需要向epoll_ctl函数的第二个参数传递EPOLL_CTL_MOD,再次设置。
可以通过位或运算同时传递多个上述参数。
目前只需了解EPOLLIN即可。
17.1.6 epoll_wait
epoll相关函数中默认最后调用该函数。
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
->成功时返回发生事件的文件描述符数,失败时返回-1
epfd:表示事件发生监视范围的 epoll例程的文件描述符
events:保存发生事件的文件描述符结合的 结构体变量地址值
maxevents:第二个参数中可以保存的最大事件数
timeout:以1/1000秒为单位的等待事件,传递-1表示一直等待到事件发生
epoll_wait函数调用方式及动态分配缓冲
int event_cnt;
struct epoll_event* ep_events;
...
ep_events = malloc(sizeof(struct epoll_event)*EPOLL_EIZE);
// epoll_size是宏定义常量
...
event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
...
调用函数后,返回发生事件的文件描述符数,同时在第二个参数指向的缓冲中保存发生事件的文件描述符集合。因此,无需像select那样插入针对所有文件描述符的循环。
17.1.7 基于epoll的回声服务器端
这里通过修改第12章的echo_selectserv.c实现了基于epoll的回声服务器端
我们先看看之前的echo_selectserv.c,详看注释
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/time.h> // select函数需要用到的两个头文件1
#include <sys/select.h> // 头文件2
#define BUF_SIZE 100
void error_handling(char *buf); // 异常处理函数
int main(int argc, char *argv[])
{
// 定义变量
int serv_sock, clnt_sock; // 套接字文件描述符
struct sockaddr_in serv_adr, clnt_adr; // 套接字地址结构体变量
struct timeval timeout; // 用于select函数中设置超时时间,过时没有问价描述符发生事件,则返回0
fd_set reads, cpy_reads; // select函数中存储文件描述符的变量结构体
socklen_t adr_sz;
int fd_max, str_len, fd_num, i;
char buf[BUF_SIZE];
if(argc!=2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
// 初始化服务器端套接字和地址信息。
serv_sock=socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_adr.sin_port=htons(atoi(argv[1]));
// 分配地址并设置接听
if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)
error_handling("bind() error");
if(listen(serv_sock, 5)==-1)
error_handling("listen() error");
// 初始化 结构体变量内数值,
FD_ZERO(&reads);
FD_SET(serv_sock, &reads); //将serv_sock注册到reads中
fd_max=serv_sock; // 最大文件描述符数量 (-1)
while(1) // 主循环
{
cpy_reads=reads; // 复制reads结构体变量
timeout.tv_sec=5; // 设置超时时间
timeout.tv_usec=5000;
// 调用select函数进行监视并阻塞,直至有文件描述符发生变化(事件发生)or超时
// 如果出错则退出循环
if((fd_num=select(fd_max+1, &cpy_reads, 0, 0, &timeout))==-1)
break;
// 如果超时重新循环
if(fd_num==0)
continue;
// 正常情况:for循环 从0开始检查每一个文件描述符的变化
for(i=0; i<fd_max+1; i++)
{
if(FD_ISSET(i, &cpy_reads)) // 如果cpy_reads中的i号文件描述符发生事件
{
if(i==serv_sock) // 如果i是服务器端套接字:代表listen函数接收到了请求
{
// 受理请求 并 将新产生的 服务器端套接字注册到监视器reads中
adr_sz=sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
FD_SET(clnt_sock, &reads);
if(fd_max<clnt_sock) // 更新文件描述符最大数量
fd_max=clnt_sock;
printf("connected client: %d \n", clnt_sock);
}
else // 如果i不是服务器端套接字:代表是客户端套接字收到客户端发来的消息
{
str_len=read(i, buf, BUF_SIZE);
if(str_len==0) // 接收到eof,代表发送结束
{
FD_CLR(i, &reads); // 清除监视器中客户端套接字
close(i); // 关闭客户端套接字
printf("closed client: %d \n", i);
}
else // 未接收到eof:代表接收到了信息,回声服务器把接收到的再传回去。
{
write(i, buf, str_len); // echo!
}
}
}
}
}
close(serv_sock); // 关闭服务器端套接字
return 0;
}
void error_handling(char *buf)
{
fputs(buf, stderr);
fputc('\n', stderr);
exit(1);
}
下面是修改后的利用epoll进行io复用的回声服务器客户端
echo_epollserv.c
仔细看注释!!!!!!!!!
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#define BUF_SIZE 100
#define EPOLL_SIZE 50
void error_handling(char *buf);
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
socklen_t adr_sz;
int str_len, i;
char buf[BUF_SIZE];
struct epoll_event *ep_events; // 为了 epoll_wait函数定义的变量指针
struct epoll_event event; // 为了epoll_ctl函数定义的结构体变量
int epfd, event_cnt;
if(argc!=2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
serv_sock=socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_adr.sin_port=htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)
error_handling("bind() error");
if(listen(serv_sock, 5)==-1)
error_handling("listen() error");
epfd=epoll_create(EPOLL_SIZE); // 创建epoll例程文件描述符epfd
// 为指针分配空间(大小为50*epoll_event大小),在epoll_wait函数中使用
// 用来在缓冲中保存发生事件的文件描述符集合
ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
// event这个变量在epoll_ctl中使用
event.events=EPOLLIN; // 设置事件为:读取数据
event.data.fd=serv_sock; // 设置事件数据信息中,文件描述符为:serv_sock表示用于监视该描述符
// epoll监视器设置:设置epfd监视器,添加监视对象:serv_sock,监视方式及相关信息:event
epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);
while(1)
{
// 开启监视器epfd,将发生事件的套接字相关信息存入ep_events缓存中, 最大可存入事件数:50
// -1:阻塞直至有事件发生
event_cnt=epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
if(event_cnt==-1)
{
puts("epoll_wait() error");
break;
}
// 循环访问发生事件的文件描述符信息
for(i=0; i<event_cnt; i++)
{
if(ep_events[i].data.fd==serv_sock) // 缓存中的数据信息中 文件描述符为 : serv_sock时
{ // 代表有listen函数接收到请求
// 受理 连接请求
adr_sz=sizeof(clnt_adr);
clnt_sock=
accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
// 继续添加监视对象!这里重复利用events变量进行设置,添加客户端文件描述符
event.events=EPOLLIN;
event.data.fd=clnt_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
printf("connected client: %d \n", clnt_sock);
}
else
{ // 缓冲数据中 ep_events[i].data.fd != serv_sock
// 代表客户端套接字发生事件:需要接收数据
str_len=read(ep_events[i].data.fd, buf, BUF_SIZE);
if(str_len==0) // 若收到eof代表 连接结束,注销监视对象并关闭套接字
{
epoll_ctl(
epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
close(ep_events[i].data.fd);
printf("closed client: %d \n", ep_events[i].data.fd);
}
else // 若未收到eof表示当前只是发送结束,并未断开连接,echo即可
{
write(ep_events[i].data.fd, buf, str_len); // echo!
}
}
}
}
close(serv_sock); // 关闭服务器套接字
close(epfd); // 关闭epoll监视
return 0;
}
void error_handling(char *buf)
{
fputs(buf, stderr);
fputc('\n', stderr);
exit(1);
}
运行结果与之前一样,不展示了
17.2条件触发与边缘触发
epoll的核心之一就是这两个概念,我们需要真正理解这两个概念的含义。
17.2.1 条件触发与边缘触发的区别在于发生事件的时间点
首先我们看看什么是条件触发与边缘触发
-
条件触发方式中,只要输入缓冲有数据就会 一直通知该事件。
例如:服务器端输入缓冲收到50字节数据,服务器端操作系统将通知该事件(注册到发生变化的文件描述符)。但是服务器读取20字节后还剩30字节,仍会再次注册时间。也就是说,条件触发方式中,只要输入缓冲中还剩数据,就将以时间方式再次注册。 -
边缘触发中输入缓冲收到数据时仅注册一次该事件。即使输入缓冲中还留有数据,也不会再进行注册。
17.2.2 条件触发的事件特性
首先我们要明白 条件触发与边缘触发的注册方式不同,导致其内部的工作内容不同。
epoll默认以条件触发方式工作,因此可以通过下面的示例验证够条件触发的特性。
echo_EPLTserv.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#define BUF_SIZE 4 // 注意这里:使用的字符数组大小为4个字节
#define EPOLL_SIZE 50
void error_handling(char *buf);
int main(int argc, char *argv[])
{
// 定义一些变量
int serv_sock, clnt_sock; // 服务器套接字 和 用于连接客户端的套接字(简称客户端套接字)
struct sockaddr_in serv_adr, clnt_adr; // 声明服务器和客户端地址结构体变量
socklen_t adr_sz; // accept函数要用
int str_len, i;
char buf[BUF_SIZE];
struct epoll_event *ep_events; // 声明监视器参数变量,用于epoll_wait函数
struct epoll_event event; // 声明监视器参数变量,用于epoll_ctl函数
int epfd, event_cnt; // 声明监视器文件描述符 以及 变化的文件描述符个数(分别用于 epoll_create epoLL_wait)
if(argc!=2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
// 初始化服务器端套接字
serv_sock=socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_adr.sin_port=htons(atoi(argv[1]));
// 分配服务器端套接字地址信息 并 listen请求
if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)
error_handling("bind() error");
if(listen(serv_sock, 5)==-1)
error_handling("listen() error");
// epoll监视器初始化,返回epoll监视器文件描述符
epfd=epoll_create(EPOLL_SIZE);
// 为保存监视器中发生变化的文件描述符 分配缓冲空间:大小为:每个保存信息的结构体的大小 * 数量
ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
// 设置event变量:填入servsock相关信息;向epfd监视器中添加serv_sock文件描述符,并使用event设置监视方式
event.events=EPOLLIN; // 监视方式为:需要读取数据(每当有数据来临时注册事件)
event.data.fd=serv_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);
// 上面设置好了相关操作的变量,循环中开始监听,并根据监听得到的信息,判断如何进行处理
while(1)
{
// 开始监听:使用epfd监视器,将注册(发生)的事件信息存入ep_events指向的缓冲中,最大监听注册的事件数为:50,阻塞直至发生事件
event_cnt=epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
// 监听期间发生了一些事件
// 1. 监听发生错误: 退出大循环,不再继续监听
if(event_cnt==-1)
{
puts("epoll_wait() error");
break;
}
// 2. 监听正常:
puts("return epoll_wait");
// for循环查看每一个发生变化的事件,查看其缓冲中的信息。
for(i=0; i<event_cnt; i++)
{
if(ep_events[i].data.fd==serv_sock) // 如果当前缓冲中的第i个注册事件为:serv_sock相关事件,代表listen接收到了连接请求
{ // 接受连接请求,并将clnt_sock添加到epfd监视器中
adr_sz=sizeof(clnt_adr);
clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
// 设置event变量:填入clnt_sock相关信息;向epfd监视器中添加clnt_sock文件描述符,并使用event设置监视方式
event.events=EPOLLIN; // 监视方式为:可能需要读取数据时(每当有数据来临时注册事件)
event.data.fd=clnt_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
printf("connected client: %d \n", clnt_sock);
}
else // 如果当前缓冲中的第i个注册事件不为:serv_sock相关事件,代表只能是注册的客户端套接字发生了事件,也就是说有数据来临时注册事件,需要这边进行读取数据
{
str_len=read(ep_events[i].data.fd, buf, BUF_SIZE);
if(str_len==0) // 客户端套接字接收到eof,也就是服务器断开了连接
{ // 注销监视器epfd中的当前套接字,并关闭客户端套接字
epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
close(ep_events[i].data.fd);
printf("closed client: %d \n", ep_events[i].data.fd);
}
else // 也就是str_len > 0 时,只要echo就行了
{
write(ep_events[i].data.fd, buf, str_len); // echo!
}
}
}
}
close(serv_sock);
close(epfd);
return 0;
}
void error_handling(char *buf)
{
fputs(buf, stderr);
fputc('\n', stderr);
exit(1);
}
注意这里服务器端代码中,BUF_SIZE大小变为 4
同时,通过打印“return epoll_wait”字符串次数,判断调用该函数调用次数。
测试结果:
正如我们之前所说,条件触发方式下的epoll函数,在每次接收到客户端数据时,都会注册该事件(发生事件),并且多次调用epoll_wait函数。
(select函数也是以条件触发的方式工作的)
17.2.3 边缘触发的服务器端准备
在实现边缘触发服务器之前,需要搞定两点新东西:
- 通过errno变量验证错误原因
- 为了完成非阻塞(Non-blocking)i/o,更改套接字特性
1)
Linux套接字相关函数一般通过返回-1通知发生了错误,但是这样无法知道产生错误的原因。为了提供额外信息,Linux声明了如下全局变量: int errno;
为了访问该变量,需要头文件error.h
,此头文件中有变量的extern声明。每种函数发生错误时,保存到errno中的值都是不不同的,我们这里只了解下面类型的错误:
“read函数发现输入缓冲中没有数据可读时,返回-1,同时在errno中保存EAGAIN常量。”
2)套接字改为非阻塞方式的方法。
使用fcntl
函数
Linux提供更改或读取文件属性的如下方法:
#include <fcntl.h>
int fcntl(int filedes, int cmd, ...):
-> 成功时返回cmd参数相关值,失败时返回-1
filedes:属性更改目标的文件描述符
cmd:表示函数调用的目地
解释一下:fcntl具有可变参数的形式
如果向cmd中传入F_GETFL
,可以获得filedes的属性(int型)。
如果向cmd中传入F_SETFL
,可以更改filedes的属性(int型)。
因此,如果希望将文件(套接字)更改为非阻塞模式:
inf flag = fcntl(fd, F_GETFL, 0); //获取属性
fcntl(fd, F_SETFL, flag|O_NONBLOCK);// 设置属性
为啥介绍这俩东西呢?因为与边缘触发的服务器端有关系呗~
- 在边缘触发方式中,接收数据时仅注册一次该事件。因此一旦发生输入相关事件,应该从输入缓冲中读取全部数据。因此需要验证输入缓冲是否为空:当read函数返回-1,变量errno中值为EAGAIN,说明没有数据可读
- 边缘触发方式下,以阻塞方式工作的read&write函数有可能引起服务器端的长时间停顿,因此边缘触发方式中一定要采用非阻塞read&write函数。
17.2.4 边缘触发的服务器端实现
echo_EPETserv.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#define BUF_SIZE 4
#define EPOLL_SIZE 50
void setnonblockingmode(int fd); // 设置非阻塞模式
void error_handling(char *buf);
int main(int argc, char *argv[])
{
// 声明变量
int serv_sock, clnt_sock; // 套接字相关
struct sockaddr_in serv_adr, clnt_adr;
socklen_t adr_sz;
int str_len, i;
char buf[BUF_SIZE];
struct epoll_event *ep_events; // epoll相关 具体说明见上面代码
struct epoll_event event;
int epfd, event_cnt;
if(argc!=2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
// 套接字地址初始化、分配地址、开始listen
serv_sock=socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_adr.sin_port=htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)
error_handling("bind() error");
if(listen(serv_sock, 5)==-1)
error_handling("listen() error");
// epoll创建监视器文件 epfd、 为epoll_wait函数中的存储分配缓冲
epfd=epoll_create(EPOLL_SIZE);
ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
// 设置套接字非阻塞模式(更改其read&write方式)
setnonblockingmode(serv_sock);
// 填写event变量、添加服务器套接字进入epfd监视器中,监视方式为:有数据进入时(需要读取数据时)
event.events=EPOLLIN;
event.data.fd=serv_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);
// 主循环:监视——》判断——》按情况处理
while(1)
{
event_cnt=epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
if(event_cnt==-1) // 监视器错误时直接退出
{
puts("epoll_wait() error");
break;
}
// 监视器正常时:for循环遍历存储着发生变化的缓冲空间
puts("return epoll_wait");
for(i=0; i<event_cnt; i++)
{
if(ep_events[i].data.fd==serv_sock);// 服务器套接字发生事件:listen工作,有请求(数据)流入。需要accept
{
adr_sz=sizeof(clnt_adr);
clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
setnonblockingmode(clnt_sock);
event.events=EPOLLIN|EPOLLET; // 设置监控方式为:有数据流入 或 边缘触发方式获得通知
event.data.fd=clnt_sock; // 添加客户端套接字进入监视器
epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
printf("connected client: %d \n", clnt_sock);
}
else
{ // 客户端套接字发生事件:有数据接入且为边缘触发方式通知
while(1) // 由于采用了EPLLLET 只会通知一次,因此如果不加while在读取4个字节后,会在
{ // epoll_wait阻塞
str_len=read(ep_events[i].data.fd, buf, BUF_SIZE);
if(str_len==0) // close request!
{
epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
close(ep_events[i].data.fd);
printf("closed client: %d \n", ep_events[i].data.fd);
break;
}
else if(str_len<0) // 如果str_len<0 且 errno == EAGAIN:代表输入缓冲无数据
{
if(errno==EAGAIN)
break;
}
else
{
write(ep_events[i].data.fd, buf, str_len); // echo!
}
}
}
}
}
close(serv_sock);
close(epfd);
return 0;
}
void setnonblockingmode(int fd)
{
int flag=fcntl(fd, F_GETFL, 0); // 获取fd设置
fcntl(fd, F_SETFL, flag|O_NONBLOCK); // 更改fd设置
}
void error_handling(char *buf)
{
fputs(buf, stderr);
fputc('\n', stderr);
exit(1);
}
测试结果:
17.2.5 边缘触发与条件触发孰优孰劣
边缘触发的优势:可以分离接收数据和处理数据的时间点!
从上面的代码中我们可能很难看出孰优孰劣,我们看下面这个例子:
我们希望从abc客户端分别接受数据,在服务器端进行整合处理,之后发送到其他主机。
为了完成,如果按照下面的流程运行,服务器端实现比较简单:
- 客户端按照abc顺序连接服务器,并依序向服务器发送数据
- 需要接收数据的客户端应在abc之前连接到服务器并等待
但是现实是:
3. c和b正在发送数据,可能a还没连上
4. abc发送数据顺序混乱
5. 服务器接收到数据,但是目标客户端没有连接到服务器
因此我们需要在输入缓冲接收到数据(注册相应事件)后,服务器也能决定读取或者处理这些数据的时间点,提升服务器灵活性。
然而条件触发也可以区分数据的接收和处理啊!!
但是!在输入缓冲收到数据的情况下,如果不读取数据(延迟处理),每次调用epoll_wait函数时都会产生相应事件!事件越来越多,服务器渐渐无法承受。