什么是多I/O的多路复用?
考虑一下这个问题,在一个程序中对两个fd进行阻塞读写,那么对任何一个fd进行阻塞读写,都会导致另一个fd没法处理,比如就算其已经有了数据也不能进行读;若采用非阻塞轮询方式进行处理,这种方法会导致cpu的负荷很大,cpu做了很多无用的轮询,若采用多进程、多线程方式开辟独立的线程分别操作一个fd,则进程、线程间的同步增加了代码的复杂性。
I/O 多路复用机制,单个线程通过记录跟踪每一个Socket (I/O流)的状态来同时管理多个 I/O 流。 发明它的原因,是尽量多的提高服务器的吞吐能力。
例如:nginx使用epoll接收请求的过程:ngnix会有很多链接进来, epoll会把他们都监视起来,然后像拨开关一样,谁有数据就拨向谁,然后调用相应的代码处理。
一、select机制
1. select函数介绍
在Unix中,select函数可以实现I/O的多路复用,传向select函数的参数告诉内核:
(1) 我们所关心的文件描述符;
(2) 对于每个描述符我们所关心的状态(是否可读,是否可写,或者是否这个描述符出现异常)
(3) 希望等待多长时间(可以永远阻塞,等待一个固定时间,或者不阻塞)
从select返回时,内核告诉我们
(1) 已经准备好的描述符的数量;
(2) 哪一个描述符已经准备好(读、写或异常条件)
使用这种返回值,我们就可以调用相应的I/O函数(一般为read或write),并确知该函数不会阻塞。
#include
int select(int maxfdpl, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *tvptr);
//正常返回准备就绪的描述符数目,超时返回 0,出错返回 -1
(1) 参数1:maxfdpl
是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1。
考虑所有 3 个描述符集,在 3 个描述符集中找出最大描述符编号值,然后加 1,这就是第一个参数值。 也可将第一个参数设置为 FD_SETSIZE,这是
(2) 参数2~4,readfds(读)、writefds(写) 和 exceptfds(异常) 是指向描述符集的指针。
这 3 个描述符集说明了我们关心的可读、可写或处于异常条件的描述符集合。每个描述符集存储在一个 fd_set 数据类型中。这种数据类型是用位图实现的,它为每一个可能的描述符分配了一位。我们可以认为它只是一个由很多二进制位构成的数组(根据maxfdpl进行遍历,而不是全部遍历,那样浪费时间)。
对于 fd_set 数据类型,唯一可以进行的处理是:分配一个这种类型的变量,将这种类型的一个变量值赋给同类型的另一个变量,或对这种类型的变量使用以下 4 个宏。
FD_ZERO(fd_set *fdset); //清除fdset中的所有位
FD_SET(int fd, fd_set *fdset); //将fdset描述符集中的指定fd位置1
FD_CLR(int fd, fd_set *fdset); //将fdset描述符集中的指定fd位清0
FD_ISSET(int fd, fd_set *fdset); //测试fdset描述符集中的指定fd位的值
在声明了一个描述符集之后,必须用 FD_ZERO 将这个描述符集置为 0,然后在其中设置我们关心的各个描述符的位。具体操作如下所示:
fd_set rset;
int fd;
FD_ZERO (&rset);
FD_SET (fd, &rset);
FD_SET (STDIN_FILEND, &rset);
从 select 返回时,用 FD_ISSET 测试该集中的一个给定位是否仍为1,注意:select返回的时候,没有事件发生的fd被置零。
if (FD_ISSET (fd, &rset))
{
....
}
select 的中间 3 个参数(指向描述符集的指针)中的任意一个(或全部)可以是空指针,这表示对相应条件并不关心。如果所有 3 个指针都是 NULL,则 select 提供了比 sleep 更精确的定时器。
(3) 参数 tvptr,它指定愿意等待的时间长度,单位为秒和微秒。
结构体 timeval 定义如下:
struct timeval{
long tv_sec; /*秒 */
long tv_usec; /*微秒 */
}
tvptr 的取值有以下 3 中情况:
tvptr == NULL
永远等待。如果捕捉到一个信号则中断此无限期等待。当所指定的描述符中的已准备好或捕捉到一个信号则返回。如果捕捉到一个信号,则 select 返回 -1,errno 设置为 EINTR。
tvptr->tv_sec == 0 && tvptr->tv_usec == 0
根本不等待。测试所有指定的描述符并立即返回。这是轮询系统找到多个描述符状态而不阻塞 select 函数的方法。
tvptr->tv_sec != 0 || tvptr->tv_usec != 0
等待指定的秒数和微妙数。当指定的描述符之一已准备好,或当指定的时间值已经超过时立即返回。如果在超时到期时还没有一个描述符准备好,则返回值是 0。(如果系统不提供微秒级的精度,则 tvptr->tv_usec 值取整到最近的支持值)与第一种情况一样,这种等待可被捕捉到的信号中断。
(4) 函数返回
select 有 3 个可能的返回值。
(i). 返回值 -1表示出错。
这是可能发生的,例如,在所指定的描述符一个都没准备好时捕捉到一个信号。在此情况下,一个描述符集都不修改。
错误码有:
EBADF An invalid file descriptor was given in one of the sets. (Perhaps a file descriptor that was already closed, or one on which an error has occurred.)
EINTR A signal was caught; see signal(7).
EINVAL nfds is negative or the value contained within timeout is invalid.
ENOMEM unable to allocate memory for internal tables.
(ii). 返回值 0 表示返回时没有描述符准备好。
若指定的描述符一个都没准备好,指定的时间就过去了,那么就会发生这种情况。此时,所有描述符集都会置 0。
(iii).一个正返回值说明了已经准备好的描述符数。
该值是 3 个描述符集中已准备好的描述符数之和,所以如果同一描述符已准备好读和写,那么在返回值中会对其计两次数。在这种情况下,3 个描述符集中仍旧打开的位是对应于已准备好的描述符。
对于“准备好”的含义要作一些更具体的说明:
(1) 若对读集(readfds)中的一个描述符进行的 read 操作不会阻塞,则认为此描述符是准备好的。
(2) 若对写集(writefds)中的一个描述符进行的 write 操作不会阻塞,则认为此描述符是准备好的。
(3) 若对异常条件集(exceptfds)中的一个描述符有一个未决异常条件,则认为此描述符是准备好的。(现在,异常条件包括:在网络连接上到达指定波特率外的数据,或者在处于数据包方式的伪终端上发生了某些条件)。
对于读、写和异常条件,普通文件的文件描述符总是返回准备好。
2. select函数使用注意事项:
(1) 可监控的文件描述符个数取决与sizeof(fd_set)的值;
(2) 将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,一是用于再select返回后,array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始 select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。
(3) 由2可知,select模型必须在select前循环array(加fd,取maxfd),select返回后循环array(FD_ISSET判断是否有事件发生,必须要使用FD_ISSET查询一遍,否则select执行不正常)。
(1) select的超时参数,select每次返回后都得重新装载值,否则下次select就变为非阻塞;
(2) 若select成功捕获到有个fd,那必须使用FD_ISSET宏进行检查(就是监控一个fd也得调用这个),如果不调用,那么下次select就不正常(捕获不到这个fd的变化)
3.select 实现原理
select的实现依赖于设备的驱动函数poll,poll的功能是检查设备的哪条条流可用(一个设备一般有三条流,读流,写流,设备发生异常的异常流),如果其中一条流可用,返回一个mask(表示可用的流),如果不可用,把当前进程加入设备的流等待队列中,例如读等待队列、写等待队列,并返回资源不可用。
select正是利用了poll的这个功能,首先让程序员告知自己关心哪些io流(用文件描述符表示,也就是上文的readfds、writefds和exceptfds),并让程序员告知自己这些流的范围(也就是上文的nfds参数)以及程序的容忍度(timeout参数),然后select会把她们拷贝到内核,在内核中逐个调用流所对应的设备的驱动poll函数,当范围内的所有流也就是描述符都遍历完之后,他会检查是否至少有一个流发生了,如果有,就修改那三个流集合,把她们清空,然后把发生的流加入到相应的集合中,并且select返回。如果没有,就睡眠,让出cpu,直到某个设备的某条流可用,就去唤醒阻塞在流上的进程,这个时候,调用select的进程重新开始遍历范围内的所有描述符。
步骤:
1、拷贝nfds、readfds、writefds和exceptfds到内核;
2、遍历[0,nfds)范围内的每个流,调用流所对应的设备的驱动poll函数;
3、检查是否有流发生,如果有发生,把流设置对应的类别,并执行4,如果没有流发生,执行5。或者timeout=0,执行4;
4、select返回;
5、select阻塞当前进程,等待被流对应的设备唤醒,当被唤醒时,执行2。或者timeout到期,执行4;
4. 实例
编写一个程序,使用select捕获标准输入,若有则打印字符到控制台,3s内未能捕捉到标准输入,则打印超时;
#include
#include
#include
#include
#include
#include
#include
int main()
{
fd_set rset; //创建可读描述符集
int fd = STDIN_FILENO; //标准输入描述符
struct timeval timeout; //设置超时
while(1)
{
timeout.tv_sec = 3; //必须多次装载,否则下次就是非阻塞
timeout.tv_usec = 0;
FD_ZERO(&rset); //清空rset
FD_SET(fd,&rset); //置位rset描述符集合中fd所对应的位
int re = select(fd+1,&rset,NULL,NULL,&timeout);
if(re==0) //3S超时
{
printf("3 seconds timeout\n");
}
if(re<0) //出错,select被中断
{
perror("select error");
}
else //正常返回
{
if(FD_ISSET(fd,&rset)) //这个必不可少,否则select不正常
{
char buf[512] = {0};
read(STDIN_FILENO,buf,sizeof(buf)-1);
printf("out:%s\n",buf);
}
}
}
return 0;
}
编译运行,可见程序确实使用select实现了对标准输入fd的捕捉。
5.select优缺点总结
与多进程/多线程服务器进行对比 它的优点在于:
(1)不需要建立多个线程、进程就可以实现一对多的通信;
(2)可以同时等待多个文件描述符,效率比起多进程多线程来说要高很多;
(3)select() 的可移植性更好,在某些Unix系统上不支持poll() ;
(4)select() 对于超时值提供了更好的精度:微秒,而poll是毫秒;
与多进程/多线程服务器进行对比 它的缺点在于:
(1)数据拷贝性能损耗:每次调用select都需要把所有FD集合从用户态拷贝到内核态,开销随着FD数目线性增长;
(2)获取就绪事件性能损耗:调用select返回后,需要遍历所有FD集合来寻找就绪FD,开销随着FD数目线性增长,时间复杂度O(N);
(3)select支持的文件描述符数量太小了,默认是1024;
二、poll机制
1.从select到poll的改进
(1) poll 函数类似于 select,但是函数参数结构有所不同。回忆一下select接口,select需要我们指定文件描述符的最大值,然后select会遍历[0,nfds)范围内的每个流,调用流所对应的设备的驱动poll函数,也就是说这个范围内存在一些不是我们感兴趣的文件描述符,cpu做了一些无用功,跟select不同的是,poll不再告知内核一个范围,而是通过struct pollfd结构体数组精确的告知内核用户关心哪些文件描述符(流)。
(2) select一次可以监测 FD_SETSIZE 数量大小的描述符,poll一次可以监测的描述符数量并没有限制,但撇开其它因素,我们每次都不得不检查就绪通知,线性扫描所有通过描述符,这样时间复杂度为 O(n)而且很慢。
(3) 虽然 poll 函数是 system V 引入进来支持 STREAMS 子系统的,但是 poll 函数可用于任何类型的文件描述符。
2.poll函数
#include
int poll(struct pollfd fdarray[], nfds_t nfds, int timeout);
//返回值:准备就绪的描述符数目;若超时,返回 0;若出错,返回 -1.
(1) 参数 fdarray
一个 pollfd 类型的数组,每个数组元素指定一个描述符 编号以及我们对该描述符感兴趣的条件。
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events(由内核设定) */
};
应将每个数组元素的 events 成员设置为上图所示值的一个或几个,通过这些值告诉内核我们关心的是每个描述符的哪些事件。返回时,revnets 成员由内核设置,用于说明每个描述符发生了哪些事件。
注意,poll 没有更改 events 成员。这与 select 不同,select 修改其参数以指示哪个描述符已准备好了(所以select每次得重装载 FD_SET)。
上图中的前 4 行测试的是可读性,接下来的 3 行测试的是可写性,最后 3 行测试的是异常条件。最后 3 行是由内核在返回时设置的。即使在 events 字段中没有指定这 3 个值,如果是相应条件发生,在 revents 中也会返回它们。
当一个描述符被挂断(POLLHUP)后,就不能再写该描述符,但是有可能仍然可以从该描述符读取到数据。
POLLIN | POLLPRI 等价于 select() 的读事件,POLLOUT |POLLWRBAND 等价于 select() 的写事件。POLLIN 等价于 POLLRDNORM |POLLRDBAND,而 POLLOUT 则等价于 POLLWRNORM。
(2) 参数 nfds
指定fdarray 数组中元素的个数。
(3) 参数 timeout
指定的是我们愿意等待多长时间,如同 select 一样,有 3 种不同的情形。
(i) timeout == -1
永远等待。(某些系统在
(ii) timeout == 0
不等待。测试所有描述符并立即返回。这是一种轮询系统的方法,可以找到多个描述符的状态而不阻塞 poll 函数。
(iii) timeout > 0
等待 timeout 毫秒。当指定的描述符之一已准备好,或 timeout 到期时还没有一个描述符准备好,则返回值是 0.(如果系统不提供毫秒级精度,则 timeout 值取整到最近的支持值)。
(4) 函数返回
成功时,poll()返回结构体中 revents 域不为 0 的文件描述符个数;
超时时,如果在超时前没有任何事件发生,poll() 返回 0;
失败时,poll()返回 -1,并设置 errno 为下列值之一:
EBADF 一个或多个结构体中指定的文件描述符无效。
EFAULTfds 指针指向的地址超出进程的地址空间。
EINTR 请求的事件之前产生一个信号,调用可以重新发起。
EINVALnfds 参数超出PLIMIT_NOFILE值。
ENOMEM 可用内存不足,无法完成请求。
3.poll 原理
poll的功能和select的功能一样,poll的底层原理也和select差不多,就不多说了。
4.实例
编写一个程序,使用poll捕获标准输入,若有则打印字符到控制台,3s内未能捕捉到标准输入,则打印超时;
#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
struct pollfd fdarray[1] = {0}; //定义pollfd数组
fdarray[0].fd = STDIN_FILENO;//标准输入描述符
fdarray[0].events = POLLIN|POLLPRI; //监控可读事件
while(1)
{
int re = poll(fdarray,1,3000); //监控fdarray数组中所有描述符,超时时间3s
if(re < 0) //被中断
{
perror("poll is interrupted!");
exit(1);
}
else if(re == 0) //超时
{
printf("3 seconds timeout\n");
}
else //正常返回
{
char buf[512]={0};
if(fdarray[0].revents && (POLLIN|POLLPRI)) //进行判断
{
read(STDIN_FILENO,buf,sizeof(buf)-1); //从标准输入中读取数据
write(STDOUT_FILENO,"read:",strlen("read:"));//将数据输出到标准输出
write(STDOUT_FILENO,buf,strlen(buf));
}
}
}
return 0;
}
编译运行,可见程序确实使用poll实现了对标准输入fd的捕捉。
三、epoll机制
参照:【Linux学习】epoll详解 (apue和unix网络编程上都没介绍)
1. 什么是epoll?
epoll是什么?按照man手册的说法:是为处理大批量句柄而作了改进的poll。当然,这不是2.6内核才有的,它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44),它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。
select一次可以监测 FD_SETSIZE 数量大小的描述符,FD_SETSIZE 通常是一个在 libc 编译时指定的小数字。
poll一次可以监测的描述符数量并没有限制,但撇开其它因素,我们每次都不得不检查就绪通知,线性扫描所有通过描述符,这样时间复杂度为 O(n)而且很慢。
epoll 没有这些固定限制,也不执行任何线性扫描。因此它可以更高效地执行和处理大量事件。
2. epoll 的接口
epoll的接口函数总共才3个,分别如下:
#include
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
(1) epoll_create(int size)
创建一个 epoll 的句柄,size 用来告诉内核这个监听的数目一共有多大。这个参数不同于 select 中的第一个参数,当创建好 epoll 句柄后,它就是会占用一个 fd 值,在 Linux 下如果查看 /proc/进程 id/fd/,是能够看到这个 fd 的,所以在使用完 epoll 后,必须调用 close()关闭,否则可能导致 fd 被耗尽。
(2) epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
epoll的事件注册函数,它不同于select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
第一个参数是epoll句柄,epoll_create()的返回值。
第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是需要监听的fd。
第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:
//保存触发事件的某个文件描述符相关的数据(是和联合数据类型,与具体使用方式有关)
typedef union epoll_data_t {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
//感兴趣的事件和被触发的事件
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
(3). epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)
等待事件的产生,类似于 select 调用。参数 events 用来从内核得到事件的集合,maxevents 告之内核这个 events 有多大,这个 maxevents 的值不能大于创建 epoll_create 时的 size,参数 timeout 是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回 0 表示已超时。
3. epoll的工作模式
epoll有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:
LT模式:是 epoll 缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你 的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.
ET模式:当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
epoll 工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
4. epoll的优点
1.支持一个进程打开大数目的socket描述符(FD)
select 最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是2048。对于那些需要支持的上万连接数目的IM服务器来说显然太少了。这时候你一是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样会带来网络效率的下降,二是可以选择多进程的解决方案(传统的 Apache方案),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的方案。不过 epoll则没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。
2.IO效率不随FD数目增加而线性下降
传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket是"活跃"的,但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对"活跃"的socket进行操作---这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有"活跃"的socket才会主动的去调用 callback函数,其他idle状态socket则不会,在这点上,epoll实现了一个"伪"AIO,因为这时候推动力在os内核。在一些 benchmark中,如果所有的socket基本上都是活跃的---比如一个高速LAN环境,epoll并不比select/poll有什么效率,相反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。
3.使用mmap加速内核与用户空间的消息传递
这点实际上涉及到epoll的具体实现了。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll是通过内核于用户空间mmap同一块内存实现的。
5. 实例
使用epoll监听服务端套接字描述符和与客户端的连接描述符,接收客户端的信息并打印在标准输出。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAXLINE 1024
#define MAXEVENT 64
//打开并返回监听描述符(搜索主机中所有可用做接受TCP连接的套接字地址,并尝试bind,并尝试listen,成功则返回)(此函数与IPV4/6协议无关)
int open_listfd(char *port)
{
struct addrinfo hints, *listp, *p;
int listenpd, optval = 1;
//获得主机中可作为服务端套接字地址的候选列表
memset(&hints, 0x00,sizeof(struct addrinfo));
hints.ai_socktype = SOCK_STREAM; //限制地址为TCP模式
hints.ai_flags |= AI_PASSIVE; //指明返回的套接字地址用来作为接收连接
hints.ai_flags |= AI_ADDRCONFIG; //指明返回的套接字地址是IPV4还是IPV6要和主机配置相同
hints.ai_flags |= AI_NUMERICSERV; //强制服务参数为十进制端口
getaddrinfo(NULL,port,&hints,&listp);
//遍历服务端套接字地址的候选列表,尝试bind
for(p=listp;p;p=p->ai_next)
{
//创建socket描述符
listenpd = socket(p->ai_family,p->ai_socktype,p->ai_protocol);
if(listenpd < 0) //套接字创建失败,尝试下一个
continue;
//避免bind函数在重复绑定同一套接字时出错
setsockopt(listenpd,SOL_SOCKET,SO_REUSEADDR,(const char*)&optval,sizeof(int));
//bind套接字
if(bind(listenpd,p->ai_addr,p->ai_addrlen) == 0)
break; //bind成功
//关闭此套接字,尝试下一个
close(listenpd);
}
//释放链表空间
freeaddrinfo(listp);
if(!p) //如果没有找到可行套接字则返回
return -1;
//监听找到的套接字
if(listen(listenpd,SOMAXCONN) < 0) //监听失败
{
close(listenpd);
return -1;
}
return listenpd;
}
//设置socket非阻塞
int set_sock_non_block(int sfd)
{
int flags,s;
//得到文件状态标志
flags = fcntl(sfd ,F_GETFL ,0);
if(flags == -1)
{
perror("fcntl error\n");
return -1;
}
//设置文件状态标志
flags |= O_NONBLOCK;
s = fcntl(sfd,F_SETFL,flags);
if(s==-1)
{
perror("fcntl error\n");
return -1;
}
return 0;
}
int main(int argc,char* argv[])
{
if(argc!= 2)
{
perror("parameter error\n");
exit(1);
}
//套接字相关
int listenfd,connfd;
socklen_t clientlen;
struct sockaddr_storage clientaddr; //sockaddr_storage能保证存放任意大的地址
char client_hostname[MAXLINE], client_port[MAXLINE];
listenfd = open_listfd(argv[1]);
if(listenfd < 0)
{
perror("can not create listenfd\n");
exit(1);
}
int re = set_sock_non_block(listenfd);
if(re<0)
{
perror("set_sock_non_block error\n");
exit(1);
}
//epoll相关
int epoll_fd;
struct epoll_event *event_ptr;
struct epoll_event event;
//1 创建epoll句柄
epoll_fd = epoll_create(MAXEVENT);
//2 注册epoll事件
event.data.fd = listenfd;
event.events = EPOLLIN|EPOLLET; //读入,边缘触发
re = epoll_ctl(epoll_fd,EPOLL_CTL_ADD,listenfd,&event);
if(re<0)
{
perror("epoll_ctl error\n");
exit(1);
}
//3 等待epoll事件
event_ptr = (struct epoll_event *)calloc(MAXEVENT,sizeof(struct epoll_event));
while(1)
{
int re_num,i;
re_num = epoll_wait(epoll_fd,event_ptr,MAXEVENT,-1); //阻塞方式等待
for(i=0;i
编译运行sever.cpp,并使用telnet与之交互,如下:
参考:
1. 《UNIX环境高级编程》
2. 博客 https://blog.csdn.net/qq_29350001/article/details/72417019
3. 博客 https://blog.csdn.net/xiajun07061225/article/details/9250579