网络和Linux网络_15(IO多路转接)reactor编程_服务器+相关笔试题

news2024/11/16 9:54:40

目录

1. reactor的服务器

1.1 Sock.hpp

1.2 加协议分割报文

1.3 序列化和反序列化

Protocol.hpp

main.cc

Epoll.hpp

TcpServer.hpp

2. 相关笔试题

答案及解析

本篇完。


1. reactor的服务器

Log.hpp和以前一样,因为下面要写ET模式所以Sock.hpp加了一个把sock设置成非阻塞的函数:(要#include <fcntl.h>

写到TcpServer.hpp的Accepter函数再改一下Sock.hpp的Accept:(加一个输出错误码的参数)

1.1 Sock.hpp

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>
#include "Log.hpp"
#include <fcntl.h>

class Sock
{
private:
    const static int gbacklog = 20; // listen的第二个参数,现在先不管
public:
    Sock()
    {}
    ~Sock()
    {}
    static int Socket()
    {
        int listensock = socket(AF_INET, SOCK_STREAM, 0); // 域 + 类型 + 0 // UDP第二个参数是SOCK_DGRAM
        if (listensock < 0)
        {
            logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));
            exit(2);
        }
        int opt = 1;
        setsockopt(listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
        logMessage(NORMAL, "create socket success, listensock: %d", listensock);
        return listensock;
    }

    static void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0")
    {
        struct sockaddr_in local;
        memset(&local, 0, sizeof local);
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        inet_pton(AF_INET, ip.c_str(), &local.sin_addr);
        if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
            exit(3);
        }
    }

    static void Listen(int sock)
    {
        if (listen(sock, gbacklog) < 0)
        {
            logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
            exit(4);
        }
        logMessage(NORMAL, "init server success");
    }

    // 一般情况下:
    // const std::string &: 输入型参数
    // std::string *: 输出型参数
    // std::string &: 输入输出型参数
    static int Accept(int listensock, std::string *ip, uint16_t *port, int *accept_errno = nullptr)
    {
        struct sockaddr_in src;
        socklen_t len = sizeof(src);
        *accept_errno = 0;
        int servicesock = accept(listensock, (struct sockaddr *)&src, &len);
        if (servicesock < 0)
        {
            *accept_errno = errno;
            // logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
            return -1;
        }
        if (port)
            *port = ntohs(src.sin_port);
        if (ip)
            *ip = inet_ntoa(src.sin_addr);
        return servicesock;
    }

    static bool Connect(int sock, const std::string &server_ip, const uint16_t &server_port)
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(server_port);
        server.sin_addr.s_addr = inet_addr(server_ip.c_str());

        if (connect(sock, (struct sockaddr *)&server, sizeof(server)) == 0)
            return true;
        else
            return false;
    }

    static bool SetNonBlock(int sock)
    {
        int fl = fcntl(sock, F_GETFL);
        if(fl < 0) 
            return false;
        fcntl(sock, F_SETFL, fl | O_NONBLOCK);
            return true;
    }
};

下面直接放一部分TcpServer.hpp代码跟着注释看:(建议复制到VSCode里看)

#pragma once

#include <iostream>
#include <functional>
#include <string>
#include <unordered_map>
#include "Sock.hpp"
#include "Log.hpp"
#include "Epoll.hpp"

class TcpServer;
class Connection;

using func_t = std::function<void(Connection *)>;
// 为了能够正常工作,常规的sock必须要有独立的接收缓冲区和发送缓冲区(写入)
class Connection // 一个链接类
{
public:
    Connection(int sock = -1) 
        : _sock(sock), _tsvr(nullptr)
    {}
    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;
    }
    ~Connection()
    {}
public:
    int _sock; // 负责进行IO的文件描述符
    func_t _recv_cb; // 三个回调方法,是对_sock进行特定读写的对应方法
    func_t _send_cb;
    func_t _except_cb;

    std::string _inbuffer; // 接收缓冲区&&发送缓冲区
    std::string _outbuffer; // 这两string暂时没有办法处理二进制流,文本是可以的

    TcpServer *_tsvr; // 设置对TcpServer的回指指针,对写事件的关心是按需打开
};

class TcpServer
{
    const static int gport = 8080;
    const static int gnum = 128;
public:
    TcpServer(int port = gport)
        : _port(port), _revs_num(gnum)
    {
        // 1. 创建listensock
        _listensock = Sock::Socket();
        Sock::Bind(_listensock, _port);
        Sock::Listen(_listensock);

        // 2. 创建多路转接对象
        _poll.CreateEpoll();

        // 3. 添加listensock到服务器中 -> 三步(类的构造函数也能调用类的成员方法,走到函数体中对象已经存在了)
        // 后三个参数是函数对象,要bind绑定返回一个函数对象->类内函数有this指针,_1是预留的参数
        AddConnection(_listensock, std::bind(&TcpServer::Accepter, this, std::placeholders::_1), nullptr, nullptr);

        // 4. 构建一个获取就绪事件的缓冲区
        _revs = new struct epoll_event[_revs_num];
    }
    void AddConnection(int sock, func_t recv_cb, func_t send_cb, func_t except_cb) // 把任意sock进行添加到TcpServer
    {
        Sock::SetNonBlock(sock); // ET模式要把sock设置成非阻塞 -> 在Sock.hpp中写成函数

        // 除了_listensock,后面还会存在大量的socket,每一个sock都必须被封装成为一个Connection
        // 当服务器中存在大量的Connection时,TcpServer需要将所有Connection进行管理:上面描述了,组织 -> unordered_map
        // 3.1 构建conn对象,封装sock
        Connection *conn = new Connection(sock);
        conn->SetCallBack(recv_cb, send_cb, except_cb);
        conn->_tsvr = this;

        // 3.2 添加sock到epoll中(任务通知)->要知道sock和事件(任何多路转接的服务器,一般只会打开读取事件,写入事件按需打开)
        _poll.AddSockToEpoll(sock, EPOLLIN | EPOLLET);

        // 3.3 将对应的Connection*对象指针添加到Connections映射表中(业务处理)
        _connections.insert(std::make_pair(sock, conn));
    }

