56 . 高效ET非阻塞IO服务器设计指南

1.使用ET+非阻塞设计一个正确的IO服务器

要注重模块化解耦

1.1 TcpServer的大致框架

bash 复制代码
#include<iostream>
#include<memory>
#include<unordered_map>
#include"Listener.hpp"
#include"Epoller.hpp"
#include"Connection.hpp"
class TcpServer{
    public:
    TcpServer()
    :_epoll_ptr(std::make_unique<Epoller>()){}
    void Start(){
        while(true)
            {
                _epoll_ptr->Wait();
            }
        }
    private:
    //1. epoll模型
    std::unique_ptr<Epoller> _epoll_ptr;

    //2. listen模块 监听连接
    std::shared_ptr<Listener> _listener_ptr;
    //3.管理所有的Connection ,本质是管理未来获取的fd
    std::unordered_map<int,std::shared_ptr<Connection>> _Connections;
};

注意,之前关于epoll的使用,是直接默认为不会阻塞,一次全部读完的,但此处不可了

原因

1 . LT buffer局内变量,下次读,buffer清空

  1. buffer也会被多个连接读取

因此需要创建一个模块来进行单个fd的读与写缓冲区,但又因为会有大量的Connection,需要进行管理,所以使用unordered_map

Connection.hpp

bash 复制代码
#include<iostream>

#include"InetAddr.hpp"
class Tcpserver;
class Connection{
    public:
    Connection(){}

    ~Connection(){}
    private:
    int _fd;
    std::string _inbuffer;
    std::string _outbuffer;

    //回调指针
    Tcpserver*owner;
    
    InetAddr* client_add;
};

1.2 Listener继承Connection

Connection类应该是一个 "基础连接类"(比如已经封装了 "网络连接的基本操作",比如创建 socket、收发数据等)。

Listener是 "专门负责监听新连接" 的模块,它需要用到 Connection 里的基础连接能力 (比如创建监听用的 socket),同时又要扩展 "监听端口、接受新连接" 的功能 ------ 所以用继承来复用 Connection 的代码,避免重复写基础连接的逻辑。

Connection

bash 复制代码
#include<iostream>

#include"InetAddr.hpp"
class Tcpserver;
class Connection{
    public:
    Connection(){}
    virtual void Recver()=0;
    virtual void Sender()=0;
    virtual void Excepter()=0;
    ~Connection(){}
    private:
    int _fd;
    std::string _inbuffer;
    std::string _outbuffer;

    //回调指针
    Tcpserver*owner;
    
    InetAddr* client_add;

    
};

Listener.hpp

bash 复制代码
#include<iostream>
#include "Socket.hpp"
#include "Connection.hpp"
#include"Common.hpp"
using namespace SocketModule;
//连接和监听 两个逻辑上差不多,进行多态,减少重复代码
class Listener :public Connection
{
    public:
    Listener(int port=defaultport)
    :_port(port){
        _listensock->BuildTcpSocketMethod(_port);
    }
    void Recver(){

    }
    void Sender(){}
    void Excepter(){}
    ~Listener(){}
    private:
    int _port;
    std::unique_ptr<Socket> _listensock;
};

1.3 初步完善 搭建大致框架

epoller.hpp

bash 复制代码
#pragma once
#include <sys/epoll.h>   // 必须包含,epoll_* 函数声明
#include <unistd.h>      // close 函数
#include <cstdlib>       // exit 函数
#include <iostream>      // 日志相关(若LogModule基于此)
#include "Log.hpp"       // 确保Log.hpp正确声明LogModule命名空间和LOG宏
#include "Common.hpp"
using namespace LogModule;

