Linux网络 | 多路转接Reactor

        前言:本节内容结束Linux网络部分。本节将要简单实现一下多路转接Reactor的代码,制作一个多路转接版本的四则运算计算器服务器。Reactor的代码相当困难,除了350多行新代码, 还要用到我们之前写的许多文件, 比如之前写的计算服务的客户端代码、 序列化反序列化的代码、Epoller文件等等。代码非常困难,友友们做好心理准备, 现在废话不多说, 开始我们的学习吧!

        ps:本节代码逻辑最好能看懂哦!

目录

ET和LT 

Reactor代码实现

准备文件 

TcpServer.hpp

Connection

TcpServer

构造函数

析构函数

Init

SetNonBlockOrDie

 AddConnection

Accepter 

Loop

Dispatcher

 Recver

DefaultOnMessage

 Sender

Excepter


Reactor是epoll的一种工作模式。 现在我们来引出这种工作模式

ET和LT 

        epoll有两种工作模式:ET与LT。

  • LT叫做: level Triggered水平触发
  • ET叫做: Edge Triggered边缘触发

        epoll的默认工作模式是LT。什么是T,就是如果我们事件就绪,里面的内容如果我们不处理,那么他就会一直提醒我们事件就绪了,事件就绪了,一直是有效的。

        ET是什么,从无到有,从有到多, 变化的时候, 才会提醒我们一次。 

        就类似于取快递,张三是只要有快递,就给你打电话一直通知你。李四是只有当新快递到来的时候才会通知你,不管你取不取走,取走多少,只通知一次,即便是假如有五个快递,你取了,但是只取走了四个,也同样不会再通知你。

        然后这里涉及到了一个面试题:就是为什么ET模式下fd必须是non_block?

        因为ET模式下只变化时通知一次。所以它会倒逼着程序员,每次通知,都必须把本轮的数据全部取走。如何全部取走? 就是循环读取,一直到读取出错。这个时候fd就是阻塞的,这样服务就挂起了,就相当于服务挂掉了。为了不让他阻塞,就必须在ET模式下,所有的fd必须是non_block。 

        然后,ET和LT谁的效率更高呢? 首先,ET的通知效率更高。 但是不仅仅如此,ET的IO效率也会更高。为什么ET的IO效率更高呢? 原因就是因为ET每次通知一次,上层程序员就把数据全部取走了。这样也能保证TCP会向对方通知一个更大的窗口,从而概率上让对方一次给我发送更多的数据。

        但是,不能说ET一定比LT效率高。因为LT也可以将所有的fd设置成non block,然后循环读取。这样就保证了在通知第一次的时候,数据就全部被取走了,就和ET一样了。 

        底层有数据了,通知上层。本质上是什么意思呢?底层数据就绪了,就是网卡中断产生数据交给网卡驱动就绪了。那么如果是ET,就是只要数据就绪,不管有没有数据,只有当中断产生数据的时候调用callback,就只回调一次callback,将节点链入到就绪队列里面。LT就是每次我们epoll_create,只要底层有数据都调用一次callback,将就绪的数据链入到就绪队列里面。

        现在我们就写一下epoll的ET工作模式。 Reactor

Reactor代码实现

准备文件 

        先简单准备一下文件, 这里准备的是暂时需要的, 后续需要别的文件再添加即可。

        其中Epoller.hpp和nocopy.hpp是上一节:Linux网络 | 多路转接epoll-CSDN博客讲述的。Socket套接字和Log.hpp上一节同样使用过。实现过程博主已经讲过:linux网络 | 序列化反序列化的概念 与 结合网络计算器深度理解-CSDN博客   ——Socket组件

linux进程间通信——命名管道、 日志程序_进程间通信日志系统-CSDN博客  ——日志组件

        我们的主要中心就放到TcpServer.hpp上面。 Reactor的主要逻辑都在这里。

TcpServer.hpp

Connection

        首先创建Connection类, 这个类意思是连接, 用来保存sockfd和对应事件。同时保存对应事件的处理方法、 异常的处理方法、sockfd对应的接收缓冲区和发送缓冲区。还能添加一个指向底层TcpServer对象的回指指针, 这个回指指针为什么要有, 怎么用, 后面说。 

        然后是接口, 说完了属性就来看看接口。 接口我们要把Sock能够获取到, 所以有一个SockFd()来获取sockfd。 然后还要能获取到接收缓冲区和发送缓冲区, 所以也要有对应的接口。然后读数据的时候, 接收缓冲区在后面要能够追加字符串。 发送数据的时候, 发送缓冲区要能够截取字符串。 Connection不提供如何将接收缓冲区的数据与发送缓冲区的数据联系起来的操作, 这些事情如何处理, 由上层去做。 因为我们Connection已经把发送缓冲区和接收缓冲区暴露出去了, 暴露给上层了。 上层怎么处理, 是上层的事情。 

        下面是Connection类的创建:

class Connection
{
public:
    Connection(int sock, shared_ptr<TcpServer> tcp_server_ptr)
        : _sock(sock), _tcp_server_ptr(tcp_server_ptr)
    {
    }
    void SetCallBack(func_t recv_cb, func_t send_cb, func_t except_cb) // 设置回调函数
    {
        _recv_cb = recv_cb;
        _send_cb = send_cb;
        _except_cb = except_cb;
    }

    int SockFd()
    {
        return _sock;
    } 

    void AppendInbuffer(const string& info)
    {
        _inbuffer += info;
    }

    void AppendOutbuffer(const string& info)
    {
        _outbuffer += info;
    }

    string& Inbuffer()
    {
        return _inbuffer;
    }

    string& Outbuffer()
    {
        return _outbuffer;
    }

    

    ~Connection()
    {
    }

private:
    int _sock;         // 链接所在的套接字
    string _inbuffer;  // string二进制流,vector, 读到的数据放到这里。
    string _outbuffer; // 未来通过套接字读取数据时,从inbuffer读取。

public:
    func_t _recv_cb;   // 读回调。 一旦数据就绪,就使用这个函数。
    func_t _send_cb;   // 写回调。
    func_t _except_cb; //

    // 添加一个指向底层TcpServer对象的回指指针。
    shared_ptr<TcpServer> _tcp_server_ptr;
    string _ip;
    uint16_t _port;
};

