Muduo网络库流程分析

目录

Tcp模块的流程

测试代码

TcpServer初始化阶段详解

TcpServer构造函数执行流程

EventLoop创建详细步骤:

Channel与Poller的关联

设置Acceptor回调函数

启动监听

启动服务器完整流程

连接建立流程详解

新连接到达流程

连接初始化详解

连接就绪详解

数据收发流程详解

数据接收流程

数据发送流程

关于Http模块的流程

HTTP请求的接收流程

HTTP响应的发送流程

模块初始化都干了什么事情

Poller对象的创建

EventLoop对象的创建

Channel对象的创建

Socket对象的创建

Acceptor对象的创建

TcpServer对象的创建

​编辑LoopThreadPool对象的创建

LoopThread对象的创建

Timer_wheel对象的创建

Connection对象的创建

[1. 连接触发监听套接字的可读事件](#1. 连接触发监听套接字的可读事件)

[2. Poller检测到事件](#2. Poller检测到事件)

[3. EventLoop处理就绪事件](#3. EventLoop处理就绪事件)

[4. Acceptor的HandleRead方法被调用](#4. Acceptor的HandleRead方法被调用)

[5. TcpServer::NewConnection方法被调用](#5. TcpServer::NewConnection方法被调用)

[6. Connection对象创建](#6. Connection对象创建)

[7. Connection::Established方法执行](#7. Connection::Established方法执行)

[8. Connection::EstablishedInLoop方法执行](#8. Connection::EstablishedInLoop方法执行)

[9. 启动读事件监控](#9. 启动读事件监控)

[10. 更新Poller中的事件监控](#10. 更新Poller中的事件监控)

[11. 调用连接建立回调函数](#11. 调用连接建立回调函数)

[12. 连接就绪,等待数据交互](#12. 连接就绪,等待数据交互)


Tcp模块的流程

EventLoop模块

构造函数

cpp 复制代码
class EventLoop {
    private:
        using Functor = std::function<void()>;
        std::thread::id _thread_id;//线程ID
        int _event_fd;//eventfd唤醒IO事件监控有可能导致的阻塞
        std::unique_ptr<Channel> _event_channel;
        Poller _poller;//进行所有描述符的事件监控
        std::vector<Functor> _tasks;//任务池
        std::mutex _mutex;//实现任务池操作的线程安全
        TimerWheel _timer_wheel;//定时器模块
    public:
        EventLoop():_thread_id(std::this_thread::get_id()), 
                    _event_fd(CreateEventFd()), 
                    _event_channel(new Channel(this, _event_fd)),
                    _timer_wheel(this) {
            //给eventfd添加可读事件回调函数,读取eventfd事件通知次数
            _event_channel->SetReadCallback(std::bind(&EventLoop::ReadEventfd, this));
            //启动eventfd的读事件监控
            _event_channel->EnableRead();
        }
};

Start接口

cpp 复制代码
        void Start() {
            while(1) {
                //1. 事件监控, 
                std::vector<Channel *> actives;
                _poller.Poll(&actives);
                //2. 事件处理。 
                for (auto &channel : actives) {
                    channel->HandleEvent();
                }
                //3. 执行任务
                RunAllTask();
            }
        }

TcpServer模块

cpp 复制代码
class TcpServer {    
    private:
        uint64_t _next_id;      //这是一个自动增长的连接ID,
        int _port;
        int _timeout;           //这是非活跃连接的统计时间---多长时间无通信就是非活跃连接
        bool _enable_inactive_release;//是否启动了非活跃连接超时销毁的判断标志
        EventLoop _baseloop;    //这是主线程的EventLoop对象,负责监听事件的处理
        Acceptor _acceptor;    //这是监听套接字的管理对象
        LoopThreadPool _pool;   //这是从属EventLoop线程池
        std::unordered_map<uint64_t, PtrConnection> _conns;//保存管理所有连接对应的shared_ptr对象

        using ConnectedCallback = std::function<void(const PtrConnection&)>;
        using MessageCallback = std::function<void(const PtrConnection&, Buffer *)>;
        using ClosedCallback = std::function<void(const PtrConnection&)>;
        using AnyEventCallback = std::function<void(const PtrConnection&)>;
        using Functor = std::function<void()>;
        ConnectedCallback _connected_callback;
        MessageCallback _message_callback;
        ClosedCallback _closed_callback;
        AnyEventCallback _event_callback;    
    public:
        TcpServer(int port):
            _port(port), 
            _next_id(0), 
            _enable_inactive_release(false), 
            _acceptor(&_baseloop, port),
            _pool(&_baseloop) {
            _acceptor.SetAcceptCallback(std::bind(&TcpServer::NewConnection, this, std::placeholders::_1));
            _acceptor.Listen();//将监听套接字挂到baseloop上
        }
        void SetThreadCount(int count) { return _pool.SetThreadCount(count); }
        void SetConnectedCallback(const ConnectedCallback&cb) { _connected_callback = cb; }
        void SetMessageCallback(const MessageCallback&cb) { _message_callback = cb; }
        void SetClosedCallback(const ClosedCallback&cb) { _closed_callback = cb; }
        void SetAnyEventCallback(const AnyEventCallback&cb) { _event_callback = cb; }
        void EnableInactiveRelease(int timeout) { _timeout = timeout; _enable_inactive_release = true; }
        //用于添加一个定时任务
        void RunAfter(const Functor &task, int delay) {
            _baseloop.RunInLoop(std::bind(&TcpServer::RunAfterInLoop, this, task, delay));
        }
        void Start() { _pool.Create();  _baseloop.Start(); }
};

Poller模块

cpp 复制代码
class Poller {
    private:
        int _epfd;
        struct epoll_event _evs[MAX_EPOLLEVENTS];
        std::unordered_map<int, Channel *> _channels;
    public:
        Poller() {
            _epfd = epoll_create(MAX_EPOLLEVENTS);
            if (_epfd < 0) {
                ERR_LOG("EPOLL CREATE FAILED!!");
                abort();//退出程序
            }
        }
};

Acceptor模块

cpp 复制代码
class Acceptor {
    private:
        Socket _socket;//用于创建监听套接字
        EventLoop *_loop; //用于对监听套接字进行事件监控
        Channel _channel; //用于对监听套接字进行事件管理

        using AcceptCallback = std::function<void(int)>;
        AcceptCallback _accept_callback;
    public:
        /*不能将启动读事件监控,放到构造函数中,必须在设置回调函数后,再去启动*/
        /*否则有可能造成启动监控后,立即有事件,处理的时候,回调函数还没设置:新连接得不到处理,且资源泄漏*/
        Acceptor(EventLoop *loop, int port): _socket(CreateServer(port)), _loop(loop), 
            _channel(loop, _socket.Fd()) {
            _channel.SetReadCallback(std::bind(&Acceptor::HandleRead, this));
        }
        void SetAcceptCallback(const AcceptCallback &cb) { _accept_callback = cb; }
        void Listen() { _channel.EnableRead(); }
        void HandleRead() {
            int newfd = _socket.Accept();
            if (newfd < 0) {
                return ;
            }
            if (_accept_callback) _accept_callback(newfd);
        }
};

Channel模块

构造函数

cpp 复制代码
class Channel {
    private:
        int _fd;
        EventLoop *_loop;
        uint32_t _events;  // 当前需要监控的事件
        uint32_t _revents; // 当前连接触发的事件
        using EventCallback = std::function<void()>;
        EventCallback _read_callback;   //可读事件被触发的回调函数
        EventCallback _write_callback;  //可写事件被触发的回调函数
        EventCallback _error_callback;  //错误事件被触发的回调函数
        EventCallback _close_callback;  //连接断开事件被触发的回调函数
        EventCallback _event_callback;  //任意事件被触发的回调函数
    public:
        Channel(EventLoop *loop, int fd):_fd(fd), _events(0), _revents(0), _loop(loop) {}
};

LoopThreadPool模块

构造函数

cpp 复制代码
class LoopThreadPool {
    private:
        int _thread_count;
        int _next_idx;
        EventLoop *_baseloop;
        std::vector<LoopThread*> _threads;
        std::vector<EventLoop *> _loops;
    public:
        LoopThreadPool(EventLoop *baseloop):_thread_count(0), _next_idx(0), _baseloop(baseloop) {}
};

Create接口

cpp 复制代码
        void Create() {
            if (_thread_count > 0) {
                _threads.resize(_thread_count);
                _loops.resize(_thread_count);
                for (int i = 0; i < _thread_count; i++) {
                    _threads[i] = new LoopThread();
                    _loops[i] = _threads[i]->GetLoop();
                }
            }
            return ;
        }

Connection模块

cpp 复制代码
class Connection;
using PtrConnection = std::shared_ptr<Connection>;
class Connection : public std::enable_shared_from_this<Connection> {
    private:
        uint64_t _conn_id;  // 连接的唯一ID,便于连接的管理和查找
        //uint64_t _timer_id;   //定时器ID,必须是唯一的,这块为了简化操作使用conn_id作为定时器ID
        int _sockfd;        // 连接关联的文件描述符
        bool _enable_inactive_release;  // 连接是否启动非活跃销毁的判断标志,默认为false
        EventLoop *_loop;   // 连接所关联的一个EventLoop
        ConnStatu _statu;   // 连接状态
        Socket _socket;     // 套接字操作管理
        Channel _channel;   // 连接的事件管理
        Buffer _in_buffer;  // 输入缓冲区---存放从socket中读取到的数据
        Buffer _out_buffer; // 输出缓冲区---存放要发送给对端的数据
        Any _context;       // 请求的接收处理上下文

        /*这四个回调函数,是让服务器模块来设置的(其实服务器模块的处理回调也是组件使用者设置的)*/
        /*换句话说,这几个回调都是组件使用者使用的*/
        using ConnectedCallback = std::function<void(const PtrConnection&)>;
        using MessageCallback = std::function<void(const PtrConnection&, Buffer *)>;
        using ClosedCallback = std::function<void(const PtrConnection&)>;
        using AnyEventCallback = std::function<void(const PtrConnection&)>;
        ConnectedCallback _connected_callback;
        MessageCallback _message_callback;
        ClosedCallback _closed_callback;
        AnyEventCallback _event_callback;
        /*组件内的连接关闭回调--组件内设置的,因为服务器组件内会把所有的连接管理起来,一旦某个连接要关闭*/
        /*就应该从管理的地方移除掉自己的信息*/
        ClosedCallback _server_closed_callback;

    public:
        Connection(EventLoop *loop, uint64_t conn_id, int sockfd):_conn_id(conn_id), _sockfd(sockfd),
            _enable_inactive_release(false), _loop(loop), _statu(CONNECTING), _socket(_sockfd),
            _channel(loop, _sockfd) {
            _channel.SetCloseCallback(std::bind(&Connection::HandleClose, this));
            _channel.SetEventCallback(std::bind(&Connection::HandleEvent, this));
            _channel.SetReadCallback(std::bind(&Connection::HandleRead, this));
            _channel.SetWriteCallback(std::bind(&Connection::HandleWrite, this));
            _channel.SetErrorCallback(std::bind(&Connection::HandleError, this));
        }
};

一个Muduo网络库的流程:

在main线程中首先创建一个TcpServer对象_server,

这个_server在构造的时候创建main_loop,从属线程池_pool,_acceptor,设置非活跃连接时间,非活跃连接关闭为false,并且把从属线程池也注册到main_loop上,把acceptor注册到main_loop上,然后给_acceptor绑定新连接到来的回调函数(连接ID递增,增加连接计数器,确保每个连接有唯一ID,创建连接对象:PtrConnection conn(new Connection(_pool.NextLoop(), _next_id, fd)),创建新的Connection智能指针对象,通过线程池获取下一个可用的EventLoop (_pool.NextLoop()),传入新的连接ID和套接字文件描述符,设置回调函数,启用非活跃连接超时检测,初始化连接:conn->Established() 将连接标记为已建立,可能包括:更改连接状态为CONNECTED,启用读事件监听,调用连接建立回调函数,保存连接:将新连接添加到连接管理容器中,使用连接ID作为键,这样服务器可以通过ID快速查找和管理连接 ),然后调用Listen接口用来监听新连接的到来

这个main_loop在构造的时候创建了_event_channel(使用智能指针管理),_poller,_event_fd,_thread_id(初始化为当前线程的ID,),_task,_mutex,_timer_wheel,然后给_event_channel绑定可读事件回调函数,并且启动eventfd的读事件监控

这个_poller****在构造的时候创建了_epfd存储由epoll_create()返回的epoll文件描述符的整数,_evs是一个epoll_event结构体数组,用于存储事件,_channels是一个哈希表,将文件描述符(int)与Channel指针关联起来,使用epoll_create()创建一个epoll实例

这个_pool在构造的时候创建了 **_thread_count;线程池中IO线程的数量,****_next_idx用于轮询分配线程的下标(实现负载均衡),****EventLoop *_baseloop指向主线程的EventLoop,****std::vector<LoopThread*> _threads保存所有IO线程对象的指针,**std::vector<EventLoop *> _loops保存所有IO线程对应的EventLoop指针

这个_acceptor****在构造的时候创建了 **_socket用于创建监听套接字,**EventLoop *_loop用于对监听套接字进行事件监控,Channel _channel用于对监听套接字进行事件管理_socket(CreateServer(port))创建监听socket并绑定端口, _loop(loop)保存主事件循环指针,_channel(loop, _socket.Fd())创建Channel,管理监听socket的事件,并且 _channel.SetReadCallback(std::bind(&Acceptor::HandleRead, this))它使用 std::bind 将 Acceptor 类的 HandleRead 成员函数绑定为当通道上有数据可读时将被调用的回调函数。

这个socket在构造的时候会创一个文件描述符_sockfd,当调用CreateServer会创建套接字,绑定地址,开始监听,设置非阻塞,启动地址重用

这个_channel在构造的时候创建了个_fd,*_loop指针,需要监控的事件类型_events,已经触发的事件类型_revents,各种事件回调函数的设置,并且初始化_loop,将Channel与特定的事件循环(EventLoop)关联起来。以便这个loop能管理和调度该Channel上的事件。

这个conn在构造的时候,会把该conn注册到传入的loop中,同时也会创建一个channel,把这个channel注册到这个loop中,同时也需要把sockfd注册到channel上,因为Channel知道它要监听哪个文件描述符(sockfd),Channel知道它的事件应该由哪个事件循环(loop)处理,并且给这个channel绑定上读/写/错误/关闭的事件回调函数,然后会创建 _in_buffer用户态输入缓冲区---存放从socket中读取到的数据。_out_buffer用户态输出缓冲区---存放要发送给对端的数据。当客户端发送数据时,SubLoop检测到读事件,调用lcpconnection::handleRead进行数据的读取,操作系统内核接收数据,暂存在socket的接收缓冲区,然后再把数据被读入InputBuffer,调用用户注册的messageCallback,将数据传递给上层应用处理,上层应用根据自己的业务逻辑处理数据。然后服务器构建好了要把数据响应给上层应用调用TcpConnection::send()发送数据,数据被写入OutputBuffer,然后把OutputBuffer的数据尝试发送到内核缓冲区,如果不能完全发送,则:将剩余数据留在OutputBuffer中,向Poller注册该连接socket的写事件,当socket可写时,SubLoop检测到写事件,继续发送OutputBuffer中的数据,数据发送完毕后,取消写事件关注

测试代码

cpp 复制代码
#include "m_server.h"

// 简单的回显服务器测试
class EchoServer {
private:
    TcpServer _server;

public:
    EchoServer(int port) : _server(port) {
        // 设置服务器回调函数
        _server.SetConnectedCallback(std::bind(&EchoServer::OnConnected, this, std::placeholders::_1));
        _server.SetMessageCallback(std::bind(&EchoServer::OnMessage, this, std::placeholders::_1, std::placeholders::_2));
        _server.SetClosedCallback(std::bind(&EchoServer::OnClosed, this, std::placeholders::_1));
        
        // 启用非活跃连接超时释放(60秒)
        _server.EnableInactiveRelease(60);
        
        // 设置工作线程数量
        _server.SetThreadCount(4);
    }

    // 连接建立回调
    void OnConnected(const PtrConnection& conn) {
        INF_LOG("新连接建立: %d", conn->Id());
        // 发送欢迎消息
        std::string welcome = "欢迎连接到回显服务器!\n";
        conn->Send(welcome.c_str(), welcome.size());
    }

    // 消息处理回调
    void OnMessage(const PtrConnection& conn, Buffer* buf) {
        // 读取所有可读数据
        std::string msg = buf->ReadAsStringAndPop(buf->ReadAbleSize());
        INF_LOG("收到消息[%d]: %s", conn->Id(), msg.c_str());
        
        // 回显消息
        std::string echo_prefix = "回显: ";
        std::string response = echo_prefix + msg;
        conn->Send(response.c_str(), response.size());
    }

    // 连接关闭回调
    void OnClosed(const PtrConnection& conn) {
        INF_LOG("连接关闭: %d", conn->Id());
    }

    // 启动服务器
    void Start() {
        INF_LOG("回显服务器启动在端口: %d", 8080);
        _server.Start();
    }
};

int main(int argc, char* argv[]) {
    int port = 8080;
    
    if (argc > 1) {
        port = atoi(argv[1]);
    }
    
    INF_LOG("启动回显服务器,端口: %d", port);
    
    // 创建并启动服务器
    EchoServer server(port);
    server.Start();
    
    return 0;
}

TcpServer初始化阶段详解

TcpServer构造函数执行流程

cpp 复制代码
private:
        uint64_t _next_id;      //这是一个自动增长的连接ID,
        int _port;
        int _timeout;           //这是非活跃连接的统计时间---多长时间无通信就是非活跃连接
        bool _enable_inactive_release;//是否启动了非活跃连接超时销毁的判断标志
        EventLoop _baseloop;    //这是主线程的EventLoop对象,负责监听事件的处理
        Acceptor _acceptor;    //这是监听套接字的管理对象
        LoopThreadPool _pool;   //这是从属EventLoop线程池
        std::unordered_map<uint64_t, PtrConnection> _conns;//保存管理所有连接对应的shared_ptr对象

        using ConnectedCallback = std::function<void(const PtrConnection&)>;
        using MessageCallback = std::function<void(const PtrConnection&, Buffer *)>;
        using ClosedCallback = std::function<void(const PtrConnection&)>;
        using AnyEventCallback = std::function<void(const PtrConnection&)>;
        using Functor = std::function<void()>;
        ConnectedCallback _connected_callback;
        MessageCallback _message_callback;
        ClosedCallback _closed_callback;
        AnyEventCallback _event_callback;
cpp 复制代码
TcpServer(int port):
    _port(port),
    _next_id(0),
    _enable_inactive_release(false),
    _acceptor(&_baseloop, port),
    _pool(&_baseloop) {
    _acceptor.SetAcceptCallback(std::bind(&TcpServer::NewConnection, this, std::placeholders::_1));
    _acceptor.Listen();//将监听套接字挂到baseloop上
}

详细步骤:

成员初始化:

  • _port: 存储服务器监听端口
  • _next_id: 连接ID计数器初始化为0
  • _enable_inactive_release: 非活跃连接释放标志设为false

当TcpServer构造函数中初始化_baseloop成员变量时,会创建EventLoop对象:

cpp 复制代码
EventLoop::EventLoop():
    _thread_id(std::this_thread::get_id()), 
    _event_fd(CreateEventFd()), 
    _event_channel(new Channel(this, _event_fd)),
    _poller(),  // 创建Poller对象
    _timer_wheel(this) {
    
    // 设置eventfd的读事件回调
    _event_channel->SetReadCallback(std::bind(&EventLoop::ReadEventfd, this));
    
    // 启动eventfd的读事件监控
    _event_channel->EnableRead();
}

EventLoop创建详细步骤:

初始化线程ID:

cpp 复制代码
   _thread_id = std::this_thread::get_id()
  • 记录创建EventLoop的线程ID,用于后续线程安全检查

创建eventfd:

cpp 复制代码
   _event_fd = CreateEventFd()
   
   static int CreateEventFd() {
       int efd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
       if (efd < 0) {
           ERR_LOG("CREATE EVENTFD FAILED!!");
           abort();
       }
       return efd;
   }
  • 创建一个eventfd用于线程间通信,设置为非阻塞模式

创建event_channel:

cpp 复制代码
   _event_channel = new Channel(this, _event_fd)
  • 创建一个Channel对象管理eventfd的事件

创建Poller对象:

cpp 复制代码
   _poller()  // 默认构造函数
  • 创建Poller对象用于事件监控

创建TimerWheel对象

cpp 复制代码
   _timer_wheel(this)
  • 创建定时器管理对象

设置eventfd回调:

cpp 复制代码
   _event_channel->SetReadCallback(std::bind(&EventLoop::ReadEventfd, this));
  • 设置eventfd的读事件回调函数

启用eventfd读事件监控:

cpp 复制代码
   _event_channel->EnableRead();
  • 启用eventfd的读事件监控,这会将eventfd添加到Poller中

Poller创建过程

当EventLoop构造函数中初始化_poller成员变量时,会创建Poller对象:

cpp 复制代码
Poller::Poller() {
    _epfd = epoll_create(MAX_EPOLLEVENTS);
    if (_epfd < 0) {
        ERR_LOG("EPOLL CREATE FAILED!!");
        abort();
    }
}

Channel与Poller的关联

当调用Channel::EnableRead()等方法时,会将Channel添加到Poller中:

Acceptor创建过程:

  • 传入&_baseloop和port构造Acceptor
  • Acceptor构造函数内部用CreateServer(port)创建监听套接字:
cpp 复制代码
Acceptor(EventLoop *loop, int port): _socket(CreateServer(port)), _loop(loop), 
    _channel(loop, _socket.Fd()) {
    _channel.SetReadCallback(std::bind(&Acceptor::HandleRead, this));
}
  • CreateServer 内部调用:
cpp 复制代码
int CreateServer(int port) {
    bool ret = _socket.CreateServer(port);
    assert(ret == true);
    return _socket.Fd();
}
  • Socket::CreateServer 执行五个关键操作:
cpp 复制代码
bool CreateServer(uint16_t port, const std::string &ip = "0.0.0.0", bool block_flag = false) {
    //1. 创建套接字
    if (Create() == false) return false;
    //2. 设置非阻塞(如果需要)
    if (block_flag) NonBlock();
    //3. 绑定地址
    if (Bind(ip, port) == false) return false;
    //4. 开始监听(默认最大连接队列1024)
    if (Listen() == false) return false;
    //5. 启动地址端口重用
    ReuseAddress();
    return true;
}
  • 创建Channel对象管理监听套接字,设置读事件回调函数为 Acceptor::HandleRead

Channel构造:

cpp 复制代码
   Channel(EventLoop *loop, int fd): _fd(fd), _events(0), _revents(0), _loop(loop) {}
  • 将监听套接字的文件描述符与Channel关联
  • 初始化事件标志和关联的EventLoop
  • 此时还未设置任何监控事件(events = 0)

设置回调函数

cpp 复制代码
   _channel.SetReadCallback(std::bind(&Acceptor::HandleRead, this));
  • 将Acceptor::HandleRead方法绑定为Channel的读事件回调
  • 当监听套接字可读时(有新连接到达),会触发此回调

HandleRead实现

cpp 复制代码
   void HandleRead() {
       int newfd = _socket.Accept();
       if (newfd < 0) {
           return;
       }
       if (_accept_callback) _accept_callback(newfd);
   }
  • 接受新连接,获取新连接的文件描述符
  • 调用预设的_accept_callback处理新连接(即TcpServer::NewConnection)

线程池创建:

  • 构造 pool(&_baseloop),传入主事件循环指针
  • 此时线程池只是初始化,没有创建工作线程,等待后续 SetThreadCount 和 Create 调用

设置Acceptor回调函数

cpp 复制代码
_acceptor.SetAcceptCallback(std::bind(&TcpServer::NewConnection, this, std::placeholders::_1));

详细解析:

回调绑定机制:

  • 使用 std::bind 创建函数对象,绑定 TcpServer::NewConnection 成员函数
  • this 指针作为第一个参数,确保回调能访问到TcpServer对象
  • std::placeholders::1 占位符表示将来自Acceptor的新连接fd传给NewConnection

NewConnection函数职责:

cpp 复制代码
void NewConnection(int fd) {
    _next_id++;
    PtrConnection conn(new Connection(_pool.NextLoop(), _next_id, fd));
    conn->SetMessageCallback(_message_callback);
    conn->SetClosedCallback(_closed_callback);
    conn->SetConnectedCallback(_connected_callback);
    conn->SetAnyEventCallback(_event_callback);
    conn->SetSrvClosedCallback(std::bind(&TcpServer::RemoveConnection, this, std::placeholders::_1));
    if (_enable_inactive_release) conn->EnableInactiveRelease(_timeout);
    conn->Established();
    _conns.insert(std::make_pair(_next_id, conn));
}

启动监听

cpp 复制代码
_acceptor.Listen();

内部实现详解:

Acceptor::Listen内部:

cpp 复制代码
   void Listen() { _channel.EnableRead(); }
  • 调用Channel的EnableRead方法启动监听套接字的读事件监控

Channel::EnableRead内部:

cpp 复制代码
     void EnableRead() { _events |= EPOLLIN; Update(); }
  • 将EPOLLIN标志添加到_events中
  • 调用Update更新事件监控

Channel::Update内部:

cpp 复制代码
     void Update() { return _loop->UpdateEvent(this); }
  • 调用EventLoop的UpdateEvent方法

EventLoop::UpdateEvent内部:

cpp 复制代码
void UpdateEvent(Channel *channel) { return _poller.UpdateEvent(channel); }
  • 调用Poller的UpdateEvent方法

Poller::UpdateEvent内部:

cpp 复制代码
void UpdateEvent(Channel *channel) {
    bool ret = HasChannel(channel);
    if (ret == false) {
        //不存在则添加
        _channels.insert(std::make_pair(channel->Fd(), channel));
        return Update(channel, EPOLL_CTL_ADD);
    }
    return Update(channel, EPOLL_CTL_MOD);
}
  • 检查Channel是否已在监控集合中
  • 如果不存在, 添加到_channels映射并调用epoll_ctl添加监控
  • 如果存在, 调用epoll_ctl修改监控事件

Poller::Update内部:

cpp 复制代码
void Update(Channel *channel, int op) {
    int fd = channel->Fd();
    struct epoll_event ev;
    ev.data.fd = fd;
    ev.events = channel->Events();
    int ret = epoll_ctl(_epfd, op, fd, &ev);
    if (ret < 0) {
        ERR_LOG("EPOLLCTL FAILED!");
    }
    return;
}
  • 准备epoll_event结构
  • 调用epoll_ctl将监听套接字添加到epoll实例
  • 此时监听套接字的可读事件(有新连接)开始被监控

启动服务器完整流程

cpp 复制代码
void Start() {
    _pool.Create();  // 创建线程池中的工作线程
    _baseloop.Start(); // 启动主事件循环
}

线程池创建:

cpp 复制代码
void Create() {
    if (_thread_count > 0) {
        _threads.resize(_thread_count);
        _loops.resize(_thread_count);
        for (int i = 0; i < _thread_count; i++) {
            _threads[i] = new LoopThread();
            _loops[i] = _threads[i]->GetLoop();
        }
    }
    return ;
}
  • 线程池创建指定数量的LoopThread
  • 调用GetLoop获取并保存每个线程的EventLoop指针
  • GetLoop会等待线程创建并初始化EventLoop
cpp 复制代码
class LoopThread {
private:
    std::mutex _mutex;          // 互斥锁
    std::condition_variable _cond;   // 条件变量
    EventLoop *_loop;       // EventLoop指针变量,这个对象需要在线程内实例化
    std::thread _thread;    // EventLoop对应的线程

private:
    void ThreadEntry() {
        EventLoop loop;
        {
            std::unique_lock<std::mutex> lock(_mutex);//加锁
            _loop = &loop;
            _cond.notify_all();
        }
        loop.Start();
    }

public:
    LoopThread():_loop(NULL), _thread(std::thread(&LoopThread::ThreadEntry, this)) {}
    
    EventLoop *GetLoop() {
        EventLoop *loop = NULL;
        {
            std::unique_lock<std::mutex> lock(_mutex);//加锁
            _cond.wait(lock, [&](){ return _loop != NULL; });//loop为NULL就一直阻塞
            loop = _loop;
        }
        return loop;
    }
};

线程创建:

cpp 复制代码
   LoopThread():_loop(NULL), _thread(std::thread(&LoopThread::ThreadEntry, this)) {}
  • 构造函数初始化_loop为NULL
  • 创建线程,给这个线程绑定入口函数为ThreadEntry
  • 线程立即开始执行
cpp 复制代码
   void ThreadEntry() {
       EventLoop loop;  // 在线程栈上创建EventLoop对象
       {
           std::unique_lock<std::mutex> lock(_mutex);  // 加锁
           _loop = &loop;  // 将指针指向线程栈上的EventLoop
           _cond.notify_all();  // 通知等待的线程
       }
       loop.Start();  // 开始事件循环
   }
  • 在线程内部创建EventLoop对象
  • 通过互斥锁保护设置_loop指针
  • 通知可能等待的GetLoop()调用
  • 启动事件循环

获取EventLoop指针

cpp 复制代码
   EventLoop *GetLoop() {
       EventLoop *loop = NULL;
       {
           std::unique_lock<std::mutex> lock(_mutex);  // 加锁
           _cond.wait(lock, [&](){ return _loop != NULL; });  // loop为NULL就一直阻塞
           loop = _loop;
       }
       return loop;
   }
  • 主线程调用此函数获取工作线程的EventLoop
  • 使用条件变量等待EventLoop创建完成
  • 返回EventLoop指针

主事件循环启动:

cpp 复制代码
void Start() {
    while(1) {
        //1. 事件监控
        std::vector<Channel *> actives;
        _poller.Poll(&actives);
        //2. 事件处理
        for (auto &channel : actives) {
            channel->HandleEvent();
        }
  • 进入无限循环,监控事件、处理就绪事件、执行任务队列
  • 此时服务器完全启动,等待客户端连接

连接建立流程详解

新连接到达流程

新连接到达,监听套接字可读事件触发,

Acceptor::HandleRead被调用

cpp 复制代码
void HandleRead() {
    int newfd = _socket.Accept();
    if (newfd < 0) {
        return;
    }
    if (_accept_callback) _accept_callback(newfd);
}
  • Poller检测到监听套接字可读(有新连接请求)
  • EventLoop的事件循环调用Channel::HandleEvent
  • Channel根据事件类型调用Acceptor::HandleRead
  • 调用Socket::Accept获取新连接的文件描述符
  • 触发_accept_callback即TcpServer::NewConnection

Socket::Accept实现:

cpp 复制代码
int Accept() {
    // int accept(int sockfd, struct sockaddr *addr, socklen_t *len);
    int newfd = accept(_sockfd, NULL, NULL);
    if (newfd < 0) {
        ERR_LOG("SOCKET ACCEPT FAILED!");
        return -1;
    }
    return newfd;
}
  • 调用系统调用accept接受新连接
  • 返回新连接的文件描述符

连接初始化详解

  1. TcpServer::NewConnection实现:
cpp 复制代码
void NewConnection(int fd) {
    _next_id++;
    PtrConnection conn(new Connection(_pool.NextLoop(), _next_id, fd));
    conn->SetMessageCallback(_message_callback);
    conn->SetClosedCallback(_closed_callback);
    conn->SetConnectedCallback(_connected_callback);
    conn->SetAnyEventCallback(_event_callback);
    conn->SetSrvClosedCallback(std::bind(&TcpServer::RemoveConnection, this, std::placeholders::_1));
    if (_enable_inactive_release) conn->EnableInactiveRelease(_timeout);
    conn->Established();
    _conns.insert(std::make_pair(_next_id, conn));
}
  1. 工作线程选择机制:
cpp 复制代码
EventLoop* NextLoop() {
    if (_thread_count == 0) {
        return _baseloop;
    }
    _next_idx = (_next_idx + 1) % _thread_count;
    return _loops[_next_idx];
}
  • 如果没有工作线程,使用主线程的EventLoop
  • 否则采用简单的轮询算法选择一个工作线程
  • 确保连接均匀分布在各个线程中
  1. Connection构造过程:
cpp 复制代码
Connection(EventLoop *loop, uint64_t conn_id, int sockfd):
    _conn_id(conn_id), 
    _sockfd(sockfd),
    _enable_inactive_release(false), 
    _loop(loop), 
    _statu(CONNECTING), 
    _socket(_sockfd),
    _channel(loop, _sockfd) {
    _channel.SetCloseCallback(std::bind(&Connection::HandleClose, this));
    _channel.SetEventCallback(std::bind(&Connection::HandleEvent, this));
    _channel.SetReadCallback(std::bind(&Connection::HandleRead, this));
    _channel.SetWriteCallback(std::bind(&Connection::HandleWrite, this));
    _channel.SetErrorCallback(std::bind(&Connection::HandleError, this));
}
  • 初始化连接ID和状态
  • 创建Socket和Channel对象管理连接
  • 设置Channel的各种事件回调
  1. 非活跃连接释放机制:
cpp 复制代码
void EnableInactiveRelease(int sec) {
    _loop->RunInLoop(std::bind(&Connection::EnableInactiveReleaseInLoop, this, sec));
}

void EnableInactiveReleaseInLoop(int sec) {
    _enable_inactive_release = true;
    if (_loop->HasTimer(_conn_id)) {
        return _loop->TimerRefresh(_conn_id);
    }
    _loop->TimerAdd(_conn_id, sec, std::bind(&Connection::Release, this));
}
  • 将操作封装为任务,确保在正确的线程中执行
  • 添加定时器任务,超时后自动释放连接
  • 使用连接ID作为定时器ID

连接就绪详解

  1. Connection::Established实现:
cpp 复制代码
void Established() {
    _loop->RunInLoop(std::bind(&Connection::EstablishedInLoop, this));
}
  • 使用RunInLoop确保在连接所属的EventLoop线程中执行
  1. Connection::EstablishedInLoop实现:
cpp 复制代码
void EstablishedInLoop() {
    assert(_statu == CONNECTING);
    _statu = CONNECTED;
    _channel.EnableRead();
    if (_connected_callback) _connected_callback(shared_from_this());
}
  • 断言确保当前状态为CONNECTING
  • 将状态更新为CONNECTED
  • 调用Channel::EnableRead启动读事件监控:
cpp 复制代码
  void EnableRead() { _events |= EPOLLIN; Update(); }
  • 最后调用用户设置的连接建立回调
  1. shared_from_this机制:
cpp 复制代码
_connected_callback(shared_from_this())
  • Connection继承自std::enable_shared_from_this
  • shared_from_this()返回管理当前对象的shared_ptr
  • 确保回调中使用的Connection对象生命周期安全
  1. 回调函数调用:
  • 回调函数是在连接分配到的工作线程中执行的
  • 这确保了每个连接的所有操作都在同一个线程中进行
  • 避免了多线程并发访问导致的竞态条件
  1. 连接保存到管理表:
cpp 复制代码
_conns.insert(std::make_pair(_next_id, conn));
  • 将连接保存到TcpServer的_conns哈希表中
  • 使用连接ID作为键,便于后续查找和管理

数据收发流程详解

数据接收流程

  1. 可读事件触发,Connection::HandleRead被调用

当客户端发送数据到服务器时,连接对应的socket变为可读状态,触发事件处理:

cpp 复制代码
// Channel::HandleEvent内部逻辑(事件分发)
void HandleEvent() {
    if ((_revents & EPOLLIN) || (_revents & EPOLLRDHUP) || (_revents & EPOLLPRI)) {
        if (_read_callback) _read_callback();
    }
    // ...其他事件处理
}
  • Poller检测到连接socket可读
  • EventLoop调用对应Channel的HandleEvent
  • Channel根据事件类型调用Connection::HandleRead

Connection::HandleRead实现详解

cpp 复制代码
void HandleRead() {
    // 1. 接收socket的数据,放到缓冲区
    char buf[65536];
    ssize_t ret = _socket.NonBlockRecv(buf, 65535);
    if (ret < 0) {
        // 出错了,不能直接关闭连接
        return ShutdownInLoop();
    }
    // 将数据放入输入缓冲区,写入之后顺便将写偏移向后移动
    _in_buffer.WriteAndPush(buf, ret);
    
    // 2. 调用message_callback进行业务处理
    if (_in_buffer.ReadAbleSize() > 0) {
        // shared_from_this--从当前对象自身获取自身的shared_ptr管理对象
        return _message_callback(shared_from_this(), &_in_buffer);
    }
}

Socket::NonBlockRecv实现

cpp 复制代码
ssize_t NonBlockRecv(void *buf, size_t len) {
    return Recv(buf, len, MSG_DONTWAIT); // MSG_DONTWAIT 表示当前接收为非阻塞
}

ssize_t Recv(void *buf, size_t len, int flag = 0) {
    // ssize_t recv(int sockfd, void *buf, size_t len, int flag);
    ssize_t ret = recv(_sockfd, buf, len, flag);
    if (ret <= 0) {
        // EAGAIN 当前socket的接收缓冲区中没有数据了,在非阻塞的情况下才会有这个错误
        // EINTR  表示当前socket的阻塞等待,被信号打断了
        if (errno == EAGAIN || errno == EINTR) {
            return 0; // 表示这次接收没有接收到数据
        }
        ERR_LOG("SOCKET RECV FAILED!!");
        return -1;
    }
    return ret; // 实际接收的数据长度
}

Buffer::WriteAndPush实现

cpp 复制代码
void WriteAndPush(const void *data, uint64_t len) {
    Write(data, len);
    MoveWriteOffset(len);
}

void Write(const void *data, uint64_t len) {
    // 1. 保证有足够空间,2. 拷贝数据进去
    if (len == 0) return;
    EnsureWriteSpace(len);
    const char *d = (const char *)data;
    std::copy(d, d + len, WritePosition());
}

调用用户消息回调

cpp 复制代码
_message_callback(shared_from_this(), &_in_buffer);
  • 传递连接管理对象和输入缓冲区
  • 在回显服务器示例中,对应EchoServer::OnMessage
  • 用户在回调中可以读取缓冲区数据并进行处理

数据发送流程

用户调用Connection::Send发送数据

cpp 复制代码
void Send(const char *data, size_t len) {
    Buffer buf;
    buf.WriteAndPush(data, len);
    _loop->RunInLoop(std::bind(&Connection::SendInLoop, this, std::move(buf)));
}
  • 创建临时Buffer对象存储数据
  • 使用std::move优化性能,避免拷贝
  • 通过RunInLoop确保在正确的线程中执行SendInLoop

Connection::SendInLoop实现

cpp 复制代码
void SendInLoop(Buffer &buf) {
    if (_statu == DISCONNECTED) return;
    _out_buffer.WriteBufferAndPush(buf);
    if (_channel.WriteAble() == false) {
        _channel.EnableWrite();
    }
}
  • 检查连接状态,断开则不发送
  • 将数据写入输出缓冲区
  • 如果未启动写事件监控,则启动

Channel::EnableWrite实现

cpp 复制代码
void EnableWrite() { _events |= EPOLLOUT; Update(); }
  • 添加EPOLLOUT事件到监控
  • 通过Update更新事件监控

连接可写事件触发,Connection::HandleWrite被调用

cpp 复制代码
void HandleWrite() {
    // _out_buffer中保存的数据就是要发送的数据
    ssize_t ret = _socket.NonBlockSend(_out_buffer.ReadPosition(), _out_buffer.ReadAbleSize());
    if (ret < 0) {
        // 发送错误就该关闭连接了
        if (_in_buffer.ReadAbleSize() > 0) {
            _message_callback(shared_from_this(), &_in_buffer);
        }
        return Release(); // 这时候就是实际的关闭释放操作了
    }
    _out_buffer.MoveReadOffset(ret); // 千万不要忘了,将读偏移向后移动
    if (_out_buffer.ReadAbleSize() == 0) {
        _channel.DisableWrite(); // 没有数据待发送了,关闭写事件监控
        // 如果当前是连接待关闭状态,则有数据,发送完数据释放连接,没有数据则直接释放
        if (_statu == DISCONNECTING) {
            return Release();
        }
    }
    return;
}

Socket::NonBlockSend实现

cpp 复制代码
ssize_t NonBlockSend(void *buf, size_t len) {
    if (len == 0) return 0;
    return Send(buf, len, MSG_DONTWAIT); // MSG_DONTWAIT 表示当前发送为非阻塞
}

ssize_t Send(const void *buf, size_t len, int flag = 0) {
    // ssize_t send(int sockfd, void *data, size_t len, int flag);
    ssize_t ret = send(_sockfd, buf, len, flag);
    if (ret < 0) {
        if (errno == EAGAIN || errno == EINTR) {
            return 0;
        }
        ERR_LOG("SOCKET SEND FAILED!!");
        return -1;
    }
    return ret; // 实际发送的数据长度
}

处理发送结果

  • 如果发送出错(ret < 0):
  • 处理剩余输入数据
  • 调用Release关闭连接
  • 如果发送成功:
  • 更新缓冲区读偏移(MoveReadOffset(ret))
  • 如果缓冲区为空,关闭写事件监控(DisableWrite())
  • 如果连接状态为DISCONNECTING且所有数据已发送,释放连接

Channel::DisableWrite实现

cpp 复制代码
void DisableWrite() { _events &= ~EPOLLOUT; Update(); }
  • 从事件标志中移除EPOLLOUT
  • 通过Update更新事件监控

关于Http模块的流程

HTTP请求的接收流程

客户端发送HTTP请求

  • 数据通过TCP连接到达服务器。

数据进入内核接收缓冲区

  • 操作系统内核接收数据,暂存在socket的接收缓冲区。

muduo通过read()读取数据到in_buffer

  • muduo检测到socket可读事件,调用read()把数据读到in_buffer。

HTTP解析器处理in_buffer

  • muduo的HTTP模块会不断检查in_buffer,尝试解析出完整的HTTP请求(包括请求行,请求头,请求体)。
  • 如果数据不完整(比如POST体还没收全),就继续等待数据到来。

解析出完整请求后,调用用户回调

  • 一旦解析出完整的HTTP请求,muduo会调用你注册的HTTP请求处理回调(如onRequest),把HttpRequest对象传给你的业务代码。

HTTP响应的发送流程

你的业务代码生成HttpResponse

  • 你在回调里构造HttpResponse对象,设置响应内容、状态码、头部等。

muduo把HttpResponse序列化到out_buffer

  • muduo会把HttpResponse对象序列化成HTTP协议格式的字符串,写入out_buffer。

尝试write()到socket

  • muduo尝试把out_buffer的数据写入socket(内核发送缓冲区)。
  • 如果一次写不完,剩余数据继续留在out_buffer,等待下次socket可写时再发。

客户端收到HTTP响应

  • 数据通过网络传输到客户端,客户端解析HTTP响应。

首先是会创建个TCPserver对象_server,然后就是然后这个_server成员列表会创建个_baseloop对象,_acceptor对象,_pool对象,会先执行它们的默认构造函数,base_loop对象的成员列表中会创建poller对象,channel对象,timer_wheel对象

模块初始化都干了什么事情

Poller对象的创建

EventLoop对象的创建

Channel对象的创建

Socket对象的创建

Acceptor对象的创建

TcpServer对象的创建

LoopThreadPool对象的创建

LoopThread对象的创建

Timer_wheel对象的创建

Connection对象的创建

1. 连接触发监听套接字的可读事件

客户端发起连接请求(connect),操作系统内核接收到SYN包,完成三次握手后,监听套接字变为可读状态(有新连接待接受)。

2. Poller检测到事件

在主事件循环(baseloop)中:

  • _poller.Poll(&actives)检测到监听套接字的可读事件
  • 将监听套接字对应的Channel添加到活跃Channel列表中
3. EventLoop处理就绪事件

在主事件循环的事件处理阶段:

cpp 复制代码
for (auto &channel : actives) {
    channel->HandleEvent();
}
  • 调用监听套接字Channel的HandleEvent方法
  • Channel根据就绪事件类型调用相应的回调函数
4. Acceptor的HandleRead方法被调用

监听套接字的可读事件触发了Channel的读回调,即Acceptor::HandleRead:

cpp 复制代码
void Acceptor::HandleRead() {
    int newfd = _socket.Accept();  // 接受新连接
    if (newfd < 0) {
        return;
    }
    if (_accept_callback) _accept_callback(newfd);  // 调用接受连接回调
}
5. TcpServer::NewConnection方法被调用

Acceptor的接受连接回调是TcpServer::NewConnection:

cpp 复制代码
void TcpServer::NewConnection(int fd) {
    _next_id++;  // 生成新的连接ID
    
    // 选择一个EventLoop来处理这个连接(负载均衡)
    EventLoop* loop = _pool.NextLoop();
    
    // 创建Connection对象管理这个连接
    PtrConnection conn(new Connection(loop, _next_id, fd));
    
    // 设置Connection的各种回调函数
    conn->SetMessageCallback(_message_callback);
    conn->SetClosedCallback(_closed_callback);
    conn->SetConnectedCallback(_connected_callback);
    conn->SetAnyEventCallback(_event_callback);
    conn->SetSrvClosedCallback(std::bind(&TcpServer::RemoveConnection, this, std::placeholders::_1));
    
    // 如果启用了非活跃连接超时释放,设置超时时间
    if (_enable_inactive_release) conn->EnableInactiveRelease(_timeout);
    
    // 完成连接初始化
    conn->Established();
    
    // 将连接添加到管理表
    _conns.insert(std::make_pair(_next_id, conn));
}
6. Connection对象创建

当执行new Connection(loop, _next_id, fd)时:

  • 初始化成员变量:连接ID、文件描述符、EventLoop指针等
  • 创建Socket对象包装文件描述符
  • 创建Channel对象关联到EventLoop和连接套接字
  • 设置Channel的各种事件回调函数
  • 连接状态设置为CONNECTING
7. Connection::Established方法执行
cpp 复制代码
void Connection::Established() {
    _loop->RunInLoop(std::bind(&Connection::EstablishedInLoop, this));
}
  • 将EstablishedInLoop任务放入连接所属的EventLoop的任务队列
  • 这确保了连接的初始化在正确的线程中执行
8. Connection::EstablishedInLoop方法执行

在连接所属的EventLoop线程中:

cpp 复制代码
void Connection::EstablishedInLoop() {
    assert(_statu == CONNECTING);  // 确保当前状态是CONNECTING
    _statu = CONNECTED;  // 修改连接状态为CONNECTED
    
    // 启动读事件监控
    _channel.EnableRead();
    
    // 调用连接建立回调函数
    if (_connected_callback) _connected_callback(shared_from_this());
}
9. 启动读事件监控

当执行_channel.EnableRead()时:

cpp 复制代码
void Channel::EnableRead() { 
    _events |= EPOLLIN; 
    Update(); 
}

void Channel::Update() {
    // 通过EventLoop更新事件监控
    _loop->UpdateEvent(this);
}
10. 更新Poller中的事件监控

EventLoop::UpdateEvent调用Poller::UpdateEvent:

cpp 复制代码
void Poller::UpdateEvent(Channel *channel) {
    bool ret = HasChannel(channel);
    if (ret == false) {
        // 不存在则添加
        _channels.insert(std::make_pair(channel->Fd(), channel));
        return Update(channel, EPOLL_CTL_ADD);
    }
    return Update(channel, EPOLL_CTL_MOD);
}

void Update(Channel *channel, int op) {
    int fd = channel->Fd();
    struct epoll_event ev;
    ev.data.fd = fd;
    ev.events = channel->Events();
    int ret = epoll_ctl(_epfd, op, fd, &ev);
    if (ret < 0) {
        ERR_LOG("EPOLLCTL FAILED!!");
    }
    return;
}
  • 将连接套接字添加到epoll的监控列表中
  • 设置监控的事件类型(EPOLLIN表示可读事件)
11. 调用连接建立回调函数

执行用户设置的连接建立回调函数:

cpp 复制代码
if (_connected_callback) _connected_callback(shared_from_this());
  • 将当前Connection的shared_ptr传递给回调函数
  • 用户可以在回调函数中处理连接建立事件
12. 连接就绪,等待数据交互

至此,新连接的处理流程完成:

  • 连接已被接受并分配了唯一ID
  • 连接被分配给了一个EventLoop进行管理
  • 连接的读事件监控已启动
  • 连接状态已设置为CONNECTED
  • 连接已添加到TcpServer的连接管理表中
  • 用户的连接建立回调函数已被调用

连接现在处于就绪状态,可以进行数据收发。当有数据到达时,会触发连接套接字的可读事件,从而调用Connection::HandleRead方法处理数据。

相关推荐
BingoGo1 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php
JaguarJack1 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php·服务端
BingoGo2 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php
JaguarJack2 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php·服务端
JaguarJack3 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
后端·php·服务端
BingoGo3 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
php
JaguarJack4 天前
告别 Laravel 缓慢的 Blade!Livewire Blaze 来了,为你的 Laravel 性能提速
后端·php·laravel
郑州光合科技余经理5 天前
代码展示:PHP搭建海外版外卖系统源码解析
java·开发语言·前端·后端·系统架构·uni-app·php
feifeigo1235 天前
matlab画图工具
开发语言·matlab
dustcell.5 天前
haproxy七层代理
java·开发语言·前端