    void Accepter(Connection *conn)
    {
        logMessage(DEBUG, "Accepter been called");
    }

    void Dispather() // 根据就绪的事件,进行特定事件的派发
    {
        while (true)
        {
            LoopOnce();
        }
    }
    void LoopOnce()
    {
        int n = _poll.WaitEpoll(_revs, _revs_num);
        for (int i = 0; i < n; i++) // 获取事件
        {
            int sock = _revs[i].data.fd;
            uint32_t revents = _revs[i].events;
            if (revents & EPOLLIN) // 读就绪
            {
                // if(Connection是存在并且_connections[sock]->_recv_cb被设置过)
                if (IsConnectionExists(sock) && _connections[sock]->_recv_cb != nullptr)
                    _connections[sock]->_recv_cb(_connections[sock]); // 调用读事件的回调
            }
            if (revents & EPOLLOUT) // 写就绪
            {
                // if(Connection是存在并且_connections[sock]->_send_cb被设置过)
                if (IsConnectionExists(sock) && _connections[sock]->_send_cb != nullptr)
                    _connections[sock]->_send_cb(_connections[sock]); // 调用写事件的回调
            }
        }
    }
    bool IsConnectionExists(int sock) // 判定Connection是否存在
    {
        auto iter = _connections.find(sock);
        if (iter == _connections.end())
            return false;
        else
            return true;
    }
    ~TcpServer()
    {
        if (_listensock >= 0)
            close(_listensock);
        if (_revs)
            delete[] _revs;
    }
private:
    int _listensock;
    int _port;
    Epoll _poll;
    std::unordered_map<int, Connection *> _connections; // 管理:sock映射到Connection
    struct epoll_event *_revs; // 就绪事件缓冲区,就绪的文件描述符投递到这里
    int _revs_num; // 就绪事件缓冲区大小
};

编译运行:

此时成功调用了Accepter,因为是ET模式,所以是阻塞的,事件没处理也没有连续打印。

写一下Accepter再测试一下:

    void Accepter(Connection *conn)
    {
        // logMessage(DEBUG, "Accepter been called");
        // 一定是listensock已经就绪了,此次读取不会阻塞,
        // 怎么保证,底层只有一个连接就绪呢 -> 循环,直到获取失败
        while (true)
        {
            std::string clientip;
            uint16_t clientport;
            int accept_errno = 0;
            // sock一定是常规的IO sock
            int sock = Sock::Accept(conn->_sock, &clientip, &clientport, &accept_errno);
            if (sock < 0) // 获取失败
            {
                if (accept_errno == EAGAIN || accept_errno == EWOULDBLOCK)
                    break;
                else if (accept_errno == EINTR)  // 概率非常低
                    continue;
                else // accept失败
                {
                    logMessage(WARNING, "accept error, %d : %s", accept_errno, strerror(accept_errno));
                    break;
                }
            }
            else // (sock>=0)获取链接成功->将sock托管给TcpServer
            {
                AddConnection(sock, std::bind(&TcpServer::Recver, this, std::placeholders::_1),
                              std::bind(&TcpServer::Sender, this, std::placeholders::_1),
                              std::bind(&TcpServer::Excepter, this, std::placeholders::_1));
                logMessage(DEBUG, "accept client %s:%d success, add to epoll&&TcpServer success, sock: %d",\
                    clientip.c_str(), clientport, sock);
            }
        }
    }

    void Recver(Connection *conn) // 读取一个正常的sock
    {
        logMessage(DEBUG, "Recver event exists, Recver() been called");
    }

    void Sender(Connection *conn)
    {
    }

    void Excepter(Connection *conn)
    {
    }

成功获取到读取事件,下面来处理一下:

先写Recver的第一个版本:直接面向字节流,进行常规读取:

    void Recver(Connection *conn) // 读取一个正常的sock
    {
        // logMessage(DEBUG, "Recver event exists, Recver() been called");
        // v1: 直接面向字节流,先进行常规读取
        const int num = 1024;
        while (true)
        {
            char buffer[num];
            ssize_t n = recv(conn->_sock, buffer, sizeof(buffer) - 1, 0);
            if (n < 0)
            {
                if (errno == EAGAIN || errno == EWOULDBLOCK) // 读取完毕了,正常的
                    break;
                else if (errno == EINTR) // 读取被中断了,重新开始读
                    continue;
                else // 真正读取失败 -> 交给异常回调
                {
                    logMessage(ERROR, "recv error, %d : %s", errno, strerror(errno));
                    conn->_except_cb(conn);
                    break;
                }
            }
            else if (n == 0)
            {
                logMessage(DEBUG, "client[%d] quit, server close [%d]", conn->_sock, conn->_sock);
                conn->_except_cb(conn);
                break;
            }
            else // 读取成功
            {
                buffer[n] = 0;
                conn->_inbuffer += buffer; // 读取到的数据全部拼接到接收缓冲区
            }
        }
        logMessage(DEBUG, "conn->_inbuffer[sock: %d]: %s", conn->_sock, conn->_inbuffer.c_str());
    }

测试就是这样的:

每个服务端都有自己的接收缓冲区,互不影响(这里回车也被输入进去了只是telnet的原因,这里不写客户端了就这么用了),但还是那句话:怎么保证你读到的是一个完整的报文呢?->就要定制协议了,写一个Protocol.hpp:

1.2 加协议分割报文

在前面加上这行:

加个类内成员:

Dispather:

    void Dispather(callback_t cb) // 根据就绪的事件,进行特定事件的派发
    {
        _cb = cb;
        while (true)
        {
            LoopOnce();
        }
    }

main.cc:

#include "TcpServer.hpp"
#include <memory>

void NetCal(Connection *conn, std::string &request)
{
    logMessage(DEBUG, "NetCal been called, get request: %s", request.c_str());
}

int main()
{
    std::unique_ptr<TcpServer> svr(new TcpServer());
    svr->Dispather(NetCal);

    return 0;
}