TcpServer

        先说TcpServer要有的属性, 因为是epoll, 所以要有epoll模型。我们将epoll模型封装成了Epoller对象, 所以即Epoller对象(Epoller文件里面的结构体实例化对象)。然后要有一个主监听套接字listensock, 专门来监听新连接。然后也要有一个保存Connection对象的容器, 因为这个Connection和sockfd是唯一对应的,所以我们可以使用哈希map存储键值对{sockfd, Connection},博主为了方便管理, 用的shared_ptr封装了一下Connection,键值对变成了{sockfd, shared_ptr<Connection>。 然后就是接收到的事件就绪数组:revs(epoll_event类型)。还有一个上层处理信息的回调函数:_OnMessage。然后端口号port, 退出标识符quit不解释。 最容易出错的一个地方还有一个属性, 就是一个指向自身的智能指针, 这个指针用来创建Connection连接的时候里面的那个TcpServer回指指针的拷贝, 注意,这里必须有指向自己的智能指针, 因为只有智能指针拷贝智能指针的时候才能增加引用计数, 如果创建Connection时候直接用this指针构造智能指针, 那么每一个Connection里面的智能指针的引用计数都是1, 不会增加引用计数, 当我们销毁Connection的时候就会出现问题。 

        我们先把里面的属性写出来, 然后接口一步一步说:

class TcpServer
{
public:


private:
    shared_ptr<Epoller> _epoller_ptr;   // epoll模型
    shared_ptr<Socket> _listensock_ptr; // 套接字, 用来监听新连接

    unordered_map<int, shared_ptr<Connection>> _Connections; // 从一个文件描述符到一个epoller链接的映射。服务器管理的所有的链接
    struct epoll_event revs[num];
    uint16_t _port;
    bool _quit;
    shared_ptr<TcpServer> _self;

    func_t _OnMessage;  //让上层处理信息,将数据交给上层
};

        然后来看一下接口:

我们上层传入的函数, 还有以后我们的发送, 接收, 异常处理。这几个接口我们都是用的是一个包装类, 使用包装器封装的回调函数类。 即func_t, 这个func_t包装如下:

using func_t = function<void(shared_ptr<Connection>)>; // 定义一个函数对象

构造函数

        然后看一下构造函数, 构造函数上层要传入端口号, 传入上层处理数据的函数。 

    TcpServer(uint16_t port, func_t OnMessage)
        : _port(port), _epoller_ptr(new Epoller()), 
        _listensock_ptr(new Socket()), _quit(false),
        _OnMessage(OnMessage),
        _self(shared_ptr<TcpServer>(this))
    {
    }

析构函数

        析构函数默认就可以, 下面是博主为了调试。

    ~TcpServer()
    {
        cout << "~  TcpServer()"<< endl;
    }

Init

        Init函数, 其实监听套接字的初始化。其中要注意的是两个接口:一个是SetNonBlockOrDie, 这个是封装的一个设置套接字变成非阻塞的接口。 第二个是AddConnection,AddConection就是创建新连接, 当有新的连接到来时就调用这个AddConnection。需要传入新到来的sockfd, 以及要关心的事件, 同时还要添加进读方法, 写方法, 异常处理方法。 以及自己的端口号和ip。然后listensock要传入的读方法和其他的文件描述符有所不同, 是一个Accepter接口,用来监听新连接。

    void Init()
    {
        _listensock_ptr->InitSocket();
        SetNonBlockOrDie(_listensock_ptr->Fd());
        _listensock_ptr->Bind(_port);
        _listensock_ptr->Listen();

        AddConnection(_listensock_ptr->Fd(), EVENT_IN, 
        bind(&TcpServer::Accepter, this, placeholders::_1)
        , nullptr, nullptr, "0.0.0.0", _port); // 监听套接字没有回调函数, 他调用的是连接管理器。
    }

SetNonBlockOrDie

        设置fd变成非阻塞, 这个我们重新在一个文件中写。

#include <iostream>
#include <unistd.h>
#include <fcntl.h>
using namespace std;

void SetNonBlockOrDie(int sockfd)
{
    int fl = fcntl(sockfd, F_GETFL);
    if (fl < 0)
        exit(1);
    fcntl(sockfd, F_SETFL, fl | O_NONBLOCK);
}

 AddConnection

        添加新连接, 就是先创建一个Connection的智能指针对象, 将参数都设置进去, 然后同时将sockfd和事件添加到epoll模型当中。 以及将连接智能指针对象保存到哈希表中。

    void AddConnection(int sock, uint32_t event, func_t recv_cb, func_t send_cb, 
        func_t except_cb, const string& ip, uint16_t port)
    {
        // 还要给sock也创建一个connection  对象,将sock添加到connection中,同时sock和connection放入connections。
        shared_ptr<Connection> new_connection = make_shared<Connection>(sock, _self); // 这里是构造connection。
        new_connection->SetCallBack(recv_cb, send_cb, except_cb);                  //构造shared_ptr, 构造自己的。但是不应该构造,应该传拷贝
        new_connection->_ip = ip;
        new_connection->_port = port;
        _Connections.insert(make_pair(sock, new_connection)); // 将sock套接字和conntection放到映射表中。


        // 我们添加对应的事件,除了要加到内核中,关注fd, event
        _epoller_ptr->EpollerUpdate(EPOLL_CTL_ADD, sock, event); // 将对应sock套接字添加到内核中只是第一步

        lg(Debug, "add a new connection success, sockfd is : %d", sock);
    }

Accepter 

        监听新连接, 和之前写的epoll监听新连接方法一样, 不同的是要获取对方的ip地址和端口号保存下来,同时还要将读写以及处理异常方法设置进去。 

    void Accepter(shared_ptr<Connection> connection)
    {
        while (true)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);

            int sock = ::accept(connection->SockFd(), (sockaddr*)&peer, &len);
            if (sock > 0) //来到新连接
            {
                uint16_t peerport = ntohs(peer.sin_port);      //网络序列转主机序列
                char ipbuf[128];
                inet_ntop(AF_INET, &peer.sin_addr.s_addr, ipbuf, sizeof(ipbuf));
                string peerip = ipbuf;
                lg(Debug, "get a new client, get info-> [%s: %d], sockfd : %d\n", ipbuf, peerport, sock);

                SetNonBlockOrDie(sock); //设置非阻塞
                //listensock只需要recv_rb, 但是其他的sock, 读, 写, 处理异常都要有
                AddConnection(sock, EVENT_IN, bind(&TcpServer::Recver, this, placeholders::_1),
                bind(&TcpServer::Sender, this, placeholders::_1),
                bind(&TcpServer::Excepter, this, placeholders::_1), peerip, peerport);   //添加新连接事件
            }
            else
            {
                if (errno == EWOULDBLOCK)  break; //底层没有数据
                else if (errno == EINTR) continue;   //EINTR为信号中断,信号导致的读取出错,挂起
                else break;
            }
        }
    }

