任何IO操作都包含两个过程:
当我们调用read()
函数,首先操作系统会先将数据从源头(如本地的硬盘、键盘、管道、远端的网络…)拷贝到内核缓冲区中。在这过程中可能由于文件很大,很久才能读完;可能用户一直没有在键盘输入;可能网络对端很久才发来消息,一直读不到。。。所以我们称这一步为等待过程。
然后read()
再将内核缓冲区中的数据拷贝到用户空间中,这一步需要的时间就相对较短
从前,我们直接调用scanf()
,cin
时,程序会一直卡在输入这一步,直到用户输入回车,我们称这样的IO为阻塞式IO,即:在内核将数据准备好之前, 系统调用会一直等待。所有的套接字默认也都是阻塞式的。
但实际开发过程中,不可能让所有的IO都阻塞,其它任务都不进行。所以,让我们看一下其它的IO模型
非阻塞IO:如果内核还未将数据准备好, 系统调用会直接返回, 并且将错误码errno
设置为EWOULDBLOCK
.
当对文件描述符设置了非阻塞状态,我们一直轮询式地调用
recvfrom
,直到某一次调用返回的不再是-1,在轮询期间我们可以插入其它任务,后面会写一段demo代码
信号驱动IO: 内核将数据准备好的时候, 使用SIGIO
信号通知应用程序进行IO操作
我们需要提前在应用程序中设定好信号处理的代码,当调用了
sigaction
,操作系统会进行等待数据到来并拷贝到内核缓冲区,然后就给用户程序发SIGIO
信号,在此过程中我们的程序可以继续执行其它的任务逻辑,当收到信号,就会中断当前任务去执行对应的信号处理代码,完成从内核缓冲区到用户空间的拷贝
多路转接:多路转接能够同时等待多个文件描述符的就绪状态.
多路转接的IO方式让我们可以同时进行多个IO,
假设我们此时有4个IO要进行,对应4个文件描述符:3,4,5,6
首先我们调用
select()
,将3,4,5,6这四个文件描述符交给操作系统照看,程序便进行阻塞一旦有任意一个或多个文件描述符完成等待过程,对应的内核缓冲区数据就绪,
select()
就会返回就绪的那些文件描述符,用户程序获得了这些文件描述符,就可以直接调用recvfrom()
把数据从内核空间拷贝到用户空间然后继续把没有读写完了文件描述符通过
select()
交给操作系统管理,直到所有IO都完成
这种IO方式的优势就在于可以同时进行多个文件描述符的等待,广撒网,等待的时间也就变短了
异步IO:由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据).
将我们读写操作从默认的阻塞方式变为非阻塞,只需要将文件描述符修改为非阻塞模式
函数原型:
#include
#include
int fcntl(int fd, int cmd, ... /* arg */ );
下面我们利用上面的fcntl()
设计一个让文件描述符非阻塞化的函数:SetNoBlock()
#include
#include
bool setNonBlock(int sock)
{
int flag = fcntl(sock, F_GETFL); // 先获取文件描述符原本的选项属性
if (flag == -1)
{
return false;
}
int n = fcntl(sock, F_SETFL, flag | O_NONBLOCK); // 设置非阻塞
if (n == -1)
{
return false;
}
return true;
}
#include "Util.hpp"
int main()
{
SetNoBlock(0); // 对0号描述符(标准输入)设置非阻塞
char buffer[1024] = {0};
while (true)
{
scanf("%s", buffer);
cout << "刚刚检测到的内容是# " << buffer << endl;
sleep(1);
}
return 0;
}
#include
#include
#include
#include
int select(int nfds,
fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
struct timeval *timeout);
//对fd_set进行增删查找的接口
void FD_ZERO(fd_set *set); //对set进行初始化
void FD_SET(int fd, fd_set *set); //在set中设置某个文件描述符
void FD_CLR(int fd, fd_set *set); //在set中清除某个文件描述符
int FD_ISSET(int fd, fd_set *set); //判断某个文件描述符是已在set中进行设置
nfds
:最大的fd+1,假设需要等待的文件描述符有1,2,4,8,nfds即设置为9,
readfds/writefds/exceptfds
:都是输入输出型参数,输入所有需要被管理的文件描述符,带出已经就绪的文件描述符,其中fd_set
是一个文件描述符集合
我们把等待的事件分为三类:
如0号文件描述符是标准输入,我们只需要关心它的读事件,就不需要在writefds
中添加
fd_set
:
fd_set
是一个有1024比特位的位图调用前,我们通过
FD_SET()
对fd_set
变量添加文件描述符,FD_SET(3, &set);`即表示将set的第3个二进制位设置为1;也就是说select最多只能对1024个文件描述符进行管理调用时,我们通过
FD_SET()
向位图中添加文件描述符,告诉了操作系统要监视那些文件描述符调用结束时,操作系统会再通过这三个参数返回已经就绪的那些
df
,通过使用FD_ISSET()
,我们可以逐个测试哪些位被设置过,从而得知哪些fd
已就绪
注意事项:
因为是输入输出型参数,每调用一次select,设置的set
都会被覆盖
所以用户需要自己维护一个表,记录当前在对哪些文件描述符进行管理
每次轮询调用select()
之前,都需要对set
进行FD_ZERO()
初始化,再FD_SET()
重新添加文件描述符
timeout
:超时退出时间
select()
可以是阻塞式的,只要没有fd
就绪,就一直阻塞;
也可以设置阻塞的时间,时间一到,即使没有fd
就绪,select()
依然返回
这个阻塞的时间就是通过timeout设置的
如果退出时间设置为0,就是非阻塞式的
如果传入空指针,就是阻塞式的
struct timeval
:
struct timeval { time_t tv_sec; /* seconds */ suseconds_t tv_usec; /* microseconds */ };
- tv_sec:以秒为单位
- tv_usec:以毫秒为单位
返回值:
接下来我们写一段demo代码,形象的理解一下select的实际使用方法:
在先前TCP网络通讯的过程中,我们对一个监听套接字进行accept()
的过程也可以看作是IO事件
也可以分为两个过程:
这样的一个获取连接的事件可以看作是一个读事件
于是,我们便可以将监听套接字作为一个读事件交给select,一旦对端发起了连接,经过三次握手,本地完成了连接,此时,select便会返回告知用户读事件就绪
如下demo,我们只关心读事件,具体关心写事件和异常事件的案例,在epoll再进行讲解
为了方便网络相关接口的调用,我们进行一个封装
Sock.hpp
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
class Sock
{
const static int gbacklog = 10;
public:
// 创建监听套接字
static int Socket()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
cerr << "[" << errno << "]: " << strerror(errno) << endl;
exit(1);
}
// 让服务器崩掉后立马可以重启此端口
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
return sockfd;
}
static void Bind(int sockfd, uint16_t port)
{
sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_addr.s_addr = htons(INADDR_ANY);
local.sin_port = htons(port);
if (bind(sockfd, (sockaddr *)&local, sizeof(local)) < 0)
{
cerr << "[" << errno << "]: " << strerror(errno) << endl;
exit(2);
}
}
static void Listen(int sockfd)
{
if (listen(sockfd, gbacklog) < 0)
{
cerr << "[" << errno << "]: " << strerror(errno) << endl;
exit(3);
}
}
static int Accept(int listenSock, std::string& clientIp, uint16_t &clientPort)
{
struct sockaddr_in peer;
socklen_t size = sizeof(peer);
int serverSock = accept(listenSock, (sockaddr *)&peer, &size);
if (serverSock < 0)
{
return -1;
}
clientIp = inet_ntoa(peer.sin_addr);
clientPort = ntohs(peer.sin_port);
return serverSock;
}
};
接下来我们创建一个监听套接字,把它交给select,一旦远端发起了连接,我们就完成accept,并在本地打印一条消息
#include "Sock.hpp"
#include
#include
#include
#include
using namespace std;
static void usage(const std::string porc)
{
std::cout << "Usage:\n\t" << porc << " port" << std::endl;
std::cout << "example:\n\t" << porc << " 8080" << std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
usage(argv[0]);
exit(0);
}
// 创建监听套接字,完成绑定和监听
int listenSock = Sock::Socket();
Sock::Bind(listenSock, atoi(argv[1]));
Sock::Listen(listenSock);
while (true)
{
// select的事件位图是一个输入输出型参数,
// 函数返回后,位图中原本的信息会被覆盖
// 所以每次调用select前都要对位图进行重新设置
// 为select提供的相关参数:
int maxfd = listenSock;
fd_set readfds; // 读事件的位图
FD_ZERO(&readfds); // 初始化位图
FD_SET(listenSock, &readfds); // 将监听套接字添加到位图
struct timeval timeout = {5, 0}; // 设置阻塞时间为5秒
// 我们将listenSocket交给select,
// 底层连接建立完成了,再唤醒上层进行accept()
int n = select(maxfd + 1, &readfds, nullptr, nullptr, &timeout);
switch (n)
{
case 0: // 没有事件就绪
/* code */
cout << "time out...: " << (unsigned long)time(nullptr) << endl;
break;
case -1: // 出错
cerr << "[" << errno << "]: " << strerror(errno) << endl;
break;
default:
string IP;
uint16_t port;
Sock::Accept(listenSock, IP, port);
cout << IP << ": " << port << " connected!" << endl;
break;
}
}
}
当服务器启动后5秒,远端发起了连接,select返回完成1,我们再完成accept,建立连接
继续将listensock交给select
再过10秒后,又有一台主机发起了连接,再次成功建立连接
但实际应用中,一台服务器不仅需要和客户端完成连接,更重要的通过这个连接为远端提供服务
而提供服务的过程中,无疑需要借助accept返回的服务套接字
,读取远端发来的信息,经过处理后,再向远端发送消息
这里的读取发送同样也是IO操作,我们将这些文件描述符也放入位图,交给select
这里就出现一个问题,当有多个用户与服务器建立了连接,哪些套接字就绪、哪些用户退出套接字关闭了、下一次还要把哪些套接字放入位图…
所以,我们要独立维护一个文件描述符表,记录当前正在维护的文件描述符
#include "Sock.hpp"
#include
#include
#include
#include
using namespace std;
// select的事件位图是一个输入输出型参数,
// 函数返回后,位图中原本的信息会被覆盖
// 所以外部要独自维护一个表,每次调用select前都要对位图进行重新设置
int fdsArray[sizeof(fd_set) * 8] = {0}; // 存储所有需要交给多路转接管理的文件描述符
int gnum = sizeof(fdsArray) / sizeof(fdsArray[0]); // 最多存储的个数
static const int DFL = -1; // 没有使用的位置设置为DLF,使用了即具体那个文件描述符
static void usage(const std::string porc)
{
std::cout << "Usage:\n\t" << porc << " port" << std::endl;
std::cout << "example:\n\t" << porc << " 8080" << std::endl;
}
void showArray(int *array, int num)
{
cout << "当前监控的文件描述符# ";
for (int i = 0; i < num; i++)
{
if (array[i] != DFL)
cout << array[i] << " ";
}
cout << endl;
}
static void handleEvent(fd_set &readfds)
{
if (FD_ISSET(fdsArray[0], &readfds)) // 处理监听套接字
{
string IP;
uint16_t port;
int sock = Sock::Accept(fdsArray[0], IP, port);
// 从数组中找一个位置把文件描述符添加进入,y
// 下一次调用select时会被添加到位图中
int i = 1;
for (; i < gnum; i++)
{
if (DFL == fdsArray[i])
{
fdsArray[i] = sock;
showArray(fdsArray, gnum);
break;
}
}
if (i == gnum)
{
cerr << "服务器已达到上限,无法承载更多连接" << endl;
close(sock);
}
cout << IP << ": " << port << " connected!" << endl;
}
for (int i = 1; i < gnum; i++) // 处理普通套接字的IO事件
{
if (DFL == fdsArray[i])
continue;
if (FD_ISSET(fdsArray[i], &readfds))
{
// 进行read/recv
char buffer[1024];
ssize_t s = recv(fdsArray[i], buffer, sizeof(buffer), 0);
if (s > 0)
{
buffer[s] = '\0';
cout << "client[" << fdsArray[i] << "]# " << buffer << endl;
}
else if (s == 0)
{
cout << "client[" << fdsArray[i] << "]quit" << endl;
close(fdsArray[i]); // 关闭文件描述符
fdsArray[i] = DFL; // 去除数组中的文件描述符
}
else
{
cout << "client[" << fdsArray[i] << "]error" << endl;
close(fdsArray[i]); // 关闭文件描述符
fdsArray[i] = DFL; // 去除数组中的文件描述符
}//end else
}//end if (FD_ISSET(fdsArray[i], &readfds))
}//end for
}//end handleEvent
int main(int argc, char *argv[])
{
if (argc != 2)
{
usage(argv[0]);
exit(0);
}
// 创建监听套接字,完成绑定和监听
int listenSock = Sock::Socket();
Sock::Bind(listenSock, atoi(argv[1]));
Sock::Listen(listenSock);
for (int i = 0; i < gnum; i++)
{
fdsArray[i] = DFL;
}
fdsArray[0] = listenSock; // 将0号设置为监听套接字
while (true)
{
// 为select提供的相关参数:
int maxfd = DFL;
fd_set readfds; // 读事件的位图
FD_ZERO(&readfds); // 初始化位图
for (int i = 0; i < gnum; i++) // 遍历fdsArray,添加描述符到位图
{
if (fdsArray[i] == DFL) // 非文件描述符
continue;
FD_SET(fdsArray[i], &readfds);
if (fdsArray[i] > maxfd) // 遍历过程中找到最大的文件描述符,select用
{
maxfd = fdsArray[i];
}
}
struct timeval timeout = {5, 0}; // 设置阻塞时间为5秒
// 我们将listenSocket交给select,
// 底层连接建立完成了,再唤醒上层进行accept()
int n = select(maxfd + 1, &readfds, nullptr, nullptr, &timeout);
switch (n)
{
case 0: // 没有事件就绪
/* code */
cout << "time out...: " << (unsigned long)time(nullptr) << endl;
break;
case -1: // 出错
cerr << "[" << errno << "]: " << strerror(errno) << endl;
break;
default:
// 有事件就绪,把位图传给处理函数
handleEvent(readfds);
break;
}//end switch
}//end while
}//end main
#include
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
作为一个多路转接的接口,poll同样也要传入文件描述符、每个文件描述符需要关心的事件,文件描述符的数量、阻塞事件,只不过这里传入的方式略有不同
fds:一个结构体数组,每个元素代表一个文件描述符
struct pollfd
:
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
所有的事件:
因为poll的传入和传出用了两个位图,并不会出现select那样数据被覆盖的现象,自然也不需要用户自己再额外维护一个数组;
同时poll的一个个文件描述符是存在数组中的,所以也不会受到位图那样的限制
nfds:nfd的元素个数
timeout:阻塞时间,以毫秒为单位
返回值:就绪的文件描述符个数
我们把上面的select代码改为poll版本
同样,poll的demo我们也只关心读事件
#include "Sock.hpp"
#include
#include
#include
#include
#include
using namespace std;
static const int DFL = -1;
static const int MAXNUM = 1024;
struct pollfd fds[MAXNUM];
static void usage(const std::string porc)
{
std::cout << "Usage:\n\t" << porc << " port" << std::endl;
std::cout << "example:\n\t" << porc << " 8080" << std::endl;
}
void showArray()
{
cout << "当前监控的文件描述符# ";
for (int i = 0; i < MAXNUM; i++)
{
if (fds[i].fd != DFL)
cout << fds[i].fd << " ";
}
cout << endl;
}
static void handleEvent()
{
if (fds[0].revents & POLLIN) // 处理监听套接字
{
string IP;
uint16_t port;
int sock = Sock::Accept(fds[0].fd, IP, port);
// 从数组中找一个位置把文件描述符添加进入,
int i = 1;
for (; i < MAXNUM; i++)
{
if (DFL == fds[i].fd)
{
fds[i].fd = sock;
fds[i].events = POLLIN;
fds[i].revents = 0;
showArray();
break;
}
}
if (i == MAXNUM)
{
cerr << "服务器已达到上限,无法承载更多连接" << endl;
close(sock);
}
cout << IP << ": " << port << " connected!" << endl;
}
for (int i = 1; i < MAXNUM; i++) // 处理普通套接字的IO事件
{
if (DFL == fds[i].fd)
continue;
if (fds[i].revents & POLLIN)
{
// 进行read/recv
char buffer[1024];
ssize_t s = recv(fds[i].fd, buffer, sizeof(buffer), 0);
if (s > 0)
{
buffer[s] = '\0';
cout << "client[" << fds[i].fd << "]# " << buffer << endl;
}
else if (s == 0)
{
cout << "client[" << fds[i].fd << "]quit" << endl;
close(fds[i].fd); // 关闭文件描述符
fds[i].fd = DFL; // 去除数组中的文件描述符
fds[i].events = 0; // 事件清空
}
else
{
cout << "client[" << fds[i].fd << "]error" << endl;
close(fds[i].fd); // 关闭文件描述符
fds[i].fd = DFL; // 去除数组中的文件描述符
fds[i].events = 0; // 事件清空
}
}
}
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
usage(argv[0]);
exit(0);
}
// 创建监听套接字,完成绑定和监听
int listenSock = Sock::Socket();
Sock::Bind(listenSock, atoi(argv[1]));
Sock::Listen(listenSock);
for (int i = 0; i < MAXNUM; i++) // 初始化poll数组
{
fds[i].fd = DFL;
fds[i].events = 0;
fds[i].revents = 0;
}
fds[0].fd = listenSock; // 将0号设置为监听套接字
fds[0].events = POLLIN; // 关心读事件
while (true)
{
int n = poll(fds, MAXNUM, 1000);
switch (n)
{
case 0: // 没有事件就绪
/* code */
cout << "time out...: " << (unsigned long)time(nullptr) << endl;
break;
case -1: // 出错
cerr << "[" << errno << "]: " << strerror(errno) << endl;
break;
default:
// 有事件就绪,把位图传给处理函数
handleEvent();
cout << "handover" << endl;
break;
}
}
}
按照man手册的说法: 是为处理大批量句柄而作了改进的poll.
它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44)
它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法.
下面来到本篇的重中之重!!!!!!!!
epoll有3个相关系统调用接口
#include
int epoll_create(int size);
创建一个epoll模型
size:自从linux2.6.8之后, size参数是被忽略的 ,但此值必须大于0
返回值:调用成功返回一个文件描述符,调用失败返回-1,errno被设置
当此句柄不再使用,需要对此文件描述符close
#include
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
向epoll模型中 增/删/改 文件描述符和关心的事件
——用户告知内核需要关心哪些事件
epoll_create()
创造的epoll模型(返回的那个文件描述符)EPOLL_CTL_ADD
:向epfd
添加文件描述符,及其事件EPOLL_CTL_MOD
:对epfd
中某个fd
所关心的事件进行修改EPOLL_CTL_DEL
:将某个fd
从epfd
中删除#include
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
等待事件就绪
——内核告诉用户哪些事件已就绪
事件结构体 :
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 */
};
其中uint32_t events
是一个位图,可以添加的事件:
当我们的硬件有数据到来,会向CPU特定的针脚发送硬件中断 ,然后CPU会调用OS在启动是预设的中断函数(通过不同的中断编号调用中断向量表中不同中断函数),这个函数就负责将数据从外设拷贝的内核缓冲区中,假设这个函数是:
void kernel_copy(void* src,void* dst, callback_t func)
{
//进行拷贝
func();
}
调用方可以向这个拷贝函数提供回调函数,函数内部完成拷贝后,会自动调用这个回调函数
epoll会对特定的一个或多个fd设定一个属于epoll的回调函数,当fd缓冲区中有数据了,就会进行回调
这里我们要先提到epoll底层的两个数据结构:
epoll底层维护众多文件描述符及事件的数据结构是红黑树(为了加快查找)
红黑树的每个节点一定会存一个文件描述符和它需要关心的事件
struct rb_node { int fd; struct epoll_event *events; //... };
我们调用
epoll_ctl()
时,就会对红黑树的节点进行增删改
于此同时,epoll还要为我们维护一个数据结构:就绪队列
这个队列的每个节点保存了文件描述符和它就绪的那些事件
struct queue_node { int fd; struct epoll_event *revents; };
而那个回调函数就会完成如下任务:
获取就绪的fd
用fd从红黑树中找到对应节点,知道此fd关心什么事件
获取就绪的事件是什么
用fd和就绪事件构建queue_node节点
将节点放入就绪队列
让epoll_wait()
停止阻塞
我们把如上的红黑树、就绪队列、回调机制统称为epoll模型,将这一套模型进行打包即可交给文件系统的某个file_struct
,返回文件描述符即掌控着这个epoll模型,这就是epoll_create()
做到事
当我们调用epoll_ctl()
进行操作
EPOLL_CTL_ADD
:向红黑树添加节点EPOLL_CTL_MOD
:在红黑树中查找节点并修改eventsEPOLL_CTL_DEL
:在红黑树中查找节点并删除节点当底层数据就绪,epoll_wait()
被回调函数唤醒,它就只需要将就内核中绪队列的文件描述符和事件拷给用户区的参数,返回即可
然而同一个就绪队列,下层回调函数要放入节点,上层epoll_wait()
要拿取节点,两个线程访问临界空间就需要保证同步和互斥,所以这个就绪队列同时也是一个生产者消费者模型,当然,这些问题epoll已经给我们解决了
log.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
// 日志等级
#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3
const char *log_leval[] = {"DEBUG", "NOTICE", "WARINING", "FATAL"};
void logMssage(int level, const char *format, ...)
{
assert(level >= DEBUG);
assert(level <= FATAL);
const char *name = getenv("USER");
char logInfo[1024];
va_list ap;
va_start(ap, format); // 让dp对应到可变部分(...)
vsnprintf(logInfo, sizeof(logInfo) - 1, format, ap);
va_end(ap); // ap = NULL
printf("%s | %u | %s | %s\n", log_leval[level],
(unsigned)time(NULL), name == NULL ? "nukonw" : name,
logInfo);
}
epollServer.hpp
#include "Sock.hpp"
#include "log.hpp"
#include "memory"
#include
#include
class EpollServer
{
private:
static const int num = 256;
using func_t = function<int(int)>;
private:
int listenSock_; // 监听套接字
int epfd_; // epoll模型
uint16_t port_; // 服务器端口号
func_t func_;
public:
EpollServer(uint16_t port, func_t func)
: listenSock_(-1),
epfd_(-1),
port_(port),
func_(func) {}
void InitEpollServer()
{
// 创建listenSock
listenSock_ = Sock::Socket();
Sock::Bind(listenSock_, port_);
Sock::Listen(listenSock_);
logMssage(DEBUG, "创建监听套接字成功: %d", listenSock_);
// 创建epoll套接字
epfd_ = epoll_create(256);
if (epfd_ < 0)
{
logMssage(FATAL, "[%d]:%s", errno, strerror(errno));
exit(4);
}
logMssage(DEBUG, "创建epoll成功: %d", epfd_);
}
void Run()
{
// 将epoll添加到epoll中去
struct epoll_event event;
event.events = EPOLLIN;
event.data.fd = listenSock_;
int n = epoll_ctl(epfd_, EPOLL_CTL_ADD, listenSock_, &event);
assert(n == 0);
while (true)
{
epoll_event revs[num];
int n = epoll_wait(epfd_, revs, num, 1000);
// 由于事件是被一个挨一个从就绪队列放到数组中去的,
// 所以n表示就绪的文件描述符的个数,也表示需要处理的数组长度
switch (n)
{
case 0: // 没有事件就绪
/* code */
logMssage(DEBUG, "time out...: %d", (unsigned long)time(nullptr));
break;
case -1: // 出错
logMssage(FATAL, "[%d]:%s", errno, strerror(errno));
break;
default:
// 有事件就绪,把位图传给处理函数
handleEvent(revs, n);
break;
}
}
}
private:
void handleEvent(epoll_event revents[], int num)
{
for (int i = 0; i < num; i++) // 处理普通套接字的IO事件
{
int sock = revents[i].data.fd;
if (sock == listenSock_) // 处理监听套接字
{
string IP;
uint16_t port;
int sock = Sock::Accept(listenSock_, IP, port);
if (sock < 0)
{
logMssage(FATAL, "[%d]:%s", errno, strerror(errno));
continue;
}
struct epoll_event event;
event.events = EPOLLIN;
event.data.fd = sock;
int n = epoll_ctl(epfd_, EPOLL_CTL_ADD, sock, &event);
assert(n == 0);
logMssage(DEBUG, "%s: %d connected!", IP.c_str(), port);
}
else if (revents[i].events & EPOLLIN)
{
int s = func_(sock);
if (s <= 0)
{
int x = epoll_ctl(epfd_, EPOLL_CTL_DEL, sock, nullptr); // epoll无需再监管
close(sock); // 关闭文件描述符,注意必须先从epoll去除再close
assert(x == 0);
logMssage(DEBUG, "client[%d]quit", sock);
}
}
}
}
};
main.cc
#include "epollServer.hpp"
static void usage(const std::string porc)
{
std::cout << "Usage:\n\t" << porc << " port" << std::endl;
std::cout << "example:\n\t" << porc << " 8080" << std::endl;
}
int handleReadEvent(int sock)
{
// 进行read/recv
char buffer[1024];
ssize_t s = recv(sock, buffer, sizeof(buffer), 0);
if (s > 0)
{
buffer[s] = '\0';
cout << "client[" << sock << "]# " << buffer << endl;
}
return s;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
usage(argv[0]);
exit(0);
}
EpollServer epServer(atoi(argv[1]), handleReadEvent);
epServer.InitEpollServer();
epServer.Run();
}
epoll有两种工作方式:
假设这样一种情况:
我们把tcp socket交给epoll
此时对端发来2KB数据
epoll_wait处的等待被唤醒,开始执行read,拿取缓冲区中的数据
但是上层并不知道有多少数据过来,一次只拿取的1KB数据
然后又继续调用 epoll_wait…
此时不同的工作模式就是又不同的效果
如果设置了LT模式,或者就是默认的LT模式
由于缓冲区的数据没有拿完,下一次调用 epoll_wait
,此事件依然是就绪状态的
即,只要缓冲区中有数据,此事件永远会处于就绪状态
ET模式称为边缘触发,也就是只有缓冲区的数据有变化(从无到有,变得更多)时才会触发唤醒 epoll_wait
如果我们一次没有读完就直接调用 epoll_wait
,此事件并不会提示就绪, epoll_wait
也就不会返回
直到有新的数据发来,缓冲区的数据变多,此事件才会就绪
所以就倒逼程序员一次将缓冲区所有数据一次性读完
我们可以 试探性的循环对缓冲区读取,直到没有数据,read()
返回0
但是如果使用默认的阻塞式,最后一次读取会阻塞住,这对于一个多路转接的程序是致命的
所有,在ET模式下,所有的socket一定要设定为非阻塞式。
LT是 epoll 的默认行为. 使用 ET 能够减少 epoll 触发的次数. 但是代价就是强逼着程序猿一次响应就绪过程中就把所有的数据都处理完.
相当于一个文件描述符就绪之后, 不会反复被提示就绪, 看起来就比 LT 更高效一些. 但是在 LT 情况下如果也能做到每次就绪的文件描述符都立刻处理, 不让这个就绪被重复提示的话, 其实性能也是一样的.另一方面, ET 的代码复杂程度更高了
至于什么是Reactor模式,我们写完服务器代码,结合案例再总结
由于网络套接字和epoll的系统调用接口使用相对较为繁琐,所以我们用一种面向过程的方式对这些接口进行封装
Sock.hpp
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
class Sock
{
const static int gbacklog = 10;
public:
// 创建监听套接字
static int Socket()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
cerr << "[" << errno << "]: " << strerror(errno) << endl;
exit(1);
}
// 让服务器崩掉后立马可以重启此端口
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
return sockfd;
}
//绑定
static void Bind(int sockfd, uint16_t port)
{
sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_addr.s_addr = htons(INADDR_ANY);
local.sin_port = htons(port);
if (bind(sockfd, (sockaddr *)&local, sizeof(local)) < 0)
{
cerr << "[" << errno << "]: " << strerror(errno) << endl;
exit(2);
}
}
//监听
static void Listen(int sockfd)
{
if (listen(sockfd, gbacklog) < 0)
{
cerr << "[" << errno << "]: " << strerror(errno) << endl;
exit(3);
}
}
//接收消息
static int Accept(int listenSock, std::string &clientIp, uint16_t &clientPort)
{
struct sockaddr_in peer;
socklen_t size = sizeof(peer);
int serverSock = accept(listenSock, (sockaddr *)&peer, &size);
if (serverSock < 0)
{
return -1;
}
clientIp = inet_ntoa(peer.sin_addr);
clientPort = ntohs(peer.sin_port);
return serverSock;
}
};
Epoller.hpp
#pragma once
#include "log.hpp"
#include
#include
#include
#include
#include
#include
class Epoller
{
public:
// 创建epoll套接字
static int CreateEpoller()
{
int epfd = epoll_create(256);
if (epfd < 0)
{
logMssage(FATAL, "epoll_create [%d]:%s", errno, strerror(errno));
exit(4);
}
logMssage(DEBUG, "创建epoll成功: %d", epfd);
return epfd;
}
//向epoll添加一个事件
static bool AddEvent(int epfd, int sock, uint32_t event)
{
epoll_event ev;
ev.events = event;
ev.data.fd = sock;
int n = epoll_ctl(epfd, EPOLL_CTL_ADD, sock, &ev);
return n == 0;
}
//修改epoll中的事件
static bool ModEvent(int epfd, int sock, int event)
{
epoll_event ev;
ev.events = event;
ev.data.fd = sock;
int n = epoll_ctl(epfd, EPOLL_CTL_MOD, sock, &ev);
return n == 0;
}
//删除一个事件
static bool DelEvent(int epfd, int sock)
{
int n = epoll_ctl(epfd, EPOLL_CTL_DEL, sock, nullptr);
return n == 0;
}
//等待事件就绪
static int LoopOnce(int epfd, epoll_event revs[], int num)
{
int n = epoll_wait(epfd, revs, num, -1);
if (n == -1)
{
logMssage(FATAL, "epoll_wait [%d]:%s", errno, strerror(errno));
}
return n;
}
};
Proctocol.hpp
#pragma once
#include
#include
#include
static const char SEP = 'X';
static const int SEP_LEN = sizeof(SEP);
void PackageSplit(std::string &inbuffer, std::vector<std::string> &result)
{
while (true)
{
std::size_t pos = inbuffer.find(SEP);
if (pos == std::string::npos)
{
break;
}
result.push_back(inbuffer.substr(0, pos));
inbuffer.erase(0, pos + SEP_LEN);
}
}
log.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
// 日志等级
#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3
const char *log_leval[] = {"DEBUG", "NOTICE", "WARINING", "FATAL"};
void logMssage(int level, const char *format, ...)
{
assert(level >= DEBUG);
assert(level <= FATAL);
const char *name = getenv("USER");
char logInfo[1024];
va_list ap;
va_start(ap, format); // 让dp对应到可变部分(...)
vsnprintf(logInfo, sizeof(logInfo) - 1, format, ap);
va_end(ap); // ap = NULL
fprintf(stdout, "%s | %u | %s | %s\n", log_leval[level],
(unsigned)time(NULL), name == NULL ? "nukonw" : name,
logInfo);
}
Util.hpp
#pragma once
#include
#include
#include
#include
class Util
{
public:
//将文件描述符设置为非阻塞
static void SetNonBlock(int fd)
{
int fl = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
};
TcpServer.hpp
#pragma once
#include "Epoller.hpp"
#include "Proctocol.hpp"
#include "Sock.hpp"
#include "Util.hpp"
#include "log.hpp"
#include
#include
#include
#include
#include
#include
// 基于Reactor模式,编写一个充分读取和写入的epoll(ET)server
class TcpServer;
class Connection;
using func_t = std::function<int(Connection *)>;
using tcp_callback_t = std::function<int(Connection *, std::string &)>;
class Connection
{
public:
// 连接的服务套接字
int sock_;
// 当前连接的输入输出缓冲区
std::string inBuffer;
std::string outBuffer;
// 连接对应的处理读、写、错误的方法
func_t recver_;
func_t sender_;
func_t excepter_;
// 方便外部函数可以通过一个Connection对象回指到TcpServer
TcpServer *R_;
public:
Connection(int sock, TcpServer *r)
: sock_(sock),
R_(r) {}
// 设置读方法
void setRecver(func_t recver) { recver_ = recver; }
// 设置写方法
void setSender(func_t sender) { sender_ = sender; }
// 设置处理异常方法
void setExcepter(func_t excepter) { excepter_ = excepter; }
};
class TcpServer
{
private:
// 1.网络socket
int listenSock_;
// 2.epoll
int epfd_;
// 3.就绪事件列表
epoll_event *revs_ = nullptr;
static const int revs_num = 64;
// 4.每个文件描述符都能映射到自己的Connection对象
// 其中有这个文件描述符的读写缓冲区和处理事件方法
unordered_map<int, Connection *> connections_;
//处理事件的方法
tcp_callback_t cb_;
public:
TcpServer(int port, tcp_callback_t cb)
: cb_(cb)
{
// 网络套接字
listenSock_ = Sock::Socket();
Sock::Bind(listenSock_, port);
Sock::Listen(listenSock_);
// 多路转接
epfd_ = Epoller::CreateEpoller();
// 初始化就绪列表
revs_ = new epoll_event[revs_num];
// 将监听套接字添加到epoll和connections
AddConnection(listenSock_, EPOLLIN | EPOLLET,
std::bind(&TcpServer::Accepter, this, placeholders::_1));
}
void Run()
{
while (true)
{
Dispatcher();
}
}
~TcpServer()
{
if (listenSock_ != -1)
{
close(listenSock_);
}
if (epfd_ != -1)
{
close(epfd_);
}
if (revs_ != nullptr)
{
delete[] revs_;
}
}
private:
friend int HandleRquest(Connection *conn, std::string &messages);
void Dispatcher() // 事件派发
{
int n = Epoller::LoopOnce(epfd_, revs_, revs_num);
for (int i = 0; i < n; i++)
{
int sock = revs_[i].data.fd;
uint32_t revent = revs_[i].events;
// 确保这个sock已经被放到过connections_
auto conit = connections_.find(sock);
if (conit == connections_.end()) // 没找到
{
logMssage(WARINING, "sock[%d] is not set in connections_", sock);
continue;
}
// 通过connection类处理各种事件
Connection *conn = conit->second;
if (revent & EPOLLHUP || revent & EPOLLERR) // 对方断开连接,发生错误
{
// 把事件交给EPOLLIN | EPOLLOUT,recv,write可以进行判断
// recv,write可以再进行判断,检测到出错会调用excepter
revent |= EPOLLIN | EPOLLOUT;
}
if (revent & EPOLLIN)
{
if (conn->recver_) // 判断此方法是否已经被设置
{
conn->recver_(conn); // 调用读回调
}
}
if (revent & EPOLLOUT)
{
if (conn->sender_) // 写回调已被定义
{
conn->sender_(conn);
}
}
}
}
// 添加一个连接
void AddConnection(int sockfd, uint32_t event,
func_t recver = func_t(),
func_t sender = func_t(),
func_t excepter = func_t())
{
if (event & EPOLLET)
{
// 将新创建的文件描述符设置为非阻塞
Util::SetNonBlock(sockfd);
}
// 添加Sock到epoll
Epoller::AddEvent(epfd_, sockfd, event);
// 为此事件进创建并映射Connection
Connection *conn = new Connection(sockfd, this);
connections_[sockfd] = conn;
// 设置方法
conn->setRecver(recver);
conn->setSender(sender);
conn->setExcepter(excepter);
}
// 为listensock获取连接
int Accepter(Connection *conn)
{
// listensock也是工作在ET的,有可能来一批事件,所以要循环读取
while (true)
{
std::string clientIp;
uint16_t clientPort = 0;
int sockfd = Sock::Accept(conn->sock_, clientIp, clientPort);
if (sockfd < 0)
{
if (errno == EINTR) // 被信号中断
{
continue;
}
else if (errno == EAGAIN || errno == EWOULDBLOCK) // 读完了
{
break;
}
else // 出错了
{
logMssage(WARINING, "accept error");
return -1;
}
}
logMssage(DEBUG, "get a new link success[%d]", sockfd);
AddConnection(sockfd, EPOLLIN | EPOLLET,
std::bind(&TcpServer::TcpRecver, this, placeholders::_1),
std::bind(&TcpServer::TcpSender, this, placeholders::_1),
std::bind(&TcpServer::TcpExcepter, this, placeholders::_1));
}
return 0;
}
int TcpRecver(Connection *conn)
{
char buffer[1024];
while (true)
{
ssize_t s = recv(conn->sock_, buffer, 1024, 0);
if (s > 0)
{
buffer[s] = '\0';
conn->inBuffer += buffer;
// logMssage(DEBUG, "client[%d]# %s", conn->sock_, buffer);
}
else if (s == 0) // 对端退出
{
conn->excepter_(conn);
logMssage(DEBUG, "client[%d] quit", conn->sock_);
break;
}
else
{
if (errno == EAGAIN || errno == EWOULDBLOCK)
{
// 读完了
break;
}
else if (errno == EINTR)
{
// 被信号中断了,继续读
continue;
}
else
{
// 出错了
logMssage(DEBUG, "recv error[%d]:%s", errno, strerror(errno));
conn->excepter_(conn);
break;
}
}
// 已经将tcp缓冲区的所有的数据拿到了Connection的缓冲区
// 现在再将Connection中的数据按照协议拆分并一块块拿出
// 不符合完整协议的留在Connection中,等下一次发来数据再组合
std::vector<std::string> result;
PackageSplit(conn->inBuffer, result);
for (auto message : result)
{
cb_(conn, message);
}
}
return 0;
}
int TcpSender(Connection *conn)
{
// 写事件要求底层有空间
// 但是最开始的时候写空间一定时就绪的,所以最开始只关心读事件
// 写的时候
// 1. 对于LT模式,如果直接写,可能因为没有空间,然后write就阻塞了,
// 所以写之前一定要先打开对写事件的关心,如果此时底层有空间,epoll会自动进行写事件派发,然后才写入
// 2. 对于ET模式,因为要求是非阻塞式的套接字,也可以采用上面的方法,不过为了追求高效,一般直接发送,
// 如果发送失败,再打开写事件关心,下一次再继续将outbuffer中数据进行写入
// 注意:读事件只有需要的时候才打开
while (true)
{
size_t n = send(conn->sock_, conn->outBuffer.c_str(), conn->outBuffer.size(), 0);
if (n > 0) // 返回值:已经发送了的数据
{
// 从connection缓冲区去除已经发送的数据
conn->outBuffer.erase(0, n);
}
else
{
if (errno == EINTR)
continue;
else if (errno == EAGAIN || errno == EWOULDBLOCK)
{
// 可能是outbuffer没有数据了,传的size是0
// 也可能是内核缓冲区满了,但是outbuffer还有数据,就需要再打开写事件
break;
}
else
{
conn->excepter_(conn);
logMssage(WARINING, "send error[%d]:%s", errno, strerror(errno));
break;
}
}
if (conn->outBuffer.empty()) // 写完了
{
Epoller::EnableReadWrite(conn->sock_, epfd_, true, false);
}
else // 还有数据
{
Epoller::EnableReadWrite(conn->sock_, epfd_, true, true);
}
}
return 0;
}
int TcpExcepter(Connection *conn)
{
// 一定要先从epoll移除再关闭文件描述符
Epoller::DelEvent(epfd_, conn->sock_);
close(conn->sock_);
int sock = conn->sock_;
delete conn; // 释放connection对象
connections_.erase(sock); // 从map中删除
return 0;
}
static void EnableReadWrite(int sock, int epfd, bool readable, bool writeable)
{
int event = 0;
if (readable)
{
event |= EPOLLIN;
}
if (writeable)
{
event |= EPOLLOUT;
}
ModEvent(epfd, sock, event);
}
};
main.cc
#include "TcpServer.hpp"
static void usage(const std::string porc)
{
std::cout << "Usage:\n\t" << porc << " port" << std::endl;
std::cout << "example:\n\t" << porc << " 8080" << std::endl;
}
int HandleRquest(Connection *conn, std::string &messages)
{
std::cout << messages << endl;
// 处理业务逻辑
string sendstr = "业务处理完成\n";
conn->outBuffer += sendstr;
conn->sender_(conn);
return 0;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
usage(argv[0]);
exit(0);
}
TcpServer server(atoi(argv[1]), HandleRquest);
server.Run();
}
运行测试:
服务器端:
客户端:
class TcpServer
{
private:
// 1.网络socket
int listenSock_;
// 2.epoll
int epfd_;
// 3.就绪事件列表
epoll_event *revs_ = nullptr;
static const int revs_num = 64;
// 4.每个文件描述符都能映射到自己的Connection对象
// 其中有这个文件描述符的读写缓冲区和处理事件方法
unordered_map<int, Connection *> connections_;
};
作为一个Tcp网络服务器,首先要有一个监听套接字,从而与远端建立连接
我们使用epoll多路转接的方式,让我们的服务器能够同时为多个用户提供服务
同时为其维护一个就绪事件列表,当epoll检测到有事件就绪,就会把哪些就绪的事件放入此列表,我们也从个列表中拿取事件从而进行处理
我们将一个个事件或者说连接(如:与用户建立的连接、监听事件)描述为一个Connection
类
用一个map把他们组织起来,通过sock套接字能够很快索引到一个Connection(因为epoll就绪会提供事件的sock)
using func_t = std::function<int(Connection *)>;
class Connection
{
public:
// 连接的服务套接字
int sock_;
// 当前连接的输入输出缓冲区
std::string inBuffer;
std::string outBuffer;
// 连接对应的处理读、写、错误的方法
func_t recver_;
func_t sender_;
func_t excepter_;
// 方便外部函数可以通过一个Connection对象回指到TcpServer
TcpServer *R_;
};
epoll
检测到某种事件就绪,就会调用对应的方法,处理该事件,处理事件的时候往往还需要访问此Connection
的读写缓冲区,所以还需要在回调函数参数设定一个Connection*
的指针能够访问此Connection的输入输出缓冲区TcpServer
的一些代码逻辑,所以设定一个TcpServer *
成员变量进行回指,从而通过一个Connection,访问到TcpServer
在main函数中我们传入端口号,创建一个TcpServer
在TcpServer
的构造函数中会:
epoll
和connection集
在前面结构分析中,我们说每个要放入epoll的事件或者说连接都需要匹配一个Connection,通过这个Connection对事件进行处理
我们把
- 将事件放入epoll、
- 创建Connection、
- 设置处理事件的方法,
- 将Connection传入TcpServer的索引表
这一系列行为封装为一个函数
void AddConnection(int sockfd, uint32_t event, func_t recver = func_t(), func_t sender = func_t(), func_t excepter = func_t()) { if (event & EPOLLET) { // 将设置了边缘触发的文件描述符设置为非阻塞 Util::SetNonBlock(sockfd); } // 添加Sock到epoll Epoller::AddEvent(epfd_, sockfd, event); // 为此事件进创建并映射Connection Connection *conn = new Connection(sockfd, this); connections_[sockfd] = conn; // 设置方法 conn->setRecver(recver); conn->setSender(sender); conn->setExcepter(excepter); }
添加监听套接字时,
- sockfd即传入listensocket
- event传入EPOLLIN | EPOLLET,表示监听套接字只关心读事件并需要设置为边缘触发(我们当前的服务器都使用ET模式)
- 三个事件处理方法中,只需要传入读事件的即可
监听套接字的读事件定义:
int Accepter(Connection *conn)
{
// listensock也是工作在ET的,有可能来一批连接,所以要循环读取
while (true)
{
std::string clientIp;
uint16_t clientPort = 0;
int sockfd = Sock::Accept(conn->sock_, clientIp, clientPort);
if (sockfd < 0)
{
if (errno == EINTR) // 被信号中断
{
continue;
}
else if (errno == EAGAIN || errno == EWOULDBLOCK)//内核建立的连接获取完了
{
break;
}
else // 出错了
{
logMssage(WARINING, "accept error");
return -1;
}
}
logMssage(DEBUG, "get a new link success[%d]", sockfd);
AddConnection(sockfd, EPOLLIN | EPOLLET,
std::bind(&TcpServer::TcpRecver, this, placeholders::_1),
std::bind(&TcpServer::TcpSender, this, placeholders::_1),
std::bind(&TcpServer::TcpExcepter, this, placeholders::_1));
}
return 0;
}
首先我们调用Sock的accept,将内核建立好的连接拿上来,获取到服务套接字
在accept过程中
如果没有发生上述问题,就代表拿取连接成功了,只需要把服务套接字传给AddConnection()
,把新事件交给epoll,创建Connection,为此事件设置三种事件处理方式
服务sock的三种事件的处理方法定义:
//读事件处理
int TcpRecver(Connection *conn)
{
char buffer[1024];
while (true)
{
ssize_t s = recv(conn->sock_, buffer, 1024, 0);
if (s > 0)
{
buffer[s] = '\0';
conn->inBuffer += buffer;
// logMssage(DEBUG, "client[%d]# %s", conn->sock_, buffer);
}
else if (s == 0) // 对端退出
{
conn->excepter_(conn);
logMssage(DEBUG, "client[%d] quit", conn->sock_);
break;
}
else
{
if (errno == EAGAIN || errno == EWOULDBLOCK)
{
// 读完了
break;
}
else if (errno == EINTR)
{
// 被信号中断了,继续读
continue;
}
else
{
// 出错了
logMssage(DEBUG, "recv error[%d]:%s", errno, strerror(errno));
conn->excepter_(conn);
break;
}
}
// 已经将tcp缓冲区的所有的数据拿到了Connection的缓冲区
// 现在再将Connection中的数据按照协议拆分并一块块拿出
// 不符合完整协议的留在Connection中,等下一次发来数据再组合
std::vector<std::string> result;
PackageSplit(conn->inBuffer, result);
for (auto message : result)
{
cb_(conn, message);
}
}
return 0;
}
//写事件处理
int TcpSender(Connection *conn)
{
// 写事件要求底层有空间
// 但是最开始的时候写空间一定时就绪的,所以最开始只关心读事件
// 写的时候
// 1. 对于LT模式,如果直接写,可能因为没有空间,然后write就阻塞了,
// 所以写之前一定要先打开对写事件的关心,如果此时底层有空间,epoll会自动进行写事件派发,然后才写入
// 2. 对于ET模式,因为要求是非阻塞式的套接字,也可以采用上面的方法,不过为了追求高效,一般直接发送,
// 如果发送失败,再打开写事件关心,下一次再继续将outbuffer中数据进行写入
// 注意:读事件只有需要的时候才打开
while (true)
{
size_t n = send(conn->sock_, conn->outBuffer.c_str(), conn->outBuffer.size(), 0);
if (n > 0) // 返回值:已经发送了的数据
{
// 从connection缓冲区去除已经发送的数据
conn->outBuffer.erase(0, n);
}
else
{
if (errno == EINTR)
continue;
else if (errno == EAGAIN || errno == EWOULDBLOCK)
{
// 可能是outbuffer没有数据了,传的size是0
// 也可能是内核缓冲区满了,但是outbuffer还有数据,就需要再打开写事件
break;
}
else
{
conn->excepter_(conn);
logMssage(WARINING, "send error[%d]:%s", errno, strerror(errno));
break;
}
}
if (conn->outBuffer.empty()) // 写完了
{
Epoller::EnableReadWrite(conn->sock_, epfd_, true, false);
}
else // 还有数据
{
Epoller::EnableReadWrite(conn->sock_, epfd_, true, true);
}
}
return 0;
}
//异常事件处理
int TcpExcepter(Connection *conn)
{
// 一定要先从epoll移除再关闭文件描述符
Epoller::DelEvent(epfd_, conn->sock_);
close(conn->sock_);
int sock = conn->sock_;
delete conn; // 释放connection对象
connections_.erase(sock); // 从map中删除
return 0;
}
然后在main函数中继续调用server.Run();
,让服务开始运行
运行逻辑就是死循环调用Dispatcher()
进行事件派发
void Dispatcher() // 事件派发
{
int n = Epoller::LoopOnce(epfd_, revs_, revs_num);
for (int i = 0; i < n; i++)
{
int sock = revs_[i].data.fd;
uint32_t revent = revs_[i].events;
// 确保这个sock已经被放到过connections_
auto conit = connections_.find(sock);
if (conit == connections_.end()) // 没找到
{
logMssage(WARINING, "sock[%d] is not set in connections_", sock);
continue;
}
// 通过connection类处理各种事件
Connection *conn = conit->second;
if (revent & EPOLLHUP || revent & EPOLLERR) // 对方断开连接,发生错误
{
// 把事件交给EPOLLIN | EPOLLOUT,recv,write可以进行判断
// recv,write可以再进行判断,检测到出错会调用excepter
revent |= EPOLLIN | EPOLLOUT;
}
if (revent & EPOLLIN)
{
if (conn->recver_) // 判断此方法是否已经被设置
{
conn->recver_(conn); // 调用读回调
}
}
if (revent & EPOLLOUT)
{
if (conn->sender_) // 写回调已被定义
{
conn->sender_(conn);
}
}
}
}
当第一次执行Dispatcher()
:
首先会阻塞在epoll_wait()
,因为此时epoll中只有一个监听事件,所以此时就是在等待有人发起连接
一旦操作系统底层与远端进行完三次握手,完成连接,底层代码便会将listensock
的读事件放到就绪列表,然后唤醒epoll_wait()
的阻塞
当我们遍历就绪列表,发现是listenSock的读事件就绪了,首先会拿这个文件描述符到connections_
索引到监听套接字对应的Connection
然后调用当初再此Connection
中设置的的读事件处理方法:Accepter()
在Accepter()
中会循环调用sock::accept()
,把底层所有的建立连接都拿取出来,同时将它们放入epoll,并创建Connection,在TcpServer的connections_创建从sock到Connection*的映射
此后再执行Dispatcher()
就会有监听套接字和服务套接字两种事件出现,不过我们并不需要主动进行区分,因为不同种类的不同事件的处理方法,在创建事件之除就在Connection中写好了
如果是监听套接字就绪,就会自动调用上述逻辑,如果是服务套接字,又需要对三种事件分别进行处理:
读事件处理会调用*TcpRecver()
*
inBuffer
缓冲区inBuffer
缓冲区拿出,如果无法组成完整报文,就留在当前的inBuffer
缓冲区中,等待下一次再接收到数据,追加到缓冲区后使报文补全HandleRquest()
*)HandleRquest()
*通过传入的消息处理完业务逻辑后,将结果写入Connection的写缓冲区,再调用Connection中的写方法,将处理的结果发回给客户端写事件的处理会调用*TcpSender()
*方法
对于epoll来说,读事件和写事件实际有所差异
写事件要求底层有空间
但是最开始的时候写空间一定时就绪的,所以最开始只关心读事件
写的时候
1. 对于LT模式,如果直接写,可能因为没有空间,然后write就阻塞了,
所以写之前一定要先打开对写事件的关心,如果此时底层有空间,epoll会自动进行写事件派发,然后才写入
2. 对于ET模式,因为要求是非阻塞式的套接字,也可以采用上面的方法,不过为了追求高效,一般直接发送,
如果发送失败,再打开写事件关心,下一次再继续将outbuffer中数据进行写入
注意:读事件只有需要的时候才打开
所以我们把缓冲区中的数据循环对sock进行写入,
写入成功则继续写入,
写入失败则有三种情况
当写入循环结束,
异常事件的处理会调用*TcpExcepter()
*
当一个连接出现异常,我们直接将其关闭即可,相当于把创建连接的过程逆向执行一遍
想我们上面的服务器
基于多路转接方案,当事件就绪的时候,采用回调的方式,进行业务处理的模式就称为反应堆模式(Reactor)
我们代码中的TcpServer就是一个反应堆,
其中一个个Connection对象就称为事件
每一个事件中都有
反应堆中有一个事件派发函数,当epoll中的某个事件就绪,事件派发函数回调用此事件的回调函数
特性: