目录
一、五种IO模型
1、阻塞IO
2、非阻塞IO
3、信号驱动
4、IO多路转接
5、异步IO
6、总结
二、高级IO重要概念
1、同步通信与异步通信
2、阻塞 vs 非阻塞
三、非阻塞IO
1、fcntl
2、实现函数SetNoBlock
四、IO多路转接select
1、select
1.1、参数解释
1.2、参数timeout取值
1.3、select函数返回值
1.4、关于fd_set结构
1.5、常见的程序片段
2、selectserver
2.1、 select执行过程
2.2、socket就绪条件
2.2.1、读就绪
2.2.2、写就绪
2.3、实现代码
2.4、select的缺点
五、IO多路转接poll
1、poll函数接口
2、poll的特点
2.1、优点
2.2、缺点
3、pollserver
六、IO多路转接epoll
1、epoll
1.1、epoll_create
1.2、epoll_ctl
1.3、epoll_wait
2、epoll原理总结
3、epoll的优点
4、epollserver 基础版本
5、epoll的工作方式
5.1、水平触发Level Triggered 工作模式
5.2、边缘触发Edge Triggered工作模式
5.3、对比LT和ET
6、epollserver ET工作方式
7、Reactor
IO = 等待资源 + 拷贝资源。
等待资源:等待要拷贝的数据、拷贝数据需要的空间。当资源就绪后,该状态称为IO事件就绪。在 TCP协议中,有一个状态标志位 PSH,本质上就是把资源设置为就绪状态。
阻塞IO:在内核将数据准备好之前,系统调用会一直等待。所有的套接字,默认都是阻塞方式。
阻塞IO是最常见的IO模型:
非阻塞IO:如果内核还未将数据准备好,系统调用仍然会直接返回,并且返回EWOULDBLOCK错误码。
非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符,这个过程称为轮询。这对CPU来说是较大的浪费,一般只有特定场景下才使用。
信号驱动IO:内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作。
IO多路转接:虽然从流程图上看起来和阻塞IO类似。实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态。
异步IO:由内核在数据拷贝完成时,通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)。
任何IO过程中,都包含两个步骤。第一是等待,第二是拷贝。而且在实际的应用场景中,等待消耗的时间往往都远远高于拷贝的时间。让IO更高效,最核心的办法就是让等待的时间尽量少。
同步和异步关注的是消息通信机制:
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。
一个文件描述符,默认都是阻塞IO。
#include
#include
int fcntl(int fd, int cmd, ... /* arg */ );
传入的cmd的值不同,后面追加的参数也不相同
fcntl函数有5种功能:
我们此处只是用第三种功能,获取/设置文件状态标记,就可以将一个文件描述符设置为非阻塞。
基于fcntl,我们实现一个SetNoBlock函数,将文件描述符设置为非阻塞。
void SetNonBlock(int fd)
{
int fl = fcntl(fd, F_GETFD); //获取指定文件的状态标志位
if(fl < 0)
{
std::cerr << "error string : " << strerror(error) << "error code: " << error << std::endl;
return;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
如果我们将文件设置为非阻塞状态,那么一旦底层没有数据就绪,就会以出错的形式返回,但是不算真正的出错,错误码会被设置为 11 ,代表临时资源没有就绪。
void SetNonBlock(int fd)
{
int fl = fcntl(fd, F_GETFD); //获取指定文件的状态标志位
if(fl < 0)
{
std::cerr << "error string : " << strerror(errno) << " error code: " << errno << std::endl;
return;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
int main()
{
char buffer[64];
SetNonBlock(0);
while(1)
{
printf(">>> ");
fflush(stdout);
ssize_t n = read(0, buffer, sizeof(buffer) - 1);
if(n > 0)
{
buffer[n - 1] = 0;
std::cout << "echo# " << buffer << std::endl;
}
else if(n == 0)
{
std::cout << "end file" << std::endl;
break;
}
else
{
if(errno == EAGAIN || errno == EWOULDBLOCK)
{
//因为底层数据没有准备好,希望下次继续来检查
std::cout << "data not really" << std::endl;
sleep(1);
continue;
}
else if(errno == EINTR)
{
//这次IO被信号中断了,也需要重新读取
continue;
}
else
{
//再下面才是真正的读取失败了
std::cout << "read error" << " error string : " << strerror(errno) << " error code: " << errno << std::endl;
break;
}
}
}
}
系统提供select函数来实现多路复用输入/输出模型。
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
错误值可能为:
其实这个结构就是一个整数数组,更严格的说,是一个 "位图"。使用位图中对应的位来表示要监视的文件描述符。这个位图的上限在Linux系统中是 1024 。
提供了一组操作 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 函数中的 fd_set 类型的参数都是输入输出型参数。
用户首先通过 fd_set 类型参数输入,告诉内核哪些 fd 需要内核关心。内核通过这些 fd_set 类型参数输出,告诉用户哪些 fd 的对应事件已经就绪了。
fs_set readset;
FD_SET(fd,&readset);
select(fd+1,&readset,NULL,NULL,NULL);
if(FD_ISSET(fd,readset)){……}
理解select模型的关键在于理解fd_set。
为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
select服务器在使用的时候,需要程序员自己维护一个第三方数组,对已经获取的sock做管理。
typedef struct FdEvent
{
int fd;
uint8_t event;
std::string clientip;
uint16_t clientport;
} type_t;
type_t _fdarray[N];
具体服务器代码如下:
#pragma once
#include
#include
#include
#include
#include "Sock.hpp"
#include "log.hpp"
const static int gport = 8888;
#define READ_EVENT (0x1 << 0)
#define WRITE_EVENT (0x1 << 1)
#define EXCEPT_EVENT (0x1 << 2)
typedef struct FdEvent
{
int fd;
uint8_t event;
std::string clientip;
uint16_t clientport;
} type_t;
// typedef int type_t;
// static const int defaultfd = -1;
static const int defaultevent = 0;
class SelectServer
{
static const int N = (sizeof(fd_set) * 8);
public:
SelectServer(uint16_t port = gport)
: _port(port)
{
}
void InitServer()
{
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
for (int i = 0; i < N; i++)
{
_fdarray[i].fd = defaultfd;
_fdarray[i].event = defaultevent;
_fdarray[i].clientport = 0;
}
}
void Accepter()
{
std::cout << "有一个新连接" << std::endl;
// 这里进行Accept就不会再被阻塞了
std::string clientip;
uint16_t clientport;
int sock = _listensock.Accept(&clientip, &clientport);
if (sock < 0)
return;
// 不能直接读,因为不知道sock有没有数据就绪
// 需要将sock交给select,让select进行管理。
logMessage(DEBUG, "[%s:%d], sock: %d", clientip.c_str(), clientport, sock);
// 只要把获得的sock添加到_fdarray数组里就可以了
int pos = 1;
for (; pos < N; pos++)
{
if (_fdarray[pos].fd == defaultfd)
break;
}
if (pos >= N)
{
// 数组满了
close(sock);
logMessage(WARNING, "sockfd array[] full");
}
else
{
_fdarray[pos].fd = sock;
_fdarray[pos].event = READ_EVENT;
_fdarray[pos].clientip = clientip;
_fdarray[pos].clientport = clientport;
}
}
void Recver(int index)
{
// serverIO();
int fd = _fdarray[index].fd;
char buffer[1024];
ssize_t s = recv(fd, buffer, sizeof(buffer) - 1, 0); // 这里读取不会被阻塞
if (s > 0)
{
buffer[s - 1] = 0;
std::cout << _fdarray[index].clientip << " : " << _fdarray[index].clientport << " : " << buffer << std::endl;
// 把数据发送回去也要被select管理
std::string echo = buffer;
echo += "[select server echo]";
send(fd, echo.c_str(), echo.size(), 0);
}
else
{
if (s == 0)
logMessage(INFO, "client quit, _fdarray[i] -> defaultfd: %d->%d", fd, defaultfd);
else
logMessage(WARNING, "recv error, _fdarray[i] -> defaultfd: %d->%d", fd, defaultfd);
close(_fdarray[index].fd);
_fdarray[index].fd = defaultfd;
_fdarray[index].event = defaultevent;
_fdarray[index].clientip.resize(0);
_fdarray[index].clientport = 0;
}
}
void HandlerEvent(fd_set &rfds, fd_set &wfds)
{
for (int i = 0; i < N; i++)
{
if (_fdarray[i].fd == defaultfd)
continue;
if ((_fdarray[i].event & READ_EVENT) && (FD_ISSET(_fdarray[i].fd, &rfds)))
{
// 处理读取,1.accept 2、recv
if (_fdarray[i].fd == _listensock.Fd())
{
Accepter();
}
else if (_fdarray[i].fd != _listensock.Fd())
{
Recver(i);
}
else
{}
}
else if((_fdarray[i].event & WRITE_EVENT) && (FD_ISSET(_fdarray[i].fd, &wfds)))
{
}
else
{}
}
}
void Start()
{
// 在网络中,新连接到来被当作读事件就绪。
//_listensock.Accept(); 不能直接进行accept,因为如果没有链接到来,程序会被阻塞住
// 此时,服务端只有一个文件描述符
// 这种写法不正确,因为直接把rfds写死了,应该是动态变化的
// struct timeval timeout = {0, 0};
// fd_set rfds;
// FD_ZERO(&rfds);
// FD_SET(_listensock.Fd(), &rfds);
_fdarray[0].fd = _listensock.Fd();
_fdarray[0].event = READ_EVENT;
while (1)
{
// 因为rfds是一个输入输出行参数,注定了每次都要对rfds进行重置。
// 重置就必须要知道历史上有哪些fd
// 因为服务器在运行中,sockfd的值一直在动态变化,所以maxfd也在一直变化
fd_set rfds;
fd_set wfds;
FD_ZERO(&rfds);
FD_ZERO(&wfds);
int maxfd = _fdarray[0].fd;
for (int i = 0; i < N; i++)
{
if (_fdarray[i].fd == defaultfd)
continue;
// 合法fd
if (_fdarray[i].event & READ_EVENT)
FD_SET(_fdarray[i].fd, &rfds);
if (_fdarray[i].event & WRITE_EVENT)
FD_SET(_fdarray[i].fd, &wfds);
if (maxfd < _fdarray[i].fd)
maxfd = _fdarray[i].fd;
}
int n = select(maxfd + 1, &rfds, &wfds, nullptr, nullptr);
switch (n)
{
case 0:
logMessage(DEBUG, "time out, %d: %s", errno, strerror(errno));
break;
case -1:
logMessage(WARNING, "%d: %s", errno, strerror(errno));
break;
default:
logMessage(DEBUG, "有一个就绪事件发生了 : %d", n);
HandlerEvent(rfds, wfds);
DebugPrint();
break;
}
sleep(1);
}
}
void DebugPrint()
{
std::cout << "fdarray[]: ";
for (int i = 0; i < N; i++)
{
if (_fdarray[i].fd == defaultfd)
continue;
std::cout << _fdarray[i].fd << " ";
}
std::cout << "\n";
}
~SelectServer()
{
_listensock.Close();
}
private:
uint16_t _port;
Sock _listensock;
type_t _fdarray[N];
};
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 */
};
参数说明:
events和revents的取值:
返回结果:
不同于 select 使用三个位图来表示三个 fdset 的方式, poll 使用一个 pollfd 的指针实现。
poll中监听的文件描述符数目增多时:
实现代码:
#pragma once
#include
#include
#include
#include
#include "Sock.hpp"
#include "log.hpp"
const static int gport = 8888;
static const int N = 4096;
const static short defaultevent = 0;
typedef struct pollfd type_t;
// static const int defaultfd = -1;
class PollServer
{
public:
PollServer(uint16_t port = gport)
: _port(port), _fdarray(nullptr)
{
}
void InitServer()
{
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
_fdarray = new type_t[N];
for (int i = 0; i < N; i++)
{
_fdarray[i].fd = defaultfd;
_fdarray[i].events = defaultevent;
_fdarray[i].revents = defaultevent;
}
}
void Accepter()
{
std::cout << "有一个新连接" << std::endl;
// 这里进行Accept就不会再被阻塞了
std::string clientip;
uint16_t clientport;
int sock = _listensock.Accept(&clientip, &clientport);
if (sock < 0)
return;
// 不能直接读,因为不知道sock有没有数据就绪
// 需要将sock交给select,让select进行管理。
logMessage(DEBUG, "[%s:%d], sock: %d", clientip.c_str(), clientport, sock);
// 只要把获得的sock添加到_fdarray数组里就可以了
int pos = 1;
for (; pos < N; pos++)
{
if (_fdarray[pos].fd == defaultfd)
break;
}
if (pos >= N)
{
// 数组满了,可以进行动态扩容
close(sock);
logMessage(WARNING, "sockfd array[] full");
}
else
{
_fdarray[pos].fd = sock;
_fdarray[pos].events = POLLIN; //POLLIN | POLLOUT
_fdarray[pos].revents = defaultevent;
}
}
void HandlerEvent()
{
for (int i = 0; i < N; i++)
{
int fd = _fdarray[i].fd;
short revent = _fdarray[i].revents;
if (fd == defaultfd)
continue;
if ((fd == _listensock.Fd()) && (revent & POLLIN))
{
Accepter();
}
else if ((fd != _listensock.Fd()) && (revent & POLLIN))
{
// serverIO();
char buffer[1024];
ssize_t s = recv(fd, buffer, sizeof(buffer) - 1, 0); // 这里读取不会被阻塞
if (s > 0)
{
buffer[s - 1] = 0;
std::cout << "client# " << buffer << std::endl;
// 把数据发送回去也要被select管理
//向event里添加写事件
_fdarray[i].events = POLLIN | POLLOUT;
std::string echo = buffer;
echo += "[select server echo]";
send(fd, echo.c_str(), echo.size(), 0);
}
else
{
if (s == 0)
logMessage(INFO, "client quit, _fdarray[i] -> defaultfd: %d->%d", fd, defaultfd);
else
logMessage(WARNING, "recv error, _fdarray[i] -> defaultfd: %d->%d", fd, defaultfd);
close(fd);
_fdarray[i].fd = defaultfd;
_fdarray[i].events = defaultevent;
_fdarray[i].revents = defaultevent;
}
}
}
}
void Start()
{
// 在网络中,新连接到来被当作读事件就绪。
//_listensock.Accept(); 不能直接进行accept,因为如果没有链接到来,程序会被阻塞住
// 此时,服务端只有一个文件描述符
// 这种写法不正确,因为直接把rfds写死了,应该是动态变化的
// struct timeval timeout = {0, 0};
// fd_set rfds;
// FD_ZERO(&rfds);
// FD_SET(_listensock.Fd(), &rfds);
_fdarray[0].fd = _listensock.Fd();
_fdarray[0].events = POLLIN;
while (1)
{
// 因为rfds是一个输入输出行参数,注定了每次都要对rfds进行重置。
// 重置就必须要知道历史上有哪些fd
// 因为服务器在运行中,sockfd的值一直在动态变化,所以maxfd也在一直变化
int timeout = -1; //设为-1,表示阻塞式调用
int n = poll(_fdarray, N, timeout); //可以对_fdarrat内容进行管理,合法的fd、event全部放入_fdarray的最左侧。
switch (n)
{
case 0:
logMessage(DEBUG, "time out, %d : %s", errno, strerror(errno));
break;
case -1:
logMessage(WARNING, "%d: %s", errno, strerror(errno));
break;
default:
logMessage(DEBUG, "有一个就绪事件发生了 : %d", n);
HandlerEvent();
DebugPrint();
break;
}
// sleep(1);
}
}
void DebugPrint()
{
std::cout << "fdarray[]: ";
for (int i = 0; i < N; i++)
{
if (_fdarray[i].fd == defaultfd)
continue;
std::cout << _fdarray[i].fd << " ";
}
std::cout << "\n";
}
~PollServer()
{
_listensock.Close();
if(_fdarray)
delete[] _fdarray;
}
private:
uint16_t _port;
Sock _listensock;
type_t *_fdarray;
};
epoll 是为处理大批量句柄而作了改进的 poll 。
它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44)。它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。
epoll 有3个相关的系统调用:
int epoll_create(int size);
创建一个epoll的句柄:
调用 epll_create 函数后,会在OS内核中创建一个红黑树。红黑树节点管理的数据类型是 struct epoll_event ,里面包含用户让OS管理的文件描述符以及对应的事件。
创建红黑树的同时,也会创建一个就绪队列。
函数调用成功后,返回文件描述符,该文件中保存一个eventpoll结构体,结构体中就存放红黑树的根节点与就绪队列的头节点。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll_ctl 函数的本质,是通过epoll模型对红黑树做操作,向红黑树中新增、删除或修改某一个节点。即对红黑树进行增删改。
epoll_ctl函数除了对红黑树进行增删改外,还要给节点结构体里文件描述符所指向的file结构体注册回调机制。
file结构体中本来就有一个回调函数指针,并在拷贝操作完成后自动调用。只不过一般这个指针都被设置为NULL,现在给改结构体注册一个回调函数,并把函数指针指向这个函数。
回调函数的功能:在文件中数据拷贝完成之后,把本文件描述符对应的红黑树节点添加到就绪队列中。
epoll的事件注册函数:
第二个参数的取值:
struct epoll_event 结构如下:
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
OS 会把已经就绪的文件描述符对应的红黑树节点中的epoll_event结构体也添加到到就绪队列里(此时这个节点及隶属于红黑树,也隶属于队列)。
epoll_wait 负责以时间复杂度为O(1)的方式,检测有没有事件就绪,即检测队列是否为空。
epoll_wait收集在epoll监控的事件中已经发送的事件:
当某一进程调用epoll_create方法时, Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关。
truct eventpoll{
....
/*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
struct rb_root rbr;
/*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
struct list_head rdlist;
....
};
struct epitem{
struct rb_node rbn;//红黑树节点
struct list_head rdllink;//双向链表节点
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向其所属的eventpoll对象
struct epoll_event event; //期待发生的事件类型
}
总结一下, epoll的使用过程就是三部曲:
实现代码:
//Epoller.hpp
#pragma once
#include
#include
#include
#include
#include
#include "log.hpp"
#include "error.hpp"
static const int defaultepfd = -1;
static const int gsize = 128;
class Epoller
{
public:
Epoller()
:_epfd(defaultepfd)
{
}
void Create()
{
_epfd = epoll_create(gsize);
if(_epfd < 0)
{
logMessage(FATAL, "listen error, code: %d, errstring: %s", errno, strerror(errno));
exit(EPOLL_CREATE_ERR);
}
}
//向epoll中添加事件
bool AddEvent(int fd, uint32_t events)
{
struct epoll_event ev;
ev.events = events;
ev.data.fd = fd; //用户数据,epoll底层不对该数据做任何处理,就是为了给未来返回准备的
int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, fd, &ev);
if(n < 0)
{
logMessage(FATAL, "epoll_ctl error, code: %d, errstring: %s", errno, strerror(errno));
return false;
}
return true;
}
bool DelEvent(int fd)
{
//epoll在操作的时候,fd必须得合法
return epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, nullptr) == 0;
}
int Wait(struct epoll_event* revs, int num, int timeout)
{
return epoll_wait(_epfd, revs, num, timeout);
}
int Fd()
{
return _epfd;
}
void Close()
{
if(_epfd != defaultepfd)
close(_epfd);
}
~Epoller()
{}
private:
int _epfd;
};
//Epollserver.hpp
#pragma once
#include
#include
#include
#include
#include "Sock.hpp"
#include "log.hpp"
#include "error.h"
#include "Epoller.hpp"
const static int gport = 8888;
using func_t = std::function;
class EpollServer
{
const static int gnum = 64;
public:
EpollServer(func_t func, uint16_t port = gport)
:_func(func)
,_port(port)
{
}
void InitServer()
{
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
_epoller.Create();
logMessage(DEBUG, "initserver success");
}
void Start()
{
// 1. 将listensock添加到epoll中
bool r = _epoller.AddEvent(_listensock.Fd(), EPOLLIN);
assert(r);
(void)r;
int timeout = 1000;
while (1)
{
int n = _epoller.Wait(_revs, gnum, timeout);
switch (n)
{
case 0:
logMessage(DEBUG, "timeout");
break;
case -1:
logMessage(WARNING, "epoll_wait filed");
break;
default:
// n就是就绪事件的个数
logMessage(DEBUG, "有%d个事件就绪了", n);
HandlerEvents(n);
break;
}
}
}
void HandlerEvents(int num)
{
for (int i = 0; i < num; i++)
{
int fd = _revs[i].data.fd;
uint32_t events = _revs[i].events;
if (events & EPOLLIN)
{
// 读事件就绪
if (fd == _listensock.Fd())
{
// 1.新连接事件到来
// logMessage(DEBUG, "get a new line ... ");
std::string clientip;
uint16_t clientport;
int sock = _listensock.Accept(&clientip, &clientport);
if (sock < 0)
continue;
logMessage(DEBUG, "%s:%d 已经连接上了服务器", clientip.c_str(), clientport);
// 1.1.此时,不能直接recv/read,因为需要使用多路转接
bool r = _epoller.AddEvent(sock, EPOLLIN);
assert(r);
(void)r;
}
else
{
// 2.读取事件
char request[1024];
ssize_t s = recv(fd, request, sizeof(request) - 1, 0); // 这里读取不会被阻塞
if (s > 0)
{
request[s - 1] = 0; // \r\n的形式为结尾的
request[s - 2] = 0;
std::string response = _func(request);
send(fd, response.c_str(), response.size(), 0);
}
else
{
if (s == 0)
logMessage(INFO, "client quit... ");
else
logMessage(WARNING, "recv error ...");
//在处理异常的时候,先移除,再关闭
_epoller.DelEvent(fd);
close(fd);
}
}
}
}
}
~EpollServer()
{
_listensock.Close();
_epoller.Close();
}
private:
uint16_t _port;
Sock _listensock;
Epoller _epoller;
struct epoll_event _revs[gnum];
func_t _func;
};
因为TCP协议中,数据是以字节流的方式发送读取的,完整报文由应用层协议规定。所以我们直接读取是没有办法保证读取到完整的报文的。
为了解决这个问题,我们就需要自定义应用层协议,并通过回调函数来处理数据,得到完整报文。
在我们使用 select、poll、epoll的时候,在最基本的情况下,一旦有事件就绪,如果上层不取, 底层就会一直通知用户事件已经就绪。这种工作方式为LT(水平触发)工作模式。
epoll默认状态下就是LT工作模式。
如果我们在第1步将socket添加到epoll描述符的时候使用了EPOLLET标志,epoll进入ET工作模式。
ET强逼程序员尽快取走所有的数据,本质上是让TCP底层更新出更大的接收窗口,从而在较大概率上,提供对方的滑块窗口的大小,提高发送效率。
//Epoll.hpp
#pragma once
#include
#include
#include
#include
#include
#include "log.hpp"
#include "error.hpp"
static const int defaultepfd = -1;
static const int gsize = 128;
class Epoller
{
public:
Epoller()
:_epfd(defaultepfd)
{
}
void Create()
{
_epfd = epoll_create(gsize);
if(_epfd < 0)
{
logMessage(FATAL, "listen error, code: %d, errstring: %s", errno, strerror(errno));
exit(EPOLL_CREATE_ERR);
}
}
//合并添加和修改
bool AddModEvent(int fd, uint32_t events, int op)
{
struct epoll_event ev;
ev.events = events;
ev.data.fd = fd; //用户数据,epoll底层不对该数据做任何处理,就是为了给未来返回准备的
int n = epoll_ctl(_epfd, op, fd, &ev);
if(n < 0)
{
logMessage(FATAL, "epoll_ctl error, code: %d, errstring: %s", errno, strerror(errno));
return false;
}
return true;
}
bool DelEvent(int fd)
{
//epoll在操作的时候,fd必须得合法
return epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, nullptr) == 0;
}
int Wait(struct epoll_event* revs, int num, int timeout)
{
return epoll_wait(_epfd, revs, num, timeout);
}
int Fd()
{
return _epfd;
}
void Close()
{
if(_epfd != defaultepfd)
close(_epfd);
}
~Epoller()
{}
private:
int _epfd;
};
//EpollServer.hpp
#pragma once
#include
#include
#include
#include
#include
#include "Sock.hpp"
#include "log.hpp"
#include "error.h"
#include "Epoller.hpp"
#include "util.hpp"
#include "Protocol.hpp"
using namespace Protocol_ns;
const static int gport = 8888;
const static int bsize = 1024;
class Connection;
class EpollServer;
using func_t = std::function;
using callback_t = std::function;
// 大号的结构体
class Connection
{
public:
Connection(const int &fd, const std::string &clientip, const uint16_t &clientport)
: _fd(fd), _clientip(clientip), _clientport(clientport)
{
}
void Register(callback_t recver, callback_t sender, callback_t excepter)
{
_recver = recver;
_sender = sender;
_excepter = excepter;
}
~Connection()
{
}
public:
// IO信息
int _fd;
std::string _inbuffer;
std::string _outbuffer;
// IO处理函数
callback_t _recver;
callback_t _sender;
callback_t _excepter;
// 用户信息, only debug
std::string _clientip;
uint16_t _clientport;
// 也可以给conn带上自己要关心的事件
uint32_t events;
// 回指指针
EpollServer *R;
};
class EpollServer
{
const static int gnum = 64;
public:
EpollServer(func_t func, uint16_t port = gport) : _func(func), _port(port)
{
}
void InitServer()
{
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
_epoller.Create();
AddConnection(_listensock.Fd(), EPOLLIN | EPOLLET);
logMessage(DEBUG, "init server success");
}
// 事件派发器
int Dispatcher() // 名字要改
{
int timeout = -1;
while (true)
{
LoopOnce(timeout);
}
}
void LoopOnce(int timeout)
{
int n = _epoller.Wait(_revs, gnum, timeout);
for (int i = 0; i < n; i++)
{
int fd = _revs[i].data.fd;
uint32_t events = _revs[i].events;
if ((events & EPOLLERR) || (events & EPOLLHUP))
//_connections[fd]->_excepter(_connections[fd]);
events |= (EPOLLIN | EPOLLOUT);
//这一步是将所有的异常情况,最后都转为recv, send的异常。
if ((events & EPOLLIN) && ConnIsExists(fd)) //判断fd还存不存在,因为有可能在处理异常的时候把fd关掉了,下面再使用就会报错
_connections[fd]->_recver(_connections[fd]);
if ((events & EPOLLOUT) && ConnIsExists(fd))
_connections[fd]->_sender(_connections[fd]);
}
}
void AddConnection(int fd, uint32_t events, const std::string &ip = "127.0.0.1", uint16_t port = gport)
{
// 设置fd是非阻塞
if (events & EPOLLET)
Util::SetNonBlock(fd);
// 为listensock创建对应的connect对象
Connection *conn = new Connection(fd, ip, port);
if (fd == _listensock.Fd())
{
conn->Register(std::bind(&EpollServer::Accepter, this, std::placeholders::_1), nullptr, nullptr);
}
else
{
conn->Register(std::bind(&EpollServer::Recver, this, std::placeholders::_1),
std::bind(&EpollServer::Sender, this, std::placeholders::_1),
std::bind(&EpollServer::Excepter, this, std::placeholders::_1));
}
// 把events赋值给conn对象,以便在下面进行处理数据的时候,判断是ET的还是LT的
conn->events = events;
conn->R = this;
// 将listensock,connection对象添加到connections中
_connections.insert(std::pair(fd, conn));
// 添加事件
bool r = _epoller.AddModEvent(fd, events, EPOLL_CTL_ADD);
assert(r);
(void)r;
logMessage(DEBUG, "addConnection success, fd: %d, clientinfo: [%s:%d]", fd, ip.c_str(), port);
}
bool EnableReadWrite(Connection* conn, bool readable, bool writeable)
{
uint32_t events = EPOLLET;
events |= (readable ? EPOLLIN : 0);
events |= (writeable ? EPOLLOUT : 0);
conn->events = events;
return _epoller.AddModEvent(conn->_fd, conn->events, EPOLL_CTL_MOD);
}
// 连接管理器
void Accepter(Connection *conn)
{
// 1.新连接事件到来
// logMessage(DEBUG, "get a new line ... ");
// 可能有多个连接同时到来,因此需要循环读取,保证每一个连接都被读取到了
do
{
int err = 0; // 用作Accept的输出型参数,如果监听失败了,用于返回accept函数的错误码
std::string clientip;
uint16_t clientport;
int sock = _listensock.Accept(&clientip, &clientport, &err);
if (sock > 0)
{
logMessage(DEBUG, "%s:%d 已经连接上了服务器", clientip.c_str(), clientport);
AddConnection(sock, EPOLLIN | EPOLLET, clientip, clientport);
}
else
{
if(err == EAGAIN || err == EWOULDBLOCK) //说明读完了,新数据没来
break;
//下面的都没读完
else if(err == EINTR)
continue;
else
{
logMessage(WARNING, "errstring: %s, errcode: %d", strerror(err), err);
continue;
}
}
} while (conn->events & EPOLLET);
}
void Recver(Connection *conn)
{
// 读取完毕本轮数据!
do
{
char buffer[bsize];
ssize_t n = recv(conn->_fd, buffer, sizeof(buffer) - 1, 0);
if (n > 0)
{
buffer[n] = 0;
conn->_inbuffer += buffer;
// 根据基本协议,进行数据分析 -- 自己定过一个!
std::string requestStr;
int n = Protocol_ns::ParsePackage(conn->_inbuffer, &requestStr);
if(n > 0)
{
requestStr = RemoveHeader(requestStr, n);
Request req;
req.Deserialize(requestStr);
_func(conn, req); // request 保证是一个完整的请求报文!
}
// logMessage(Debug, "inbuffer: %s, [%d]", conn->inbuffer_.c_str(), conn->fd_);
}
else if (n == 0)
{
conn->_excepter(conn);
}
else
{
if (errno == EAGAIN || errno == EWOULDBLOCK)
break;
else if (errno == EINTR)
continue;
else
conn->_excepter(conn);
}
} while (conn->events & EPOLLET);
}
void Sender(Connection *conn)
{
do
{
ssize_t n = send(conn->_fd, conn->_outbuffer.c_str(), conn->_outbuffer.size(), 0);
if(n > 0) //发送成功,n是这次发送,发送了多少数据
{
conn->_outbuffer.erase(0, n); //移除已经发送了的数据。
std::cout << "you can see me" << std::endl;
if(conn->_outbuffer.empty()) //如果数据已经发送完了,就把写关心去掉
{
EnableReadWrite(conn, true, false);
break;
}
else
{
EnableReadWrite(conn, true, true);
}
}
else
{
if(errno == EAGAIN || errno == EWOULDBLOCK)
break;
else if(errno == EINTR)
continue;
else
{
conn->_excepter(conn);
break;
}
}
}while(conn->events & EPOLLET);
}
void Excepter(Connection *conn)
{
logMessage(DEBUG, "Excepter..., fd: %d, clientinfo: [%s:%d]", conn->_fd, conn->_clientip.c_str(), conn->_clientport);
}
bool ConnIsExists(int fd)
{
return _connections.find(fd) != _connections.end();
}
~EpollServer()
{
_listensock.Close();
_epoller.Close();
}
private:
uint16_t _port;
Sock _listensock;
Epoller _epoller;
struct epoll_event _revs[gnum];
func_t _func;
std::unordered_map _connections;
};
//main.cc
#include "EpollServer.hpp"
#include
Response calculaterHelper(const Request& req)
{
Response resp(0, 0);
switch(req._op)
{
case '+':
resp._result = req._x + req._y;
break;
case '-':
resp._result = req._x - req._y;
break;
case '*':
resp._result = req._x * req._y;
break;
case '/':
if(req._y == 0)
resp._code = 1;
else
resp._result = req._x / req._y;
break;
case '%':
if(req._y == 0)
resp._code = 2;
else
resp._result = req._x % req._y;
break;
default:
resp._code = 3;
break;
}
return resp;
}
void Calculate(Connection* conn, const Request& req)
{
Response resp = calculaterHelper(req);
std::string sendStr;
resp.Serialize(&sendStr);
sendStr = Protocol_ns::AddHeader(sendStr);
//发送
//在epoll中,关于fd的读取,一般要常设置(一直要让epoll关心)
//对于fd的写入,一般是按需设置(不能常设值),只有需要发的时候,才设置
//V1
conn->_outbuffer += sendStr;
//开启对写事件的关心
conn->R->EnableReadWrite(conn, true, true); //一般初次设置对写事件的关心,对应的fd会立刻触发一次就绪(因为发送buffer一定是空的)
}
int main()
{
std::unique_ptr svr(new EpollServer(Calculate));
svr->InitServer();
svr->Dispatcher();
return 0;
}
Reactor是基于多路转接,包含事件派发器、连接管理器等的半同步,半异步的IO服务器。
半同步,半异步体现在,Reactor可以只负责事件派发,这是同步的,而数据读写、业务处理交给上层的线程池来处理,这是异步的。
由epoll进行驱动,并进行事件派发,这种服务器就是Reactor服务器。
Reactor模式翻译过来是反应堆模式,他就像一个反应堆,连接接了很多connection对象,由epoll进行管理,如果那个connction对象就绪了,epoll就会通知上层,进行处理。
几个重要的注意事项:
关于连接管理,在客户端进行服务器连接时,服务器要把所有的客户端连接都管理起来,这无疑占用了服务器很多资源。由于可能由很多客户端仅仅与服务器建立了连接,却没有与服务器进行信息交互,因此服务器为了节省资源需要定期清理这些超时的客户端。
为了实现这个功能,在连接类中增加了一个时间变量lasttime,用于记录最后一次读取的时间,每次进行读取时更新这个时间。在事件派发器中进行检测工作,判断客户端最后一次访问的时间并进行与超时时间对比。
一个服务器中,多数客户端剩余的连接时间都是不同的,所以把epoll的等待时间timeout设置为所有客户端中剩余时间最短的那一个就好了。这个逻辑可使用最小堆来实现。
Reactor服务器:
//Epoll.hpp
#pragma once
#include
#include
#include
#include
#include
#include "log.hpp"
#include "error.hpp"
static const int defaultepfd = -1;
static const int gsize = 128;
class Epoller
{
public:
Epoller()
:_epfd(defaultepfd)
{
}
void Create()
{
_epfd = epoll_create(gsize);
if(_epfd < 0)
{
logMessage(FATAL, "listen error, code: %d, errstring: %s", errno, strerror(errno));
exit(EPOLL_CREATE_ERR);
}
}
//合并添加和修改
bool AddModEvent(int fd, uint32_t events, int op)
{
struct epoll_event ev;
ev.events = events;
ev.data.fd = fd; //用户数据,epoll底层不对该数据做任何处理,就是为了给未来返回准备的
int n = epoll_ctl(_epfd, op, fd, &ev);
if(n < 0)
{
logMessage(FATAL, "epoll_ctl error, code: %d, errstring: %s", errno, strerror(errno));
return false;
}
return true;
}
bool DelEvent(int fd)
{
//epoll在操作的时候,fd必须得合法
return epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, nullptr) == 0;
}
int Wait(struct epoll_event* revs, int num, int timeout)
{
return epoll_wait(_epfd, revs, num, timeout);
}
int Fd()
{
return _epfd;
}
void Close()
{
if(_epfd != defaultepfd)
close(_epfd);
}
~Epoller()
{}
private:
int _epfd;
};
//EpollServer.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include "Sock.hpp"
#include "log.hpp"
#include "error.h"
#include "Epoller.hpp"
#include "util.hpp"
#include "Protocol.hpp"
using namespace Protocol_ns;
const static int gport = 8888;
const static int bsize = 1024;
const static int linkTimeOut = 30; //连接保持时间
class Connection;
class EpollServer;
using func_t = std::function;
using callback_t = std::function;
// 大号的结构体
class Connection
{
public:
Connection(const int &fd, const std::string &clientip, const uint16_t &clientport)
: _fd(fd), _clientip(clientip), _clientport(clientport)
{
}
void Register(callback_t recver, callback_t sender, callback_t excepter)
{
_recver = recver;
_sender = sender;
_excepter = excepter;
}
~Connection()
{
}
public:
// IO信息
int _fd;
std::string _inbuffer;
std::string _outbuffer;
// IO处理函数
callback_t _recver;
callback_t _sender;
callback_t _excepter;
// 用户信息, only debug
std::string _clientip;
uint16_t _clientport;
// 也可以给conn带上自己要关心的事件
uint32_t events;
// 回指指针
EpollServer *R;
//时间戳
time_t lasttime; //该connection最近一次就绪的时间
};
class EpollServer
{
const static int gnum = 64;
public:
EpollServer(func_t func, uint16_t port = gport) : _func(func), _port(port)
{
}
void InitServer()
{
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
_epoller.Create();
AddConnection(_listensock.Fd(), EPOLLIN | EPOLLET);
logMessage(DEBUG, "init server success");
}
// 事件派发器
int Dispatcher()
{
int timeout = 1000;
while (true)
{
LoopOnce(timeout);
checkLink();
}
}
void LoopOnce(int timeout)
{
int n = _epoller.Wait(_revs, gnum, timeout);
for (int i = 0; i < n; i++)
{
int fd = _revs[i].data.fd;
uint32_t events = _revs[i].events;
if ((events & EPOLLERR) || (events & EPOLLHUP))
//_connections[fd]->_excepter(_connections[fd]);
events |= (EPOLLIN | EPOLLOUT);
// 这一步是将所有的异常情况,最后都转为recv, send的异常。
if ((events & EPOLLIN) && ConnIsExists(fd)) // 判断fd还存不存在,因为有可能在处理异常的时候把fd关掉了,下面再使用就会报错
_connections[fd]->_recver(_connections[fd]);
if ((events & EPOLLOUT) && ConnIsExists(fd))
_connections[fd]->_sender(_connections[fd]);
}
}
void AddConnection(int fd, uint32_t events, const std::string &ip = "127.0.0.1", uint16_t port = gport)
{
// 设置fd是非阻塞
if (events & EPOLLET)
Util::SetNonBlock(fd);
// 为listensock创建对应的connect对象
Connection *conn = new Connection(fd, ip, port);
if (fd == _listensock.Fd())
{
conn->Register(std::bind(&EpollServer::Accepter, this, std::placeholders::_1), nullptr, nullptr);
}
else
{
conn->Register(std::bind(&EpollServer::Recver, this, std::placeholders::_1),
std::bind(&EpollServer::Sender, this, std::placeholders::_1),
std::bind(&EpollServer::Excepter, this, std::placeholders::_1));
}
// 把events赋值给conn对象,以便在下面进行处理数据的时候,判断是ET的还是LT的
conn->events = events;
conn->R = this;
conn->lasttime = time(nullptr);
// 将listensock,connection对象添加到connections中
_connections.insert(std::pair(fd, conn));
// 添加事件
bool r = _epoller.AddModEvent(fd, events, EPOLL_CTL_ADD);
assert(r);
(void)r;
logMessage(DEBUG, "addConnection success, fd: %d, clientinfo: [%s:%d]", fd, ip.c_str(), port);
}
// 在多路转接中,对于任何描述符,读要常设值,写要按需设置
bool EnableReadWrite(Connection *conn, bool readable, bool writeable)
{
uint32_t events = EPOLLET;
events |= (readable ? EPOLLIN : 0);
events |= (writeable ? EPOLLOUT : 0);
conn->events = events;
return _epoller.AddModEvent(conn->_fd, conn->events, EPOLL_CTL_MOD);
}
// 连接管理器
void Accepter(Connection *conn)
{
// 1.新连接事件到来
// logMessage(DEBUG, "get a new line ... ");
// 可能有多个连接同时到来,因此需要循环读取,保证每一个连接都被读取到了
do
{
int err = 0; // 用作Accept的输出型参数,如果监听失败了,用于返回accept函数的错误码
std::string clientip;
uint16_t clientport;
int sock = _listensock.Accept(&clientip, &clientport, &err);
if (sock > 0)
{
logMessage(DEBUG, "%s:%d 已经连接上了服务器", clientip.c_str(), clientport);
AddConnection(sock, EPOLLIN | EPOLLET, clientip, clientport);
}
else
{
if (err == EAGAIN || err == EWOULDBLOCK) // 说明读完了,新数据没来
break;
// 下面的都没读完
else if (err == EINTR)
continue;
else
{
logMessage(WARNING, "errstring: %s, errcode: %d", strerror(err), err);
continue;
}
}
} while (conn->events & EPOLLET);
}
void HandlerRequest(Connection *conn)
{
int quit = false;
while (!quit)
{
std::string requestStr;
// 1.提取完整报文
int n = Protocol_ns::ParsePackage(conn->_inbuffer, &requestStr);
if (n > 0)
{
// 2.提取有效载荷
requestStr = RemoveHeader(requestStr, n);
// 3.进行反序列化
Request req;
req.Deserialize(requestStr);
// 4.进行业务处理
Response resp = _func(req); // request 保证是一个完整的请求报文!
// 5.序列化
std::string RespStr;
resp.Serialize(&RespStr);
// 6.添加报头
RespStr = AddHeader(RespStr);
// 7.进行返回
conn->_outbuffer += RespStr;
}
else
quit = true;
}
}
bool RecverHelper(Connection *conn)
{
int ret = true;
conn->lasttime = time(nullptr); //更新conn最近访问的时间
// 读取完毕本轮数据!
do
{
char buffer[bsize];
ssize_t n = recv(conn->_fd, buffer, sizeof(buffer) - 1, 0);
if (n > 0)
{
buffer[n] = 0;
conn->_inbuffer += buffer;
// 根据基本协议,进行数据分析 -- 自己定过一个!
// 可以边读取边分析,也可以读完之后,一起分析。
// logMessage(Debug, "inbuffer: %s, [%d]", conn->inbuffer_.c_str(), conn->fd_);
}
else if (n == 0)
{
conn->_excepter(conn);
ret = false;
break;
}
else
{
if (errno == EAGAIN || errno == EWOULDBLOCK)
break;
else if (errno == EINTR)
continue;
else
{
conn->_excepter(conn);
ret = false;
break;
}
}
} while (conn->events & EPOLLET);
return ret;
}
void Recver(Connection *conn)
{
// 读取数据
if(!RecverHelper(conn))
return;
// 分析数据
HandlerRequest(conn);
// 一般在进行写入时,直接写入,没写完才交给epoll
if (!conn->_outbuffer.empty())
{
conn->_sender(conn);
}
}
void Sender(Connection *conn)
{
bool safe = true;
do
{
ssize_t n = send(conn->_fd, conn->_outbuffer.c_str(), conn->_outbuffer.size(), 0);
if (n > 0) // 发送成功,n是这次发送,发送了多少数据
{
conn->_outbuffer.erase(0, n); // 移除已经发送了的数据。
// 如果发送缓冲区已经为空,直接break;
if (conn->_outbuffer.empty())
break;
}
else
{
// 把对方接收缓冲区写满了,退出
if (errno == EAGAIN || errno == EWOULDBLOCK)
{
break;
}
else if (errno == EINTR)
continue;
else
{
safe = false;
conn->_excepter(conn);
break;
}
}
} while (conn->events & EPOLLET);
if (!safe)
return;
// 如果对方接收缓冲区已经被写满了满了,写不下了,并且发送缓冲区中还有数据
if (!conn->_outbuffer.empty())
{
// 设置写关心,交给epoll管理
EnableReadWrite(conn, true, true);
}
else
{
EnableReadWrite(conn, true, false);
}
}
//这个函数要防止重复调用,因此在上面很多函数中都添加了bool值进行判断
void Excepter(Connection *conn)
{
// 1.先从epoll中移除fd
_epoller.DelEvent(conn->_fd);
// 2.移除unordered_map中的KV关系
_connections.erase(conn->_fd);
// 3.关闭fd
close(conn->_fd);
// 4.将connction对象释放
delete conn;
logMessage(DEBUG, "Excepter...done, fd: %d, clientinfo: [%s:%d]", conn->_fd, conn->_clientip.c_str(), conn->_clientport);
}
bool ConnIsExists(int fd)
{
return _connections.find(fd) != _connections.end();
}
//检查是否超时,断开连接
void checkLink()
{
time_t curr = time(nullptr);
for(auto& connection : _connections)
{
if(connection.second->lasttime + linkTimeOut < curr) //如果最后一次访问的时间 + 超时时间 > 当前时间,就要断开连接了。
Excepter(connection.second);
else
continue;
}
}
~EpollServer()
{
_listensock.Close();
_epoller.Close();
}
private:
uint16_t _port;
Sock _listensock;
Epoller _epoller;
struct epoll_event _revs[gnum];
func_t _func;
std::unordered_map _connections;
};
//main.cc
#include "EpollServer.hpp"
#include
Response Calculater(const Request& req)
{
Response resp(0, 0);
switch(req._op)
{
case '+':
resp._result = req._x + req._y;
break;
case '-':
resp._result = req._x - req._y;
break;
case '*':
resp._result = req._x * req._y;
break;
case '/':
if(req._y == 0)
resp._code = 1;
else
resp._result = req._x / req._y;
break;
case '%':
if(req._y == 0)
resp._code = 2;
else
resp._result = req._x % req._y;
break;
default:
resp._code = 3;
break;
}
return resp;
}
int main()
{
std::unique_ptr svr(new EpollServer(Calculater));
svr->InitServer();
svr->Dispatcher();
return 0;
}
上面的Reactor服务器也可以实现成多线程版本。
在设计多线程服务器时,最需要防备的是多个线程同时对同一个文件描述符进行读写,这样会使数据混乱。
因此在使用多线程进行Reactor设计时,有一个原则:一个fd以及connection,一定只能有一个线程来进行管理。