Loop

        Loop其实就相当于Start函数, 就是启动服务。一直死循环下去。 然后每一个循环都要调用Dispatcher函数分发事件。 

    void Loop()
    {
   
        _quit = false;

        while (!_quit)
        {
            Dispatcher(3000);
            PrintConnection();
        }

        _quit = true;
    }

Dispatcher

        谈完Loop那么就谈Dispatcher, Dispatcher, 就是调用找到就绪的文件描述符, 根据事件类型对文件描述符执行不同的操作。这里面的IsConnectionSafe()其实就是一个判断sockfd是否安全的函数, 如果安全就返回真, 否则返回假。


    void Dispatcher(int timeout)
    {
        int n = _epoller_ptr->EpollErWait(revs, num, timeout); // 获得n个就绪事件

        for (int i = 0; i < n; i++)
        {
            // 事件就绪处理
            uint32_t event = revs[i].events;
            int sock = revs[i].data.fd;
            //如果是EPOLLERR和EPOLLHUP, 也就是事件出错了, 异常了。统一把所有的事件异常转化为读写问题。
            //这样就能让异常集中处理, 不再扩散到代码的任意位置。
            if (event & EPOLLERR) 
                event |= (EPOLLIN | EPOLLOUT);
            if (event & EPOLLHUP)
                event |= (EPOLLIN | EPOLLHUP);

            //事件派发里, 只需要处理EPOLLIN和EPOLLOUT就可以了。因为出异常只可能出现在这两个时间里面。
            if ((event & EPOLLIN) && IsConnectionSafe(sock))
            {
                if (_Connections[sock]->_recv_cb)  //处理写事件
                {
                    _Connections[sock]->_recv_cb(_Connections[sock]);   //这里传过去的就是就绪的文件描述符所在的连接
                }
            }
            if ((event & EPOLLOUT) && IsConnectionSafe(sock))  //写事件被设置成关心, 去调用Sender方法
            {
                if (_Connections[sock]->_send_cb)
                {
                    _Connections[sock]->_send_cb(_Connections[sock]);
                }
            }
        }
    }

 Recver

        当一个文件描述符读事件就绪了, 那么根据Dispatcher的逻辑, 就直接进入了Recver函数。

Recver函数因为是ET模式, 所以必须要把底层的内核缓冲区里面的数据全部读完。那么最后一次读, 就一定是EWOULDBLOCK, 即错误码11。 所以如果是EWOULDBLOCK, 那么就退出循环,但是不return, 只是退出循环去执行后面的上层数据处理函数。 如果是读取数据读取到了0的时候, 就代表对面把连接关掉了, 那么sockfd就没有用了, 异常处理就行。 异常处理函数为Excepter, 保存在_except_cb成员里面, 后面会讲到。同样的, 如果是信号中断了一下, 这个时候还可以继续接着读取,就不用退出循环。其他情况下的异常, 都要Excepter。

    void Recver(shared_ptr<Connection> connection)  //这里传过来的
    {
        //
        int sock = connection->SockFd(); //得到文件描述符

        //ET模式, 必须把数据全部读完
        while (true)
        {
            char buffer[g_buffer_size];
            memset(buffer, 0, sizeof(buffer));
            ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0);  //设置为0,原fd为非阻塞读取, 所以这里就是非阻塞读取。
            if (n > 0)    //读取成功
            {
               connection->AppendInbuffer(buffer); //读取成功, 就不断的将buffer缓冲区里面的数据放到connection里面维护的inbuffer里面。 
            }
            else if (n == 0)  //对端把连接关了
            {
                lg(Info, "sockfd: %d, client info %s : %d quit...", connection->SockFd(), connection->_ip.c_str(), connection->_port);  
                //一旦对方连接关了, 我们对应的这个连接, 就直接进入异常处理的流程:
                connection->_except_cb(connection); 
                return;
            }
            else  //读取出错
            {
                //非阻塞处理的时候:出错了errno == EWOULDBLACK, 表明本轮数据已经读完。那么文件描述符没有问题。
                //如果errno == EINET, 表明是因为被某个信号中断了,文件描述符没有问题, 并且数据可能没有读完, 继续读取.
                //如果errno == 其他的, 那么就是说明读取出错了, 文件描述符出问题了, 这个连接就可以Excepter了。
                if (errno == EWOULDBLOCK)
                {
                    break;
                }
                else if (errno == EINTR)
                {
                    continue;
                }
                else
                {
                    lg(Waring, "sockfd: %d, client info %s : %d recv error...", connection->_ip.c_str(), connection->_port);  
                    connection->_except_cb(connection);
                    return;
                }
                
            }
        }

        //数据有了, 但不一定全。1、先检测。2、如果有完整报文, 就处理。
        _OnMessage(connection);   //读到的所有的数据, 都在connection内部。读完之后回调给上层。
    }

