找工作小项目:day12-主从Reactor模式

day12-主从Reactor模式

单Reactor多线程
多Reactor多线程
我们今天需要完成的是由图一向图二的转变,我们来分析一下为何需要多Reactor多线程来完成事件处理流程。

首先在单Reactor多线程中需要主Reactor完成监听及响应的动作,之后交由Handler进行分配发送,由线程进行处理,处理结束后Handler将相应响应返回给Reactor。这就导致了在面对瞬间高并发的场景时Reactor需要处理的事件过于复杂,导致性能下降。

为了能够应对瞬间高并发场景,多Reactor多线程将响应事件分发出去,交由Sub-Reactor完成,即main-Reactor只完成初始化、分发连接(将新的连接请求分发给各个子 Reactor),main-Reactor不再处理链接,子进程 accept 新连接后就放到自己的 Reactor 进行处理,不会再分配给其他子进程。

以上就是今天的思路,创建主从Reactor并将accept分发给子Reactor。

1、错误检测机制

2、地址创建、Socket创建

今天将这两部分合并为一个部分,服务端客户端可共用。同时创建过程发生了一些变化:

class InetAddress
{
private:
    struct sockaddr_in addr;
public:
    InetAddress();
    InetAddress(const char* _ip, uint16_t _port);
    ~InetAddress();

    void setInetAddr(sockaddr_in _addr);
    sockaddr_in getAddr();
    char* getIp();
    uint16_t getPort();
};

差别主要在多了getIp和getPort两个方法,这两个方法是用来获取IP和端口信息的方法。之前都是通过调用clnt_addr->getAddr().来获取,现在有了方法,更加不关注内部。

InetAddress::InetAddress() {
    bzero(&addr, sizeof(addr));
}
InetAddress::InetAddress(const char* _ip, uint16_t _port) {
    bzero(&addr, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr(_ip);
    addr.sin_port = htons(_port);
}

InetAddress::~InetAddress(){
}

void InetAddress::setInetAddr(sockaddr_in _addr){
    addr = _addr;
}

sockaddr_in InetAddress::getAddr(){
    return addr;
}

char* InetAddress::getIp(){
    return inet_ntoa(addr.sin_addr);
}

uint16_t InetAddress::getPort(){
    return ntohs(addr.sin_port);
}

之后创建Socket进行监听绑定接收连接无阻塞等一系列操作,由于在accept和connect过程中需要面临高并发状态。

在accept方法中如果使用的是非阻塞模式,则accept立即返回,如果没有客户端连接到服务器,此时调用accept函数会返回-1,并将errno设置为EAGAIN或EWOULDBLOCK,表示暂时没有连接可接受。程序需要不断地轮询accept函数,直到有客户端连接到服务器或者发生错误。

在accept方法中如果使用的是阻塞模式,accept函数会一直等待,直到有客户端连接到服务器,或者发生错误。一旦有客户端连接到服务器,accept函数会返回一个新的文件描述符,用于表示与客户端建立的连接。

//声明
class Socket
{
private:
    int fd;
public:
    Socket();
    Socket(int _fd);
    ~Socket();

    void bind(InetAddress*);
    void listen();
    int accept(InetAddress*);

    void connect(InetAddress*);

    void setnonblocking();
    int getFd();
};
//实现
Socket::Socket() : fd(-1){
    fd = socket(AF_INET, SOCK_STREAM, 0);
    errif(fd == -1, "socket create error");
}
Socket::Socket(int _fd) : fd(_fd){
    errif(fd == -1, "socket create error");
}

Socket::~Socket(){
    if(fd != -1){
        close(fd);
        fd = -1;
    }
}

void Socket::bind(InetAddress *_addr){
    struct sockaddr_in addr = _addr->getAddr();
    errif(::bind(fd, (sockaddr*)&addr, sizeof(addr)) == -1, "socket bind error");
}

void Socket::listen(){
    errif(::listen(fd, SOMAXCONN) == -1, "socket listen error");
}
void Socket::setnonblocking(){
    fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);
}