class Epoller
{
public:
    Epoller()
        : _epfd(-1)
    {
        _epfd = epoll_create(126);
        if (_epfd < 0)
        {
            LOG(LogLevel::ERROR) << "epoll_create fail";
            exit(EPOLL_CREATE_ERROR);
        }
        LOG(LogLevel::INFO) << "epoll_create success , _epfd is :" << _epfd;
    }
    void AddEvent(int fd, uint32_t event)
    {
        struct epoll_event ev;
        ev.data.fd = fd;
        ev.events = event;
        int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, fd, &ev);
        if (n < 0)
        {
            LOG(LogLevel::ERROR) << "epoll_ctl error";
            return;
        }
        LOG(LogLevel::INFO) << "epoll_ctl success: " << fd;
    }
    void DelEvent() {}
    void ModEvent() {}
    void Wait() {}
    ~Epoller() {
        if(_epfd>0)
        {
            close(_epfd);

            _epfd = -1; // 置为非法值,防止后续误操作
        }
    }

private:
    int _epfd;
};

connection.hpp

bash 复制代码
#pragma once
#include<iostream>

#include"InetAddr.hpp"
class Tcpserver;
class Connection{
    public:
    Connection(){}
    virtual void Recver()=0;
    virtual void Sender()=0;
    virtual void Excepter()=0;
    int GetFd(){return _fd;}
    void SetFd(int fd)
    {
        _fd=fd;
    }
    uint32_t GetEvent(){return _event;}
    void SetEvent(const uint32_t event)
    {
        _event=event;
    }
    ~Connection(){}
    private:
    int _fd;
    std::string _inbuffer;
    std::string _outbuffer;

    //回调指针
    Tcpserver*owner;
    
    InetAddr* client_add;

    //关心事件
    uint32_t _event;
};

listener.hpp

bash 复制代码
#pragma once

#include <iostream>
#include <sys/epoll.h>
#include "Socket.hpp"
#include "Connection.hpp"
#include "Common.hpp"
using namespace SocketModule;
// 连接和监听 两个逻辑上差不多,进行多态,减少重复代码
class Listener : public Connection
{
public:
    Listener(int port = defaultport)
        : _port(port), _listensock(std::make_unique<TcpSocket>())
    {
        // 将监视的添加到
        _listensock->BuildTcpSocketMethod(_port);
        SetEvent(EPOLLIN); // ET todo
        SetFd(_listensock->Fd());
    }

    void Recver()
    {
    }
    void Sender() {}
    void Excepter() {}
    ~Listener() {}

private:
    int _port;
    std::unique_ptr<Socket> _listensock;
};

1.4 完成等待事件就绪的封装

bash 复制代码
int Wait(struct epoll_event*events,int maxevents,int timeout)
     {
        int n=epoll_wait(_epfd,events,maxevents,timeout);
        if(n<0)
            LOG(LogLevel::ERROR)<<"epoll_wait faial";
        return n;
     }

1.5 完成等待就绪后的事件操作

这里我们不再只关注写与读,而是其他的也要关注

但我们通过将其他错误转化为IO错话,然后再转化为一个函数解决,减少我们的麻烦

bash 复制代码
 void Start(){
        if(IsConnectionEmpty())
            return;
        _isrunning=true;
        while(_isrunning)
        {
            int timeout=-1;
            int n= _epoll_ptr->Wait(_revs,rev_num,timeout);
            //循环直接排除了没有的条件
            for(int i=0;i<n;++i)
            {
                int sockfd=_revs[i].data.fd;//前面刚连接的时候,已经设置进入内核
                uint32_t revents=_revs[i].events;
               // 1. 将所有的异常处理,统一转化成IO错误 2. 所有的IO异常,统一转换成为一个异常处理函数
                if(revents &EPOLLERR) 
                    revents|=(EPOLLIN|EPOLLOUT);// 1. 将所有的异常处理,统一转化成IO错误
                if(revents& EPOLLHUP)
                    revents|=(EPOLLIN|EPOLLOUT);// 1. 将所有的异常处理,统一转化成IO错误
                if(revents&EPOLLIN)
                    {
                        if(IsConnectionExits(sockfd));
                        //不用再区分是listen监视 还是普通socket ,因为使用了继承 多态,虽然调用同一个函数 ,但会进行重写
                        _connections[sockfd]->Recver();
                    }
                if(revents&EPOLLOUT)
                {
                    if(IsConnectionExits(sockfd)); //判断是真的就绪了,还是因为出错导致设置的
                        _connections[sockfd]->Sender();
                }
            }
        }
    }

