【Linux网络编程】多路转接reactor——ET模式的epoll
文章目录
- 一、ET与LT:
- 二、代码实现:
- 1、准备文件:
- 2、前置头文件:
- Socket.hpp
- log.hpp
- Epoller.hpp
- nocopy.hpp
- 3、服务器代码实现:
- 整体架构:
- 关键类型与全局变量:
- 事件类型:
- 常量定义:
- 回调函数类型
- Connection类:
- 成员变量:
- 核心方法:
- TcpServer类:
- 成员变量:
- 核心成员解析:
- Init()初始化:
- SetNonBlockOrDie:
- AddConnection:
- Accepter ():
- Start ()
- Dispatcher ()
- Recver ()
- _OnMessage:
- Sender:
- Excepter:
一、ET与LT:
ET与LT是epoll的两种工作模式,这两种模式的核心区别在于事件通知的方式,直接影响程序的处理逻辑和性能
LT模式叫做水平触发,是epoll的默认工作模式,只要文件描述符(fd)处于就绪状态,epoll_wait就会持续通知该事件,直到fd不在处于就绪状态,例如:当socket接收缓冲区中有数据未读完,每次调用epoll_wait都会返回该socket的可读事件
ET模式叫做边缘触发,更高效,触发条件更严格,仅当文件描述符(fd)的状态从非就绪变为就绪时,epoll_wait才会通知一次,例如:socket接收缓冲区从空变为有数据,或者从有数据变为更多数据,触发一次可读事件;若数据未读完,后续不会再通知,直到有新数据到来
面试题:
为什么ET模式下fd必须设置成非阻塞
当fd触发可读事件后,若最后一次未读完所有数据(如接收缓冲区仍有剩余数据),由于ET不会再次通知,后续数据会被遗漏;更严重的是,若此时调用recv读取剩余数据,会因缓冲区暂时无新数据而进入阻塞状态,导致程序无法处理其他事件,彻底卡住
而将fd设为非阻塞模式后,当数据读完时,recv 会立即返回EAGAIN/EWOULDBLOCK错误,程序可借此退出读取循环,继续处理其他事件,避免阻塞,这是 ET 模式正确工作的必要条件
一般来说,ET模式效率比LT模式要高,这体现在通知效率高,IO效率也高,为什么IO效率高呢?——因为ET每次通知之后,上层都会把数据全部取走,这样TCP也会向对方发送一个更大的窗口,对方也就会发更多的数据回来
特性 | LT 模式(水平触发) | ET 模式(边缘触发) |
---|---|---|
通知时机 | 只要 fd 处于就绪状态,就持续通知 | 仅在 fd 状态从非就绪→就绪时通知一次 |
数据处理要求 | 可分多次处理,无需一次处理完 | 必须一次处理完所有数据(循环操作) |
fd 状态要求 | 可阻塞,也可非阻塞 | 必须非阻塞(避免操作卡住) |
系统调用次数 | 较多(重复通知) | 较少(一次通知) |
性能 | 一般 | 更高(减少冗余通知) |
接下来代码实现以下epoll的ET模式:
二、代码实现:
1、准备文件:
咋一看很吓人,但本次编码主要在TcpServer.hpp这个文件中,其他的要不是以前写过的,要不就是比较短的
以前编写过的有:ClientCal.cc,Epoller.hpp,log.hpp,nocopy.hpp,Protocal.hpp,ServerCal.hpp,Socket.hpp
我们会在接下来的编码思路中逐渐引入
2、前置头文件:
Socket.hpp
这里封装了网路通信的系统调用接口
#pragma once
#include <iostream>
#include <unistd.h>
#include <string.h>#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>#include "log.hpp"enum{SOCKET_ERR = 2,BIND_ERR,LISTEN_ERR
};const int backlog = 10;class Sock
{
public:Sock(){}~Sock(){}
public:void Socket(){_sockfd = socket(AF_INET, SOCK_STREAM,0);if(_sockfd < 0){lg(FATAL,"socket err,%s:%d",strerror(errno),errno);exit(SOCKET_ERR);}int opt = 1;setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));}void Bind(uint16_t port){struct sockaddr_in local;local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = INADDR_ANY;socklen_t len = sizeof(local);int n = bind(_sockfd,(struct sockaddr*)&local,len);if(n < 0){lg(FATAL,"bind err,%s,%d",strerror(errno),errno);exit(BIND_ERR);}}void Listen(){int n = listen(_sockfd,backlog);if(n < 0){lg(FATAL,"listen err,%s,%d",strerror(errno),errno);exit(LISTEN_ERR);}}int Accept(std::string *clientip, uint16_t *clientport){struct sockaddr_in client;socklen_t len = sizeof(client);int n = accept(_sockfd,(struct sockaddr*)&client,&len);if(n < 0){lg(WARNING, "accept error, %s: %d", strerror(errno), errno);return -1;}char in_buffer[64];inet_ntop(AF_INET,&client,in_buffer,sizeof(in_buffer));*clientip = in_buffer;*clientport = ntohs(client.sin_port);return n;}bool Connect(const std::string &ip, const uint16_t &port){struct sockaddr_in server;memset(&server,0,sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(port);inet_pton(AF_INET,ip.c_str(),&(server.sin_addr));socklen_t len = sizeof(server);int n = connect(_sockfd,(struct sockaddr*)&server,len);if(n < 0){std::cerr << "connect to " << ip << ":" << port << " error" << std::endl;return false;}return true;}void Close(){close(_sockfd);}int Getsockfd(){return _sockfd;}public:int _sockfd;
};
log.hpp
这个封装了打印日志的底层实现
#pragma once#include <iostream>
#include <ctime>
#include <cstdarg>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>#define INFO 0
#define DEBUG 1
#define WARNING 2
#define ERROR 3
#define FATAL 4#define SCREEN 1
#define ONEFILE 2
#define MOREFILE 3#define SIZE 1024
#define logname "log.txt"using namespace std;class Log
{
public:Log():printstyle(SCREEN),path("./log/")// 默认路径是当前路径下的log文件夹{// mkdir(path.c_str(),0765);}void change(int style){printstyle = style;}string leveltostring(int level){switch (level){case INFO:return "INFO";case DEBUG:return "DEBUG";case WARNING:return "WARNING";case ERROR:return "ERROR";case FATAL:return "FATAL";default:return "NON";}}void operator()(int level, const char *format, ...){// 处理时间time_t now = time(nullptr);// 将时间戳转为本地时间struct tm *local_time = localtime(&now);char leftbuffer[SIZE];snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", leveltostring(level).c_str(),local_time->tm_year + 1900, local_time->tm_mon + 1, local_time->tm_mday,local_time->tm_hour, local_time->tm_min, local_time->tm_sec);// 处理可变参数va_list s;va_start(s, format);char rightbuffer[SIZE];vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);va_end(s);// 将两个消息组合起来成为一个完整的日志消息// 默认部分+自定义部分char logbuffer[SIZE * 2];snprintf(logbuffer, sizeof(logbuffer), "%s %s", leftbuffer, rightbuffer);printlog(level, logbuffer);}void printlog(int level, const string &logbuffer) // 这里引用避免大型字符串的拷贝开销,优化性能{switch (printstyle){case SCREEN:cout << logbuffer << endl;break;case ONEFILE:printonefile(logname, logbuffer);break;case MOREFILE:printmorefile(level, logbuffer);break;}}void printonefile(const string &_logname, const string &logbuffer){string __logname = path + _logname;int fd = open(__logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);if (fd < 0)return;write(fd, logbuffer.c_str(), logbuffer.size());close(fd);}void printmorefile(int level, const string &logbuffer){// 思路:通过不同的文件名进行区分string _logname = logname;_logname += ".";_logname += leveltostring(level);printonefile(_logname, logbuffer);}~Log(){}private:int printstyle;string path;
};Log lg;
Epoller.hpp
这是将epoll的接口进行封装
#include <iostream>
#include <sys/epoll.h>
#include <cerrno>
#include <cstring>#include "nocopy.hpp"
#include "log.hpp"// 将Epoll的接口进行封装
class Epoller : public nocopy
{static const int size = 128;public:Epoller(){// epoll_create创建,返回值用成员描述符接收,并且进行判断返回成功 or 失败_epfd = epoll_create(size);if (_epfd == -1){lg(ERROR, "epoll_create 失败:%s", strerror(errno));}else{lg(INFO, "epoll_create 成功:%d", _epfd);}}// int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);// 判断是否已经就绪的接口int Epoll_Wait(struct epoll_event events[], int maxevents){int n = epoll_wait(_epfd, events, maxevents, /*_timeout*/-1);return n;}// int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);// epfd在成员变量中,op和fd在外面传进来,event结构体中监控的时间类型也在外面传进来int Epoll_Update(int oper, int sock, uint32_t events){int n = 0;// 根据是删除就绪队列中的还是增加或者修改就绪队列进行分开写// 删除:也就是根据传进来的oper参数进行宏判断;判断返回值if (oper == EPOLL_CTL_DEL){// 因为是删除,不需要关心最后一个参数所对应的监控事件是啥,所以传nullptr,这也是为什么要将删除和其他两分开写的原因n = epoll_ctl(_epfd, oper, sock, nullptr);if (n != 0){lg(ERROR, "epoll_ctl delete error!");}}else{// 增加或者修改监控事件// 填充epoll_event结构体,填充events和联合体中的fdstruct epoll_event ev;ev.events = events;ev.data.fd = sock; // 为了方便后期得知是哪一个fd就绪了n = epoll_ctl(_epfd, oper, sock, &ev);if (n != 0){lg(ERROR, "epoll_ctl add or mod error!");}}// 返回 epoll_ctl的返回值return n;}~Epoller(){// 如果文件描述符是合法的,就关闭if (_epfd >= 0)close(_epfd);}private:// epoll创建后的文件描述符int _epfd;// timeoutint _timeout = 3000;
};
nocopy.hpp
这是实现了一个不能够拷贝的类,让我们的服务器继承这个类,这样服务器也就不会被乱拷贝了
#pragma onceclass nocopy
{
public:nocopy(){}nocopy(const nocopy &) = delete;const nocopy&operator=(const nocopy &) = delete;
};
3、服务器代码实现:
整体架构:
主要包含两个核心类:Connection和TcpServer
其中Connection:是封装单个客户端连接的属性和操作
其中TcpServer:服务器核心类,负责监听端口,管理所有连接,事件分发和处理(基于 epoll)
整体流程:服务器初始化后通过epoll监听事件(新连接、数据读写、异常),并通过回调函数分发处理,实现高并发 IO 操作
关键类型与全局变量:
事件类型:
uint32_t EVENT_IN = (EPOLLIN | EPOLLET); // 边缘触发的读事件
uint32_t EVENT_OUT = (EPOLLOUT | EPOLLET); // 边缘触发的写事件
常量定义:
const int maxnum = 128;
const int defaultport = 8888;
const int g_buffer_size = 128;
maxnum:epoll一次最多处理的事件数
defaultport:默认端口(8888)
g_buffer_size:读写缓冲区大小(128 字节)
回调函数类型
using func_t = std::function<void(std::shared_ptr<Connection>)>;
统一回调函数,用于处理连接的读、写、异常事件,参数是共享指针,指向的是Connection类,返回值是void
Connection类:
成员变量:
private:int _sock;std::string _inbuffer; // 读缓冲区std::string _outbuffer; // 写缓冲区public:func_t _recv_cb; // 读回调,当读事件就绪自动调用,下面也是一样的func_t _send_cb; // 写回调func_t _except_cb; // 错误回调// 这是一个指向底层TcpServer对象的回指指针std::weak_ptr<TcpServer> _tcp_server_str;// 方便打印日志std::string _ip;uint16_t _port;
其中:
_sock:客户端套接字描述符
_inbuffer/_outbuffer:读 / 写缓冲区(存储接收 / 待发送 的数据)
_recv_cb/_send_cb/_except_cb:读 / 写 / 异常事件的回调函数
_tcp_server_str:指向 TcpServer 的回指指针,采用weak_ptr是哦为了避免TcpServer与Connection循环引用导致内存泄漏
_ip/_port:客户端的IP和端口(用于日志和调试)
核心方法:
void SetCallback(func_t recv_cb, func_t send_cb, func_t except_cb){_recv_cb = recv_cb;_send_cb = send_cb;_except_cb = except_cb;}void AppendInbuffer(const std::string &info){_inbuffer += info;}void AppendOutbuffer(const std::string &info){_outbuffer += info;}int Getsock(){return _sock;}void SetSharePtr(std::weak_ptr<TcpServer> tcp_server_ptr){_tcp_server_str = tcp_server_ptr;}std::string &Inbuffer(){return _inbuffer;}std::string &Outbuffer(){return _outbuffer;}
其中:
SetCallback:设置读、写、异常回调函数
AppendInbuffer/AppendOutbuffer:向接受 / 发送缓冲区添加数据
Getsock:获取套接字描述符
Inbuffer/Outbuffer:返回缓冲区的引用(用于读写数据)
Connection类只需要把发送/接收缓冲区暴露给上层即可,至于怎么做,这是上层所需要做的事
TcpServer类:
该类是服务器的核心,负责初始化、连接管理、事件监听和分发,继承 std::enable_shared_from_this(方便在回调中获取自身 shared_ptr)和 nocopy(禁止拷贝)
成员变量:
private:// _epoller_ptr,_listensock_ptr,std::shared_ptr<Epoller> _epoller_ptr;std::shared_ptr<Sock> _listensock_ptr;// 接收就绪队列的数组struct epoll_event _revs[maxnum];// 从一个文件描述符到一个epoller链接的映射。服务器管理的所有的链接std::unordered_map<int, std::shared_ptr<Connection>> _connections;// 端口号,服务器状态uint16_t _port;bool _quit;// 上层处理回调func_t _OnMessage;
_epoller_ptr:epoll操作的封装对象(负责事件的添加 / 删除 / 修改和等待)
_listensock_ptr:监听套接字的封装对象
_revs:存储epoll就绪事件的数组
_connections:哈希表(sockfd -> Connection),管理所有客户端连接,通过文件描述符映射对应的Connection类
_port:服务器监听端口
_quit:服务器运行状态标志(false 表示运行中)
_OnMessage:上层业务回调(当数据接收完成后,交给上层处理)
核心成员解析:
Init()初始化:
void Init(){// 监听套接字创建绑定监听,并设置成非阻塞_listensock_ptr->Socket();SetNonBlockOrDie(_listensock_ptr->Getsockfd());_listensock_ptr->Bind(_port);_listensock_ptr->Listen();// 将监听套接字进行AddConnection,设置回调之类的AddConnection(_listensock_ptr->Getsockfd(), EVENT_IN,std::bind(&TcpServer::Accepter, this, std::placeholders::_1),nullptr, nullptr, "0.0.0.0", _port);}
将监听套接字设置为非阻塞,配合 epoll 边缘触发模式
通过 AddConnection 将监听套接字注册到 epoll,并绑定新连接处理回调 Accepter
理解bind:创建一个函数对象,当调用这个函数对象时,会调用当前对象(this 所指的对象)的 TcpServer::Accepter 成员函数,并且调用时需要传入一个参数(由 placeholders::_1 占位)
&TcpServer::Accepter:这是要绑定的成员函数指针,TcpServer 是一个类,Accepter 是该类中的一个成员函数
this:表示将成员函数 Accepter 绑定到当前对象(即调用 bind 时所在的对象)因为成员函数需要通过对象来调用,所以这里传递 this 指针,使得绑定后的函数对象在调用时,是针对当前对象的 Accepter 方法
placeholders::_1:这是 std::placeholders 中的占位符,用于表示绑定后的函数对象在被调用时,需要接收一个参数,这个参数会传递给Accepter函数
所以接下来实现SetNonBlockOrDie与AddConnection
SetNonBlockOrDie:
这是在Comm文件中进行封装的
#pragma once
// 一个函数,将一个文件描述符设置成非阻塞
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include "Socket.hpp"void SetNonBlockOrDie(int sock)
{int f1 = fcntl(sock, F_GETFL);if (f1 < 0)exit(1);fcntl(sock, F_SETFL, f1 | O_NONBLOCK);
}
AddConnection:
这是连接管理
void AddConnection(int sock, uint32_t event,func_t recv_rb, func_t send_cb, func_t except_cb,const string &ip = "0.0.0.0", uint16_t port = 8080){// 创建Connection智能指针对象// std::shared_ptr<Connection> new_connection = make_shared<Connection>(sock);std::shared_ptr<Connection> new_connection(new Connection(sock));new_connection->SetSharePtr(shared_from_this());// 设置回调new_connection->_recv_cb = recv_rb;new_connection->_send_cb = send_cb;new_connection->_except_cb = except_cb;// 设置IP与端口号new_connection->_ip = ip;new_connection->_port = port;// 添加到哈希表维护的数组中实现映射_connections.insert(std::make_pair(sock, new_connection));// 将关心的事件添加到内核中int n = _epoller_ptr->Epoll_Update(EPOLL_CTL_ADD, sock, event);// Debug日志,add a new connection success, sockfd is : %d"lg(DEBUG, "成功增加一个链接,sockfd is : %d", sock);}
主要作用两个:
- 为新套接字(监听套接字或客户端套接字)创建 Connection 对象,绑定回调函数
- 将连接加入 _connections 哈希表管理,并通过 epoll_ctl 注册到 epoll 实例
Accepter ():
这是新连接处理的接口
void Accepter(shared_ptr<Connection> connection){while (true){// accept 获取连接struct sockaddr_in peer;socklen_t len = sizeof(peer);int sock = ::accept(connection->Getsock(), (struct sockaddr *)&peer, &len); // ???这不能写死为监听套接字的描述符吗if (sock > 0){// 网络序列转主机序列,端口和IP地址都要转,用来AddConnection传参uint16_t peerport = ntohs(peer.sin_port);char ipbuf[128];inet_ntop(AF_INET, &peer.sin_addr.s_addr, ipbuf, sizeof(ipbuf));lg(DEBUG, "得到一个新的客户端连接,客户端IP:%s,客户端端口号:%d,客户端套接字:%d", ipbuf, peerport, sock);// 将得到的连接设置成非阻塞SetNonBlockOrDie(sock);// AddConnection进行回调,listensock只需要recv_rb,但是其他的sock,读,写,处理异常都要有AddConnection(sock, EVENT_IN,std::bind(&TcpServer::Recver, this, std::placeholders::_1), std::bind(&TcpServer::Sender, this, std::placeholders::_1), std::bind(&TcpServer::Excepter, this, std::placeholders::_1), ipbuf, peerport);}else{// 根据errno进行判断if (errno == EWOULDBLOCK)break;else if (errno == EINTR)continue;elsebreak;// 获取连接失败,可能是信号中断(EINTR),这样就进行continue,\如果是底层没有数据也就是读完了,把底层所有连接都拿上来了(EWOULDBLOCK)就break\也可能就是出错也break}}}
- 循环调用 accept 获取所有新连接(边缘触发需一次性处理完)
- 新客户端套接字设置为非阻塞,并通过AddConnection注册到epoll,绑定数据读写回调(Recver/Sender)和异常回调(Excepter)
Start ()
这是启动服务器接口:
void Start(){// 启动服务quit设置服务器状态_quit = false;while (!_quit){// 调用事件派发器Dispatcher();// PrintDebug();}// 服务器状态_quit = true;}
启动服务器主循环,不断调用Dispatcher(事件分发器)处理就绪事件,直到 _quit 为 true(停止服务器)
Dispatcher ()
这是事件分发器:
void Dispatcher(){// 通过epoll获取底层就绪的事件_epoller_ptr->Epoll_Wait(_revs, maxnum);// 对拿上来的连接进行处理 forfor (int i = 0; i < maxnum; i++){// 拿到当前连接所关心的事件和fdint fd = _revs[i].data.fd;uint32_t event = _revs[i].events;// 统一把事件异常转化为读写问题(EPOLLERR和EPOLLHUP)if (event & EPOLLERR)event |= (EPOLLIN | EPOLLOUT);if (event & EPOLLHUP)event |= (EPOLLIN | EPOLLOUT);// 接着只需要处理EPOLLIN和EPOLLOUT,如果是并且在哈希表中真的存在就检测当前连接的读回调是否被设置,\如果被设置了就将事件派发给读回调if ((event & EPOLLIN) && (IsConnectionSafe(fd))){if (_connections[fd]->_recv_cb)_connections[fd]->_recv_cb(_connections[fd]);}// 写回调是同样的道理if ((event & EPOLLOUT) && (IsConnectionSafe(fd))){if (_connections[fd]->_send_cb)_connections[fd]->_send_cb(_connections[fd]);}}}
- 核心事件循环:通过 epoll_wait 获取就绪事件,根据事件类型(读 / 写)调用对应回调函数
- 异常事件(EPOLLERR/EPOLLHUP)被转化为读写事件,触发对应回调处理
其中IsConnectionSafe是判断sockfd是否安全的函数, 如果安全就返回真, 否则返回假,说白了就是看当前文件描述符在不在Connections,也就是服务器管理的所有的链接
bool IsConnectionSafe(int fd){auto iter = _connections.find(fd);if (iter == _connections.end())return false;elsereturn true;}
Recver ()
当一个文件描述符读事件就绪了, 那么根据Dispatcher的逻辑, 就会进行Recver回调
void Recver(shared_ptr<Connection> connection){// 得到当前文件描述符int fd = connection->Getsock();// ET模式,一直读读读,直到把数据读完while (true){// 创建接收数组并且初始化,然后用recv将数据读到数组中char buffer[g_buffer_size];memset(buffer, 0, sizeof(buffer));ssize_t n = recv(fd, buffer, sizeof(buffer) - 1, 0);// 读取成功,不断将数据append到接收缓冲区中if (n > 0){connection->AppendInbuffer(buffer);}// == 0对方把链接关了,接着进入异常处理流程else if (n == 0){lg(INFO, "sockfd: %d, client info %s : %d quit...", fd, connection->_ip.c_str(), connection->_port);connection->_except_cb(connection);return;}// < 0读取出错else{if (errno == EWOULDBLOCK)break; // 底层没有数据也就是读完了,把底层所有连接都拿上来了else if (errno == EINTR)continue; // 获取连接失败,可能是信号中断(EINTR),这样就进行continueelse // 出错也break,然后进入异常处理流程{lg(WARNING, "sockfd: %d, client info %s : %d recv error...", connection->_ip.c_str(), connection->_port);connection->_except_cb(connection);return;}}}// 到这里是读完了底层数据读完了,那么就交给上层进行处理_OnMessage(connection);}
对于Recver来说,因为是ET模式,就需要一直读读读,直到把数据全部读完,循环读取数据到 _inbuffer
如果是将底层的数据全部读完了,那么错误码就是11,即EWOULDBLOCK,所以如果发现读取失败后,并且错误码是EWOULDBLOCK,那么就break跳出循环,接着回调_OnMessage,将数据交给上层进行处理
如果是EINTR错误码,证明获取连接失败,可能是信号中断(EINTR),这样就进行continue重新读取
都不是就证明真的出错了,直接返回即可
如果是读取数据读取到了0的时候,就代表对面把连接关掉了,那么sockfd就没有用了,进行异常处理即可
_OnMessage:
这是上层处理业务就是之前写过的序列化反序列化,在之前网络版本计算机中有实现的,这里直接上代码,具体实现可看之前博客
ServerCal.hpp
// 如何处理整个报文
#pragma once#include <iostream>
#include "Protocal.hpp"enum
{Div_Zero = 1,Mod_Zero,unKnow
};
class ServerCal
{
public:ServerCal(){}Response CalculatorHelper(const Request &req){Response res(0, 0);switch (req._op){case '+':res._result = req._x + req._y;break;case '-':res._result = req._x - req._y;break;case '*':res._result = req._x * req._y;break;case '/':{if (req._y == 0){res._code = Div_Zero;break;}res._result = req._x / req._y;break;}case '%':{if (req._y == 0){res._code = Mod_Zero;break;}res._result = req._x % req._y;break;}default:res._code = unKnow;break;}return res;}std::string Calculator(std::string &package){// 此时肯定是收到了已经封装好报头的消息// 所以第一步是解析报头Request req;std::string content;//std::cout << "content:" << content << "xxxxx" << std::endl;int r = Decode(package, &content);//std::cout << "解码后content:" << content << "xxxxx" << std::endl;if (!r)return "";// 进行反序列化r = req.Deserialize(content);if (!r)return "";// 进行数据处理Response res = CalculatorHelper(req);// 序列化content = "";res.Serialize(&content);// 封装报头content = Encode(content);// std::cout << "构造后的响应content:" << content << "xxxxx" << std::endl;return content;}~ServerCal(){}
};
Protocal.hpp
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>// #define Myself 1// 分割符
const std::string blank_space_sep = " ";
const std::string protocol_sep = "\n";// 添加报头
// "len"\n"x op y"\n
std::string Encode(std::string &content)
{std::string package = std::to_string(content.size());package += protocol_sep;package += content;package += protocol_sep;return package;
}// 解析报头
// "len"\n"x op y"\n -> "x op y"
bool Decode(std::string &package, std::string *content)
{std::size_t pos = package.find(protocol_sep);if (pos == std::string::npos)return false;std::string len_str = package.substr(0, pos);std::size_t len = stoi(len_str);// 判断报头是否符合要求std::size_t total_len = len + len_str.size() + 2;if (package.size() < total_len)return false;// std::cout << "移除报文成功" << std::endl;*content = package.substr(pos + 1, len);package.erase(0, total_len);return true;
}class Request
{
public:Request(int data1, int data2, char oper): _x(data1), _y(data2), _op(oper){}Request(){}// 构建报文有效载荷// struct -> string,"x op y"bool Serialize(std::string *out){
#ifdef Myselfstd::string s = std::to_string(_x);s += blank_space_sep;s += _op;s += blank_space_sep;s += std::to_string(_y);*out = s;return true;
#else// 序列化Json::Value root;root["x"] = _x;root["y"] = _y;root["op"] = _op;Json::FastWriter w;*out = w.write(root);return true;#endif}// 提取有效载荷,反序列化// "x op y" -> structbool Deserialize(const std::string &in){
#ifdef Myself// 提取左边size_t left = in.find(blank_space_sep);if (left == std::string::npos)return false;std::string part_x = in.substr(0, left);// 提取右边size_t right = in.rfind(blank_space_sep);if (right == std::string::npos)return false;std::string part_y = in.substr(right + 1);// 进行判断if (left + 2 != right)return false;// 填充结构体_x = stoi(part_x);_y = stoi(part_y);_op = in[left + 1];return true;
#elseJson::Value root;Json::Reader rd;rd.parse(in,root);_x = root["x"].asInt();_y = root["y"].asInt();_op = root["op"].asInt();return true;#endif}void DebugPrint(){std::cout << "新请求构建完成: " << _x << _op << _y << "=?" << std::endl;}public:int _x;int _y;char _op;
};class Response
{
public:Response(int res, int code): _result(res), _code(code){}Response(){}// 构建报文有效载荷// struct -> string,"_result _code"bool Serialize(std::string *out){
#ifdef Myselfstd::string s = std::to_string(_result);s += blank_space_sep;s += std::to_string(_code);*out = s;return true;#elseJson::Value root;root["result"] = _result;root["code"] = _code;Json::FastWriter w;*out = w.write(root);return true;
#endif}// 反序列化// string,"_result _code" -> structbool Deserialize(const std::string &in){
#ifdef Myselfstd::size_t res = in.find(blank_space_sep);if (res == std::string::npos)return false;std::string left = in.substr(0, res);std::string right = in.substr(res + 1);_result = std::stoi(left);_code = std::stoi(right);return true;#elseJson::Value root;Json::Reader rd;rd.parse(in,root);_result = root["result"].asInt();_code = root["code"].asInt();return true;
#endif}void DebugPrint(){std::cout << "结果响应完成, result: " << _result << ", code: " << _code << std::endl;}public:int _result;int _code;
};
那么在主函数那里就需要进行DefaultOnMessage,然后将默认OnMessage传给服务器,然后服务器回调上层处理业务逻辑
OnMessage
void OnMessage(std::shared_ptr<Connection> connection)
{ServerCal calculator;std::shared_ptr<TcpServer> server = connection->_tcp_server_str.lock(); // 通过 weak_ptr 获取 shared_ptrstd::cout << "上层得到了数据: " << connection->Inbuffer() << std::endl;std::string response_n = calculator.Calculator(connection->Inbuffer());if(response_n.empty()) return;// 将得到的数据发送出去connection->AppendOutbuffer(response_n);server->Sender(connection); // 使用 shared_ptr 调用 Sender
}
接下来实现Sender写方法即可
Sender:
关于发送我们有以下几点理解:
- epol/select/poll,因为写事件(发送缓冲区是否有空间,经常是OK的),经常就是就绪的
- 如果我们设置对EPOLLOUT关心,EPOLLOUT几乎每次都有就绪
- 导致epollserver经常返回,浪费CPU的资源
所以:
- 结论:对于读,设置常关心;对于写,按需设置
- 怎么处理写呢?直接写入,如果写入完成,就结束。如果写入完成,但是数据没有写完,outbuffer里还有内容,我们就需要设置对写事件进行关心了。如果写完了,去掉对写事件的关心
void Sender(shared_ptr<Connection> connection){// 得到发送缓冲区std::string &outbuffer = connection->Outbuffer();int fd = connection->Getsock();// 一直发发发,直到发完为止while (true){ssize_t n = send(fd, outbuffer.c_str(), outbuffer.size(), 0);if (n > 0) // 发送成功{outbuffer.erase(0, n);if (outbuffer.empty())break;}else if (n == 0){connection->_except_cb(connection);return;}else // n < 0{if (errno == EWOULDBLOCK)break;else if (errno == EINTR)continue;else{lg(WARNING, "sockfd: %d, client info %s : %d send error...", connection->_ip.c_str(), connection->_port);connection->_except_cb(connection);return;}}}// 走到这有两种情况,要么是成功发送完,要么是非阻塞模式下缓冲区已满,暂时无法发送更多数据// 简单来说就是要么数据全发完了,要么当前发不出去了,此时就需要关心写事件if (!outbuffer.empty()){// 此时就证明还有数据没有发完,但是底层的缓冲区已经没有数据了// 那么就需要关心写事件是否就绪EnableEvent(fd, true, true);}else{// 此时就证明数据写完了,没有数据了,然后关闭对写时间的关心EnableEvent(fd, true, false);}}
以上使用了EnableEvent函数来设置,以下是实现:
void EnableEvent(int sock, bool readable, bool writeable){uint32_t events = 0;events |= ((readable ? EPOLLIN : 0) | (writeable ? EPOLLOUT : 0) | EPOLLET);_epoller_ptr->Epoll_Update(EPOLL_CTL_MOD, sock, events);}
Excepter:
最后是异常处理,我们已经将所有事件异常转换成读写异常了:
并且在Recver和Sender在出现异常都会回调Excepter,所以这个服务器的所有异常都在这处理即可:
处理思路:
移除关心事件,关闭异常的文件描述符,将fd->connection映射表中将对应的连接进行移除,也就是从unordered_map中移除
void Excepter(shared_ptr<Connection> connection){int sockfd = connection->Getsock();lg(DEBUG, "Excepter hander sockfd : %d, client info %s : %d Excepter hander...",sockfd, connection->_ip.c_str(), connection->_port);// 进行异常处理// 移除关心事件_epoller_ptr->Epoll_Update(EPOLL_CTL_DEL, sockfd, 0);// 关闭异常的文件描述符lg(DEBUG, "close %d done...\n", sockfd);close(sockfd);// 将fd->connection映射表中将对应的连接进行移除,也就是从unordered_map中移除lg(DEBUG, "remove %d from _connections...\n", sockfd);_connections.erase(sockfd);}