改进的Recver:

    void Recver(Connection *conn) // 读取一个正常的sock
    {
        const int num = 1024;
        bool err = false;
        // logMessage(DEBUG, "Recver event exists, Recver() been called");
        while (true)
        {
            char buffer[num];
            ssize_t n = recv(conn->_sock, buffer, sizeof(buffer) - 1, 0);
            if (n < 0)
            {
                if (errno == EAGAIN || errno == EWOULDBLOCK) // 读取完毕了,正常的
                    break;
                else if (errno == EINTR) // 读取被中断了,重新开始读
                    continue;
                else // 真正读取失败 -> 交给异常回调
                {
                    logMessage(ERROR, "recv error, %d : %s", errno, strerror(errno));
                    conn->_except_cb(conn);
                    err = true;
                    break;
                }
            }
            else if (n == 0)
            {
                logMessage(DEBUG, "client[%d] quit, server close [%d]", conn->_sock, conn->_sock);
                conn->_except_cb(conn);
                err = true;
                break;
            }
            else // 读取成功
            {
                buffer[n] = 0;
                conn->_inbuffer += buffer; // 读取到的数据全部拼接到接收缓冲区
            }
        }
        logMessage(DEBUG, "conn->_inbuffer[sock: %d]: %s", conn->_sock, conn->_inbuffer.c_str());
        if (!err) // 如果错误码还是false就是正常break的
        {
            std::vector<std::string> messages;
            SpliteMessage(conn->_inbuffer, &messages);
            // 保证走到这里,就是一个完整报文
            for (auto &msg : messages)
            {    // 可以在这里将message封装成为task,然后push到任务队列,任务处理交给后端线程池,这里不处理
                _cb(conn, msg);
            }
        }
    }

一部分Protocol.hpp:(这里用大写X作为切分)

#pragma once

#include <iostream>
#include <cstring>
#include <string>
#include <vector>

// 1. 报文和报文之间,我们采用特殊字符来进行解决粘包问题
// 2. 获取一个一个独立完整的报文,序列和反序列化 -- 自定义
// 100+19X100+19X100+19
#define SEP "X"
#define SEP_LEN strlen(SEP)

// 要把传入进来的缓冲区进行切分,要求:
// 1. buffer被切走的,也同时要从buffer中移除
// 2. 可能会存在多个报文,多个报文依次放入out
void SpliteMessage(std::string &buffer, std::vector<std::string> *out) // 分割报文
{   // buffer: 输入输出型参数,out: 输出型参数
    while (true)
    {
        auto pos = buffer.find(SEP); // 在缓冲区里找分隔符
        if (std::string::npos == pos) // 找不到就break
            break;
        std::string message = buffer.substr(0, pos); // 提取子串:前闭后开区间
        buffer.erase(0, pos + SEP_LEN); // 移除子串和衡娥福
        out->push_back(message); // push_back完整的子串
        // std::cout << "debug: " << message << " : " << buffer << std::endl;
        // sleep(1);
    }
}

编译运行:

此时就成功把报文分开了。

1.3 序列化和反序列化

把以前自己写的序列化和反序列化复制到Protocol.hpp:

Protocol.hpp

#pragma once

#include <iostream>
#include <cstring>
#include <string>
#include <vector>

// 1. 报文和报文之间,我们采用特殊字符来进行解决粘包问题
// 2. 获取一个一个独立完整的报文,序列和反序列化 -- 自定义
// 100+19X100+19X100+19
#define SEP "X"
#define SEP_LEN strlen(SEP)

// 要把传入进来的缓冲区进行切分,要求:
// 1. buffer被切走的,也同时要从buffer中移除
// 2. 可能会存在多个报文,多个报文依次放入out
void SpliteMessage(std::string &buffer, std::vector<std::string> *out) // 分割报文
{   // buffer: 输入输出型参数,out: 输出型参数
    while (true)
    {
        auto pos = buffer.find(SEP); // 在缓冲区里找分隔符
        if (std::string::npos == pos) // 找不到就break
            break;
        std::string message = buffer.substr(0, pos); // 提取子串:前闭后开区间
        buffer.erase(0, pos + SEP_LEN); // 移除子串和衡娥福
        out->push_back(message); // push_back完整的子串
        // std::cout << "debug: " << message << " : " << buffer << std::endl;
        // sleep(1);
    }
}

// 自己手写序列反序列化
#define SPACE " "
#define SPACE_LEN strlen(SPACE)

std::string Encode(std::string& s)
{
    return s + SEP;
}

class Request
{
public:
    std::string Serialize()
    {
        std::string str;
        str = std::to_string(_x);
        str += SPACE;
        str += _op;
        str += SPACE;
        str += std::to_string(_y);
        return str;
    }
    bool Deserialized(const std::string& str) // 1 + 1
    {
        std::size_t left = str.find(SPACE);
        if (left == std::string::npos)
            return false;
        std::size_t right = str.rfind(SPACE);
        if (right == std::string::npos)
            return false;
        _x = atoi(str.substr(0, left).c_str());
        _y = atoi(str.substr(right + SPACE_LEN).c_str());
        if (left + SPACE_LEN > str.size())
            return false;
        else
            _op = str[left + SPACE_LEN];
        return true;
    }

public:
    Request()
    {}
    Request(int x, int y, char op) 
        : _x(x), _y(y), _op(op)
    {}
    ~Request() 
    {}
public:
    int _x;
    int _y;
    char _op; // '+' '-' '*' '/' '%'
};

class Response
{
public:
    std::string Serialize() // "code_ result_"
    {

        std::string s;
        s = std::to_string(_code);
        s += SPACE;
        s += std::to_string(_result);

        return s;
    }
    bool Deserialized(const std::string& s)
    {
        std::size_t pos = s.find(SPACE);
        if (pos == std::string::npos)
            return false;
        _code = atoi(s.substr(0, pos).c_str());
        _result = atoi(s.substr(pos + SPACE_LEN).c_str());
        return true;
    }
public:
    Response()
    {}
    Response(int result, int code) 
        : _result(result), _code(code)
    {}
    ~Response()
    {}
public:
    int _result; // 计算结果
    int _code;   // 计算结果的状态码
};

main.cc

#include "TcpServer.hpp"
#include <memory>

