【Linux C++】实现Reactor服务器:封装EventLoop类|封装TcpServer类|封装Acceptor类|封装Connection类
日期:2025.2.18
学习内容:
- 封装EventLoop类
- 封装TcpServer类
- 封装Acceptor类
- 封装Connection类
个人总结:
EventLoop:
这里和下面的封装TcpServer的难度下来了,有点无聊,就是封装之上的继续封装。
本篇主要的看点在后面的两个封装。
直接看代码:
EventLoop el;
Channel* serv_ch = new Channel(el.GetEP(), listen.GetFD());
serv_ch->EnableReading();
serv_ch->SetReadCallback(std::bind(&Channel::NewConnection, serv_ch, &listen));
el.GetEP()->UpdateChannel(serv_ch);
el.Run();
这里把Epoll放进了EventLoop里面去,然后把while1的循环放到了EventLoop里面去。
类的声明也比较简单:
#pragma once
#include "Epoll.h"
class EventLoop {
private:
Epoll* m_ep;
public:
EventLoop();
~EventLoop();
Epoll* GetEP();
void Run();
};
具体代码的实现就都是copy之前的。。。
没有然后了。。。。
TcpServer:
将main代码里的Socket,EventLoop和InetAddress都放了进去:
int main(int argc, char* argv[]) {
if (argc != 3) {
cout << "./server 192.168.11.132 5005" << endl;
return 0;
}
TcpServer tcp_serv(argv[1], atoi(argv[2]));
tcp_serv.Start();
return 0;
}
TcpServer:
//TcpServer.h
#pragma once
#include <string>
#include <unistd.h>
#include<stdint.h>
#include "EventLoop.h"
class TcpServer {
private:
EventLoop m_el;
public:
TcpServer(const std::string& ip, const uint16_t& port);
~TcpServer();
void Start();
};
//TcpServer.cpp
#include "EventLoop.h"
#include "Socket.h"
#include "InetAddress.h"
#include "Channel.h"
#include "TcpServer.h"
#include <string>
TcpServer::TcpServer(const std::string& ip, const uint16_t& port) {
Socket* listen = new Socket(CreatNonBlocking());
int listenfd = listen->GetFD();
listen->KeepAlive(1);
listen->NoDelay(1);
listen->ReuseAddr(1);
listen->ReusePort(1);
InetAddress serv_addr(ip, port);
listen->Bind(serv_addr);
listen->Listen(128);
Channel* serv_ch = new Channel(m_el.GetEP(), listen->GetFD());
serv_ch->EnableReading();
serv_ch->SetReadCallback(std::bind(&Channel::NewConnection, serv_ch, listen));
m_el.GetEP()->UpdateChannel(serv_ch);
}
TcpServer::~TcpServer() {
}
void TcpServer::Start() {
m_el.Run();
}
有一个地方是TcpServer的构造函数里,listen从实例变成了指针,这样就不会触发它的析构函数。
封装Acceptor类
其实我们细分下去,Channel类并不一定能够很好的满足我们的需求,因为就目前来看,我们有专门用于监听的,还有对于客户端进行数据传输的,他们都用Channel来表示,使得Channel类中的功能既有监听然后Accept的,也有数据传输,很混乱。于是我们决定可以在Channel之上,继续封装出来两个新的类:Acceptor和Connection。
Acceptor就专门用于创建出来对于客户端数据传输的类(也就是Connection),而Connection类就专门用于数据传输功能。
我们先看Acceptor:
class Acceptor {
private:
EventLoop* m_el;
Channel* m_ch;
Socket* m_listen;
std::function<void(Socket*)> m_new_connection;
public:
Acceptor(EventLoop* el, const std::string& ip, const uint16_t& port);
~Acceptor();
void SetNewConnection(std::function<void(Socket*)> func);
void NewConnection();
};
(请先忽略function,等会讲到)。因为本身是对Channel的高级封装,所以我们的成员就是Channel,Socket还有一个之前对Epoll封装过的EventLoop。这个function等会会说。
然后成员函数里我们把之前在Channel里的NewConnection拿了过来。函数的内容基本没有什么变化:
void Acceptor::NewConnection() {
InetAddress client_addr1;
Socket* client = new Socket(m_listen->Accept(client_addr1));
client->SetAddr(client_addr1);
m_new_connection(client);
}
然后这里有个地方是:之前的话,我们会直接在NewConnection里new出来client的Channel,那就像刚才说的,这里按道理,我们就应该会new出来Connection类,但是这里并没有,而是用了一个回调函数,功能上其实是都一样的,但是这里却回调到了TcpServer类里面去。原因是因为按照设计上说,Acceptor和Connection应该是同一级别的,如果我们在Acceptor里就new出来了,就代表他的生命周期是由Acceptor掌管,但是并不是,我们希望是由TcpServer来管理生命周期,所以我们需要在TcpServer里new出来,然后在delete掉。这就是使用回调函数的原因。
与之对应的是:
Acceptor里多了一个Set函数:
void Acceptor::SetNewConnection(std::function<void(Socket*)> func) {
m_new_connection = func;
}
还有就是在TcpServer里也多了一个NewConnection函数:
void TcpServer::NewConnection(Socket* client) {
Connection* m_conn = new Connection(&m_el, client);
printf("New Client fd:%d ip:%s port:%hu \n", client->GetFD(), client->GetIP().c_str(), client->GetPort());
}
这样就在TcpServer里new了,那如何delete呢?
由于一个TcpServer里会有很多个客户端,所以我们用一个容器存起来,这里我选择的是map:
class TcpServer {
private:
EventLoop m_el;
Acceptor* m_acpt;
std::map<int, Connection*> m_con_mp;
public:
TcpServer(const std::string& ip, const uint16_t& port);
~TcpServer();
void Start();
void NewConnection(Socket* client);
};
成员变量就多了一个map,然后在析构函数里析构就好了。(这里的优化在下面的封装Connection里)
TcpServer::~TcpServer() {
delete m_acpt;
for (auto& one : m_con_mp) {
delete one.second;
}
}
这里在放上Acceptor类的代码:
#include "Acceptor.h"
#include "Socket.h"
#include "InetAddress.h"
#include "Channel.h"
#include "EventLoop.h"
#include "Connection.h"
Acceptor::Acceptor(EventLoop* el, const std::string& ip, const uint16_t& port) :m_el(el) {
m_listen = new Socket(CreatNonBlocking());
m_listen->KeepAlive(1);
m_listen->NoDelay(1);
m_listen->ReuseAddr(1);
m_listen->ReusePort(1);
InetAddress serv_addr(ip, port);
m_listen->Bind(serv_addr);
m_listen->Listen(128);
m_ch = new Channel(m_el->GetEP(), m_listen->GetFD());
m_ch->EnableReading();
m_ch->SetReadCallback(std::bind(&Acceptor::NewConnection, this));
m_el->Update(m_ch);
}
Acceptor::~Acceptor() {
delete m_listen;
delete m_ch;
}
void Acceptor::SetNewConnection(std::function<void(Socket*)> func) {
m_new_connection = func;
}
void Acceptor::NewConnection() {
InetAddress client_addr1;
Socket* client = new Socket(m_listen->Accept(client_addr1));
client->SetAddr(client_addr1);
m_new_connection(client);
}
封装Connection类:
在实现了Acceptor类之后,其实Connection也是大同小异,我们将之前Channel类的OnMessage函数挪过来就好了,这里放上函数的具体实现:
#include "Connection.h"
#include "Channel.h"
#include "Socket.h"
#include "EventLoop.h"
Connection::Connection(EventLoop* el, Socket* client) :m_el(el), m_client(client) {
m_ch = new Channel(m_el->GetEP(), client->GetFD());
m_ch->EnableET();
m_ch->EnableReading();
m_ch->SetReadCallback(std::bind(&Connection::OnMessage, this));
m_el->Update(m_ch);
}
Connection::~Connection() {
delete m_ch;
delete m_client;
}
void Connection::OnMessage() {
char buf[1024];
while (true) {
memset(buf, 0, sizeof buf);
ssize_t read_siz = read(m_client->GetFD(), buf, sizeof buf);
if (read_siz > 0) {
printf("recv: (fd: %d) %s\n", m_client->GetFD(), buf);
send(m_client->GetFD(), buf, sizeof buf, 0);
}
else if (read_siz == -1 and errno == EINTR) {
continue;
}
else if (read_siz == -1 and (errno == EAGAIN || errno == EWOULDBLOCK)) {
break;
}
else if (read_siz == 0) {
printf("disconnect: (fd: %d) \n", m_client->GetFD());
close(m_client->GetFD());
break;
}
}
}
关于析构Connection:
我们可以想到,像上面提到的用map的析构方法并不太好,因为我们希望客户端断开之后就可以析构,而不是直到最后才析构。
这里我们先优化一下前面的内容:
关于Channel的Handle函数:
void Channel::HandleEvent() {
if (this->GetRevent() & EPOLLIN) {
read_callback();
}
else if (this->GetRevent() & EPOLLOUT) {
}
else {
error_callback();
// printf("error: (fd: %d) \n", this->GetFD());
// close(this->GetFD());
}
}
这里我们用回调函数,就像之前用read_callback一样,不再使用注释的内容了,而这样做,我们就需要像下面这样:
std::function<void()> read_callback;
std::function<void()> close_callback;
std::function<void()> error_callback;
void SetReadCallback(std::function<void()> func);
void SetCloseCallback(std::function<void()> func);
void SetErrorCallback(std::function<void()> func);
多设置出来两个function和两个setfunction的函数。
而set函数的调用,我们就需要到Connection的构造函数里:
Connection::Connection(EventLoop* el, Socket* client) :m_el(el), m_client(client) {
m_ch = new Channel(m_el->GetEP(), client->GetFD());
m_ch->EnableET();
m_ch->EnableReading();
m_ch->SetReadCallback(std::bind(&Connection::OnMessage, this));
m_ch->SetCloseCallback(std::bind(&Connection::CloseCallback, this));
m_ch->SetErrorCallback(std::bind(&Connection::ErrorCallback, this));
m_el->Update(m_ch);
}
可以看到中间多了两个刚才写的SetCallback函数的调用,那内容该怎么写呢?
我们要记得,Connection想要被delete掉,要在TcpServer里才可以。
所以我们有两个解决方法,一个是在Connection类里再加上TcpServer指针,这样在setclosecallback函数的里面就可以填上TcpServer的指针(如果没有TcpServer的指针,我们就无法在TcpServer里delete。
第二个方法就是就像刚才做的一样,我们在Connection里也用上回调函数,把刚才做的套娃一遍,这样在没有TcpServer的指针的情况下也可以做到在TcpServer里delete了,这里我使用的第二个方法解决。
class Connection {
private:
EventLoop* m_el;
Channel* m_ch;
Socket* m_client;
std::string m_ip;
uint16_t m_port;
std::function<void(Connection*)> m_close_callback;
std::function<void(Connection*)> m_error_callback;
public:
Connection(EventLoop* el, Socket* client);
~Connection();
void OnMessage();
void SetCloseCallback(std::function<void(Connection*)> func);
void SetErrorCallback(std::function<void(Connection*)> func);
void CloseCallback();
void ErrorCallback();
};
这里因为我们要delete Connection,所以回调函数有参数,是Connection的指针类型。
然后我们就接着回到TcpServer里new Connection的函数部分:
void TcpServer::NewConnection(Socket* client) {
Connection* m_conn = new Connection(&m_el, client);
m_conn->SetCloseCallback(std::bind(&TcpServer::CloseConnection, this, std::placeholders::_1));
m_conn->SetErrorCallback(std::bind(&TcpServer::ErrorConnection, this, std::placeholders::_1));
printf("New Client fd:%d ip:%s port:%hu \n", client->GetFD(), client->GetIP().c_str(), client->GetPort());
}
using std::cout;
using std::endl;
void TcpServer::CloseConnection(Connection* con) {
delete con;
cout << "CLoseCon" << endl;
}
void TcpServer::ErrorConnection(Connection* con) {
delete con;
cout << "ErrorCon" << endl;
}
可以看到,我们也加上了SetCloseCallback的部分,因为要有Connection*的部分,所以用placeholders先暂时代替。
运行代码后,会看到有CloseCon的内容,代表我们确实使用回调函数delete的。
最后附上Connection.cpp的内容:
#include "Connection.h"
#include "Channel.h"
#include "Socket.h"
#include "EventLoop.h"
#include "TcpServer.h"
Connection::Connection(EventLoop* el, Socket* client) :m_el(el), m_client(client) {
m_ch = new Channel(m_el->GetEP(), client->GetFD());
m_ch->EnableET();
m_ch->EnableReading();
m_ch->SetReadCallback(std::bind(&Connection::OnMessage, this));
m_ch->SetCloseCallback(std::bind(&Connection::CloseCallback, this));
m_ch->SetErrorCallback(std::bind(&Connection::ErrorCallback, this));
m_el->Update(m_ch);
}
Connection::~Connection() {
delete m_ch;
delete m_client;
}
void Connection::OnMessage() {
char buf[1024];
while (true) {
memset(buf, 0, sizeof buf);
ssize_t read_siz = read(m_client->GetFD(), buf, sizeof buf);
if (read_siz > 0) {
printf("recv: (fd: %d) %s\n", m_client->GetFD(), buf);
send(m_client->GetFD(), buf, sizeof buf, 0);
}
else if (read_siz == -1 and errno == EINTR) {
continue;
}
else if (read_siz == -1 and (errno == EAGAIN || errno == EWOULDBLOCK)) {
break;
}
else if (read_siz == 0) {
printf("disconnect: (fd: %d) \n", m_client->GetFD());
CloseCallback();
// m_close_callback(this);
// close(m_client->GetFD());
// break;
}
}
}
void Connection::SetCloseCallback(std::function<void(Connection*)> func) {
m_close_callback = func;
}
void Connection::SetErrorCallback(std::function<void(Connection*)> func) {
m_error_callback = func;
}
void Connection::CloseCallback() {
m_close_callback(this);
}
void Connection::ErrorCallback() {
m_error_callback(this);
}
所以最后我们总结一下:
设B类是A类的下层级,当我们想要A的函数来进行操作B的时候,我们就给B的成员加上function,还有加上SetFunction函数去设置回调函数。然后就找到A中new B的部分,补上SetFunction的内容,将其想要的函数内容设置成A类有的函数。这样就能做到在A中实现某些事情。例如如果我们想要在A中delete B的话,就可以使用以上内容。
Others:
把输出连接的信息挪到了TcpServer里,所以我在Acceptor和Connection里都多加了ip和port变量。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战