1.6 对start进一步解耦

bash 复制代码
 bool IsConnectionExits(int sockfd)
    {
        auto pos = _connections.find(sockfd);
        if (pos == _connections.end())
            return false;
        return true;
    }
    bool IsConnectionEmpty()
    {
        return _connections.empty();
    }
    int LoopOnce()
    {
        int timeout = -1;
        return _epoll_ptr->Wait(_revs, rev_num, timeout);
    }
    // 事件派发器
    void Distribute(int n)
    {
        // 循环直接排除了没有的条件
        for (int i = 0; i < n; ++i)
        {
            int sockfd = _revs[i].data.fd; // 前面刚连接的时候,已经设置进入内核   前面写入内核,这里从内核取回
            uint32_t revents = _revs[i].events;
            // 1. 将所有的异常处理,统一转化成IO错误 2. 所有的IO异常,统一转换成为一个异常处理函数
            if (revents & EPOLLERR)
                revents |= (EPOLLIN | EPOLLOUT); // 1. 将所有的异常处理,统一转化成IO错误
            if (revents & EPOLLHUP)
                revents |= (EPOLLIN | EPOLLOUT); // 1. 将所有的异常处理,统一转化成IO错误
            if (revents & EPOLLIN)
            {
                if (IsConnectionExits(sockfd))
                    ;
                // 不用再区分是listen监视 还是普通socket ,因为使用了继承 多态,虽然调用同一个函数 ,但会进行重写
                _connections[sockfd]->Recver();
            }
            if (revents & EPOLLOUT)
            {
                // if(IsConnectionExits(sockfd)); //判断是真的就绪了,还是因为出错导致设置的
                // _connections[sockfd]->Sender();
            }
        }
    }
bash 复制代码
  void Start()
    {
        if (IsConnectionEmpty())
            return;
        _isrunning = true;
        while (_isrunning)
        {
            int n = LoopOnce();
            Distribute(n);
        }
    }

1.7 完善Listener的Rcver 注意阻塞与非阻塞

fd的非阻塞设置

bash 复制代码
void SetNonBlock(int fd)
    {
        int fl=fcntl(fd,F_GETFD);
        if(fl<0) return ;
        fcntl(fd,F_SETFD,fl|O_NONBLOCK);
    }
bash 复制代码
    void Recver()
    {
        // accept
        // 新连接就绪了,你能保证只有 一个连接到来吗? 一次把所有的连接全部获取上来
        // while, ET, sockfd设置为非阻塞!! ---- listensock本身设置为非阻塞
        LOG(LogLevel::INFO) << "进入到了Listener的Recver模块...";
        InetAddr client;
        while (true)
        {

            int sockfd = _listensock->Accept(&client);
            if (sockfd == -1)
            {
                LOG(LogLevel::ERROR) << " accept done";
                break;
            }
            else if (sockfd == -2)
            {
                LOG(LogLevel::ERROR) << "continue";
                continue;
            }
            else if (sockfd == -3)
            {
                LOG(LogLevel::ERROR)<<"err";
                break;
            }
            else {
                //说明此时就是普通的描述符了,那么跟Listener一样,channel也创建一个类,继承Connection
                
            }
        }
    }

1.8 普通fd 回调指针立大功

就如上面等待注释所说,要对普通的fd就绪再进行封装

bash 复制代码
#pragma once
#include<iostream>
#include"Connection.hpp"
#include"Log.hpp"
using namespace LogModule;
class Channel :public Connection{
    public:
    Channel(int sockfd,const InetAddr&client)
    :_sockfd(sockfd),_client_addr(client){SetFd(sockfd);}

