网络编程4 poll和epoll
网络编程4
了解多路复用IO poll
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
函数说明: 跟select类似, 监控多路IO, 但poll不能跨平台.
参数说明:
fds: 传入传出参数, 实际上是一个结构体数组
fds.fd: 要监控的文件描述符
fds.events:
POLLIN---->读事件
POLLOUT---->写事件
fds.revents: 返回的事件
nfds: 数组实际有效内容的个数
timeout: 超时时间, 单位是毫秒.
-1:永久阻塞, 直到监控的事件发生
0: 不管是否有事件发生, 立刻返回
0: 直到监控的事件发生或者超时
poll返回值:
=0,没有文件描述符的变化
成功:返回就绪事件的个数
失败: 返回-1
若timeout=0, poll函数不阻塞,且没有事件发生, 此时返回-1, 并且errno=EAGAIN, 这种情况不应视为错误.
struct pollfd
{
int fd; /* file descriptor */ 监控的文件描述符
short events; /* requested events */ 要监控的事件---不会被修改
short revents; /* returned events */ 返回发生变化的事件 ---由内核返回
};
说明:
1 当poll函数返回的时候, 结构体当中的fd和events没有发生变化, 究竟有没有事件发生由revents来判断, 所以poll是请求和返回分离.
2 struct pollfd结构体中的fd成员若赋值为-1, 则poll不会监控.
3 相对于select, poll没有本质上的改变; 但是poll可以突破1024的限制.
在/proc/sys/fs/file-max查看一个进程可以打开的socket描述符上限.
如果需要可以修改配置文件: /etc/security/limits.conf
加入如下配置信息, 然后重启终端即可生效.
* soft nofile 1024
* hard nofile 100000
soft和hard分别表示ulimit命令可以修改的最小限制和最大限制
poll开发服务器端思路
1 创建socket,得到监听文件描述符 lfd----socket()
2 设置端口复用---------setsocket()
3 绑定-----bind()
4 struct pollfd client[1024]
client[0].fd=lfd
client[0].events = POLLIN
for(i=1;i<1024i++)
client[i].fd=-1;
int maxi=0;
while(1)
{
nready = poll(client,maxi+1,-1);
if(nready<0)//异常情况
{
if(errnoEINTR)//信号被中断
continue;
break;
}
//有客户端连接请求到来
if(client[0].reventsPOLLIN)
{
接受新客户端连接
cfd=accept(lfd,NULL,NULL)
寻找可用位置
for(i=0;i<1024;i++)
{
if(client[i].fd-1)
{
client[i].fd=cfd;
client[i].events=POLLIN;
break;
}
}
客户端连接数达到最大值
if(1024i)
{
close(cfd);
continue;
}
修改数组大小
if(maxi <i)
maxi=i;
if(--nready0)
{continue;}
}
有客户端发送数据的情况
for(i=1;i<maxi;++i)
{
sockfd = client[i].fd;
如果client数组fd已经为-1,表示已经不再让你内核监控了,已经close
if(client[i].fd-1)
continue;
if(client[i].revents==POLLIN)
{
read数据
n=read(sockfd,buf,sizeof(buf))
if(n<=0)
{关闭客户端连接
close(sockfd);
client[i].fd=-1;
}
else
{
发送给客户端
write(sockfd,buf,n)
}
}
}
}
熟练使用epoll多路IO模型
关于epoll函数介绍
#include<sys/epoll.h>
int epoll_create(int size);
函数说明: 创建一个树根
参数说明:
- size: 最大节点数, 此参数在linux 2.6.8已被忽略, 但必须传递一个大于0的数.
返回值:
成功: 返回一个大于0的文件描述符, 代表整个树的树根.
失败: 返回-1, 并设置errno值.
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
函数说明: 将要监听的节点在epoll树上添加, 删除和修改
参数说明:
-
epfd: epoll树根
-
op:
EPOLL_CTL_ADD: 添加事件节点到树上
EPOLL_CTL_DEL: 从树上删除事件节点
EPOLL_CTL_MOD: 修改树上对应的事件节点 -
fd: 事件节点对应的文件描述符
-
event: 要操作的事件节点
typedef union epoll_data {
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 */ };
event.events常用的有:
EPOLLIN: 读事件
EPOLLOUT: 写事件
EPOLLERR: 错误事件
EPOLLET: 边缘触发模式
event.fd: 要监控的事件对应的文件描述符
*int epoll_wait(int epfd, struct epoll_event events, int maxevents, int timeout);
函数说明:等待内核返回事件发生
参数说明:
- epfd: epoll树根
- events: 传出参数, 其实是一个事件结构体数组
- maxevents: 数组大小
- timeout:
-1: 表示永久阻塞
0: 立即返回
0: 表示超时等待事件
返回值:
成功: 返回发生事件的个数
失败: 若timeout=0, 没有事件发生则返回; 返回-1, 设置errno值,
epoll_wait的events是一个传出参数, 调用epoll_ctl传递给内核什么值, 当epoll_wait返回的时候, 内核就传回什么值,不会对struct event的结构体变量的值做任何修改.
epoll模型服务器代码编写思路
1 创建socket,得到监听文件描述符 lfd----socket()
2 设置端口复用---------setsocket()
3 绑定-----bind()
4 监听-----listen()
5 创建epoll树
int epfd = epoll_create()
监听文件描述符上树
struct epoll_event ev
ev.evetns = EPOLLIN
ev.data.fd=lfd
epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev)
while(1)
{
nready = epoll_wait(epfd,events,1024,-1)
if(nready<0)
{
if(errnoEINTR)
continue; //信号被中断
break
}
for(i=0;i<nready;i++)
{
sockfd=events[i].data.fd
有客户端连接请求到了
if(sockfdlfd)
{
cfd=accept(lfd,NULL,NULL)
将cfd对应的读事件上epoll树
ev.data.fd=cfd
ev.evetns=EPOLLIN
epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev);
continue;
}
有客户端数据发送到了
n=read(sockfd,buf,sizeof(buf))
if(n<=0)发送异常
{
close(sockfd);
下树,将socket对应的事件节点从epoll树上删除
epoll_ctl(epfd,EPOLL_CTL_DEL,sockfd,NULL)
perr("read error");
continue;
}
else
{
write(sockfd,buf,n)
}
}
}
关闭事件描述符
close(lfd)
epoll ET/LT触发模式并且实现
epoll的两种模式ET和LT模式
- 水平触发
Level Triggered (LT)
: 高电平代表1
epoll默认的模式,只要缓冲区中有数据, 就一直通知 - 边缘触发
Edge Triggered (ET)
: 电平有变化就代表1
缓冲区中有数据只会通知一次, 之后再有数据才会通知.(若是读数据的时候没有读完, 则剩余的数据不会再通知, 直到有新的数据到来)
修改成ET模式
ev.evetns=EPOLLIN | EPOLLET;
如果在ET模式下把数据一口气读完?
循环读数据,直到读完,而且需要把通信描述符设置成非阻塞模式。
//将cfd设置为非阻塞
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
...
//有客户端发送数据过来
memset(buf, 0x00, sizeof(buf));
while(1)
{
n = Read(sockfd, buf, 2);
printf("n==[%d]\n", n);
//读完数据的情况
if(n==-1)
{
printf("read over, n==[%d]\n", n);
break;
}
//对方关闭连接或者读异常
if(n==0 || (n<0&&n!=-1))
{
printf("n==[%d], buf==[%s]\n", n, buf);
close(sockfd);
//将sockfd对应的事件就节点从epoll树上删除
epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);
break;
}
else //正常读到数据的情况
{
printf("n==[%d], buf==[%s]\n", n, buf);
for(k=0; k<n; k++)
{
buf[k] = toupper(buf[k]);
}
Write(sockfd, buf, n);
}
}
了解epoll反应堆模式
epoll反应堆实际上是应用了C++的封装思想,一个事件的产生会触发一系列连锁反应,事件产生之后最终调用的是回调函数。
epoll反应堆的主要用途是处理高并发的I/O事件。在网络编程中,当服务器需要同时处理来自多个客户端的连接请求或数据交换时,使用epoll反应堆可以显著提高服务器的性能和响应速度。通过监听多个文件描述符的状态变化,epoll反应堆能够在单个线程或进程内高效地处理多个并发事件,避免了传统多线程或多进程模型中的线程切换和同步开销。