static Response calculator(const Request &req)
{
    Response resp(0, 0);
    switch (req.op_)
    {
    case '+':
        resp.result_ = req.x_ + req.y_;
        break;
    case '-':
        resp.result_ = req.x_ - req.y_;
        break;
    case '*':
        resp.result_ = req.x_ * req.y_;
        break;
    case '/':
        if (0 == req.y_)
            resp.code_ = 1;
        else
            resp.result_ = req.x_ / req.y_;
        break;
    case '%':
        if (0 == req.y_)
            resp.code_ = 2;
        else
            resp.result_ = req.x_ % req.y_;
        break;
    default:
        resp.code_ = 3;
        break;
    }
    return resp;
}

void NetCal(Connection *conn, std::string &request)
{
    logMessage(DEBUG, "NetCal been called, get request: %s", request.c_str());
    Request req; // 1. 反序列化,1 + 1    2 + 3
    if(!req.Deserialized(request)) 
        return;

    Response resp = calculator(req); // 2. 业务处理

    std::string sendstr = resp.Serialize(); // 3. 序列化,构建应答
    sendstr = Encode(sendstr);

    conn->_outbuffer += sendstr; // 4. 交给服务器conn

    // 5. 想办法,让底层的TcpServer开始发送 -> 需要有完整的发送逻辑
    // 触发发送的动作,一旦开启EPOLLOUT,epoll会自动立马触发一次发送事件就绪,如果后续保持发送的开启,epoll会一直发送
    conn->_tsvr->EnableReadWrite(conn, true, true); // 写完EnableReadWrite才发现回指指针的作用
}

int main()
{
    std::unique_ptr<TcpServer> svr(new TcpServer());
    svr->Dispather(NetCal);

    return 0;
}

Sender函数:

   void Sender(Connection *conn)
    {
        while(true)
        {
            ssize_t n = send(conn->_sock, conn->_outbuffer.c_str(), conn->_outbuffer.size(), 0);
            if(n > 0) // 发送成功 -> 移除
            {
                conn->_outbuffer.erase(0, n);
                if(conn->_outbuffer.empty())  // 发完了 -> break
                    break;
            }
            else
            {
                if(errno == EAGAIN || errno == EWOULDBLOCK) // 缓冲区满了 -> break下次再发
                    break;
                else if(errno == EINTR) // 发送被中断 -> 重新发送
                    continue;
                else // 真正读取失败 -> 交给异常回调
                {
                    logMessage(ERROR, "send error, %d : %s", errno, strerror(errno));
                    conn->_except_cb(conn);
                    break;
                }
            }
        }
        // 不确定有没有发完,但是可以保证,如果没有出错,一定是要么发完,要么发送条件不满足,下次再发
        if(conn->_outbuffer.empty())
            EnableReadWrite(conn, true, false);
        else
            EnableReadWrite(conn, true, true);
    }
    void EnableReadWrite(Connection *conn, bool readable, bool writeable) // 控制读写开关
    {
        uint32_t events = ((readable ? EPOLLIN : 0) | (writeable ? EPOLLOUT : 0));
        bool res = _poll.CtrlEpoll(conn->_sock, events);
        assert(res);
    }

Epoll.hpp

加了打开和删除就完整了:

#pragma once

#include <iostream>
#include <sys/epoll.h>

class Epoll
{
    const static int gnum = 128;
    const static int gtimeout = 5000;
public:
    Epoll(int timeout = gtimeout)
        : _timeout(timeout)
    {}
    void CreateEpoll()
    {
        _epfd = epoll_create(gnum);
        if (_epfd < 0)
            exit(5);
    }
    bool DelFromEpoll(int sock) // 移除sock的所有事件
    {
        int n = epoll_ctl(_epfd, EPOLL_CTL_DEL, sock, nullptr);
        return n == 0;
    }
    bool CtrlEpoll(int sock, uint32_t events) // 打开sock的事件
    {
        events |= EPOLLET;
        struct epoll_event ev;
        ev.events = events;
        ev.data.fd = sock;
        int n = epoll_ctl(_epfd, EPOLL_CTL_MOD, sock, &ev);
        return n == 0;
    }
    bool AddSockToEpoll(int sock, uint32_t events)
    {   // 添加sock到epoll中(任务通知)->要知道sock和事件(任何多路转接的服务器,一般只会打开读取事件,写入事件按需打开)
        struct epoll_event ev;
        ev.events = events;
        ev.data.fd = sock;
        int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, sock, &ev);
        return n == 0;
    }
    int WaitEpoll(struct epoll_event revs[], int num)
    {
        return epoll_wait(_epfd, revs, num, _timeout);
    }
    ~Epoll()
    {}

private:
    int _epfd;
    int _timeout;
};

Excepter函数:

    void Excepter(Connection *conn)
    {
        if(!IsConnectionExists(conn->_sock)) // _sock不存在就返回
            return;
        bool res = _poll.DelFromEpoll(conn->_sock); // 1. 从epoll中移除
        assert(res);

        _connections.erase(conn->_sock); // 2. 从unorder_map中移除

        close(conn->_sock); // 3. 关闭sock

        delete conn; // 4. 释放 conn;
        logMessage(DEBUG, "Excepter 回收完毕所有的异常情况");
    }

编译运行:

此时代码就结束了,可以自己拓展一下。下面放一下完整的TcpServer.hpp

TcpServer.hpp

#pragma once

#include <iostream>
#include <functional>
#include <string>
#include <cassert>
#include <unordered_map>
#include <vector>
#include "Sock.hpp"
#include "Log.hpp"
#include "Epoll.hpp"
#include "Protocol.hpp"

class TcpServer;
class Connection;

using func_t = std::function<void(Connection *)>;
using callback_t = std::function<void (Connection*, std::string &request)>; // 上层业务处理的方法

// 为了能够正常工作,常规的sock必须要有独立的接收缓冲区和发送缓冲区(写入)
class Connection // 一个链接类
{
public:
    Connection(int sock = -1)
        : _sock(sock), _tsvr(nullptr)
    {
    }
    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;
    }
    ~Connection()
    {
    }

