应用程序通常需要处理来自多条事件流中的事件,比如我现在用的电脑,需要同时处理键盘鼠标的输入、中断信号等等事件,再比如 web 服务器如 nginx,需要同时处理来自 N 个客户端的事件,面对这些事件,我们最先想到的策略就是采用进程或者线程的方式去对其进行处理,例如服务器处理客户端的事务,我们可以来一个客户端就建立一个线程去处理和这个客户端的数据交互,但是对与这种多线程多进程的方式,对资源的占用是非常大的,有创建的成本,CPU 切换的成本,还有线程间的竞争风险,因此我们需要一种可以在单线程/进程中完成多个事务的处理的策略,这种策略就是 IO 多路复用,IO 多路复用可以实现在单线程的条件下对多个事务的一个监听,在了解它们的实现方式之前,我们先了解一下 IO 模型。
I/O 模型
目前 linux 提供了五种 IO 模型,分别是:
1.阻塞 IO
这是最常用的简单的 IO 模型。阻塞 IO 意味着当我们发起一次 IO 操作后一直等待成功或失败之后才返回,在这期间程序不能做其它的事情。阻塞 IO 操作只能对单个文件描述符进行操作。
2.非阻塞 IO
我们在发起 IO 时,通过对文件描述符设置 O_NONBLOCK flag 来指定该文件描述符的IO 操作为非阻塞。非阻塞 IO 通常发生在一个 for 循环当中,因为每次进行 IO 操作时要么 IO 操作成功,要么当 IO 操作会阻塞时返回错误 EWOULDBLOCK/EAGAIN,然后再根据需要进行下一次的 for 循环操作,这种类似轮询的方式会浪费很多不必要的 CPU 资源,是一种糟糕的设计。和阻塞 IO 一样,非阻塞 IO 也是通过调用 read 或 write 来进行操作的,也只能对单个描述符进行操作。
3.IO 多路复用
IO 多路复用在 Linux 下包括了三种,select、poll、epoll,抽象来看,他们功能是类似的,但具体细节各有不同:首先都会对一组文件描述符进行相关事件的注册,然后阻塞等待某些事件的发生或等待超时。允许同时对多个 IO 事件进行控制(select/poll/epoll),这几个函数也会造成进程阻塞,但是和阻塞 IO 不同的是,可以同时对多个阻塞 IO 进行操作
4.信号驱动 IO
信号驱动 IO 是利用信号机制,让内核告知应用程序文件描述符的相关事件。但信号驱动 IO 在网络编程的时候通常很少用到,因为在网络环境中,和 socket 相关的读写事件太多了,比如下面的事件都会导致 SIGIO 信号的产生:
1. TCP 连接建立
2. 一方断开 TCP 连接请求
3. 断开 TCP 连接请求完成
4. TCP 连接半关闭
5. 数据到达 TCP socket
6. 数据已经发送出去(如:写 buffer 有空余空间)
上面所有的这些都会产生 SIGIO 信号,但我们没办法在 SIGIO 对应的信号处理函数中区分上述不同的事件,SIGIO 只应该在 IO 事件单一情况下使用,比如说用来监听端口的socket,因为只有客户端发起新连接的时候才会产生 SIGIO 信号。进程不会阻塞,当 IO 事件就绪时,进程收到信号,再去处理 IO 事件
5.异步 IO
异步 IO 和信号驱动 IO 差不多,但它比信号驱动 IO 可以多做一步:相比信号驱动 IO 需要
在程序中完成数据从用户态到内核态(或反方向)的拷贝,异步 IO 可以把拷贝这一步也帮我们完成之后才通知应用程序。我们使用 aio_read 来读,aio_write 写。只是发起 IO 事件,当事件就绪的时候,并不会自己处理 IO 事件
同步 IO 和异步 IO
同步 IO vs 异步 IO
1. 同步 IO 指的是程序会一直阻塞到 IO 操作如 read、write 完成
2. 异步 IO 指的是 IO 操作不会阻塞当前程序的继续执行
所以根据这个定义,上面阻塞 IO 当然算是同步的 IO,非阻塞 IO 也是同步 IO,因为当
文件操作符可用时我们还是需要阻塞的读或写,同理 IO 多路复用和信号驱动 IO 也是
同步 IO,只有异步 IO 是完全完成了数据的拷贝之后才通知程序进行处理,没有阻塞
的数据读写过程。
I/O 多路复用方式一(select)
select()API 功能介绍
头文件:
#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);
void FD_CLR(int fd, fd_set *set); //把 fd 从 set 指向的集合中删除
int FD_ISSET(int fd, fd_set *set); //判断 fd 是否存在于 set 指向的集合,当 select 返回后,select 只保留有消息就绪的描述符,抛弃没有就绪的描
述符
void FD_SET(int fd, fd_set *set); //把 fd 加入到 set 指向的集合
void FD_ZERO(fd_set *set); //清空 set 指向的集合
作用:
select 函数可以对一个集合中的文件描述符进行监听,readfds 可读监听集合,writefds 为可写监听集合,exceptfds 出错监听集合。
参数含义:
nfds:所有监听的文件的文件描述符的最大值+1
readfds: 指向你要监听是否可读的文件描述符集合(如果不需要则为 NULL), select返回时,集合中保存的是已经可读的文件描述符集合
writefds:指向你要监听是否可写的文件描述符集合(如果不需要则为 NULL) select 返回时,集合中保存的是已经可写的文件描述符集合
exceptfds:指向你要监听是否出错的文件描述符集合(如果不需要则为 NULL) select返回时,集合中保存的是已经出错的文件描述符集合
timeout:超时时间。
a.填 NULL 表示永远的等待下去(没有 fd 就绪就一直阻塞)
b.设置一个时间值(相对时间),等待一段固定的时间,
c.timeout 设置为 0,根本不等待,检测一次后立刻 select 返回
返回值:
>0 表示已经就绪的文件描述符的个数, 由于监听的文件描述符可能有多个,至于是哪一些文件描述符就绪了,需要在 select 返回后,使用 FD_ISSET 一个一个去测试
=0 超时了
-1 表示 select 出错了,errno 被设置。
备注:
select 返回后,timeout 里面的值被改变,设置成剩余的时间数
timeval 结构体:
struct timeval
{
long tv_sec;//秒
long tv_usec;//微秒
};
fd_set 结构体:
/* fd_set for select and pselect. */
typedef struct
{
/* XPG4.2 requires this member name. Otherwise avoid thename
from the global namespace. */
#ifdef __USE_XOPEN
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else
__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
} fd_set;
它是一个监听集合结构体,可以保存待监听的文件描述符
使用示例:
使用 select 去对 TCP 服务器做多客户端连接操作
//./main ip port
int main(int argc,char *argv[])
{
if(argc != 3) //通过参数指定服务器 ip 和端口
{
printf("please input ip + port!\n");
return 0;
}
int fd_buf[1024]; //用来保存套接字文件
int fd_num = 0; //待监听的文件个数
int fd_max = -1; //保存最大的文件描述符
fd_set SOCK_set; //声明一个监听集合结构体
//1.创建一个服务器套接字
int sockfd = socket(AF_INET, SOCK_STREAM,0);
fd_buf[fd_num++]=sockfd; //加入监听数组
fd_max = sockfd; //暂时只有服务器套接字加入,则最大文件描述符为服务器套接字
//2.绑定一个通信 IP 地址(作为服务器本身的 IP)
struct sockaddr_in saddr; //保存服务器的地址(IP+port)
memset(&saddr,0,sizeof(struct sockaddr_in)); //清空结构体
saddr.sin_family = AF_INET;
inet_aton(argv[1], &saddr.sin_addr);
saddr.sin_port = htons(atoi(argv[2]));
int ret = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
//3.开启对一个套接字的监听
listen(sockfd,250);
//4.客户端的连接和接收客户端信息
while(1)
{
FD_ZERO(SOCK_set); //清空集合
for(int i=0;i<fd_num;i++)
{
FD_SET(fd_buf[i],&SOCK_set); //将监听数组中的套接字描述符加入监听集合
}
select(fd_max+1,&SOCK_set,NULL,NULL,NULL); //阻塞等待集合中的描述符就绪
if(FD_ISSET(sockfd,&SOCK_set)) //判断是否是服务器套接字就绪,如果是,则必是有客户端连接
{
struct sockaddr_in caddr; //保存客户端的地址(IP+port)
socklen_t len = sizeof(caddr);
fd_buf[fd_num++] = accept(sockfd,(struct sockaddr*)&caddr,&len); //连接客户端,加入监听数组
if(fd_buf[fd_num-1] > fd_max) //判断新连接的客户端套接字描述符是否大于原最大的描述符
{
fd_max = fd_buf[fd_num-1];
}
continue;
}
//否则就是客户端发送来的消息
for(int i=1;i<fd_num;i++)
{
if(FD_ISSET(fd_buf[i],&SOCK_set)) //判断是哪些客户端发送给服务器消息
{
char buf[1024] = {0};
ret = read(fd_buf[i],buf,1024);
if(ret < 0) //表示客户端断开
{
//从监听数组中删除该套接字
}
printf("recv size:%d,recv data:%s\n",ret,buf);
}
}
}
//关闭套接字
shutdown(sockfd,SHUT_RDWR);
return 0;
}
为简化程序,示例中不考虑函数失败场景。
select()缺点
缺点:
1.select 监听文件描述符去判断文件是否就绪是通过一个 bitmap 数据去处理的
__u8 bitmap [ 128 ];
可以看到 bitmap 一共有 128 个字节,也就是 1024 位,每一位表示一个文件描述符是否就
绪,也就是说,它最多可以监听 1024 个文件描述符,虽然可以调节,但是还是不可避免有
个上限。
2.监听集合不可重用。观察上面程序就能发现,监听集合在循环中每次都要被 FD_SET 设
置,因为 select 返回后,监听集合会被修改,会把没有消息就绪的描述符从集合中抛弃,
只留下有消息就绪的描述符,所以每次都要拷贝
3.每次 FD_SET 都会将用户态的文件描述符拷贝到内核态去监听,从用户态到内核态还是会
存在很大的开销
4.当描述符就绪后,select 是不会知道是哪个描述符就绪的,所以就要从头遍历一边,时
间复杂度高
I/O 多路复用方式二(poll)
poll()API 功能介绍
poll 和 select 的功能类似,只不过在内核轮询的时候,poll 使用链表存储文件描述符,而 select 使用 bitmap 标记文件描述符,使得可监听的文件描述符有上限(select 最多只能监听 1024 个文件描述符)
头文件:
#include <poll.h>
函数原型:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
作用:
监听 fds 中所记录的文件描述符,监听的个数为 nfds。
参数含义:
fds:一般是监听结构体数组的首地址,保存了待监听的文件描述符
nfds:fds 数组中保存的文件描述符个数
timeout:超时时间,单位为毫秒
返回值:
>0 表示就绪的文件个数
=0 超时了
-1 表示出错了
pollfd 结构体:
struct pollfd
{
int fd; //要监听的文件描述符
short events; //你要监听的事件,使用事件码来表示,事件码是使用位域实现
//POLLIN 可读事件
//POLLOUT 可写事件
//POLLERR 出错
//...
short revents; //已经发生的事件
};
当 poll 返回时,代表有监听的文件描述符就绪,此时 revents 字段就会被修改。
使用示例:
使用 poll 去对 TCP 服务器做多客户端连接操作
//./main ip port
int main(int argc,char *argv[])
{
if(argc != 3) //通过参数指定服务器 ip 和端口
{
printf("please input ip + port!\n");
return 0;
}
struct pollfd fd_buf[1024]; //用来保存套接字文件
int fd_num = 0; //待监听的文件个数
//1.创建一个服务器套接字
int sockfd = socket(AF_INET, SOCK_STREAM,0);
/*加入监听数组*/
fd_buf[fd_num].fd = sockfd; //待监听的套接字描述符
fd_buf[fd_num].events = POLLIN;//监听的事件为可读事件
fd_buf[fd_num].revents = 0;//初始化为 0,表示此事件还没有发生
fd_num++;
//2.绑定一个通信 IP 地址(作为服务器本身的 IP)
struct sockaddr_in saddr; //保存服务器的地址(IP+port)
memset(&saddr,0,sizeof(struct sockaddr_in)); //清空结构体
saddr.sin_family = AF_INET;
inet_aton(argv[1], &saddr.sin_addr);
saddr.sin_port = htons(atoi(argv[2]));
int ret = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
//3.开启对一个套接字的监听
listen(sockfd,250);
//4.客户端的连接和接收客户端信息
while(1)
{
poll(fd_buf,fd_num,NULL); //poll 阻塞监听
if(fd_buf[0].revents&POLLIN) //判断是否是服务器套接字可读事件就绪,如果是,则必是有客户端连接
{
fd_buf[0].revents = 0; //读取标记后,标记初始化,这样就不用像 select 一样每次都需要全部重新初始化一边
struct sockaddr_in caddr; //保存客户端的地址(IP+port)
socklen_t len = sizeof(caddr);
fd_buf[fd_num].fd = accept(sockfd,(struct sockaddr*)&caddr,&len); //连接客户端,加入监听数组
fd_buf[fd_num].events = POLLIN; //监听可读事件
fd_buf[fd_num].revents = 0; //初始化为 0,表示此事件还没有发生
fd_num++;
continue;
}
//否则就是客户端发送来的消息
for(int i=1;i<fd_num;i++)
{
if(fd_buf[i].revents&POLLIN) //判断是哪些客户端发送给服务器消息
{
char buf[1024] = {0};
ret = read(fd_buf[i].fd,buf,1024);
if(ret < 0) //表示客户端断开
{
//从监听数组中删除该套接字
}
printf("recv size:%d,recv data:%s\n",ret,buf);
}
}
}
//关闭套接字
shutdown(sockfd,SHUT_RDWR);
return 0;
}
poll 相比于 select 的优缺点
首先是相对于 select 的优点:
1.poll 是使用一个 pollfd 结构体去保存文件描述符的,在结构体中有文件就绪的标志位,而 select 是通过一个 bitmap 位去标记文件就绪标志的,所以 select 可以监听的文件是有上限的, 一般是 1024,而 poll 则没有这个上限。
2.select 每次返回都会改变监听集合中的数据,只保留发生事件的描述符,不可重用,所以每次
select 读取后都需要重新拷贝一次文件描述符,而 poll 是使用结构体中的 revents 字段去标记该
文件描述符是否就绪,如果就绪则改变这个标记,所以我们读取完这个标记后只需要将这个标记置零就可以了,不需要对整个集合进行重新赋值
然后他的缺点也和 select 一样
1.每次 poll/select 文件描述符都需要复制两次(效率比较低)
2.当描述符就绪后,select 是不会知道是哪个描述符就绪的,所以就要从头遍历一边,时
间复杂度高
I/O 多路复用方式三(epoll)
epoll 改进了 select 的所有缺点。epoll 的首先方式通过三个函数去处理
创建一个 epoll 的句柄(epoll_create)
头文件:
#include <sys/epoll.h>
函数原型:
int epoll_create (int __size)
作用:
创建 epoll 实例。返回新实例的 fd。“size”参数是一个提示,指定要与新实例关联的文件描述符的数量。epoll_create()返回的 fd 应该用 close()关闭
参数含义:
__size:是表示要监听文件描述符的数量。size 参数只是告诉内核这个 epoll 对象会处理的事件大致数目,而不是能够处理的事件的最大个数。在 Linux 最新的一些内核版本的实现中,这个 size 参数没有任何意义,但还是要传入一个大于 0 的参数。
返回值:
成功返回一个 epoll 的实例(文件描述符)
失败返回-1,同时 errno 被设置
备注:
当创建好 epoll 句柄后,它就是会占用一个 fd 值,在 linux 下如果查看/proc/进程id/fd/,是能够看到这个 fd 的,所以在使用完 epoll 后,必须调用 close()关闭,否则可能导致 fd 被耗尽
底层原理:
调用 epoll_create()
方法创建一个 epoll 实例的句柄,可以将这里的句柄理解为一个eventpoll 结构体实例,而这个结构体中有一个红黑树和一个队列,红黑树中主要存储需要监听的文件描述符,而队列则是在所监听的文件描述符中有指定的事件发生时就会将这些事件添加到队列中,如下图所示为 eventpoll 的示意图:
struct eventpoll {
...
/*红黑树的根节点,这棵树中存储着所有添加到 epoll 中的事件 也就是这个 epoll 监控的事件*/
struct rb_root rbr;
/*双向链表 rdllist 保存着将要通过 epoll_wait 返回给用户的、满足条件的事件*/
struct list_head rdllist;
...
};
我们在调用 epoll_create 时,内核除了帮我们在 epoll 文件系统里建了个 file 结点,在内核 cache 里建了个红黑树用于存储以后 epoll_ctl 传来的 socket 外,还会再建立一个rdllist 双向链表,用于存储准备就绪的事件,当 epoll_wait 调用时,仅仅观察这个 rdllist 双向链表里有没有数据即可。有数据就返回,没有数据就 sleep,等到 timeout 时间到后即使链表没数据也返回。所以,epoll_wait 非常高效。 所有添加到 epoll 中的事件都会与设备(如网卡)驱动程序建立回调关系,也就是说相应事 件的发生时会调用这里的回调方法。这个回调方法在内核中叫做 ep_poll_callback,它会把这 样的事件放到上面的 rdllist 双向链表中。 在 epoll 中对于每一个事件都会建立一个 epitem 结构体,如下所示:
struct epitem {
...
//红黑树节点
struct rb_node rbn;
//双向链表节点
struct list_head rdllink;
//事件句柄等信息
struct epoll_filefd ffd;
//指向其所属的 eventepoll 对象
struct eventpoll *ep;
//期待的事件类型
struct epoll_event event;
...
}; // 这里包含每一个事件对应着的信息。
当调用 epoll_wait 检查是否有发生事件的连接时,只是检查 eventpoll 对象中的 rdllist双向链表是否有 epitem 元素而已,如果 rdllist 链表不为空,则这里的事件复制到用户态内存(使用共享内存提高效率)中,同时将事件数量返回给用户。因此 epoll_waitx 效率非常高。 epoll_ctl 在向 epoll 对象中添加、修改、删除事件时,从 rbr 红黑树中查找事件也非常快, 也就是说 epoll 是非常高效的,它可以轻易地处理百万级别的并发连接。
对文件描述符的增删改查操作(epoll_ctl)
头文件:
#include <sys/epoll.h>
函数原型:
int epoll_ctl(int __epfd, int __op, int __fd,struct epoll_event *__event)
作用:
这个为 epoll 的事件注册函数,epoll_ctl 向 epoll 对象中添加、修改或者删除感兴趣的事件
参数含义:
__epfd: 一个 epoll 的实例,epoll_create 的返回值,表示你要操作哪一个 EPOLL 句柄
__op: 具体对 epoll 实例的哪种操作
{
EPOLL_CTL_ADD:增加一个文件描述符 fd 到 epoll 所表示的实例
EPOLL_CTL_MOD:修改 fd 的要监听的事件
EPOLL_CTL_DEL:从 epoll 所表示的实例删除 fd
}
__fd: 你要操作(add,mod,delete)的文件描述符
__event: 要监听 fd 的哪一些事件
返回值:
成功返回 0,
失败返回-1,同时 errno 被设置
epoll_event 事件结构体:
struct epoll_event
{
uint32_t events; //要监听的事件,使用位域实现
epoll_data_t data; //用来保存一些用户数据
//一般用来保存上面的 fd 值
//目的就是返回的时候知道到底是哪一个文件描述符就绪了
} __EPOLL_PACKED;
//data 结构体
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
可以监听的一些事件:
EPOLLIN 监听可读的事件
EPOLLOUT 监听可写的事件
EPOLLRDHUP 这个事件,用来监听 TCP 套接字是否已经关闭
EPOLLERR 监听出错的事件
EPOLLLT 水平触发,默认的触发方式
EPOLLET 边缘触发,没有设置为水平触发
//例子:把服务器的描述符加入到 epoll 实例
struct epoll_event event;
event.events = EPOLLIN | EPOLLET;
event.data.fd = sockfd;//把 sockfd 当做用户数据存储起来
int r = epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&event);
边沿触发和水平触发:
LT(水平触发)模式下,只要这个文件描述符还有数据可读,每次 epoll_wait 都会返回它的事件,提醒用户程序去操作;
ET(边缘触发)模式下,在它检测到有 I/O 事件时,通过 epoll_wait 调用会得到有事件通知的文件描述符,对于每一个被通知的文件描述符,如可读,则必须将该文件描述符一直读到空,让 errno 返回 EAGAIN 为止,否则下次的 epoll_wait 不会返回余下的数据,会丢掉事件。如果 ET 模式不是非阻塞的,那这个一直读或一直写势必会在最后一次阻塞。
一般我们都是使用边沿触发方式,原因在于:
如果采用 EPOLLLT 模式的话,系统中一旦有大量你不需要读写的就绪文件描述符,它们每 次调用 epoll_wait 都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率.。而采用 EPOLLET 这种边缘触发模式的话,当被监控的文件描述符上有可读写事件发生时, epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你!!!这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符。
等待事件触发(epoll_wait)
头文件:
#include <sys/epoll.h>
函数原型:
int epoll_wait(int __epfd, struct epoll_event *__events,int __maxevents, int __timeout);
作用:
等待事件的产生,类似于 select()调用。参数 events 用来从内核得到事件的集合, maxevents 告之内核这个 events 有多大,这个 maxevents 的值不能大于创建 epoll_create()时的 size,参数 timeout 是超时时间(毫秒,0 会立即返回,-1 将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回 0 表示已超时。如果返回–1,则表示出现错误,需要检查 errno 错误码判断错误类型。
参数含义:
__epfd: 一个 epoll 的实例,epoll_create 的返回值,表示你要操作哪一个 EPOLL 句柄
__events:一个 epoll_event 结构体数组,用来保存发生事件的描述符,epoll 将会把发生的事件复制到 events 数组中(events 不可以是空指针,内核只负责把数据复制到这个 events数组中,不会去帮助我们在用户态中分配内存。内核这种做法效率很高)
__maxevents: 表示本次可以返回的最大事件数目,maxevents 参数与预分配的 events 数组的大小是相等的,也就是可以保存的最大监听数量。
__timeout:超时时间(毫秒)
返回值:
>0 表示就绪的文件个数
=0 超时了
-1 表示出错了
使用示例:
使用 epoll 去对 TCP 服务器做多客户端连接操作
//./main ip port
int main(int argc,char *argv[])
{
if(argc != 3) //通过参数指定服务器 ip 和端口
{
printf("please input ip + port!\n");
return 0;
}
struct epoll_event fd_Ev[1024]; //用来保存发生事件的描述符
int epfd = epoll_create(100); //创建一个 epoll 实例
//1.创建一个服务器套接字
int sockfd = socket(AF_INET, SOCK_STREAM,0);
struct epoll_event event; //用来保存待监听的描述符
event.events = EPOLLIN | EPOLLET; //加入监听的事件为边沿读事件
event.data.fd = sockfd; //加入服务器套接字
epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&event); //加入红黑树节点进行监听
//2.绑定一个通信 IP 地址(作为服务器本身的 IP)
struct sockaddr_in saddr; //保存服务器的地址(IP+port)
memset(&saddr,0,sizeof(struct sockaddr_in)); //清空结构体
saddr.sin_family = AF_INET;
inet_aton(argv[1], &saddr.sin_addr);
saddr.sin_port = htons(atoi(argv[2]));
int ret = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
//3.开启对一个套接字的监听
listen(sockfd,250);
//4.客户端的连接和接收客户端信息
while(1)
{
int ev_num = epoll_wait(epfd,fd_Ev,1024,1000); //监听返回
if(ev_num == 0) //超时
{
continue;
}
else if(ev_num == -1) //监听失败
{
close(epfd); //关闭句柄
return -1;
}
for(int i = 0;i<ev_num;i++)
{
if(fd_Ev[i].data.fd == sockfd) //判断是否是服务器套接字就绪,如果是,则必是有客户端连接
{
struct sockaddr_in caddr; //保存客户端的地址(IP+port)
socklen_t len = sizeof(caddr);
struct epoll_event Ievent; //用来保存待监听的描述符
Ievent.data.fd = accept(sockfd,(struct sockaddr*)&caddr,&len); //连接客户端,加入监听数组
Ievent.events = EPOLLIN | EPOLLET; //加入监听的事件为边沿读事件
epoll_ctl(epfd,EPOLL_CTL_ADD,Ievent.data.fd,&Ievent); //加入红黑树节点进行监听
}
else
{
char buf[1024] = {0};
ret = read(fd_Ev[i].data.fd,buf,1024);
if(ret < 0) //表示客户端断开
{
//从监听红黑树中中删除该套接字
struct epoll_event Ievent; //用来保存待监听的描述符
Ievent.data.fd = fd_Ev[i].data.fd; //待断开的客户端套接字
epoll_ctl(epfd,EPOLL_CTL_DEL,Ievent.data.fd,&Ievent); //加入红黑树节点进行监听
}
printf("recv size:%d,recv data:%s\n",ret,buf);
}
}
}
//关闭套接字
close(epfd); //关闭句柄
shutdown(sockfd,SHUT_RDWR);
return 0;
}