DefaultOnMessage

        这个函数是放到主函数的, 也就是我们说的上层处理数据方法, 传给tcp服务的。这里的序列化反序列化的接口以及处理数据的接口我们使用的是以前写计算服务的代码。 这里不写了, 直接贴过来。

Calculator.hpp

#include "protocol.hpp"
#include<iostream>
using namespace std;

//上层业务
class Calculator
{
public:
    Response CalculatorHelper(Request req)
    {
        Response res(0, 0);
        res.result_ = res.code_ = 0;

        switch (req.op_)
        {
        case '+':
            res.result_ = req.x_ + req.y_;
            break;
        case '-':
            res.result_ = req.x_ - req.y_;
            break;
        case '*':
            res.result_ = req.x_ * req.y_;
            break;
        case '/':
        {
            if (req.y_ == 0)
            {
                res.code_ = 1;
                res.result_ = 0;
            }
            else
            {
                res.result_ = req.x_ / req.y_;
                res.code_ = 0;
            }
            break;
        }
        case '%':
        {
            if (req.y_ == 0)
            {
                res.code_ = 1;
                res.result_ = 0;
            }
            else
            {
                res.result_ = req.x_ % req.y_;
                res.code_ = 0;
            }
            break;
        }
        default:
        {
            res.code_ = 1;
            res.result_ = 0;
            break;
        }
        }
        return res;
    }

    string Handler(string &package) // 接受一个报文
    {
        string content;
        bool r = Decode(package, &content); // 将报文解包,如果这个时候报文, 第一个数字是\n说明后面一定是一个 然后把数据放到新的字符串中
        if (!r)
            return "";
        //
        Request req;              // 重新创建一个变量
        req.DeSerialize(content); // 然后将变量反序列化
        Response res = CalculatorHelper(req);

        content.clear();
        res.Serialize(&content);
        content = Encode(content);  
        cout << content << endl;
        return content;
    }
};

 protocal.hpp

#pragma once
#include<iostream>
using namespace std;
#include<string>
#include<jsoncpp/json/json.h>


// #define Myself 1

//封装报头
string Encode(string& content)
{
    string package = to_string(content.size());
    package += "\n";
    package += content;
    package += "\n";

    
    return package;

}

//解开报头
bool Decode(string& package, string* content)
{
    int pos = package.find('\n', 0); //查找第一个\n
    if (pos == string::npos) return false;
    
    string len_str = package.substr(0, pos);
    int len = stoi(len_str.c_str());
    ssize_t size = len_str.size() + len + 2;
    if (package.size() < size) return false;

    *content = package.substr(pos + 1, len);
    package.erase(0, size);
    return true;
}


class Request
{
public:
    Request(){}
    Request(int data1, int data2, char oper) : x_(data1), y_(data2), op_(oper){}
    ~Request(){}
    bool Serialize(string* out)
    {
#ifdef Myself

        string s;
        s += to_string(x_);
        s += " ";   
        s += op_;
        s += " ";
        s += to_string(y_);
        
        *out = s;
        return true;
#else
        Json::Value root;
        root["x"] = x_;
        root["y"] = y_;
        root["op"] = op_;

        Json::FastWriter w;
        *out = w.write(root);
        return true;    
#endif

    }

    bool DeSerialize(string in)
    {
#ifdef Myself
        string s = in;
        
        int left = s.find(' ', 0);
        if (left == string::npos) return false;
        string part_x = s.substr(0, left);

        int right = s.find(' ', left + 1);
        if (right == string::npos) return false;
        char part_op = s[left + 1];

        string part_y = s.substr(right + 1);
        x_ = stoi(part_x);
        y_ = stoi(part_y);
        op_ = part_op;
        return true;
#else
    Json::Value root;
    Json::Reader r;
    r.parse(in, root);
    x_ = root["x"].asInt();
    y_ = root["y"].asInt();
    op_ = root["op"].asInt();
    return true;

#endif
    }

