IO多路转接模型–select、poll、epoll模型
文章目录
前言
多路转接模型常用于高并发服务器中技术的使用,是针对大量的描述符IO事件进行就绪监控的技术。
使进程可以仅针对就绪的事件描述符进行IO操作,提高任务的处理效率;避免进程因为对于未就绪描述符进行操作,从而导致阻塞情况的发生。
其实现技术有:select模型、poll模型、epoll模型。
一、五种常见的IO模型
IO指的是数据的输出输入,IO的过程可以分解为两个步骤,等待IO就绪和数据拷贝。
1 阻塞IO
发起IO操作,若当前不具备IO条件则等待,直到条件满足,完成IO后才进行返回。
优点:
- 流程简单。
缺点:
- 资源利用率较低,效率相对低下。
2 非阻塞IO
发起IO操作,若当前具备IO操作条件,则直接完成IO操作后返回;若当前不具备IO操作条件,则直接报错返回。
优点:
- 相对于阻塞IO,对资源的利用率更高了。
缺点:
- 需要循环进行操作,流程变复杂了。对资源利用率提高了,但并不实时。
3 信号驱动IO
定义IO信号处理方式,IO就绪通过信号通知进程,然后发起IO调用。
优点:
- 资源利用更充分。
- IO操作更实时。
缺点:
- 操作流程复杂度增加。
4 异步IO
发起异步IO操作,IO的等待以及数据拷贝都由系统完成,完成后通知进程。
优点:
- 资源利用率提升到最高。
缺点:
- 操作流程复杂度提升到最高。
5 比较
- 阻塞与非阻塞
- 阻塞:发起一个操作,若当前操作条件不满足则一直等待。
- 非阻塞:发起一个操作,若当前操作条件不满足则直接报错返回。
- 相同:通常都是操作接口特性。
- 不同:发起一个接口调用后,接口是否会立即返回。
- 同步与异步
- 同步:功能由进程自身完成,通常为串行化。
- 异步:功能并不由进程自身完成,而是由系统完成,完成不一定是串行化的。
- 相同:通常用于讨论一个任务的完成流程。
- 不同:功能是否由当前执行流自身完成。
二、select模型
IO事件:可读事件、可写事件、异常事件。
1 操作流程
- 定义指定IO事件的描述符集合。
- 将需要对指定事件进行监控的描述符添加到指定的集合中。
- 将事件的描述符集合拷贝到内核中去,进行事件监控。
- 先对集合中的所有描述符进行依次遍历,若事件未就绪则将其描述符挂到内核的IO事件队列。
- 若在监控的过程中,有某个描述符就绪了所要监控的事件,则唤醒该进程的阻塞。
- 进程被唤醒后,select会再次将描述符集合遍历一遍,将集合中没有就绪的描述符进行移除。
- select监控返回后,判断哪个描述符还在集合中,则哪个描述符就就绪了哪个事件。
- 进程就根据就绪的不同事件对描述符进行不同的IO处理。
监控过程进行了两次集合的遍历:第一次是判断事件是否就绪,未就绪就挂起;第二次是进程被唤醒后进行遍历,移除集合中未就绪的描述符。
2 相关接口
(1)定义集合。
fd_set set;
集合set,实质上是一个比特位图,默认拥有1024个比特位,取决于_FD_SETSIZE,故select对描述符进行IO事件监控有最大描述符数量监控。
(2)初始化集合,将需要监控的描述符添加到集合中。
- 初始化清空集合
void FD_ZERO(fd_set *set);
- 将fd描述符添加到set集合中
void FD_SET(int fd,fd_set *set);
- 将fd描述符从set集合移除
void FD_CLR(int fd,fd_set *set);
(3)对事件描述符开始监控。
//函数
int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);
//参数解释:
int nfds:所有需要监控的集合中,最大描述符+1的值,提高监控遍历效率
fd_set *readfds:可读事件的描述符集合
fd_set *writefds:可写事件的描述符集合
fd_set *exceptfds:异常事件的描述符集合
timeval *timeout:设置本次的监控的阻塞时长,NULL表示一直阻塞,直到有描述符就绪或是被信号打断
//struct timeval结构体
struct timeval
{
time_t tv_sec; //秒
time_t tv_usec; //微秒
}
//返回值:
>0 ==>返回实际就绪的事件个数
==0 ==>监控超时
-1 ==>表示出错了
(4)判断描述符fd是否在指定集合中。
int FD_ISSET(int fd,fd_set *set);
3 模型优缺点
优点:
- 遵循posix标准,跨平台移植性良好。
- 监控超时时间以微妙为单位。
缺点:
- 监控描述符数量有上限,取决于_FD_SETSIZE,默认为1024。
- 监控过程需多次遍历描述符记恨,故监控描述符越多,性能就越低。
- 每次监控都需修改描述符集合,每次监控都需要重新添加描述符到集合中。
- 只能先判断描述符是否在集合中,才可判断描述符对应的事件是就绪的。
POSIX标准:
POSIX:可移植操作系统接口(Portable Operating System Interface of UNIX,缩写为 POSIX)。
POSIX是IEEE为要在各种UNIX操作系统上运行的软件而定义的一系列API标准的总称,其正式称呼为IEEE 1003,而国际标准名称为ISO/IEC 9945。
POSIX.1 已经被国际标准化组织(International Standards Organization,ISO)所接受,被命名为 ISO/IEC 9945-1:1990 标准。
POSIX
4 应用
搭建tcp服务器,涉及到服务器为每一个客户端连接都创建一个新的套接字进行通信,需要对大量的描述符进行IO操作。有可能会因为对没有就绪的描述符进行操作而导致程序流程阻塞。
多执行流的解决方案:为每一个客户端的通信都创建执行流。最好的解决方法是:多路转接模型搭配线程池一块儿使用。
select和epoll应用都只是在单执行流中对大量描述符进行轮询处理。
(1) 封装tcpsocket类。
//tcpsocket.hpp
#include <iostream>
#include <cstdio>
#include <string>
#include <vector>
#include <unistd.h>
#include <arpa/inet.h> //地址转换函数
#include <netinet/in.h> //ipv4与ipv6地址格式定义头文件
#include <sys/socket.h>
using namespace std;
// 设置最大的监听数
#define MAX_LISTEN 5
class TcpSocket
{
private:
int _sockfd;
public:
TcpSocket() : _sockfd(-1) {}
TcpSocket(int fd)
: _sockfd(fd)
{
}
~TcpSocket()
{
close(_sockfd);
}
void Setfd(int fd)
{
_sockfd = fd;
}
int Getfd()
{
return _sockfd;
}
// 创建套接字
bool Socket()
{
_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (_sockfd < 1)
{
perror("socket error");
return false;
}
return true;
}
// 进行端口绑定
bool Bind(const string &ip, int port)
{
struct sockaddr_in addr;
addr.sin_family = AF_INET; // 地址类型
addr.sin_port = htons(port); // 端口号
addr.sin_addr.s_addr = inet_addr(ip.c_str()); // IP地址
socklen_t len = sizeof(addr);
int ret = bind(_sockfd, (struct sockaddr *)&addr, len);
if (ret < 0)
{
perror("bind error");
return false;
}
return true;
}
// 建立连接
bool Connect(const string &ip, int port)
{
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
socklen_t len = sizeof(addr);
int ret = connect(_sockfd, (struct sockaddr *)&addr, len);
if (ret < 0)
{
perror("connect error");
return false;
}
return true;
}
// 开始监听套接字
bool Listen(int backlog = MAX_LISTEN)
{
int ret = listen(_sockfd, backlog);
if (ret < 0)
{
perror("listen error");
return false;
}
return true;
}
// 获取新建连接
bool Accept(TcpSocket *sock)
{
// struct sockaddr_in addr;
// socklen_t len=sizeof(addr);
// accept(_sockfd,(struct sockaddr*)&addr,&len);
int newfd = accept(_sockfd, NULL, NULL);
if (newfd < 0)
{
perror("accept error");
return false;
}
sock->_sockfd=newfd;
return true;
}
// 接收数据
bool Recv(string *body)
{
char tmp[4096] = {0};
int ret = recv(_sockfd, tmp, 4096, 0);
if (ret < 0)
{
// EAGAIN:表示当前接收数据的时候socket缓冲区没有数据
// 若是阻塞操作就会一直等待,若是非阻塞就会立即报错EAGAIN返回
// EINTR:当前接收数据的时候,被信号给打断了
if (errno == EAGAIN || errno == EINTR)
{
return true;
}
perror("recv error");
return false;
}
else if(ret==0)
{
printf("peer shutdown!\n");
return false;
}
body->assign(tmp, ret);
return true;
}
// 发送数据
bool Send(const string &body)
{
int len = 0;
while (len < body.size())
{
int ret = send(_sockfd, &body[len], body.size() - len, 0);
if (ret < 0)
{
perror("send error");
return false;
}
len += ret;
}
return true;
}
// 关闭套接字
bool Close()
{
if (_sockfd >= 0)
{
close(_sockfd);
_sockfd = -1;
}
return true;
}
};
(2) 搭建tcp服务端。
//tcp_srv.cpp
//搭建tcp服务器
#include "select.hpp"
#define CHECK_RET(v) if((v)==false) { return false; }
int main()
{
TcpSocket lis_sock;
// 创建套接字
CHECK_RET(lis_sock.Socket());
// 绑定地址信息
CHECK_RET(lis_sock.Bind("0.0.0.0", 9000));
// 开始监听
CHECK_RET(lis_sock.Listen());
while (1)
{
// 获取新建连接
TcpSocket new_sock;
bool ret = lis_sock.Accept(&new_sock);
if (ret == false)
{
continue;
}
//收发数据
string body;
ret=new_sock.Recv(&body);
if(ret==false)
{
new_sock.Close();
continue;
}
cout << "client say: " << body << endl;
body.clear();
cout << "server say: ";
// 刷新缓冲区
fflush(stdout);
cin >> body;
ret = new_sock.Send(body);
if (ret == false)
{
new_sock.Close();
continue;
}
}
//关闭套接字
lis_sock.Close();
return 0;
}
(3) 搭建tcp客户端。
//tcp_cli.cpp
//搭建tcp客户端
#include"select.hpp"
#define CHECK_RET(v) if((v)==false) { return false; }
int main()
{
//创建套接字
TcpSocket cli_sock;
CHECK_RET(cli_sock.Socket());
//连接服务器
CHECK_RET(cli_sock.Connect("127.0.0.1",9000));
//收发数据
while(1)
{
string body;
cout<<"client say: ";
fflush(stdout);
cin>>body;
CHECK_RET(cli_sock.Send(body));
body.clear();
CHECK_RET(cli_sock.Recv(&body));
cout<<"server say: "<<body<<endl;
}
//关闭套接字
cli_sock.Close();
return 0;
}
(4) 服务端与客户端进行连接通信。
①运行服务端,在另一个终端查看指定端口是否被正常监听。
[dev@localhost multi]$ netstat -anptu | grep 9000
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
tcp 0 0 0.0.0.0:9000 0.0.0.0:* LISTEN 7571/./tcp_srv
可以看到,在服务端与客户端进行了一次交互之后连接就断开了。这是因为获取的新建连接的new_sock是一个临时对象,在进行一次通信后就会被释放掉,而在TcpSocket类的析构函数中有关闭了套接字的语句。
②若套接字不进行释放,即给获取的新建连接申请一块空间。那这样可以吗?
TcpSocket *new_sock=new TcpSocket();
可以看到,这样也是不可以的,当交互一次之后,客户端再次发送数据给服务端,服务端就不会接收到了,因为在一次通信之后并没有新连接的到来,即事件并未就绪。若重新开设一个终端,其与服务器也只能有效通信一次。
所以将就绪的事件都添加到select中,从而只针对就绪事件进行操作。
(4) select,让程序只针对那些就绪的事件进行监控,防止进程因为事件未就绪产生阻塞。
封装select类,通过select类实例化对象,改进tcp服务端,使其只针对就绪事件进行处理。
//select.hpp
#include<cstdio>
#include<iostream>
#include<string>
#include<vector>
#include<sys/select.h>
#include"tcpsocket.hpp"
using namespace std;
class Select
{
private:
int _maxfd;
fd_set _rfds;//可读事件的描述符集合
public:
Select()
:_maxfd(-1)
{
//进行集合的清空初始化
FD_ZERO(&_rfds);
}
//添加一个描述符的可读事件监控
bool Add(TcpSocket sock)
{
int fd=sock.Getfd();
//将描述符添加到集合中
FD_SET(fd,&_rfds);
//重新找到最大的描述符
_maxfd=_maxfd>fd?_maxfd:fd;
return true;
}
//移除一个描述符的可读事件监控
bool Del(TcpSocket sock)
{
int fd=sock.Getfd();
//将描述符从集合中进行移除
FD_CLR(fd,&_rfds);
//重新找到当前最大的描述符
for(int i=_maxfd;i>=0;--i)
{
if(FD_ISSET(i,&_rfds))
{
_maxfd=i;
break;
}
}
return true;
}
//开始进行循环监控
bool Wait(vector<TcpSocket> *active_socket,int timeout)
{
//int select(maxfd+1,rfds,wfds,efds,tv);
fd_set tmp_set=_rfds;
struct timeval tv;
tv.tv_sec=timeout;
tv.tv_usec=0;
int ret=select(_maxfd+1,&tmp_set,NULL,NULL,&tv);
if(ret==-1)
{
perror("select error");
return false;
}
else if(ret==0)
{
//时间超时
active_socket->clear();
return true;
}
for(int i=0;i<=_maxfd;++i)
{
if(FD_ISSET(i,&tmp_set))
{
//就绪了可读事件
TcpSocket sock;
sock.Setfd(i);
active_socket->push_back(sock);
}
}
return true;
}
};
// select,搭建tcp服务器,只针对就绪事件进行处理
//tcp_srv.cpp
#include "select.hpp"
#define CHECK_RET(v) if((v)==false) { return false; }
int main()
{
TcpSocket lis_sock;
// 创建套接字
CHECK_RET(lis_sock.Socket());
// 绑定地址信息
CHECK_RET(lis_sock.Bind("0.0.0.0", 9000));
// 开始监听
CHECK_RET(lis_sock.Listen());
Select s;
s.Add(lis_sock);
while (1)
{
vector<TcpSocket> array;
bool ret = s.Wait(&array, 3);
if (ret == false)
{
continue;
}
for (auto sock : array)
{
if (sock.Getfd() == lis_sock.Getfd())
{
// 获取新建连接
TcpSocket new_sock;
bool ret = lis_sock.Accept(&new_sock);
if (ret == false)
{
continue;
}
s.Add(new_sock);
}
else
{
// 收发数据
string body;
ret = sock.Recv(&body);
if (ret == false)
{
sock.Close();
continue;
}
cout << "client say: " << body << endl;
body.clear();
cout << "server say: ";
// 刷新缓冲区
fflush(stdout);
cin >> body;
ret = sock.Send(body);
if (ret == false)
{
sock.Close();
continue;
}
}
}
}
// 关闭套接字
lis_sock.Close();
return 0;
}
服务端与客户端持续通信,对事件进行监控,只针对就绪的事件进行处理。
5 reactor模型
高并发服务器中的一种并发模型。
(1)思想
使用多路转接模型对大量描述符进行事件监控,哪个描述符监控的事件就绪了就处理其对应事件。
(2)分类
- 单reactor单线程:在一个线程中,进行事件监控以及事件处理。
- 单reactor多线程:在一个线程中进行reactor事件监控,触发事件后交给其他线程进行事件处理。
- 多reactor多线程:在一个线程中进行新连接到来事件的监控,有事件触发则获取新建连接,将新建连接分发给其他的reactor线程,其他的reactor进行描述符的事件监控以及IO操作。
三、poll模型
1 操作流程及接口
(1)定义一个事件结构体数组。
struct pollfd
{
int fd; //要监控的文件描述符
short events; //想要监控的事件,POLLIN--可读事件;POLLOUT--可写事件
short revents; //监控返回后,存储实际就绪的事件
};
struct pollfd fds[MAX];
(2)描述符需要监控什么事件,就在数组中进行设置。
//描述符为0的可读事件
fds[0].fd=0;
fds[0].events=POLLIN;
//描述符为1的可写事件
fds[1].fd=1;
fds[1].events=POLLOUT;
events和revents的取值:
(3)开始监控。
将数组中有效数据拷贝到内核中,进行多次轮询遍历。
- 第一次遍历:遍历数组,将没有就绪的事件的描述符挂到监控队列中。
- 第二次遍历:进程的阻塞被唤醒后,进行遍历,对每个元素的revents设置实际就绪的事件。
//函数:
int poll(struct pollfd *fds,nfds_t maxevents,int timeout);
//参数解释:
pollfd *fds:定义的事件结构体数组的首地址
maxevents:数组中有效元素的个数
timeout:监控阻塞的超时时间,以毫秒(ms)为单位
//返回值:
>0 ==>返回实际就绪的事件个数
==0 ==>表示超时了
<0 ==>表示出错了
(4)调用返回后,遍历事件结构体数组,根据revents成员确定描述符是否就绪了某个事件,进而对描述符进行操作。
2 模型优缺点
优点:
events=POLLIN(可读事件)
或
events=POLLOUT(可写事件)
- 使用事件结构体代替了事件集合,相较于select操作,其简洁性提高了很多。
- 所能监控的描述符数量不受限制。
缺点:
- 每次监控都得将信息拷贝到内核中去。
- 监控过程涉及到对数组的遍历操作,性能会随着描述符数量的增多而下降。
- 每次监控完毕后,依然需要遍历整个事件数组,才能确认哪个描述符就绪了哪个事件。
3 应用
捕捉键盘输入的可读事件。
//poll.c
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<poll.h>
#define MAX_POLL_SIZE 10
int main()
{
//定义事件结构体数组
struct pollfd fds[MAX_POLL_SIZE];
//添加要监控的描述符事件信息
fds[0].fd=0;
fds[0].events=POLLIN;
//有效事件个数
int vaild_count=1;
while(1)
{
int ret=poll(fds,vaild_count,3000);
//出错了
if(ret<0)
{
perror("poll error");
continue;
}
//表示超时了
else if(ret==0)
{
printf("poll timeout\n");
continue;
}
//返回了就绪的事件个数,进行遍历
for(int i=0;i<vaild_count;++i)
{
//如果就绪的事件为可读事件
if(fds[i].revents & POLLIN)
{
char buf[1024]={0};
read(fds[i].fd,buf,1023);
printf("buf:[%s]\n",buf);
}
//如果就绪的事件为可写事件
else if(fds[i].revents & POLLOUT)
{
printf("POLLOUT EVENTS\n");
}
}
}
return 0;
}
四、epoll模型
1 操作流程
(1)在内核中创建epoll句柄eventpoll结构。
int epoll_create(int size);
//参数解释:
size:所能监控的描述符数量上限,在Linux 2.6.8后被忽略,但必须大于0
//返回值:
成功 --->返回epoll的描述符
失败 --->返回-1
//eventpoll结构体
struct eventpoll
{
...
/*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
struct rb_root rbr;
/*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
struct list_head rdlist;
...
};
(2)向内核的句柄中,添加、移除或修改所要监控的描述符及其对应的事件结构。
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *ev);
//参数解释:
epfd:epoll_create返回的epoll描述符
op:对epoll要进行的操作,添加(EPOLL_CTL_ADD)/移除(EPOLL_CTL_DEL)/修改(EPOLL_CTL_MOD)
fd:要操作的描述符
struct epoll_event *ev:对描述符要进行操作的详细信息
//epoll_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_data_t data;
};
(3)开始监控。
epoll监控是一个异步阻塞监控,由epoll发起监控调用,告诉系统,可以开始进行监控,监控由系统来完成,系统内部为epoll的每个描述的就绪事件都挂了一个回调函数。
回调函数功能就是一旦描述符就绪了指定的事件,就会将事件信息拷贝一份到rdlist中,rdlist双向链表就是用来存放就绪的描述符对应的事件结构。
一旦系统监控有描述符就绪了,就会唤醒进程的阻塞,进程一旦被唤醒,就会查看rdlist双向链表中是否有数据,就可以确定是否有描述符就绪。
int epoll_wait(int epfd,struct epoll_event *evs,int maxevents,int timeout);
//参数解释:
epfd:epoll_create返回的epoll描述符
struct epoll_event *evs:epoll_event结构体数组的空间首地址,用来接收就绪事件
maxevents:数组的最大元素个数,表示当前想要获取的最大事件个数
timeout:设置的监控时间,毫秒(ms)为单位
//返回值:
>0 ==>返回实际就绪的事件个数
==0 ==>表示超时了
<0 ==>表示出错了
2 epoll事件触发方式
select和poll只有水平触发,epoll默认为水平触发。
(LT)水平触发:
- 可读事件:缓冲区中数据大小大于高水位标记(默认为1)便会触发可读事件
- 可写事件:缓冲区中剩余空间大小大于高水位标记(默认为1)便会触发可写事件
- 思想:只要满足触发条件,就会触发相应的事件
(ET)边缘触发:
- 可读事件:有新数据到来就会触发一次事件
- 可写事件:缓冲区剩余空间从无到有就会触发一次事件
- 思想:有新数据到来才会触发事件。
- 场景:http请求接收
在一次请求的接收处理中,发现缓冲区中的数据不足以进行一次处理,若将其取出来就需要额外的存储,若不将其取出来,则水平触发就会一直触发可读事件。此情况下就会希望,能够在新数据到来的时候再去进行数据的处理,此时就适合用边缘触发。尽量让用户在一次事件触发过程中,将能处理的数据都处理完毕,尽量减少事件触发次数,减少运行态切换次数。
3 模型优缺点
优点:
- 所能监控的描述符无数量限制。
- 监控性能并不会随着描述符的增多而降低。
- 直接返回就绪描述符对应的事件结构,减少了外界的空遍历。
- 描述符监控的事件信息,只需要向内核中添加一次,不需要每次监控都添加。
缺点:
- 跨平台移植性差,只能在Unix平台下使用。
4 实际应用
//epoll.hpp
#include <iostream>
#include <cstdio>
#include <vector>
#include <cassert>
#include <sys/epoll.h>
#include "tcpsocket.hpp"
#define MAX_EVENTS 10
class Epoll
{
private:
int _epfd;
public:
Epoll() : _epfd(-1)
{
_epfd = epoll_create(1);
assert(_epfd != -1);
}
bool Add(TcpSocket &sock)
{
// int epoll_ctl(int epfd,int op,int fd,epoll_event *ev)
int fd = sock.Getfd();
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = fd;
int ret = epoll_ctl(_epfd, EPOLL_CTL_ADD, fd, &ev);
if (ret < 0)
{
if (errno == EEXIST)
{
return true;
}
perror("epoll add error");
return false;
}
return true;
}
bool Del(TcpSocket &sock)
{
int fd=sock.Getfd();
int ret=epoll_ctl(_epfd,EPOLL_CTL_DEL,fd,NULL);
if(ret<0)
{
if(errno==ENOENT)
{
return true;
}
perror("epoll del error");
return false;
}
return true;
}
bool Wait(vector<TcpSocket> *active_socket,int ms)
{
//int epoll_wait(epfd,evs,maxevent,timeout)
struct epoll_event evs[MAX_EVENTS];
int ret=epoll_wait(_epfd,evs,MAX_EVENTS,ms);
if(ret<0)
{
perror("epoll wait error");
return false;
}
else if(ret==0)
{
printf("epoll timeout\n");
return true;
}
for(int i=0;i<ret;++i)
{
if(evs[i].events & EPOLLIN)
{
TcpSocket sock(evs[i].data.fd);
active_socket->push_back(sock);
}
}
return true;
}
};
// epoll,搭建tcp服务器
//tcp_srv.cpp
#include "select.hpp"
#include "epoll.hpp"
#define CHECK_RET(v) if ((v) == false) { return false; }
int main()
{
TcpSocket lis_sock;
// 创建套接字
CHECK_RET(lis_sock.Socket());
// 绑定地址信息
CHECK_RET(lis_sock.Bind("0.0.0.0", 9000));
// 开始监听
CHECK_RET(lis_sock.Listen());
//Select s;
//s.Add(lis_sock);
Epoll e;
e.Add(lis_sock);
while (1)
{
vector<TcpSocket> array;
bool ret = e.Wait(&array, 3000);
if (ret == false)
{
continue;
}
for (auto sock : array)
{
if (sock.Getfd() == lis_sock.Getfd())
{
// 获取新建连接
TcpSocket new_sock;
bool ret = lis_sock.Accept(&new_sock);
if (ret == false)
{
continue;
}
e.Add(new_sock);
}
else
{
// 收发数据
string body;
ret = sock.Recv(&body);
if (ret == false)
{
//将监控套接字先进行移除再关闭
e.Del(sock);
sock.Close();
continue;
}
cout << "client say: " << body << endl;
body.clear();
cout << "server say: ";
// 刷新缓冲区
fflush(stdout);
cin >> body;
ret = sock.Send(body);
if (ret == false)
{
//将监控套接字先进行移除再关闭
e.Del(sock);
sock.Close();
continue;
}
}
}
}
// 关闭套接字
lis_sock.Close();
return 0;
}
5 回想
边缘触发,只有新数据的到来时才会触发一次事件,而若一次事件触发后的处理中并未将所有数据处理完毕,那么在下一次新数据的到来前,这些剩余的数据都得不到处理。这种情况我们就希望在一次新数据的到来触发一次事件就将所有的数据处理完毕。
(ˇˍˇ) 想~:那如何将数据在一次处理中全部取出处理掉呢?
要想将数据一次性全部取出,得循环取出数据;但在循环读取数据的时候,在套接字接收数据的时候,可能会因为socket没有数据而阻塞。
可以将套接字的阻塞属性设置为非阻塞,之后套接字的所有操作都没非阻塞。
int fcntl(int fd,int op,int arg);
//参数
op:
F_GETFL(获取文件访问属性及状态标志)
F_SETFL(设置文件的访问属性或状态标志)---O_NONBLOCK(非阻塞)
//获取文件的原有属性
int flag=fcntl(fd,F_GETFL,0);
//在原有属性基础上添加非阻塞属性
fcntl(fd,F_SETFL,flag|O_NONBLOCK);
总结
对于select、poll、epoll来说,不论是哪一种模型,都针对大量描述符进行IO事件监控,但同一时间内少量活跃的场景,当活跃连接较多的情况下,得搭配多执行流来进行处理,充分利用系统资源。
相较之下,select和poll适用于单个描述符的事件监控以及超时管理,而epoll适用于大量描述符的事件监控场景。