         void Recver()override
        {
            LOG(LogLevel::INFO)<<"事件派发到Channel";
        }
     void Sender()override
     {

     }
     void Excepter()override
     {

     }
    ~Channel(){
        close(_sockfd); // 关闭了Channel的_sockfd
    close(GetFd()); // 又关闭了Connection的_fd(与_sockfd是同一个值,重复关闭)
    // 若某次_sockfd=0,就会意外关闭fd=0
    }
    private:
    int _sockfd;
    InetAddr _client_addr;
};
bash 复制代码
else {
                //说明此时就是普通的描述符了,那么跟Listener一样,channel也创建一个类,继承Connection
                std::shared_ptr<Connection> coon=std::make_shared<Channel>(sockfd,client);
                coon->SetEvent(EPOLLIN|EPOLLET);
                GetOwner()->AddConnection(coon);
            }

而经过回调后,listener channel eooll的关系

1.9 从缓冲区读取数据

bash 复制代码
#pragma once
#include <iostream>
#include <functional>
#include "Connection.hpp"
#include "Log.hpp"
#define SIZE 1024
using namespace LogModule;
class Channel : public Connection
{
public:
    Channel(int sockfd, const InetAddr &client)
        : _sockfd(sockfd), _client_add(client)
    {
        SetNonBlock(_sockfd);
    }
    // 问题一:怎么保证本轮数据读完
    // 问题二:即使你把本轮数据读完 你怎么知道是完整报文呢   如果有多个报文呢?粘包问题?
    void Recver() override
    {
        LOG(LogLevel::INFO) << "事件派发到Channel";
        char buffer[SIZE];
        while (true)
        {
            buffer[0] = 0;
            ssize_t n = recv(_sockfd, buffer, sizeof(buffer) - 1, 0); // 非阻塞读取
            if (n > 0)
            {
                buffer[n] = 0;
                _inbuffer += buffer;
            }
            else if (n == 0)
            {
                Excepter();

                return;
            }
            else
            {

                if (errno == EAGAIN || errno == EWOULDBLOCK)
                    break;
                if (errno == EINTR)
                    continue;
                else
                {
                    Excepter();
                    return;
                }
            }
            LOG(LogLevel::DEBUG) << "Channel: Inbuffer:\n"
                                 << _inbuffer;
            if (!_inbuffer.empty())
                _outbuffer += _handler(_inbuffer); // 和protocol相关的匿名函数里面!
        }
    }
    void Sender() override
    {
    }
    void Excepter() override
    {
    }
    int GetFd() override
    {
        return _sockfd;
    }

    ~Channel()
    {
        close(_sockfd); // 关闭了Channel的_sockfd
        // 若某次_sockfd=0,就会意外关闭fd=0
    }

private:
    int _sockfd;
    std::string _inbuffer; // 充当缓冲区
    std::string _outbuffer;

    InetAddr _client_add;
};
相关推荐
SilentSamsara4 小时前
类型注解进阶:Union、Optional、Any 与 Callable
开发语言·python·青少年编程
@SmartSi4 小时前
AgentScope Java 入门:如何使用 DashScopeChatModel 集成百练模型
java·agentscope
恣艺4 小时前
Python 游戏开发与文件处理:PyGame + Turtle + openpyxl + python-docx + PyPDF2
开发语言·python·pygame
爱编程的小新☆4 小时前
JAVA实现Manus智能体
java·react·cot·智能体·spring ai·manus·agent loop
用户3721574261354 小时前
Java 如何插入和删除 Excel 行和列
java
翼龙云_cloud4 小时前
云代理商:Hermes Agent在量化交易中的实战应用
运维·服务器·人工智能·ai智能体·hermes agent
@SmartSi4 小时前
AgentScope Java 入门:如何使用 OpenAIChatModel 集成兼容 OpenAI 协议模型
java·agentscope
南境十里·墨染春水4 小时前
数据结构 —— 顺序表
数据结构