    void DebugPrint()
    {
        cout << "x = " << x_;
        cout << "; lop = " << op_;
        cout << "; y = " << y_ << endl; 
    }
    

    int x_;
    int y_;
    char op_;
};



class Response
{
public:
    Response(){}
    Response(int result, int code) : result_(result), code_(code)
    {}
    ~Response(){}
    bool Serialize(string* out)
    {
#ifdef Myself
        string s;
        s += to_string(result_);
        s += " ";
        s += to_string(code_);
        *out = s;

        return true;

#else
        Json::Value root;
        root["result"] = result_;
        root["code"] = code_;

        Json::FastWriter w;
        *out = w.write(root);
        return true;    
#endif

    }
    
    bool DeSerialize(string in)
    {
#ifdef Myself
        string s = in;

        int pos = s.find(' ', 0);
        if (pos == string::npos) return false;

        string part_res = s.substr(0, pos);
        string part_code = s.substr(pos + 1);
        
        result_ = stoi(part_res);
        code_ = stoi(part_code);


        return true;
#else
    Json::Value root;
    Json::Reader r;
    r.parse(in, root);
    result_ = root["result"].asInt();
    code_ = root["code"].asInt();
    return true;
#endif
    }
    
    void DebugPrint()
    {
        cout << "结果响应完成, result: " << result_ << "  , code: " << code_ << endl;
    }


    int result_;
    int code_;
};

Calculator calculator;

void DefaultOnMessage(shared_ptr<Connection> connection_ptr)
{
    lg(Debug, "上层已经得到数据\b");
    //处理数据
    string response = calculator.Handler(connection_ptr->Inbuffer());
    if(response.empty()) return;

    //发送出去
    lg(Debug, "%s", response.c_str());
    //response发送出去
    connection_ptr->AppendOutbuffer(response);  //将结果拷贝到输出缓冲区里面。
    //正确理解发送
    connection_ptr->_tcp_server_ptr->Sender(connection_ptr);
}

 Sender

        Sender方法就是写方法,其实我们从讲多路转接开始,从来没有讲过发送的问题。现在我们来看看如何正确理解发送?

        把数据真正发出去,在select、poll、epoll中,因为写事件的本质就是发送缓冲区是否有空间,这个经常是OK的,所以经常就是就绪的。如果我们设置对EPOLLOUT关心,那么EPOLLOUT几乎每次都有就绪。就导致epollserver经常返回,浪费cpu资源。

        所以结论:对于读,设置长关心。对于写,按需设置。就是如下代码:

    void Sender(shared_ptr<Connection> connection)
    {
        auto& outbuffer = connection->Outbuffer();
        while (true)
        {
            ssize_t n = send(connection->SockFd(), outbuffer.c_str(), outbuffer.size(), 0); 
            if (n > 0)
            {
                outbuffer.erase(0, n);
                if (outbuffer.empty()) break;

            }
            else if(n == 0)
            {
                connection->_except_cb(connection);
                return;
            }
            else
            {
                if (errno == EWOULDBLOCK) break;
                else if (errno == EINTR) continue;
                else 
                {
                    lg(Waring, "sockfd: %d, client info %s : %d send error...", connection->_ip.c_str(), connection->_port);  
                    connection->_except_cb(connection);
                    return;
                }
            }
        }

        if (!outbuffer.empty())
        {
            //开启对写事件的关心
            EnableEvent(connection->SockFd(), true, true);
        }
        else
        {
            //关闭对写事件的关心
            EnableEvent(connection->SockFd(), true, false);
        }

    }

Excepter

        excepter是对一场做处理, 当全部读完, 或者全部写完的时候, 都需要异常处理。

    void Excepter(shared_ptr<Connection> connection)
    {
        int sockfd = connection->SockFd();
        lg(Debug, "Excepter hander sockfd : %d, client info %s : %d Excepter hander...",
            sockfd, connection->_ip.c_str(), connection->_port);  
        //异常处理
        //1、先移除关心事件
        _epoller_ptr->EpollerUpdate(EPOLL_CTL_DEL, sockfd, 0);
        //2、关闭异常的文件描述符
        close(sockfd);
        
        //3、将fd->connection映射表中将对应的连接进行移除。即从unordered_map中移除
        _Connections.erase(sockfd); //erase释放, 就是释放整个节点, 这个节点是{sockfd, connection_ptr} //释放对应的节点。但是
        //出现了双重释放。 双重释放了什么? 
        lg(Debug, "remove %d from, _connections...\n", connection->SockFd()); 
    }

        以上重要代码就都讲完了, 后面会有整个的代码博主会上传到资源里面, 有兴趣的友友们自行下载。

