AndreaDO

导航

网络编程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].revents
POLLIN)
{
接受新客户端连接
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(1024
i)
{
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(sockfd
lfd)
{
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反应堆能够在单个线程或进程内高效地处理多个并发事件,避免了传统多线程或多进程模型中的线程切换和同步开销。

posted on 2024-03-16 16:42  AndreaDO  阅读(19)  评论(0编辑  收藏  举报