目录
一、Reactor模式
1.1 Reactor模式定义
1.2 Reactor模式的角色构成
1.3 Reactor模式的工作流程
二、epoll ET服务器(Reactor模式)
2.1 设计思路
2.2 Connection结构
2.3 TcpServer类
2.3.1 AddConnection函数
2.3.2 Dispatcher函数(初始分发器)
2.3.3 EnableReadWrite函数
2.4 回调函数
2.4.1 Accepter
2.4.2 Recver
2.4.3 Sender
2.4.4 Excepter
2.5 Socket套接字
2.6 服务器测试
三、总结
Reactor反应器模式,也被称为分发者模式或通知者模式,是一种将就绪事件派发给对应服务处理程序的事件设计模式
epoll ET服务器
当epoll ET服务器监测到某一事件就绪后,就会将该事件交给对应的服务处理程序进行处理
Reactor模式的五个角色
在这个epoll ET服务器中,Reactor模式中的五个角色对应如下:
Dispatcher函数的工作即为:调用epoll_wait函数等待事件发生,有事件发生后将就绪的事件派发给对应的服务处理程序即可
Connection类
在Reactor的工作流程中说到,在注册事件处理器时需要将其与Handle关联,本质上就是需要将读回调、写回调和异常回调与某个文件描述符关联起来。这样做的目的就是为了当某个文件描述符上的事件就绪时可以找到其对应的各种回调函数,进而执行对应的回调方法来处理该事件。可以设计一个Connection类,该类中的成员包括了一个文件描述符,以及该文件描述符对应的各种回调函数,以及其他成员
TcpServer类
对此可以设计一个Reactor类
epoll ET服务器的工作流程
Connection结构中除了包含文件描述符和其对应的读回调、写回调和异常回调外,还包含一个输入缓冲区_inBuffer、一个输出缓冲区_outBuffer以及一个回指指针_svrPtr
Connection结构中需提供一个管理回调的成员函数,便于外部对回调进行设置
class Connection
{
public:
Connection(int sock = -1):_socketFd(sock),_svrPtr(nullptr) {}
~Connection() {}
public:
void SetCallBack(func_t recvCb, func_t sendCb, func_t exceptCb) {
_recvCb = recvCb;
_sendCb = sendCb;
_exceptCb = exceptCb;
}
public:
int _socketFd;
func_t _recvCb;
func_t _sendCb;
func_t _exceptCb;
string _inBuffer;//无法处理二进制流
string _outBuffer;
TcpServer* _svrPtr;
};
在TcpServer类中有一个unordered_map成员,用于建立文件描述符和与其对应的Connection结构之间的映射,还有一个_epoll成员,该成员是封装的Epoll对象。在初始化TcpServer对象时就可以调用封装的EpollCreate函数创建Epoll对象,并将该epoll模型对应的文件描述符记录在该对象的成员变量_epollFd中,便于后续使用。TcpServer对象析构时,Epoll对象的析构会自动调用close函数将epoll模型关闭
封装Epoll类
#pragma once
#include
#include
class Epoll
{
public:
Epoll() {}
~Epoll() { if(_epollFd > 0) close(_epollFd); }
public:
void EpollCreate() {
_epollFd = epoll_create(128);
if(_epollFd < 0) exit(5);
}
bool AddSockToEpoll(int socket, uint32_t event)
{
struct epoll_event ev;
ev.events = event;
ev.data.fd = socket;
int n = epoll_ctl(_epollFd, EPOLL_CTL_ADD, socket, &ev);
return n == 0;
}
bool EpollCtrl(int socket, uint32_t event)
{
event |= EPOLLET;
struct epoll_event ev;
ev.events = event;
ev.data.fd = socket;
int n = epoll_ctl(_epollFd, EPOLL_CTL_MOD, socket, &ev);
return n == 0;
}
int EpollWait(struct epoll_event* revs, int revsNum) {
return epoll_wait(_epollFd, revs, revsNum, 5000);
}
bool DelFromEpoll(int socket)
{
int n = epoll_ctl(_epollFd, EPOLL_CTL_DEL, socket, 0);
return n == 0;
}
private:
int _epollFd;
};
TcpServer类部分代码
class TcpServer
{
public:
TcpServer(uint16_t port = 8080, int revsNum = 128):_port(port), _revsNum(revsNum)
{
//创建listenSocket
_listenSocketFd = Socket::SocketCreate();
Socket::Bind(_listenSocketFd, _port);
Socket::Listen(_listenSocketFd);
//创建多路转接对象
_epoll.EpollCreate();
}
~TcpServer() {
if(_listenSocketFd >= 0) close(_listenSocketFd);
}
private:
int _listenSocketFd;
uint16_t _port;
unordered_map _connections;//管理服务器链接
Epoll _epoll;
struct epoll_event* _revs;//获取就绪事件的缓冲区
int _revsNum;//缓冲区大小
callback_t _cb;//上层业务处理
};
TcpServer类中的AddConnection函数用于进行事件注册
在注册事件时需要传入一个文件描述符和三个回调函数,表示当该文件描述符上的事件(默认只关心读事件)就绪后应该执行的回调方法。
在AddConnection函数内部要做的就是,设置套接字为非阻塞(ET模型要求),将套接字和回调函数等属性封装为一个Connection,在将套接字添加到epoll模型中,对象建立文件描述符和Connection的映射关系并管理
void AddConnection(int socket, func_t reavCb, func_t sendCb, func_t exceptCb) //将套接字封装为链接并添加至服务器的管理中
{
//设置套接字为非阻塞
Socket::SetNonBlock(socket);
//将套接字封装为链接,设置链接的各个属性
Connection* con = new Connection(socket);
con->SetCallBack(reavCb, sendCb, exceptCb);//监听套接字只需读取回调函数
con->_svrPtr = this;
//添加套接字到epoll中
_epoll.AddSockToEpoll(socket, EPOLLIN | EPOLLET);//一般多路转接服务器默认监视读事件,其他事件按需设置
//对应的链接添加到映射表中管理
_connections.insert(make_pair(socket, con));
}
TcpServer中的Dispatcher函数即初始分发器,其要做的就是调用epoll_wait函数等待事件发生。当某个文件描述符上的事件发生后,先通过unordered_map找到该文件描述符对应的Connection结构,然后调用Connection结构中对应的回调函数对该事件进行处理即可
class TcpServer
{
public:
void LoopOnce() {
int number = _epoll.EpollWait(_revs, _revsNum);
for(int i = 0; i < number; ++i) {
int socket = _revs[i].data.fd;
uint32_t revent = _revs[i].events;
//将所有异常交给read和write处理
if(revent & EPOLLERR) revent |= (EPOLLIN | EPOLLOUT);
if(revent & EPOLLHUP) revent |= (EPOLLIN | EPOLLOUT);
if(revent & EPOLLIN) {
if((_connections.find(socket) != _connections.end()) && (_connections[socket]->_recvCb != nullptr)) {//存在且回调不为空
_connections[socket]->_recvCb(_connections[socket]);
}
}
if(revent & EPOLLOUT) {
if((_connections.find(socket) != _connections.end()) && (_connections[socket]->_sendCb != nullptr)) {
_connections[socket]->_sendCb(_connections[socket]);
}
}
}
}
void Dispatcher(callback_t cb)//根据就绪事件,进行特定事件的派发
{
_cb = cb;
while(true)
{
LoopOnce();
}
}
private:
int _listenSocketFd;
uint16_t _port;
unordered_map _connections;//管理服务器链接
Epoll _epoll;
struct epoll_event* _revs;//获取就绪事件的缓冲区
int _revsNum;//缓冲区大小
callback_t _cb;//上层业务处理
};
TcpServer类中的EnableReadWrite函数,用于使能某个文件描述符的读写事件
void EnableReadWrite(Connection* con ,bool readable, bool writable)
{
uint32_t event = (readable ? EPOLLIN : 0) | (writable ? EPOLLOUT : 0);
bool ret = _epoll.EpollCtrl(con->_socketFd, event);
assert(ret);
}
为某个文件描述符创建Connection结构时,可以调用Connection类提供的SetCallBack函数,将这些回调函数添加到Connection结构中
Accepter回调用于处理连接事件,其工作流程如下:
下一次Dispatcher在进行事件派发时就会关注该套接字对应的事件,当事件就绪时就会执行该套接字对应的Connection结构中对应的回调方法
void Accepter(Connection* con)
{
while(true)
{
string clientIp;
uint16_t clientPort;
int acceptErrno = 0;
int socket = Socket::Accept(con->_socketFd, &clientIp, &clientPort, &acceptErrno);
if(socket < 0)
{
if(acceptErrno == EAGAIN || acceptErrno == EWOULDBLOCK) break;//底层已无链接
else if(acceptErrno == EINTR) continue;//信号中断
else {//读取失败
LogMessage(WARNING, "Accept error, %d : %s", acceptErrno, strerror(acceptErrno));
break;
}
}
AddConnection(socket, bind(&TcpServer::Recver, this, std::placeholders::_1), \
bind(&TcpServer::Sender, this, std::placeholders::_1), bind(&TcpServer::Excepter, this, std::placeholders::_1));
LogMessage(DEBUG, "Accept client [%s : %d] success, socket: %d", clientIp.c_str(), clientPort, socket);
}
}
本博客实现的ET模式下的epoll服务器,因此在获取底层连接时需要循环调用accept函数进行读取,并且监听套接字必须设置为非阻塞
设置非阻塞
设置文件描述符为非阻塞时,需先调用fcntl函数获取该文件描述符对应的文件状态标记,然后在该文件状态标记的基础上添加非阻塞标记O_NONBLOCK,最后调用fcntl函数对该文件描述符的状态标记进行设置即可
static bool SetNonBlock(int socket) {
int fl = fcntl(socket, F_GETFL);
if(fl < 0) return false;
fcntl(socket, F_SETFL, fl | O_NONBLOCK);
return true;
}
监听套接字设置为非阻塞后,当底层连接不就绪时,accept函数会以出错的形式返回,因此当调用accept函数的返回值小于0时,需继续判断错误码
accept、recv和send等IO系统调用为什么会被信号中断?
IO系统调用函数出错返回并且将错误码设置为EINTR,表明本次在进行数据读取或数据写入之前被信号中断了,即IO系统调用在陷入内核,但并没有返回用户态的时候内核去处理其他信号
写事件按需打开
Accepter获取上来的套接字在添加到Dispatcher中时,只添加了EOPLLIN和EPOLLET事件,即只让epoll关心该套接字的读事件
之所以没有添加写事件,是因为并没有要发送的数据,因此没有必要让epoll关心写事件。一般读事件是会被设置的,而写事件则是按需打开的,只当有数据要发送时才会将写事件打开,并且在数据全部写入完毕后又会立即将写事件关闭
recver回调用于处理读事件,其工作流程如下:
void Recver(Connection* con)
{
bool error = false;
while(true)
{
char buffer[1024];
ssize_t num = recv(con->_socketFd, buffer, sizeof(buffer) - 1, 0);
if(num < 0)
{
if(errno == EAGAIN || errno == EWOULDBLOCK) break;
else if(errno == EINTR) continue;
else {
LogMessage(WARNING, "recv error %d : %s", errno, strerror(errno));
error = true;
con->_exceptCb(con);
break;
}
}
else if(num == 0) {
LogMessage(DEBUG, "client[%d] quit, serve close %d", con->_socketFd, con->_socketFd);
error = true;
con->_exceptCb(con);
break;
}
else {
buffer[num] = '\0';
con->_inBuffer += buffer;//放入链接的输入缓冲区中
}
}
LogMessage(DEBUG, "socket: %d , con->_inBuffer: %s", con->_socketFd, (con->_inBuffer).c_str());
if(!error)//无错
{
vector messages;
SpliteMessage(con->_inBuffer, &messages);
for(auto& msg : messages) _cb(con, msg);
}
}
报文切割
报文切割本质就是为了防止粘包问题,而粘包问题还涉及到协议定制
void SpliteMessage(string &buffer, vector *out)
{
while (true)
{
size_t pos = buffer.find(SEP);
if (pos == string::npos)
break;
string message = buffer.substr(0, pos);
buffer.erase(0, pos + SEP_LENGTH);
out->push_back(message);
}
}
业务处理函数
下一次Dispatcher在进行事件派发时就会关注该套接字的写事件,当写事件就绪时就会执行该套接字对应的Connection结构中写回调方法,进而将_outBuffer中的响应数据发送给客户端
void NetCal(Connection* con, string& request)
{
LogMessage(DEBUG, "NetCal been call, Get request: %s", request.c_str());
//反序列化
Request req;
if(!req.Deserialized(request)) return;
//业务处理
Response resp = calculator(req);
//构建应答
string sendstr = resp.Serialize();
sendstr = Encode(sendstr);
//递交
con->_outBuffer += sendstr;
//"提醒"TCP服务器处理
con->_svrPtr->EnableReadWrite(con, true, true);
}
协议定制
string Encode(string &s) {
return s + SEP;
}
class Request
{
public:
string Serialize()
{
std::string str;
str = std::to_string(x_);
str += SPACE;
str += op_; // TODO
str += SPACE;
str += std::to_string(y_);
return str;
}
bool Deserialized(const std::string &str)
{
std::size_t left = str.find(SPACE);
if (left == std::string::npos)
return false;
std::size_t right = str.rfind(SPACE);
if (right == std::string::npos)
return false;
x_ = atoi(str.substr(0, left).c_str());
y_ = atoi(str.substr(right + SPACE_LEN).c_str());
if (left + SPACE_LEN > str.size())
return false;
else
op_ = str[left + SPACE_LEN];
return true;
}
public:
Request() {}
Request(int x, int y, char op) : x_(x), y_(y), op_(op) {}
~Request() {}
public:
int x_;
int y_;
char op_; // '+' '-' '*' '/' '%'
};
class Response
{
public:
string Serialize()
{
string s;
s = std::to_string(code_);
s += SPACE;
s += std::to_string(result_);
return s;
}
bool Deserialized(const string &s)
{
size_t pos = s.find(SPACE);
if (pos == string::npos)
return false;
code_ = atoi(s.substr(0, pos).c_str());
result_ = atoi(s.substr(pos + SPACE_LEN).c_str());
return true;
}
public:
Response() {}
Response(int result, int code) : result_(result), code_(code) {}
~Response() {}
public:
int result_; // 计算结果
int code_; // 计算结果的状态码
};
void Sender(Connection* con)
{
while(true)
{
ssize_t size = send(con->_socketFd, con->_outBuffer.c_str(), con->_outBuffer.size(), 0);
if(size > 0) {
con->_outBuffer.erase(0,size);
if(con->_outBuffer.empty()) break;
}
else {
if(errno == EAGAIN || errno == EWOULDBLOCK) break;
else if(errno == EINTR) continue;
else {
LogMessage(WARNING, "send error %d : %s", errno, strerror(errno));
con->_exceptCb(con);
break;
}
}
}
if(con->_outBuffer.empty()) EnableReadWrite(con, true, false);
else EnableReadWrite(con, true, true);
}
void Excepter(Connection* con)
{
if(!(_connections.find(con->_socketFd) != _connections.end())) return;
else //还存在
{
//从epoll中移除
bool ret = _epoll.DelFromEpoll(con->_socketFd);
assert(ret);
//从映射表中移除
_connections.erase(con->_socketFd);
//关闭
close(con->_socketFd);
//释放链接对象
delete con;
}
LogMessage(DEBUG, "Excepter 回收完毕");
}
封装有关网络通信的接口
//网络套接字封装
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "Log.hpp"
class Socket
{
const static int gbacklog = 20;
public://服务端客户端通用
static int SocketCreate() {
int SocketFd = socket(AF_INET, SOCK_STREAM, 0);
if(SocketFd < 0) {
LogMessage(FATAL, "socket create fail, %d:%s", errno, strerror(errno));
exit(1);
}
LogMessage(NORMAL, "socket create success, SocketFd:%d", SocketFd);
return SocketFd;
}
static bool SetNonBlock(int socket) {
int fl = fcntl(socket, F_GETFL);
if(fl < 0) return false;
fcntl(socket, F_SETFL, fl | O_NONBLOCK);
return true;
}
public://服务端专用
static void Bind(int listenSocketFd, uint16_t serverPort, std::string serverIp = "0.0.0.0") {
struct sockaddr_in local;
memset(&local, '\0', sizeof local);
local.sin_family = AF_INET;
local.sin_port = htons(serverPort);
inet_pton(AF_INET, serverIp.c_str(), &local.sin_addr);
if(bind(listenSocketFd, (struct sockaddr*)&local, sizeof local) < 0) {
LogMessage(FATAL, "bind fail, %d:%s", errno, strerror(errno));
exit(2);
}
LogMessage(NORMAL, "bind success, serverPort:%d", serverPort);
}
static void Listen(int listenSocketFd) {
if(listen(listenSocketFd, gbacklog) < 0) {
LogMessage(FATAL, "listen fail, %d:%s", errno, strerror(errno));
exit(3);
}
LogMessage(NORMAL, "listen success");
}
static int Accept(int listenSocketFd, std::string* clientIp, uint16_t* clientPort, int* acceptErrno) {
struct sockaddr_in client;
socklen_t length = sizeof client;
int serviceSocketFd = accept(listenSocketFd, (struct sockaddr*)&client, &length);
if(serviceSocketFd < 0) {
*acceptErrno = errno;
return -1;
}
if(clientIp != nullptr) *clientIp = inet_ntoa(client.sin_addr);
if(clientPort != nullptr) *clientPort = ntohs(client.sin_port);
return serviceSocketFd;
}
public://客户端专用
bool Connect(int clientSocketFd, std::string& serverIp, uint16_t& serverPort) {
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(serverIp.c_str());
server.sin_port = htons(serverPort);
if(connect(clientSocketFd, (struct sockaddr*)&server, sizeof server) == 0) return true;
else return false;
}
public:
Socket() {}
~Socket() {}
};
启动服务器后就可以发现监听套接字为3号文件描述符
当客户端连接服务器后,在服务器端会显示客户端使用的是5号文件描述符,因为4号文件描述符已被epoll模型使用了
此时客户端可以向服务器发送一些简单计算任务,计算任务间用"X"隔开,服务器收到计算请求处理后会将计算结果发送给客户端,计算结果之间也是用"X"隔开的。若发送的不是完整报文,则会保存在socket对应的Connection结构中的_inBuffer中
由于使用了多路转接技术,虽然epoll服务器是一个单进程的服务器,但却可同时为多个客户端提供服务
当客户端退出后服务器端也会将对应的文件描述符从epoll模型中删除
基于多路转接方案,当事件就绪的时候,采用回调的方式,进行业务处理的模式就被称为反应堆模式(Reactor)。上述代码中的TcpServer就是一个反应堆,其中一个个Connection对象就称为事件。每一个事件中都有:
反应堆中有一个事件派发函数,当epoll中的某个事件就绪,事件派发函数回调用此事件的回调函数
特性