——————以上就是本节全部内容哦, 如果对友友们有帮助的话可以关注博主, 方便学习更多知识哦!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/972013.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

数控机床设备分布式健康监测与智能维护系统MTAgent

数控机床设备分布式健康监测与智能维护系统MTAgent-v1.1融合了目前各种先进的信号处理以及信息分析算法以算法工具箱的方式&#xff0c;采用了一种开发的、模块化的结构实现信号各种分析处理&#xff0c;采用Python编程语言&#xff0c;满足不同平台需求(包括Windows、Linux)。…

Opencv项目实战:26 信用卡号码识别与类型判定

项目介绍 在日常生活中&#xff0c;信用卡的使用越来越普遍。本项目的主要目标是通过图像处理技术自动识别信用卡号码&#xff0c;并根据信用卡号码的第一个数字判定信用卡的类型&#xff08;如Visa、MasterCard等&#xff09;。项目结合了图像预处理、轮廓检测、模板匹配等技…

利用websocket检测网络连接稳定性

浏览器中打开F12&#xff0c;控制台中输入以下内容 > 回车 > 等待结果 连接关闭 表示断网 let reconnectDelay 1000; // 初始重连间隔 let pingInterval null; let socketManuallyClosed false; // 标志是否手动关闭function createWebSocket() {if (socketManuallyCl…

WPF9-数据绑定进阶

目录 1. 定义2. 背景3. Binding源3.1. 使用Data Context作为Binding的源3.2. 使用LINQ检索结果作为Binding的源 4. Binding对数据的转换和校验4.1. 需求4.2. 实现步骤4.3. 值转换和校验的好处4.3.1. 数据转换的好处 4.4. 数据校验的好处4.5. 原理4.5.1. 值转换器原理4.5.2. 数据…

【Unity Shader编程】之图元装配与光栅化

执行方式&#xff1a;自动完成 图元装配自动化流程 顶点坐标存入装配区 → 按绘制模式连接顶点 → 生成完整几何图元 示例&#xff1a;gl.drawArrays(gl.TRIANGLES, 0, 3)自动生成三角形 会自动自动裁剪超出屏幕范围&#xff08;NDC空间外&#xff09;的三角形&#xff0c;仅保…

ssm121基于ssm的开放式教学评价管理系统+vue(源码+包运行+LW+技术指导)

项目描述 临近学期结束&#xff0c;还是毕业设计&#xff0c;你还在做java程序网络编程&#xff0c;期末作业&#xff0c;老师的作业要求觉得大了吗?不知道毕业设计该怎么办?网页功能的数量是否太多?没有合适的类型或系统?等等。这里根据疫情当下&#xff0c;你想解决的问…

网工项目理论1.11 网络出口设计

本专栏持续更新&#xff0c;整一个专栏为一个大型复杂网络工程项目。阅读本文章之前务必先看《本专栏必读》。 一.网络出口接入技术 二.单一出口网络结构 三.同运营商多出口结构 四.多运营商多出口结构——出向流量 五.多运营商多出口结构——服务器访问流量 六.多运营商多出口…

Django 5 实用指南(一)安装与配置

1.1 Django5的背景与发展 Django 自从2005年由Adrian Holovaty和Simon Willison在 Lawrence Journal-World 新闻网站上首次发布以来&#xff0c;Django 一直是 Web 开发领域最受欢迎的框架之一。Django 框架经历了多个版本的演进&#xff0c;每次版本更新都引入了新功能、改进了…

Redis实战-扩展Redis

扩展Redis 1、扩展读性能2、扩展写性能和内存容量3、扩展复杂的查询3.1 扩展联合查询3.2 扩展分片排序 如有侵权&#xff0c;请联系&#xff5e; 如有错误&#xff0c;也欢迎批评指正&#xff5e; 本篇文章大部分是来自学习《Redis实战》的笔记 1、扩展读性能 单台Redis服务器…

【AI面板识别】

题目描述 AI识别到面板上有N&#xff08;1 ≤ N ≤ 100&#xff09;个指示灯&#xff0c;灯大小一样&#xff0c;任意两个之间无重叠。 由于AI识别误差&#xff0c;每次别到的指示灯位置可能有差异&#xff0c;以4个坐标值描述AI识别的指示灯的大小和位置(左上角x1,y1&#x…

