代码地址:https://gitee.com/tgwTTT/super-238/tree/server/Tcpepollserver
建议阅读本文时先阅读博主以前的文章:http://www.tgwttt.xyz/?p=677
引言:最近博主在写课设,在在web服务时,闲来无事(后端逻辑由室友用java写了插不上手),于是想着自己写一个类似nginx的代理服务器,由于条件有限,没有做负载均衡逻辑。
总体设计图:

在网络通信中,代理服务器扮演着重要的角色,它可以用于负载均衡、安全过滤、缓存加速、网络穿透等多种场景。本文将介绍一个基于Reactor模式实现的高性能TCP代理服务器,该服务器采用了多线程架构、非阻塞IO和事件驱动模型,能够高效地处理大量并发连接。
架构设计
Reactor模式
Reactor模式是一种事件驱动的设计模式,主要用于处理并发连接。它的核心思想是将事件的检测和处理分离,通过一个事件循环来监听和分发事件。
在我的代理服务器中,Reactor模式的实现主要包括以下几个部分:
1. 事件源:网络连接(socket)
2. 事件多路分发器:epoll
3. 事件处理器:Channel类
4. 分发器:Reactor类
多线程架构:
1. 主线程(Master Reactor)**:负责监听新的客户端连接
2. 工作线程(Worker Reactor)**:负责处理已建立的连接的数据传输
这种设计将连接的建立和数据的处理分离,避免了主线程的阻塞,提高了服务器的并发处理能力。
核心组件:
Common.hpp
Common.hpp定义了一些通用的工具类和函数:
–NetAddr类:封装了网络地址的处理,包括IP和端口的设置、转换和获取
– NoCopy类:禁止对象的拷贝和赋值操作,用于资源管理
– SetNonBlock函数:将文件描述符设置为非阻塞模式
Connection.hpp
Connection.hpp定义了连接的抽象接口:
class Connection
{
public:
using handler_t =std::function<void(std::shared_ptr<Channel>)>;
Connection():handler(nullptr),events(0), owner(nullptr) {}
virtual void Recever() = 0;
virtual void Sender() = 0;
virtual void Excepter() = 0;
// 其他成员函数…
};
所有具体的连接类型(如Channel)都必须实现这个接口。
Channel.hpp
Channel类是Connection接口的具体实现,负责处理单个连接的数据传输:
– Recever方法:接收来自客户端或目标服务器的数据
– Sender方法:发送数据到客户端或目标服务器
– Excepter方法:处理异常情况,如连接关闭
在代理服务器模式下,每个Channel都有一个对应的peerChannel,用于实现数据的双向转发:
// 设置对端Channel
void setPeerChannel(std::shared_ptr<Channel> peer) {
this->peerChannel = peer;
// 建立双向关联
if (peer) {
peer->peerChannel = shared_from_this();
}
}
Reactor.hpp
Reactor类是整个事件驱动模型的核心,负责事件的监听和分发:
void Loop()
{
isRunning = true;
while (isRunning)
{
int timeout = -1;
int n=LoopOnce(timeout);
Dispatch(n);
}
isRunning = false;
}
Loop方法是事件循环的主入口,它不断地调用LoopOnce方法获取就绪的事件,然后调用Dispatch方法分发这些事件到对应的处理器。
在代理服务器模式下,Reactor类还负责连接到目标服务器:
“`cpp
std::shared_ptr<Channel> connectToTargetServer(const std::string& targetAddr, int targetPort) {
// 创建socket并连接到目标服务器
// …
// 创建Channel对象
std::shared_ptr<Channel> channel = std::make_shared<Channel>(sockfd, targetAddr, targetPort);
// …
return channel;
}
Listener.hpp
Listener类负责监听新的客户端连接,并将它们分发给工作线程:
void Recever() override
{
NetAddr client_addr;
while (true)
{
int client_fd = listen_socket->Accept(&client_addr);
// …
// 合法的fd,使用轮询方式分发给工作线程
if (!workerReactors.empty()) {
SetNonBlock(client_fd);
std::shared_ptr<Reactor> reactor = workerReactors[nextWorkerIndex];
reactor->addSocketFd(client_fd);
nextWorkerIndex = (nextWorkerIndex + 1) % workerReactors.size();
}
}
}
代理工作原理
当代理服务器接收到客户端的连接请求时,它会执行以下步骤:
1. 接收连接:Listener类的Recever方法接收客户端的连接请求,并将客户端的文件描述符分发给工作线程。
2. 创建Channel:工作线程的Reactor创建一个Channel对象来管理这个连接。
3. 连接目标服务器:如果是代理模式,Reactor会创建一个新的socket连接到目标服务器,并创建一个对应的Channel对象。
4. 建立关联:客户端的Channel和目标服务器的Channel建立双向关联,形成一个代理链路。
5. 数据转发:当客户端发送数据时,客户端的Channel接收到数据后,会将数据转发到目标服务器的Channel;当目标服务器返回数据时,目标服务器的Channel接收到数据后,会将数据转发到客户端的Channel。
性能优化
1. 非阻塞IO:所有的socket都设置为非阻塞模式,避免了IO操作的阻塞。
2. ET模式:epoll使用边缘触发模式,可以减少事件的触发次数,提高效率。
3. 多线程:采用多线程架构,将连接的建立和数据的处理分离,提高了服务器的并发处理能力。
4. 事件驱动:使用事件驱动模型,只有当有事件发生时才会处理,避免了轮询的开销。
5. 数据缓冲区:使用缓冲区来存储数据,减少了系统调用的次数。
今天的更新就到这里,如有错误欢饮指出
评论
还没有任何评论,你来说两句吧!