public:
    int _sock;       // 负责进行IO的文件描述符
    func_t _recv_cb; // 三个回调方法,是对_sock进行特定读写的对应方法
    func_t _send_cb;
    func_t _except_cb;

    std::string _inbuffer;  // 接收缓冲区&&发送缓冲区
    std::string _outbuffer; // 这两string暂时没有办法处理二进制流,文本是可以的

    TcpServer *_tsvr; // 设置对TcpServer的回指指针,对写事件的关心是按需打开
};

class TcpServer
{
    const static int gport = 8080;
    const static int gnum = 128;

public:
    TcpServer(int port = gport)
        : _port(port), _revs_num(gnum)
    {
        // 1. 创建listensock
        _listensock = Sock::Socket();
        Sock::Bind(_listensock, _port);
        Sock::Listen(_listensock);

        // 2. 创建多路转接对象
        _poll.CreateEpoll();

        // 3. 添加listensock到服务器中 -> 三步(类的构造函数也能调用类的成员方法,走到函数体中对象已经存在了)
        // 后三个参数是函数对象,要bind绑定返回一个函数对象->类内函数有this指针,_1是预留的参数
        AddConnection(_listensock, std::bind(&TcpServer::Accepter, this, std::placeholders::_1), nullptr, nullptr);

        // 4. 构建一个获取就绪事件的缓冲区
        _revs = new struct epoll_event[_revs_num];
    }
    void AddConnection(int sock, func_t recv_cb, func_t send_cb, func_t except_cb) // 把任意sock进行添加到TcpServer
    {
        Sock::SetNonBlock(sock); // ET模式要把sock设置成非阻塞 -> 在Sock.hpp中写成函数

        // 除了_listensock,后面还会存在大量的socket,每一个sock都必须被封装成为一个Connection
        // 当服务器中存在大量的Connection时,TcpServer需要将所有Connection进行管理:上面描述了,组织 -> unordered_map
        // 3.1 构建conn对象,封装sock
        Connection *conn = new Connection(sock);
        conn->SetCallBack(recv_cb, send_cb, except_cb);
        conn->_tsvr = this;

        // 3.2 添加sock到epoll中(任务通知)->要知道sock和事件(任何多路转接的服务器,一般只会打开读取事件,写入事件按需打开)
        _poll.AddSockToEpoll(sock, EPOLLIN | EPOLLET);

        // 3.3 将对应的Connection*对象指针添加到Connections映射表中(业务处理)
        _connections.insert(std::make_pair(sock, conn));
    }

    void Accepter(Connection *conn)
    {
        // logMessage(DEBUG, "Accepter been called");
        // 一定是listensock已经就绪了,此次读取不会阻塞,
        // 怎么保证,底层只有一个连接就绪呢 -> 循环,直到获取失败
        while (true)
        {
            std::string clientip;
            uint16_t clientport;
            int accept_errno = 0;
            // sock一定是常规的IO sock
            int sock = Sock::Accept(conn->_sock, &clientip, &clientport, &accept_errno);
            if (sock < 0) // 获取失败
            {
                if (accept_errno == EAGAIN || accept_errno == EWOULDBLOCK)
                    break;
                else if (accept_errno == EINTR) // 概率非常低
                    continue;
                else // accept失败
                {
                    logMessage(WARNING, "accept error, %d : %s", accept_errno, strerror(accept_errno));
                    break;
                }
            }
            else // (sock>=0)获取链接成功->将sock托管给TcpServer
            {
                AddConnection(sock, std::bind(&TcpServer::Recver, this, std::placeholders::_1),
                              std::bind(&TcpServer::Sender, this, std::placeholders::_1),
                              std::bind(&TcpServer::Excepter, this, std::placeholders::_1));
                logMessage(DEBUG, "accept client %s:%d success, add to epoll&&TcpServer success, sock: %d",
                           clientip.c_str(), clientport, sock);
            }
        }
    }

    void Recver(Connection *conn) // 读取一个正常的sock
    {
        const int num = 1024;
        bool err = false;
        // logMessage(DEBUG, "Recver event exists, Recver() been called");
        while (true)
        {
            char buffer[num];
            ssize_t n = recv(conn->_sock, buffer, sizeof(buffer) - 1, 0);
            if (n < 0)
            {
                if (errno == EAGAIN || errno == EWOULDBLOCK) // 读取完毕了,正常的
                    break;
                else if (errno == EINTR) // 读取被中断了,重新开始读
                    continue;
                else // 真正读取失败 -> 交给异常回调
                {
                    logMessage(ERROR, "recv error, %d : %s", errno, strerror(errno));
                    conn->_except_cb(conn);
                    err = true;
                    break;
                }
            }
            else if (n == 0)
            {
                logMessage(DEBUG, "client[%d] quit, server close [%d]", conn->_sock, conn->_sock);
                conn->_except_cb(conn);
                err = true;
                break;
            }
            else // 读取成功
            {
                buffer[n] = 0;
                conn->_inbuffer += buffer; // 读取到的数据全部拼接到接收缓冲区
            }
        }
        logMessage(DEBUG, "conn->_inbuffer[sock: %d]: %s", conn->_sock, conn->_inbuffer.c_str());
        if (!err) // 如果错误码还是false就是正常break的
        {
            std::vector<std::string> messages;
            SpliteMessage(conn->_inbuffer, &messages);
            // 保证走到这里,就是一个完整报文
            for (auto &msg : messages)
            {    // 可以在这里将message封装成为task,然后push到任务队列,任务处理交给后端线程池,这里不处理
                _cb(conn, msg);
            }
        }
    }

