目录
一、Reactor模式
1.1 Reactor模式定义
Reactor反应器模式,也被称为分发者模式或通知者模式,是一种将就绪事件派发给对应服务处理程序的事件设计模式
1.2 Reactor模式的角色构成
1.3 Reactor模式的工作流程
- 当向初始分发器注册具体事件处理器时,会标识出该事件处理器希望初始分发器在某个事件发生时向其通知,该事件与Handle关联
- 初始分发器会要求每个事件处理器向其传递内部的Handle,该Handle向操作系统标识了事件处理器
- 当所有的事件处理器注册完毕后,启动初始分发器的事件循环,这时初始分发器会将每个事件处理器的Handle合并起来,并使用同步事件分离器等待这些事件的发生
- 当某个事件处理器的Handle变为Ready状态时,同步事件分离器会通知初始分发器
- 初始分发器会将Ready状态的Handle作为key,来寻找其对应的事件处理器
- 初始分发器会调用其对应事件处理器中对应的回调方法来响应该事件
二、epoll ET服务器(Reactor模式)
2.1 设计思路
epoll ET服务器
- 读事件:若是监听套接字读事件就绪则调用accept函数获取底层连接,若是其他套接字读事件就绪则调用recv函数读取客户端发来的数据
- 写事件:写事件就绪则将待发送的数据写入到发送缓冲区中
- 异常事件:当某个套接字的异常事件就绪时不做过多处理,直接关闭该套接字
当epoll ET服务器监测到某一事件就绪后,就会将该事件交给对应的服务处理程序进行处理
Reactor模式的五个角色
在这个epoll ET服务器中,Reactor模式中的五个角色对应如下:
- 句柄:文件描述符
- 同步事件分离器:I/O多路复用epoll
- 事件处理器:包括读回调、写回调和异常回调
- 具体事件处理器:读回调、写回调和异常回调的具体实现
- 初始分发器:TcpServer中的Dispatcher函数
Dispatcher函数的工作即为:调用epoll_wait函数等待事件发生,有事件发生后将就绪的事件派发给对应的服务处理程序即可
Connection类
在Reactor的工作流程中说到,在注册事件处理器时需要将其与Handle关联,本质上就是需要将读回调、写回调和异常回调与某个文件描述符关联起来。这样做的目的就是为了当某个文件描述符上的事件就绪时可以找到其对应的各种回调函数,进而执行对应的回调方法来处理该事件。可以设计一个Connection类,该类中的成员包括了一个文件描述符,以及该文件描述符对应的各种回调函数,以及其他成员
TcpServer类
- 在Reactor的工作流程中说到,当所有事件处理器注册完毕后,会使用同步事件分离器等待这些事件发生,当某个事件处理器的Handle变为Ready状态时,同步事件分离器会通知初始分发器,然后初始分发器会将Ready状态的Handle作为key来寻找其对应的事件处理器,并调用该事件处理器中对应的回调方法来响应该事件
- 本质就是当事件注册完毕后,会调用epoll_wait函数来等待这些事件发生,当某个事件就绪时epoll_wait函数会告知调用方,然后调用方就根据就绪的文件描述符来找到其对应的各种回调函数,并调用对应的回调函数进行事件处理
对此可以设计一个Reactor类
- 该类当中有一个成员函数Dispatcher,即初始分发器,在该函数内部会调用epoll_wait函数等待事件的发生,当事件发生后会告知Dispatcher已经就绪的事件
- 当事件就绪后需要根据就绪的文件描述符来找到其对应的各种回调函数,由于会将每个文件描述符及其对应的各种回调都封装到一个Connection结构中,所以可以根据文件描述符找到其对应的Connection结构
- 使用C++ STL中的unordered_map,来建立各个文件描述符与其对应的Connection结构之间的映射,这个unordered_map可以作为TcpServer类的一个成员变量,当需要找某个文件描述符的Connection结构时就可以通过该成员变量找到
- TcpServer类中还需要提供成员函数AddConnection,用于向初始分发器中注册事件
epoll ET服务器的工作流程
- epoll ET服务器的初始化:需进行套接字的创建、绑定、监听,创建epoll模型
- 为监听套接字创建对应的Connection结构,并调用TcpServer类中提供的AddConnection函数将监听套接字添加到epoll模型中,并建立监听套接字与其对应的Connection结构之间的映射关系
- 之后就可以不断调用TcpServer类中的Dispatcher函数进行事件派发
- 在事件处理过程中,会不断向Dispatcher中新增事件,每个事件就绪时都会自动调用其对应的回调函数处理,不断调用Dispatcher函数进行事件派发即可
2.2 Connection结构
Connection结构中除了包含文件描述符和其对应的读回调、写回调和异常回调外,还包含一个输入缓冲区_inBuffer、一个输出缓冲区_outBuffer以及一个回指指针_svrPtr
- 当某个文件描述符的读事件就绪时,调用recv函数读取客户端发来的数据,但并不能保证读到了一个完整报文,因此需要将读取到的数据暂时存放到该文件描述符对应的_inBuffer中,当_inBuffer中可以分离出一个完整的报文后再将其分离出来进行数据处理,_inBuffer本质就是用来解决粘包问题的
- 当处理完一个报文请求后,需将响应数据发送给客户端,但并不能保证底层TCP的发送缓冲区中有足够的空间写入,因此需将要发送的数据暂时存放到该文件描述符对应的_outBuffer中,当底层TCP的发送缓冲区中有空间,即写事件就绪时,再依次发送_outBuffer中的数据
- Connection结构中设置回指指针_svrPtr,便于快速找到TcpServer对象,因为后续需要根据Connection结构找到这个TcpServer对象。如上层业务处理函数NetCal函数向_outBuffer输出缓冲区递交数据后,需通过Connection中的回指指针,"提醒"TcpServer进行处理
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;
};
2.3 TcpServer类
在TcpServer类中有一个unordered_map成员,用于建立文件描述符和与其对应的Connection结构之间的映射,还有一个_epoll成员,该成员是封装的Epoll对象。在初始化TcpServer对象时就可以调用封装的EpollCreate函数创建Epoll对象,并将该epoll模型对应的文件描述符记录在该对象的成员变量_epollFd中,便于后续使用。TcpServer对象析构时,Epoll对象的析构会自动调用close函数将epoll模型关闭
封装Epoll类
#pragma once
#include <iostream>
#include <sys/epoll.h>
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<int, Connection*> _connections;//管理服务器链接
Epoll _epoll;
struct epoll_event* _revs;//获取就绪事件的缓冲区
int _revsNum;//缓冲区大小
callback_t _cb;//上层业务处理
};
2.3.1 AddConnection函数
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));
}
2.3.2 Dispatcher函数(初始分发器)
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<int, Connection*> _connections;//管理服务器链接
Epoll _epoll;
struct epoll_event* _revs;//获取就绪事件的缓冲区
int _revsNum;//缓冲区大小
callback_t _cb;//上层业务处理
};
- 本代码没有用switch或if语句对epoll_wait函数的返回值进行判断,而是借用for循环对其返回值进行了判断
- 若epoll_wait的返回值为-1则说明epoll_wait函数调用失败,此时不会进入到for循环内部进行事件处理
- 若epoll_wait的返回值为0则说明epoll_wait函数超时返回,此时也不会进入到for循环内部进行事件处理
- 若epoll_wait的返回值大于0则说明epoll_wait函数调用成功,此时才会进入到for循环内部调用对应的回调函数对事件进行处理
- 事件处理时先对异常事件进行处理,将异常事件交给回调函数进行处理
2.3.3 EnableReadWrite函数
TcpServer类中的EnableReadWrite函数,用于使能某个文件描述符的读写事件
- 调用EnableReadWrite函数时需要传入一个文件描述符,表示需要设置的是哪个文件描述符对应的事件
- 传入两个bool值,分别表示是否关心读事件以及是否关心写事件
- EnableReadWrite函数内部会调用封装EpollCtrl函数修改该文件描述符的监听事件
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);
}
2.4 回调函数
- Accepter:当连接事件到来时调用该回调函数获取底层建立好的连接
- Recver:当读事件就绪时调用该回调函数读取客户端发来的数据并处理
- Sender:当写事件就绪时调用该回调函数向客户端发送响应数据
- Excepter:当异常事件就绪时调用该函数进行一系列资源的释放
为某个文件描述符创建Connection结构时,可以调用Connection类提供的SetCallBack函数,将这些回调函数添加到Connection结构中
- 监听套接字对应的Connection结构中的_recvCb为Accepter,因为监听套接字的读事件就绪就意味着连接事件就绪了,而监听套接字一般只关心读事件,因此监听套接字对应的_sendCb和_exceptCb可以设置为nullptr
- 当Dispatcher监测到监听套接字的读事件就绪时,会调用监听套接字对应的Connection结构中的_recvCb回调,此时就会调用Accepter回调获取底层建立好的连接
- 对于与客户端建立连接的套接字,其对应的Connection结构中的_recvCb、_sendCb和_exceptCb分别为Recver、Sender和Excepter
- 当Dispatcher监测到这些套接字的事件就绪时,就会调用其对应的Connection结构中对应的回调函数,即Recver、Sender和Excepter
2.4.1 Accepter
Accepter回调用于处理连接事件,其工作流程如下:
- 调用封装的Accept函数获取底层建立好的连接
- 使用AddConnection函数将获取到的套接字封装为Connection并添加至服务器的管理中
- 此时套接字及其对应需要关心的事件就已注册到Dispatcher中
下一次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函数进行读取,并且监听套接字必须设置为非阻塞
- 因为ET模式下只有当底层建立的连接从无到有或是从有到多时才会通知上层,若没有一次性将底层建立好的连接全部获取,并且此后再也没有建立好的连接,那么底层没有读取完的连接就相当于丢失了
- 循环调用accept函数也意味着,当底层连接全部被获取后再调用accept函数,此时就会因为底层已经没有连接了而被阻塞住,因此需要将监听套接字设置为非阻塞,这样当底层没有连接时accept就不会被阻塞。accept获取到的新的套接字也需设置为非阻塞,为了避免将来循环调用recv、send等函数时被阻塞
- 设置非阻塞的操作都在AddConnection函数中的SetNonBlock函数完成
设置非阻塞
设置文件描述符为非阻塞时,需先调用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时,需继续判断错误码
- 若错误码为EAGAIN或EWOULDBLOCK,说明本次出错返回是因为底层已经没有可获取的连接了,此时底层连接全部获取完毕,这时可以返回0,表示本次Accepter调用成功。
- 若错误码为EINTR,说明本次调用accept函数获取底层连接时被信号中断了,这时还应该继续调用accept函数进行获取。
- 除此之外,才说明accept函数是真正调用失败了,此时可以返回-1,表示本次accepter调用失败
accept、recv和send等IO系统调用为什么会被信号中断?
IO系统调用函数出错返回并且将错误码设置为EINTR,表明本次在进行数据读取或数据写入之前被信号中断了,即IO系统调用在陷入内核,但并没有返回用户态的时候内核去处理其他信号
- 在内核态返回用户态之前会检查信号的pending位图,即未决信号集,若pending位图中有未处理的信号,那么内核就会对该信号进行处理
- IO系统调用函数在进行IO操作前就被信号中断了,这是一个特例,因为IO过程分为"等"和"拷贝"两个步骤,一般"等"的过程比较漫长,而在这个过程中执行流其实是处于闲置状态的,因此在"等"的过程中若有信号产生,内核就会立即进行信号的处理
写事件按需打开
Accepter获取上来的套接字在添加到Dispatcher中时,只添加了EOPLLIN和EPOLLET事件,即只让epoll关心该套接字的读事件
之所以没有添加写事件,是因为并没有要发送的数据,因此没有必要让epoll关心写事件。一般读事件是会被设置的,而写事件则是按需打开的,只当有数据要发送时才会将写事件打开,并且在数据全部写入完毕后又会立即将写事件关闭
2.4.2 Recver
recver回调用于处理读事件,其工作流程如下:
- 循环调用recv函数读取数据,并将读取到的数据添加到该套接字对应Connection结构的_inBuffer中
- 对_inBuffer中的数据进行切割,将完整的报文切割出来,剩余的留在inbuffer中
- 调用业务处理函数
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<string> messages;
SpliteMessage(con->_inBuffer, &messages);
for(auto& msg : messages) _cb(con, msg);
}
}
- 当recv函数的返回值小于0时需要进一步判断错误码,若错误码为EAGAIN或EWOULDBLOCK则说明底层数据读取完毕了,若错误码为EINTR则说明读取过程被信号中断了,此时还需继续调用recv函数进行读取,否则就是读取出错了
- 当读取出错时直接调用该套接字对应的_exceptCb回调,在_exceptCb回调中将该套接字进行关闭
报文切割
报文切割本质就是为了防止粘包问题,而粘包问题还涉及到协议定制
- 需要根据协议知道如何将各个报文进行分离,如UDP分离报文采用的就是定长报头+自描述字段
- 本博客目的是演示整个数据处理的过程,为了简单起见就不进行过于复杂的协议定制了,就以"X"作为各个报文之间的分隔符,每个报文的最后都会以一个"X"作为报文结束的标志
- 因此现在要做的就是以"X"作为分隔符对_inBuffer中的字符串进行切割
- SpliteMessage函数要做的就是对_inBuffer中的字符串进行切割,将切割出来的一个个报文放到vector中,对于最后无法切出完整报文的数据就留在_inBuffer中
void SpliteMessage(string &buffer, vector<string> *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);
}
}
业务处理函数
- 对切割出来的完整报文进行反序列化
- 业务处理
- 业务处理后形成响应报文
- 将响应报头添加到对应Conection结构的_outBuffer中,并打开写事件
下一次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_; // 计算结果的状态码
};
2.4.3 Sender
- 循环调用send函数发送数据,并将发送出去的数据从该套接字对应Connection结构的_outBuffer中删除。
- 若循环调用send函数后该套接字对应的_outBuffer中的数据被全部发送,此时就需要将该套接字对应的写事件关闭,因为已没有要发送的数据了,若_outBuffer中的数据还有剩余,那么该套接字对应的写事件就应继续打开
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);
}
- send函数的返回值小于0时需进一步判断错误码,若错误码为EAGAIN或EWOULDBLOCK则说明底层TCP发送缓冲区已被写满了,这时将已经发送的数据从_outBuffer中移除
- 若错误码为EINTR则说明发送过程被信号中断了,此时还需要继续调用send函数进行发送,否则就是发送出错了
- 当发送出错时直接调用该套接字对应的_exceptCb回调,在_exceptCb回调中将该套接字进行关闭
- 若最终_outBuffer中的数据全部发送成功,则_outBuffer被清空,可以关闭对写事件的关心
2.4.4 Excepter
- 对于异常事件就绪的套接字不做过多处理,调用close函数将该套接字关闭即可
- 但在关闭该套接字前,需先将该套接字从epoll模型中删除,并取消该套接字与其对应的Connection结构的映射关系
- 释放Connection对象
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 回收完毕");
}
2.5 Socket套接字
封装有关网络通信的接口
//网络套接字封装
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <unistd.h>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <fcntl.h>
#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() {}
};
2.6 服务器测试
启动服务器后就可以发现监听套接字为3号文件描述符
当客户端连接服务器后,在服务器端会显示客户端使用的是5号文件描述符,因为4号文件描述符已被epoll模型使用了
此时客户端可以向服务器发送一些简单计算任务,计算任务间用"X"隔开,服务器收到计算请求处理后会将计算结果发送给客户端,计算结果之间也是用"X"隔开的。若发送的不是完整报文,则会保存在socket对应的Connection结构中的_inBuffer中
由于使用了多路转接技术,虽然epoll服务器是一个单进程的服务器,但却可同时为多个客户端提供服务
当客户端退出后服务器端也会将对应的文件描述符从epoll模型中删除
三、总结
基于多路转接方案,当事件就绪的时候,采用回调的方式,进行业务处理的模式就被称为反应堆模式(Reactor)。上述代码中的TcpServer就是一个反应堆,其中一个个Connection对象就称为事件。每一个事件中都有:
- 文件描述符
- 独立的缓冲区
- 回调方法
- 回指向反应堆的指针
反应堆中有一个事件派发函数,当epoll中的某个事件就绪,事件派发函数回调用此事件的回调函数
特性
- 单进程:既负责事件派发又负责IO
- 半异步半同步:异步,事件到来是随机的。同步:当前线程参与IO