朴素模式匹配算法与KMP算法(有next[]和nextval[]详细讲解

这篇文章是建立在上篇文章的基础上的,看此篇文章要有串的基本知识 举个例子引进我们今天的知识 假设我们这里有两个字符串,一个主串,一个子串 主串: aaa223aa225 子串: aa22 我们这里需要进行匹配,传统的朴素模式匹配算法,就是主串下标i从1开始,主串j从1开始…

文件操作(PHP)(小迪网络安全笔记~

免责声明&#xff1a;本文章仅用于交流学习&#xff0c;因文章内容而产生的任何违法&未授权行为&#xff0c;与文章作者无关&#xff01;&#xff01;&#xff01; 附&#xff1a;完整笔记目录~ ps&#xff1a;本人小白&#xff0c;笔记均在个人理解基础上整理&#xff0c;…

【分治法】棋盘覆盖问题 C/C++(附代码和测试实例及算法分析)

问题描述 在一个 2 k 2 k 2^k \times 2^k 2k2k大小的棋盘中&#xff0c;有一个与其他方块不同的特殊方块&#xff0c;如下图红色方块。另有4种形态不同的L型骨块&#xff08;见下图&#xff09;&#xff0c;要用图示四种骨块覆盖棋盘上除特殊方格外的其他所有方格&#xff0c…

el-table的hasChildren不生效?子级没数据还显示箭头号?树形数据无法展开和收缩

问题&#xff1a;明明子级只有一条数据&#xff0c;还显示箭头号 原因&#xff1a;最开始row-key写的是id,父级和子级都有该属性&#xff0c;所以展开失效了。 解决方法&#xff1a;row-key&#xff1a;id改成 row-key"name"

2002-2019年各省人口老龄化程度数据

2002-2019年各省人口老龄化程度数据 1、时间&#xff1a;2002-2019年 2、来源&#xff1a;国家统计局、统计年鉴 3、指标&#xff1a;地区、年度、六十五岁以上占比 4、范围&#xff1a;31省 5、指标解释&#xff1a;人口老龄化是指人口生育率降低和人均寿命延长导致的总人…

面向机器学习的Java库与平台简介、适用场景、官方网站、社区网址

Java机器学习的库与平台 最近听到有的人说要做机器学习就一定要学Python&#xff0c;我想他们掌握的知道还不够系统全面。本文作者给大家介绍几种常用Java实现的机器学习库&#xff0c;快快收藏加关注吧&#xff5e; Java机器学习库表格 Java机器学习库整理库/平台概念适合场…

MySQL 之服务器配置和状态(MySQL Server Configuration and Status)

MySQL 之服务器配置和状态 1 MySQL 架构和性能优化 1.3 服务器配置和状态 设置 MySQL 服务的特性&#xff0c;可以通过 mysqld 服务选项&#xff0c;服务器系统变量和服务器状态变量这三个方面来进行设置和查看。 官方文档 https://dev.mysql.com/doc/refman/8.0/en/serve…

Linux的基础指令和环境部署,项目部署实战(下)

目录 上一篇&#xff1a;Linxu的基础指令和环境部署&#xff0c;项目部署实战&#xff08;上&#xff09;-CSDN博客 1. 搭建Java部署环境 1.1 apt apt常用命令 列出所有的软件包 更新软件包数据库 安装软件包 移除软件包 1.2 JDK 1.2.1. 更新 1.2.2. 安装openjdk&am…

LabVIEW无刷电机控制器检测系统

开发了一种基于LabVIEW的无刷电机控制器检测系统。由于无刷电机具有高效率、低能耗等优点&#xff0c;在电动领域有取代传统电机的趋势&#xff0c;而无刷电机的核心部件无刷电机控制器产量也在不断增长。然而&#xff0c;无刷电机控制器的出厂检测仍处于半自动化状态&#xff…

《仙台有树》里的馅料(序)

《仙台有树》一起追剧吧&#xff08;二&#xff09;&#xff1a;馅料合集概览 ●德爱武美玩&#xff0c;全面发展 ●猜猜我是谁&真假美清歌 ●失忆的风还是吹到了仙台 ●霸道师徒强制收&你拜我&#xff0c;我拜你&#xff0c;师徒徒师甜蜜蜜 ●霸道总裁强制爱 ●仙台有…