int Socket::accept(InetAddress *_addr){
    // for server socket
    int clnt_sockfd = -1;
    struct sockaddr_in addr;
    bzero(&addr, sizeof(addr));
    socklen_t addr_len = sizeof(addr);
    if(fcntl(fd, F_GETFL) & O_NONBLOCK){
        while(true){
            clnt_sockfd = ::accept(fd, (sockaddr*)&addr, &addr_len);
            if(clnt_sockfd == -1 && ((errno == EAGAIN) || (errno == EWOULDBLOCK))){
                continue;
            } else if(clnt_sockfd == -1){
                errif(true, "socket accept error");
            } else{
                break;
            }
        }
    }
    else{
        clnt_sockfd = ::accept(fd, (sockaddr*)&addr, &addr_len);
        errif(clnt_sockfd == -1, "socket accept error");
    }
    _addr->setInetAddr(addr);
    return clnt_sockfd;
}

void Socket::connect(InetAddress *_addr){
    // for client socket
    struct sockaddr_in addr = _addr->getAddr();
    if(fcntl(fd, F_GETFL) & O_NONBLOCK){
        while(true){
            int ret = ::connect(fd, (sockaddr*)&addr, sizeof(addr));
            if(ret == 0){
                break;
            } else if(ret == -1 && (errno == EINPROGRESS)){
                continue;
            } else if(ret == -1){
                errif(true, "socket connect error");
            }
        }
    } else{
        errif(::connect(fd, (sockaddr*)&addr, sizeof(addr)) == -1, "socket connect error");
    }
}

int Socket::getFd(){
    return fd;
}

同样connect和accept使用相似的机制。

3、高并发Epoll

4、Channel

channel不再使用线程池进行处理,而是使用sub-Reactor进行处理因此需要将Channel中有关线程池的部分删除掉。

5、EventLoop

同样将有关线程池的部分进行删除。

6、Acceptor

同样将有关线程池的部分删减。

7、Connection

删除线程部分,之前由于多线程引发的deleteConnectionCallback错误由于在后续中不同Reactor负责处理各自负责的事件进而不会导致重复读取也就此消失。

8、Buffer

9、ThreadPool

10、服务器类

可以预见的是服务器类中需要创建一个Main-Reactor和多个Sub-Reactor,并且由于之前各个部分作用区分并不明显,因此线程池就是直接将各个事件加入到池中,而现在需要Reactor亲手将自己的事件加入到池中。
来看声明,主从Reactor,线程池,连接记录,接收连接,构造析构,处理读事件,建立新连接,删除连接。

class EventLoop;
class Socket;
class Acceptor;
class Connection;
class ThreadPool;
class Server
{
private:
    EventLoop *mainReactor;
    Acceptor *acceptor;
    std::map<int, Connection*> connections;
    std::vector<EventLoop*> subReactors;
    ThreadPool *thpool;
public:
    Server(EventLoop*);
    ~Server();

    void handleReadEvent(int);
    void newConnection(Socket *sock);
    void deleteConnection(int sockfd);
};

首先是构造函数,由于需要的Reactor更多了,对应的构造函数也就更复杂了,首先需要创建main-Acceptor,之后进行初始化,通过std::thread::hardware_concurrency()推测所需要的sub-Reactor数量,进行sub-Reactor的创建,并将EventLoop中事件处理的部分绑定到sub-Reactor上,之后再将处理事件加入到线程池中。

Server::Server(EventLoop *_loop) : mainReactor(_loop), acceptor(nullptr){ 
    acceptor = new Acceptor(mainReactor);
    std::function<void(Socket*)> cb = std::bind(&Server::newConnection, this, std::placeholders::_1);
    acceptor->setNewConnectionCallback(cb);

    int size = std::thread::hardware_concurrency();
    thpool = new ThreadPool(size);
    for(int i = 0; i < size; ++i){
        subReactors.push_back(new EventLoop());
    }

    for(int i = 0; i < size; ++i){
        std::function<void()> sub_loop = std::bind(&EventLoop::loop, subReactors[i]);
        thpool->add(sub_loop);
    }
}

