代码地址: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. 数据缓冲区:使用缓冲区来存储数据,减少了系统调用的次数。

今天的更新就到这里,如有错误欢饮指出