   void Sender(Connection *conn)
    {
        while(true)
        {
            ssize_t n = send(conn->_sock, conn->_outbuffer.c_str(), conn->_outbuffer.size(), 0);
            if(n > 0) // 发送成功 -> 移除
            {
                conn->_outbuffer.erase(0, n);
                if(conn->_outbuffer.empty())  // 发完了 -> break
                    break;
            }
            else
            {
                if(errno == EAGAIN || errno == EWOULDBLOCK) // 缓冲区满了 -> break下次再发
                    break;
                else if(errno == EINTR) // 发送被中断 -> 重新发送
                    continue;
                else // 真正读取失败 -> 交给异常回调
                {
                    logMessage(ERROR, "send error, %d : %s", errno, strerror(errno));
                    conn->_except_cb(conn);
                    break;
                }
            }
        }
        // 不确定有没有发完,但是可以保证,如果没有出错,一定是发完了,或者发送条件不满足,下次再发
        if(conn->_outbuffer.empty()) // 发完了->不用关心写
            EnableReadWrite(conn, true, false);
        else // 发送条件不满足,下次再发
            EnableReadWrite(conn, true, true);
    }
    void EnableReadWrite(Connection *conn, bool readable, bool writeable) // 控制读写开关
    {            // 下面的三目:如readable为真就关心读事件,否则为0,writeable就关心写事件
        uint32_t events = ((readable ? EPOLLIN : 0) | (writeable ? EPOLLOUT : 0));
        bool res = _poll.CtrlEpoll(conn->_sock, events);
        assert(res);
    }

    void Excepter(Connection *conn)
    {
        if(!IsConnectionExists(conn->_sock)) // _sock不存在就返回
            return;
        bool res = _poll.DelFromEpoll(conn->_sock); // 1. 从epoll中移除
        assert(res);

        _connections.erase(conn->_sock); // 2. 从unorder_map中移除

        close(conn->_sock); // 3. 关闭sock

        delete conn; // 4. 释放 conn;
        logMessage(DEBUG, "Excepter 回收完毕所有的异常情况");
    }

    void Dispather(callback_t cb) // 根据就绪的事件,进行特定事件的派发
    {
        _cb = cb;
        while (true)
        {
            LoopOnce();
        }
    }
    void LoopOnce()
    {
        int n = _poll.WaitEpoll(_revs, _revs_num);
        for (int i = 0; i < n; i++) // 获取事件
        {
            int sock = _revs[i].data.fd;
            uint32_t revents = _revs[i].events;
            if (revents & EPOLLIN) // 读就绪
            {
                // if(Connection是存在并且_connections[sock]->_recv_cb被设置过)
                if (IsConnectionExists(sock) && _connections[sock]->_recv_cb != nullptr)
                    _connections[sock]->_recv_cb(_connections[sock]); // 调用读事件的回调
            }
            if (revents & EPOLLOUT) // 写就绪
            {
                // if(Connection是存在并且_connections[sock]->_send_cb被设置过)
                if (IsConnectionExists(sock) && _connections[sock]->_send_cb != nullptr)
                    _connections[sock]->_send_cb(_connections[sock]); // 调用写事件的回调
            }
        }
    }
    bool IsConnectionExists(int sock) // 判定Connection是否存在
    {
        auto iter = _connections.find(sock);
        if (iter == _connections.end())
            return false;
        else
            return true;
    }
    ~TcpServer()
    {
        if (_listensock >= 0)
            close(_listensock);
        if (_revs)
            delete[] _revs;
    }

private:
    int _listensock;
    int _port;
    Epoll _poll;
    std::unordered_map<int, Connection *> _connections; // 管理:sock映射到Connection
    struct epoll_event *_revs;                          // 就绪事件缓冲区,就绪的文件描述符投递到这里
    int _revs_num;                                      // 就绪事件缓冲区大小

    callback_t _cb; // 处理上层的业务的回调函数
};

2. 相关笔试题

1. 以下说法不正确的是()

A.ET事件发生仅通知一次的原因是只被添加到rdlist中一次,而LT可以有多次添加的机会

B.当时用ET模式的时候描述符最好设置为非阻塞模式

C.epoll理论上而言可以高效的监视无限多的文件描述符

D.LT模式也被称之为边沿触发

2. 以下关于事件放入epoll等待队列说法不正确的是()

A.当LT模式下,有新数据到来才会加入到epoll等待队列中

B.有老数据,并且通过epoll_ctl设置EPOLL_CTL_MOD(ET模式)

C.数据可写,并且通过epoll_ctl设置EPOLL_CTL_MOD(ET模式)

D.以上说法中都不正确

3. 以下关于LT模式说法错误的是()

A.通常情况下ET模式效率比LT模式高

B.LT模式下,当epoll_wait检测到fd上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件

C.客户端发送数据,I/O函数会提醒描述符fd有数据---->recv读数据,若一次没有读完,I/O函数会一直提醒服务端fd上有数据,直到recv缓冲区里的数据读完

D.LT模式读取激活事件后,如果还有未处理的数据。事件不会放入EPOLL等待队列

4. 以下关于ET模式说法错误的是()

A.epoll_wait只有在客户端每次发数据时才会返回,除此以外即使接收缓冲区里还有数据也不会触发事件返回

B.在使用ET模式的时候描述符必须设置为阻塞模式

C.使用ET模式的时候最好使用循环读取,将自己需要处理的数据全部处理完毕。

5. 以下关于ET模式和LT模式说法错误的是()

A.ET模式是边缘触发,LT模式是水平触发

B.ET模式是epoll的缺省工作模式

C.ET模式每当状态变化时,触发一个事件

D.LT模式只要满足条件,就触发一个事件,即只要有数据没有被获取,内核就不断通知用户

6. 请简述select的优缺点,分点简述。

7. 请简述poll的优缺点,分点简述。

8. 请简述epoll的优缺点,分点简述。


答案及解析

1. D

答案解析:

        ET模式叫边缘触发:表示描述符状态发生变化时触发一次事件,在没有新的状态变化时不会通知第二次,对于IO读事件来说缓冲区有新的数据到来的时候才会触发一次事件,不管是否处理都只会触发一次,因此尽可能的一次事件处理中循环将自己需要处理的数据处理完,为了避免在循环读取数据中因为没有数据而阻塞,因此最好将描述符设置为非阻塞。

        LT模式叫水平触发:表示描述符状态发生变化,但是没有被处理,就会触发事件,对于IO读写操作来说,只要接收缓冲区中有数据或者发送缓冲区有剩余空间就会触发事件,不断通知用户

D错误:LT是水平触发

2. C