Server::~Server(){
    delete acceptor;
    delete thpool;
}

之后将事件随即绑定到一个sub-Reactor上进行创建连接和处理事件,这个方法是由main-Reactor调用将事件随即分发给sub-Reactor。

void Server::newConnection(Socket *sock){
    if(sock->getFd() != -1){
        int random = sock->getFd() % subReactors.size();
        Connection *conn = new Connection(subReactors[random], sock);
        std::function<void(int)> cb = std::bind(&Server::deleteConnection, this, std::placeholders::_1);
        conn->setDeleteConnectionCallback(cb);
        connections[sock->getFd()] = conn;
    }
}

void Server::deleteConnection(int sockfd){
    if(sockfd != -1){
        auto it = connections.find(sockfd);
        if(it != connections.end()){
            Connection *conn = connections[sockfd];
            connections.erase(sockfd);
            // close(sockfd);
            delete conn;
        }
    }
}

11、总结

day12完成了多Reactor多线程,最主要的差别是在分配处理的方式上,即将newConnection绑定到main-Reactor上并在newConnection中进行分配,分配方案使用的是哈希表分配,这个调度方案使得每个线程负载相同,然而,不同的业务传输的数据极有可能不一样,也可能受到网络条件等因素的影响,极有可能会造成一些subReactor线程十分繁忙,而另一些subReactor线程空空如也,这只需要在事件中加入相应的标志,并根据标志利用线程池进行任务优先级分配,下面是一段类似于伪代码的改写。

//根据优先级进行事件准备
void Server::newConnection(Socket *sock, int priority){
    if(sock->getFd() != -1){
        Reactor* selectedReactor = nullptr;
        
        // 根据优先级选择子反应器
        if(priority == HIGH_PRIORITY){
            selectedReactor = highPrioritySubReactor;
        } else {
            selectedReactor = lowPrioritySubReactor;
        }

        Connection *conn = new Connection(selectedReactor, sock);
        std::function<void(int)> cb = std::bind(&Server::deleteConnection, this, std::placeholders::_1);
        conn->setDeleteConnectionCallback(cb);
        connections[sock->getFd()] = conn;
    }
}
  • 15
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
主从 Reactor 多线程是一种常见的网络编程模型,它通常用于高并发服务器的开发。该模型的核心思想是将网络 I/O 与业务逻辑分离,并通过多线程来实现并发处理。 在主从 Reactor 多线程模型中,主线程(也称为 Acceptor 线程)负责监听客户端连接请求,并将连接分配给工作线程(也称为 EventLoop 线程)进行处理。工作线程负责处理连接上的读写事件和业务逻辑,并将需要执行的任务交给线程池中的线程进行处理。 主从 Reactor 多线程模型主要包含以下组件: 1. Acceptor:负责监听客户端连接请求,并将连接分配给工作线程进行处理。 2. EventLoop:负责处理连接上的读写事件和业务逻辑,并将需要执行的任务交给线程池中的线程进行处理。 3. Thread Pool:用于执行异步任务,例如数据库查询和计算密集型任务等。 在实现主从 Reactor 多线程模型时,需要注意以下几点: 1. Acceptor 线程与工作线程之间应该使用线程安全的队列进行通信。 2. 每个工作线程应该拥有一个 EventLoop 对象,用于处理连接上的读写事件和业务逻辑。 3. 线程池中的线程应该使用异步方式执行任务,以避免阻塞工作线程。 总之,主从 Reactor 多线程模型是一种高效的网络编程模型,可以有效地提高服务器的并发处理能力。但是它的实现比较复杂,需要考虑线程同步、线程安全和性能等方面的问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值