高级I/O(Advanced I/O)是指在计算机系统中进行输入和输出操作时使用的一种更高级的接口和技术。也就是当我们进行输入输出的时候本质其实都要进行等待内核缓冲区中数据到来才能进行读取和写入到用户缓冲区。而往往在等待的阶段都是需要进行阻塞的。而高级I/O提供了比传统的基本I/O操作更丰富和灵活的功能。
五中IO模型(以网路传输为例)
IO的本质其实就是等待+拷贝。等待其实就是在等待缓冲区的数据到来,而拷贝其实就是将内核缓冲区的数据拷贝到用户缓冲区。而实现高效IO其实就是在单位时间内减少等待的时间。
以下是五种IO模型:
阻塞IO
- 在阻塞I/O模型中,应用程序发起I/O请求后会阻塞等待I/O操作完成,无法同时处理其他任务,直到数据就绪或超时才能继续执行后续操作。
- 适用于I/O操作相对简单且数据量较小的场景。
过程:像我们在调用recvfrom函数阻塞等待对端网络数据的发送的过程其实就是阻塞IO,知道数据到来才等待成功,后续进行数据拷贝。
非阻塞(轮训)IO
- 在非阻塞I/O模型中,应用程序发起I/O请求后不会阻塞等待I/O操作完成,可以继续执行其他任务。
- 当I/O操作完成后,应用程序通过轮询或事件通知等方式来处理结果。
- 非阻塞I/O提高了系统的并发性能,但可能会增加CPU的占用率。
过程: 非阻塞轮训等待其实就是不会当数据未到时在recvfrom函数上进行阻塞,而是非阻塞的形式,也就是当数据未到来时,recvfrom函数会立马进行返回,此时错误码会被设置成EWOULDBLOCK(EAGAIN),那么此时可以执行其他的任务,然后再轮训回来继续调用recvfrom。
信号驱动IO
- 信号驱动I/O模型结合了阻塞和非阻塞I/O的特点,通过信号机制来通知应用程序I/O操作的状态。
- 当I/O操作完成时,操作系统会向应用程序发送一个信号,应用程序可以处理该信号来继续执行后续操作(需要自己进行拷贝)。
过程:信号驱动IO其实就是主函数正常执行其他代码,不用在主函数中调用recvfrom函数,不过要提前设置信号处理handler方法,当SIGIO信号到来时会触发中断,此时会执行信号处理时设置好的的handler方法,也就是进行调用recvfrom函数。执行handler方法之后,进程会回到被SIGIO信号打断之前的状态(恢复文件上下文)继续执行剩余代码。
IO多路转接
- I/O多路转接是一种在单个线程(进程)中管理多个输入/输出通道(文件描述符)的技术,它允许应用程序同时监听多个I/O事件。
- 通过使用I/O复用器(如select、poll、epoll等),应用程序可以在一个线程中处理多个I/O操作,提高了系统的并发性能和吞吐量。
过程:其实IO多路转接本质就是进行等待多个文件描述符,只要有文件描述符有数据到来就可以通过判断是读事件就绪还是写事件就绪,然后进行相应的处理。
异步IO
- 在异步I/O模型中,应用程序发起I/O请求后无需等待I/O操作完成,可以继续执行其他任务。
- 异步IO是由内核在数据拷贝完成时, 通知应用程序(区别于信号驱动IO:信号驱动是告诉应用程序何时可以开始拷贝数据)
- 异步I/O是一种非阻塞式的I/O模型,适用于需要并发处理多个I/O操作或I/O操作耗时较长的场景。
过程:其实异步IO就是调用对应的系统调用函数aio_read去执行IO操作(读取+拷贝), 并不会阻塞主线程代的执行,到文件数据读取拷贝完毕以后系统会通过信号或回调函数通知用户程序。
同步IO和异步IO
以上是五种IO模型。前四种IO被称为同步IO,最后一种只是发起IO不参与过程的称为异步IO。
- 同步IO就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了,简单来说,就是由调用者主动等待这个调用的结果。(需要执行等或拷贝的动作)
- 异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果,换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果,而是在调用发出后,被调用者通过状态信号通知调用者,或通过回调函数处理这个调用。(不需要执行等和拷贝的动作)
实现非阻塞轮训IO
我们在等待文件描述符的数据的时候默认都是进行阻塞等待,但是我们可以将其设置为非阻塞状态。
fcntl 函数
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
//fcntl函数有5种功能:
//复制一个现有的描述符(cmd=F_DUPFD).
//获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
//获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).
//获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
//获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).
设置一个文件描述符为非阻塞
void SetNoBlock(int fd)
{
int fl = fcntl(fd, F_GETFL); // 获取状态标记
if (fl == -1)
{
cerr << "获取状态标记失败..." << endl;
exit(-1);
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK); // 设置状态标记
}
实现非阻塞轮训IO代码段
int main()
{
SetNoBlock(0); // 将0号文件描述符设置为非阻塞状态
// 1.文件非阻塞状态且没有读到数据时返回值按照读取错误进行返回-1
while (true)
{
char buffer[1024];
ssize_t n = read(0, buffer, sizeof(buffer) - 1); // C语言接口自带\0
if (n > 0)
{
buffer[n] = 0;
cout << buffer << endl;
}
else if (n == 0)
{
cerr << "对端关闭连接,读取结束..." << endl;
}
else // 返回值为-1:1.数据没有准备好 2.真的读取出错
{
if (errno == EWOULDBLOCK) // EAGAIN
{
cout << "数据未就绪..." << endl;
}
else if (errno == EINTR)
{
cout << "收到信号导致读取中断..." << endl;
}
else
{
cerr << "读取出错..." << endl;
break;
}
}
sleep(1);
}
return 0;
}
IO多路转接
I/O多路转接是一种在单个线程(进程)中管理多个输入/输出通道(文件描述符)的技术,它允许应用程序同时监听多个I/O事件。以下是IO多路转接的三种实现方式。并且都是实现网络通信的方式,所以提前将网络套接字代码准备:
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <vector>
#include<thread>
#include<functional>
#include<algorithm>
using namespace std;
class Inet_data
{
public:
Inet_data(int sockfd,const sockaddr_in &tmp)
: _ip(tmp.sin_addr.s_addr), _port(tmp.sin_port),_sockfd(sockfd),_sock(tmp)
{}
uint16_t get_port()
{
return _port;
}
in_addr_t get_ip()
{
return _ip;
}
int get_sockfd()
{
return _sockfd;
}
int get_sockfd() const
{
return _sockfd;
}
bool operator==(const Inet_data &tmp)
{
return _ip==tmp._ip&&_port==tmp._port;
}
bool operator==(const Inet_data &tmp) const
{
return _ip==tmp._ip&&_port==tmp._port;
}
void Inet_info()
{
string client_ip = inet_ntoa(_sock.sin_addr);
uint16_t client_port = ntohs(_port);
cout << "sock_fd = "<<_sockfd<<" client_ip = " << client_ip << " client_port = " << to_string(_port) << endl;
}
void Inet_info() const
{
string client_ip = inet_ntoa(_sock.sin_addr);
uint16_t client_port = ntohs(_port);
cout << "sock_fd = "<<_sockfd<<" client_ip = " << client_ip << " client_port = " << to_string(_port) << endl;
}
private:
int _sockfd;
sockaddr_in _sock;
in_addr_t _ip;
uint16_t _port;
};
#pragma once
#include "Inet_data.h"
#include "SetNoBlock.h"
using namespace std;
namespace Network_module
{
static const int g_backlog = 5;
class Tcp
{
public:
Tcp(uint16_t port)
: _port(port)
{
}
void Creat_socket()
{
// 1.创建套接字(创建文件细节)
_sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建套接字(域、套接字类型、协议)参数2基本可以固定tcp/udp
if (_sockfd < 0)
{
cerr << "创建套接字失败" << endl;
exit(-1);
}
}
void Bind()
{
// 2.绑定(网络信息与文件信息相关联)
struct sockaddr_in local; //
local.sin_family = AF_INET; // 域,用于sockaddr类型接收时的辨别字段
local.sin_port = htons(_port); // 端口号 主机转网络(用于发送)
local.sin_addr.s_addr = INADDR_ANY; // 地址任意
int n = bind(_sockfd, (sockaddr *)&local, sizeof(local));
if (n != 0)
{
cerr << "套接字绑定失败!!!" << endl;
}
}
void Listen() // 监听,等待客户端进行connect
{
if (listen(_sockfd, g_backlog) == -1) // 参数二是全连接队列
{
cerr << "listen fails!!!" << endl;
exit(-1);
}
}
int Build_Listensocket()
{
// 1.网络(创建tcp listen套接字)
Creat_socket();
SetNoBlock(_sockfd); // 将listen套接字设置为非阻塞状态
Bind();
Listen();
return _sockfd;
}
int Accept() // 返回一个连接套接字,用于网络数据通信
{
while (true)
{
struct sockaddr_in local;
socklen_t len;
int _connect_sockfd = accept(_sockfd, (sockaddr *)&local, &len);
if (_connect_sockfd < 0)
{
cout << "accept error ,continue..." << endl;
continue;
}
cout << "成功接收客户端连接..." << endl;
Inet_data tmp(_connect_sockfd, local);
tmp.Inet_info();
return _connect_sockfd;
}
}
bool Recv(int fd, string &buffer, int sz)
{
char tmp[sz];
int n = recv(fd, tmp, sz, 0);
if (n > 0)
{
tmp[n] = 0;
buffer += tmp;
if (buffer == "quit\r\n")
{
cout << "已断开与客户端的连接..." << endl;
return false;
}
return true;
}
else
return false;
}
uint16_t Get_sockfd()
{
return _sockfd;
}
~Tcp()
{
}
private:
int _sockfd; // socket创建的是监听套接字,用于服务器listen
uint16_t _port;
};
}
IO多路转接-select
select只负责等待套接字中的数据,其实会存在三个类似位图的结构体,分别对应读事件、写事件、异常事件,其中位图置为1表示需要关心该位置下文件描述符的事件。在每一次的select其实都是在检测此时是否有事件就绪,如果有事件就绪就会返回就绪文件描述符的个数。同时fd_set结构体也会针对就绪事件的位图数值置为1。
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout)
//~参数nfds是需要监视的最大的文件描述符值+1;
//~readfds,writefds,exceptfds分别对应于需要检测的可读、可写、异常文件描述符的集合;
//~参数timeout为结构timeval,用来设置select()的等待时间
//fd_set结构其实就可以看做是一个位图,其中每个位置的标识其实就是对应文件描述符标号
//提供了一组操作fd_set的接口, 来比较方便的操作位图
void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位
int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd 的位是否为真
void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位
select代码段
#pragma once
#include <sys/select.h>
#include "socket_server.h"
#define default_port 8888
class Select
{
public:
Select(uint16_t port = default_port)
: _tcp_socket(make_unique<Tcp>(port)), _port(port)
{
}
void Init()
{
_tcp_socket->Creat_socket();
_tcp_socket->Bind();
_tcp_socket->Listen();
// 将需要进行select的监听套接字直接添加进来
_fds.push_back(_tcp_socket->Get_sockfd());
}
void Run()
{
while (true)
{
fd_set rfds;
FD_ZERO(&rfds); // 初始化读文件描述符集
int max_fd = 0; // 用于标识最大文件描述符的值
for (const auto &fd : _fds)
{
if (fd != -1) // 会存在有关闭的文件描述符
{
FD_SET(fd, &rfds);
max_fd = max(max_fd, (int)fd);
}
}
timeval timeout({5, 0});
int n = select(max_fd + 1, &rfds, nullptr, nullptr, &timeout); // 负责等待多个文件描述符
switch (n)
{
case 0:
cout << "select timeout, retrying..." << endl;
break;
case -1:
cerr << "select erro..." << endl;
break;
default: // n>0(n个文件描述符状态发生变化)
cout << "select success, residue time:" << timeout.tv_sec << '.' << timeout.tv_usec << 's' << endl;
Handler_select(rfds);
break;
}
}
}
~Select()
{
}
private:
unique_ptr<Tcp> _tcp_socket;
uint16_t _port;
vector<uint16_t> _fds;
private:
void Handler_select(fd_set &rfds)
{
for (auto &fd : _fds)
{
if (fd == -1)
continue; // 连接已被关闭
if (FD_ISSET(fd, &rfds)) // 读事件就绪
{
if (fd == _tcp_socket->Get_sockfd()) // 1.获取新的连接
{
uint16_t connect_sockfd = _tcp_socket->Accept();
cout << "get a newsockfd: " << connect_sockfd << endl;
_fds.push_back(connect_sockfd);
}
else // 2.收到数据
{
string buffer;
bool ret = _tcp_socket->Recv(fd, buffer, 1024);
if (!ret)
{
cerr << "读取失败..." << endl;
close(fd);
fd = -1; // 关闭的文件描述符置为-1
}
else
{
cout << "client say: " << buffer << endl;
buffer.clear();
}
}
}
}
}
};
select的缺点
- 每次调用select,都需要手动设置fd集合,从接口使用角度来说也非常不便。
- 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大。
- 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大。
- select支持的文件描述符数量有限(取决于fd_set位图大小)。
IO多路转接-poll
poll其实就是在select的基础上进行优化的,大致上和select没什么区别。poll在参数上有一个pollfd的结构体字段,也就是文件描述符的描述字段,包含文件描述符的数值、关心的事件、就绪的事件。
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
// pollfd结构
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
//参数说明:
~ fds是一个poll函数监听的结构列表,每一个元素中, 包含了三部分内容: 文件描述符, 监听的事件集合, 返
回的事件集合.
~ nfds表示fds数组的长度.
~ timeout表示poll函数的超时时间, 单位是毫秒(ms).
poll代码段
#pragma once
#include <poll.h>
#include "socket_server.h"
#define default_port 8888
static int sz = 1024;
class Poll
{
public:
Poll(uint16_t port = default_port)
: _tcp_socket(make_unique<Tcp>(port)), _port(port), _inode(0)
{
_rfds = new pollfd[sz]; // 开辟空间,表明起初可以承载文件描述符的数量
// 后期也可以空间不够动态扩容
}
void Init()
{
_tcp_socket->Creat_socket();
_tcp_socket->Bind();
_tcp_socket->Listen();
// 初始化_rfds
for (int i = 0; i < sz; i++)
{
_rfds[i].fd = -1;
_rfds[i].events = _rfds[i].revents = 0;
}
// 将需要进行poll的监听套接字直接添加进来
_rfds[_inode].fd = _tcp_socket->Get_sockfd();
_rfds[_inode].events |= POLLIN; // 新连接到来属于可读事件POLLIN
_inode++;
}
void Run()
{
while (true)
{
int n = poll(_rfds, _inode, 3000); // 3000ms,为-1表示阻塞等待
switch (n)
{
case 0:
cout << "poll timeout, retrying..." << endl;
break;
case -1:
cerr << "poll erro...poll一个已经关闭的文件描述符" << endl;
break;
default: // n>0(n个文件描述符状态发生变化)
cout << "poll success..." << endl;
Handler_poll();
break;
}
}
}
~Poll()
{
delete[] _rfds;
}
private:
unique_ptr<Tcp> _tcp_socket;
uint16_t _port;
struct pollfd *_rfds;
nfds_t _inode; // typedef unsigned long nfds_t
private:
void Handler_poll()
{
for (int i = 0; i < _inode; i++)
{
pollfd cur = _rfds[i];
if (cur.revents & POLLIN) // 当前的pollfd(读事件)事件就绪
{
// 读事件分两类:1.新连接 2.新数据
if (cur.fd == _tcp_socket->Get_sockfd()) // 1.获取新的连接
{
uint16_t connect_sockfd = _tcp_socket->Accept();
cout << "get a newsockfd: " << connect_sockfd << endl;
// 将新的connect套接字描述符添加到pollfd中
if (_inode < sz)
{
_rfds[_inode].fd = connect_sockfd;
_rfds[_inode].events |= POLLIN;
_inode++;
}
else // 数组达到上限了
{
sz += 512;
pollfd *newrfds = new pollfd[sz];
memcpy(newrfds, _rfds, sizeof(pollfd) * (sz - 512));
delete[] _rfds;
_rfds = newrfds;
}
}
else // 2.收到数据
{
string buffer;
bool ret = _tcp_socket->Recv(cur.fd, buffer, 1024);
if (!ret)
{
cerr << "读取失败..." << endl;
// 取消poll的关心
cur.fd = -1; // 关闭的文件描述符置为-1
cur.events = 0; // 读取事件置为0
close(cur.fd);
}
else
{
cout << "client say: " << buffer << endl;
string repo = "hello:" + buffer;
send(cur.fd, repo.c_str(), repo.size(), 0);
buffer.clear();
}
}
}
else
{
cout << "当前无事件就绪..." << endl;
}
}
}
};
poll的缺点
- 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。
- 每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中。
-
同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长, 其效率也会线性下降
IO多路转接-epoll
epoll也就是IO多路转接中最常用的一种,主要步骤就是1.创建epoll模型 2.epoll事件注册 3.epoll监听事件就绪。
认识epoll相关接口:
epoll_create
//创建epoll模型
int epoll_create(int size);
epoll_create函数底层其实就是创建红黑树和就绪队列,当调用epoll_ctl时,会将需要进行关心的文件描述符所构建的节点放到红黑树当中,然后调用epoll_waite时将就绪的文件描述符所对应的节点拷贝到就绪队列中 。
ps:图片源自网络 csdn博主:当当响
epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);//修改红黑树
- 第一个参数是epoll_create()的返回值。
- 第二个参数表示动作,用三个宏来表示。
- 第三个参数是需要监听的fd。
- 第四个参数是告诉内核需要监听什么事件。
参数二的取值:
- EPOLL_CTL_ADD :注册新的fd到epfd中。
- EPOLL_CTL_MOD :修改已经注册的fd的监听事件。
- EPOLL_CTL_DEL :从epfd中删除一个fd。
struct 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 events */
epoll_data_t data; /* User data variable */
} __EPOLL_PACKED;
events的定义:
EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
EPOLLOUT : 表示对应的文件描述符可以写;
EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
EPOLLERR : 表示对应的文件描述符发生错误;
EPOLLHUP : 表示对应的文件描述符被挂断;
EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要
再次把这个socket加入到EPOLL红黑树中;
其中 结构体events字段就是关心的事件,表示要监听的事件(传给内核)。而data字段用于存储用户自定义的数据,比如文件描述符或者指针等(用户使用)。
epoll_waite
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)
- 参数events是分配好的epoll_event结构体数组。
- epoll将会将已经就绪的事件赋值到events数组中 (events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存)。
- maxevents参数是告诉内核这个events有多大,如果就绪事件大于maxevents值,就会在就绪队列中拿maxevents个事件到epoll_event指针里。这个 maxevents的值不能大于创建epoll_create()时的size。
- 参数timeout是超时时间 (毫秒,0会非阻塞,-1是永久阻塞)。
- 如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时,返回小于0表示函数失败。
该函数在过了timeout时间之后,会将已经就绪的文件所对应的epoll_event对象返回给events指针。而返回值就是就绪的事件个数。
epoll代码段
#pragma once
#include <sys/epoll.h>
#include <unistd.h>
#include <cstring>
#include <iostream>
namespace Epoll_module
{
const static int default_fd = -1;
const static int default_epoll_size = 7;
class Epoll
{
public:
Epoll() : _epoll_fd(default_fd)
{
}
void Epoll_Init()
{
_epoll_fd = ::epoll_create(default_epoll_size); // 系统调用::
if (_epoll_fd == -1)
{
std::cerr << "epoll_create error... " << strerror(errno) << std::endl;
exit(-1);
}
}
void Epoll_addevent(int sockfd, uint32_t event)
{
epoll_event ev;
ev.events |= event;
ev.data.fd = sockfd; // 不是给内核的,给用户的
int n = epoll_ctl(_epoll_fd, EPOLL_CTL_ADD, sockfd, &ev); // 添加套接字
if (n == -1)
{
std::cerr << "EPOLL_CTL_ADD error... " << strerror(errno) << std::endl;
}
}
int Epoll_wait(epoll_event *events, int maxevents, int timeout)
{
int n = epoll_wait(_epoll_fd, events, maxevents, timeout);
return n;
}
void Del_event(int sockfd)
{
int n = epoll_ctl(_epoll_fd,EPOLL_CTL_DEL,sockfd,nullptr);
if (n < 0)
{
std::cerr << "EPOLL_CTL_DEL error... " << strerror(errno) << std::endl;
}
}
~Epoll()
{
if (_epoll_fd > 0)
close(_epoll_fd);
std::cout << "epoll close" << std::endl;
}
private:
int _epoll_fd;
};
}
#pragma once
#include "socket_server.h"
#include "Epoll.h"
#include <unistd.h>
#include <fcntl.h>
#define default_port 8888
using namespace Network_module;
using namespace Epoll_module;
class Epoll_server
{
const static int g_maxevents = 37;
public:
Epoll_server(uint16_t port = default_port)
: _tcp_socket(make_unique<Tcp>(port)), _port(port), _epoller(make_unique<Epoll>())
{
}
void Init()
{
// 1.网络(创建tcp listen套接字)
_tcp_socket->Creat_socket();
_tcp_socket->Bind();
_tcp_socket->Listen();
// 2.多路转接-epoll(创建epoll模型)
_epoller->Epoll_Init();
// 3.listen_sockfd添加到epoll(红黑树)中
_epoller->Epoll_addevent(_tcp_socket->Get_sockfd(), EPOLLIN);
}
void Run()
{
while (true)
{
int n = _epoller->Epoll_wait(_revents, g_maxevents, -1); // 5000->等待5s
// Epoll_wait将已经等待成功的事件放到_revents(就绪队列)里
switch (n)
{
case 0:
cout << "no file descriptor became ready during the requested timeout..." << endl;
break;
case -1:
std::cerr << "epoll_wait error... " << strerror(errno) << std::endl;
break;
default:
// cout << "event preparation..." << endl;
Handler_event(n);
break;
}
}
}
~Epoll_server()
{
}
private:
unique_ptr<Tcp> _tcp_socket;
unique_ptr<Epoll> _epoller;
uint16_t _port;
epoll_event _revents[g_maxevents];
private:
void Handler_event(int n)
{
for (int i = 0; i < n; i++)
{
int sockfd = _revents[i].data.fd;
uint32_t event = _revents[i].events;
if (event & EPOLLIN) // 读事件就绪
{
if (sockfd == _tcp_socket->Get_sockfd()) // 1.新连接
{
int newsockfd = _tcp_socket->Accept();
_epoller->Epoll_addevent(newsockfd, EPOLLIN); // 将新的连接添加到epoll(红黑树)
}
else // 2.数据到来
{
// 读取数据// tcp数据流,数据不一定会一次全部读完
char buffer[1024];
int n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
if (n > 0)
{
n -= strlen("\r\n"); // telnet发送数据,末尾自带换行符\r\n
buffer[n] = 0;
cout << "sockfd = " << sockfd << " say: " << buffer << endl;
}
else
{
if (n == 0)
cout << "客户端关闭连接..." << endl;
else
cerr << "数据读取失败..." << endl;
// 取消epoll关心,关闭文件
_epoller->Del_event(sockfd);
close(sockfd);
}
// 返回数据
string ret = (string) "hello->" + buffer + "\r\n";
send(sockfd, ret.c_str(), ret.size(), 0);
}
}
}
}
};
epoll的优点
- 接口使用更加合理方便。
- 拷贝轻量。不用每次循环都重新进行关心文件描述符事件的拷贝。
- 事件回调机制,从而避免使用遍历。每当事件就绪时,都会自动调用回调函数将红黑树中的就绪节点拷贝到就绪队列中,而不用遍历红黑树。
- 文件描述符无上限。
对比LT和ET
LT(水平触发)模式
-
工作原理:
- 当epoll_wait检测到监听文件描述符上有事件发生时,会通知应用程序。
- 如果应用程序没有处理该事件,下次调用epoll_wait时,该事件还会被通告,直到该事件被处理完毕。
ET(边缘触发)模式
-
工作原理:
- 当文件描述符从未就绪变为就绪时,内核会通过epoll通知应用程序。
- 内核会假设应用程序知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到应用程序做了某些操作导致那个文件描述符不再为就绪状态。
LT与ET比较
LT模式 | ET模式 | |
工作原理 | 事件发生时持续通知,直到处理完毕 | 状态改变时通知一次 |
对socket的支持 | 同时支持block和non-block socket | 只支持non-block socket(一次把数据全部读完) |
编程难度 | 相对较低,不易出错 | 相对较高,需要细致处理 |
效率 | 相对较低 | 非常高,尤其在高并发、大流量情况下 |
应用场景 | 实时性要求不是非常高,代码编写要求简单的场景 | 高并发、大流量的网络服务器等需要高效处理IO事件的场景 |
如果需要ET模式下的完整代码可以去我的gitee中查看:陈瑞 (chen-rui-RIO) - Gitee.comhttps://gitee.com/chen-rui-RIO