A错误:LT水平触发,对于读是接收缓冲区中有数据可读,也就是有数据就加入等待队列, 对于写是发送缓冲区有剩余空间

B错误:ET边缘触发,有新数据到来的时候才会触发事件,放入EPOLL等待队列

C错误;ET边缘触发,对于写来说是状态从不可写变为可写时才会触发事件,放入等待队列

3. D

A正确:在ET模式下,可以减少epoll的系统调用次数,并且减少每次返回的就绪事件信息,因此能够一定量的提高部分效率。

B正确:LT模式下,若不处理触发的事件,则下次监控依然会触发事件,因此可以不立即处理该事件。

C正确:LT模式下,对于IO读操作来说,就是缓冲区中只要有数据就会一直触发事件,直到缓冲区数据被读完。

D错误:LT模式下,如果还有未处理的数据,事件会再次被触发,并放入EPOLL等待队列。

4. B

        ET模式指的是边缘触发模式,表示只有描述符状态发生改变的时候才会触发一次事件,对于读事件来说指的是只有新数据到来时触发一次,后续不管这次数据是否处理都不会触发第二次事件,直到有下次新数据到来。

        因为边缘触发是只有新数据到来才会触发一次事件,因此使用ET模式的时候最好使用循环读取,将自己需要处理的数据全部处理,避免因为没有新数据到来而导致不触发新事件,使剩下的数据因为无法触发事件而得不到处理

B错误:在边缘模式下,通常是尽量设置为非阻塞操作,而并非阻塞操作

5. B

        ET模式叫边缘触发:表示描述符状态发生变化时触发一次事件,在没有新的状态变化时不会通知第二次,对于IO读事件来说缓冲区有新的数据到来的时候才会触发一次事件,不管是否处理都只会触发一次

        LT模式叫水平触发:表示描述符状态发生变化,但是没有被处理,就会触发事件,对于IO读写操作来说,只要接收缓冲区中有数据或者发送缓冲区有剩余空间就会触发事件,不断通知用户

epoll的缺省工作模式是LT模式,也就是水平触发模式

6. 请简述select的优缺点,分点简述。


select优点:

        select模型是Windows sockets中最常见的IO模型。它利用select函数实现IO 管理。通过对select函数的调用,应用程序可以判断套接字是否存在数据、能否向该套接字写入数据,可以等待多个套接字。


select缺点:

  1. 每次调用select,都需要手动设置fd集合, 从接口使用角度来说也非常不便。
  2. 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大。
  3. 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大。
  4. select支持的文件描述符数量太小。
  5. 编码比较复杂,这是上面的缺点导致的,前面的简易select服务器还没加读和写就挺复杂的了。

7. 请简述poll的优缺点,分点简述。


优点:

  1. 不同与select使用三个位图来表示三个fdset的方式,poll使用一个pollfd的指针实现。
  2. pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式,接口使用比select更方便。
  3. poll并没有最大等待文件描述符数量限制 (但是数量过大后性能也是会下降)。

缺点:

  1. 和select一样,poll返回后,需要轮询struct pollfd数组来获取就绪的描述符。
  2. 每次调用poll都需要把大量的struct pollfd结构从用户层拷贝到内核中。
  3. 同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长, 其效率也会线性下降。
  4. 代码的编写也比较复杂(比select简单)

8. 请简述epoll的优点,分点简述。


epoll缺点:

在编写轻量型的服务器时,和select相比,提升效果不大。


epoll的优点和select的缺点对应:

  1. 接口使用方便:虽然拆分成了三个函数,但是反而使用起来更方便高效,不需要每次循环都设置关注的文件描述符,也做到了输入输出参数分离开。
  2. 数据拷贝轻量:只在合适的时候调用epoll_ctl将文件描述符结构拷贝到内核中,这个操作并不频繁(而select/poll是每次循环都要进行拷贝)。
  3. 事件回调机制:避免使用遍历检测,而是使用回调函数的方式,将就绪的文件描述符结构加入到就绪队列中。epoll_wait返回直接访问就绪队列就知道哪些文件描述符就绪,这个操作时间复杂度是O(1),即使文件描述符数目很多, 效率也不会受到影响。
  4. 没有数量限制:文件描述符数目无上限。
  5. 编码相对简单,虽然epoll的机制更复杂,但是它用起来更方便也更高效。

本篇完。

        此篇应该是此专栏的最后一篇了,多路转接的代码都建议自己敲一敲,复习复习自己做两个项目就能投简历找工作了,后面也会更新算法和数据库的内容。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[Python] 什么是集成算法,什么是随机森林?随机森林分类器(RandomForestClassifier)及其使用案例

什么是集成算法&#xff1f; 集成算法是一种机器学习方法&#xff0c;它将多个基本的学习算法&#xff08;也称为弱学习器&#xff09;组合在一起&#xff0c;形成一个更强大的预测模型。集成算法通过对基本模型的预测进行加权平均或多数投票等方式&#xff0c;来产生最终的预…

【Leetcode】两数之和

目录 题目&#xff1a; 解法1&#xff1a;暴力双for 1.想到的第一种方法两for循环解 复杂度分析 解法2&#xff1a;hash表 总结&#xff1a; 笔记&#xff1a; 题目&#xff1a; 给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标…

简单区间DP

文章目录 什么是区间DpAcWing 282. 石子合并题意分析思路解析状态表示状态计算 CODE需要注意的问题 什么是区间Dp 区间Dp指的是某些问题可以用区间来划分解决。 AcWing 282. 石子合并 题目链接&#xff1a;穿梭时间的画面的钟 题意分析 从一排石子中选择相邻的两堆进行合并…

2024-01-25 力扣高频SQL50题目1193每月交易

1.1193每月交易 1 count可以这样用。。 COUNT(IF(state approved, 1, NULL)) AS approved_count 如果 COUNT(if(state approved,1,0))&#xff0c;这里变成0&#xff0c;就不对了。因为count计数时候&#xff0c;只要里面不是null&#xff0c;就会算进去。 sum(if(state …

(学习日记)2024.01.27

写在前面&#xff1a; 由于时间的不足与学习的碎片化&#xff0c;写博客变得有些奢侈。 但是对于记录学习&#xff08;忘了以后能快速复习&#xff09;的渴望一天天变得强烈。 既然如此 不如以天为单位&#xff0c;以时间为顺序&#xff0c;仅仅将博客当做一个知识学习的目录&a…

行为型设计模式—迭代器模式

迭代器模式&#xff1a;也叫作游标模式&#xff0c;能在不暴露复杂数据结构内部细节的情况下遍历其中所有的元素。在迭代器的帮助下&#xff0c; 客户端可以用一个迭代器接口以相似的方式遍历不同集合中的元素。 当集合背后为复杂的数据结构&#xff0c;且希望对客户端隐藏其复…

漏洞原理文件上传漏洞

一 文件上传漏洞介绍&#xff08;理论&#xff09; 文件上传漏洞是一种常见的web应用程序漏洞&#xff0c;允许攻击者向服务器上传恶意文件。这种漏洞可在没有恰当的安全措施的情况下&#xff0c;将任意类型的文件上传到服务器上&#xff0c;从而可能导致以下安全问题&#xff…

【lesson1】高并发内存池项目介绍

文章目录 这个项目做的是什么&#xff1f;这个项目的要求的知识储备和难度&#xff1f;什么是内存池池化技术内存池内存池主要解决的问题malloc 这个项目做的是什么&#xff1f; 当前项目是实现一个高并发的内存池&#xff0c;他的原型是google的一个开源项目tcmalloc&#xf…

Python 字典及常见应用(Python Dctionary)

字典是python的内置基本数据类型之一&#xff0c;其他语言中可能会被称为“关联存储”或“关联数组”。它是一种映射关系&#xff0c;以包含在{}中的"键:值"对表示。字典是一种可变对象&#xff0c;键没有顺序。其主要用途是通过关键字存储、提取值。 目录 一、字典的…

3分钟搞定幻兽帕鲁联机,一键部署专属服务器

3分钟搞定幻兽帕鲁联机&#xff0c;一键部署专属服务器 访问帕鲁专题活动页 登录阿里云官网&#xff0c;用钉钉或者支付宝app扫码注册新用户&#xff08;新用户福利较多&#xff0c;优惠力度大&#xff09; 进入阿里云游戏联机服务器专题页&#xff0c;点击 一键购买及部署 即可…

记录一条sql查询:以逗号隔开的id字符串的查询

目录 前言表结构sql语句 前言 在一个项目中有两张表&#xff0c;一张是商品码表&#xff0c;一张是记录出库单明细的出库记录表&#xff0c;记录表中有一个字段保存了以逗号隔开的商品码表的id字符串&#xff0c;需要根据出库明细id查找到对应出库的商品码。 表结构 goods_det…

TypeScript(六) 循环语句

1. TypeScript循环语句 1.1. 简述 有的时候&#xff0c;我们可能需要多次执行同一块代码。一般情况下&#xff0c;语句是按顺序执行的&#xff1a;函数中的第一个语句先执行&#xff0c;接着是第二个语句&#xff0c;依此类推。   循环语句允许我们多次执行一个语句或语句组…

【开源】JAVA+Vue.js实现大学兼职教师管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、研究内容三、界面展示3.1 登录注册3.2 学生教师管理3.3 课程管理模块3.4 授课管理模块3.5 课程考勤模块3.6 课程评价模块3.7 课程成绩模块3.8 可视化图表 四、免责说明 一、摘要 1.1 项目介绍 大学兼职教师管理系统&#xff0c;旨…

阿里云0基础1分钟搞定幻兽帕鲁游戏联机服务器搭建

幻兽帕鲁&#xff08;Palworld&#xff09;是一款备受欢迎的多人在线角色扮演游戏&#xff0c;让玩家可以与其他玩家一起探索、组队和战斗。为了能顺畅地体验游戏&#xff0c;搭建一个高效、稳定的游戏服务器至关重要。阿里云提供快速、简化的服务器搭建方案&#xff0c;新手小…

代码随想录 Leetcode112. 路径总和

题目&#xff1a; 代码(首刷看解析 2024年1月30日 递归回溯 逻辑清晰版&#xff09;&#xff1a; class Solution { public:bool traversal(TreeNode* cur,int sum) {if (!cur->left && !cur->right && sum 0) return true;if (!cur->left &&am…

iOS 17.4 苹果公司正在加倍投入人工智能

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

开发AI软件,构建多用户AIGC系统,实现图文创作及源码交付

在AI技术不断进步的今天&#xff0c;AI软件开发已成为一个热门的领域。而多用户AIGC系统作为AI软件开发的重要项目之一&#xff0c;呈现出极大的潜力和前景。 多用户AIGC系统旨在为用户提供一个全面的图文创作平台&#xff0c;借助AI的力量&#xff0c;使创作过程更加智能化和…

代码随想录算法训练营DAY7 | 哈希表(2)

一、LeetCode 454 四数相加II 题目链接&#xff1a;454.四数相加IIhttps://leetcode.cn/problems/4sum-ii/description/ 思路&#xff1a;建立HashMap&#xff0c;Key存储nums1、nums2数对之和&#xff0c;Value存储数对和出现次数&#xff0c;再遍历nums3、nums4数对确定答案…

研学活动报名平台源码开发方案

一、项目背景与目标 &#xff08;一&#xff09;项目背景 研学活动报名平台旨在为活动组织者提供方便快捷的研学活动管理工具&#xff0c;同时为用户提供全面的活动搜索、报名和支付等功能。通过该系统&#xff0c;活动组织者能够更好地管理活动报名信息&#xff0c;用户也可…

【Lazy ORM 整合druid 实现mysql监控】

Lazy ORM 整合druid 实现mysql监控 JDK 17 Lazy ORM框架地址 up、up欢迎start、issues 当前项目案例地址 框架版本描述spring-boot3.0.7springboot框架wu-framework-web1.2.2-JDK17-SNAPSHOTweb容器Lazy -ORM1.2.2-JDK17-SNAPSHOTORMmysql-connector-j8.0.33mysql驱动druid-…