最新资讯

  • 仿muduo库实现并发服务器(1)

仿muduo库实现并发服务器(1)

2026-01-29 02:13:50 栏目:最新资讯 5 阅读

一、Channel和Poller协调合作测试

测试思路:

创建一个监听套接字的Channel:lst_channel,对可读回调函数void SetReadCallback(const EventCallback &cb)传入Acceptor()获取新连接,同时启动可读事件监控

这里会通过Channel::EnableRead()的Update模块(void Channel::Update() { _poller->UpdateEvent(this); }),将lst_channel添加到Poller的 _channels管理模块并且对监听套接字进行可读事件监控设置epoll_ctl。当Poller的 Poll(std::vector *active)开始监控,客户端发起连接,可读事件就绪,由active返回这个活跃lst_channel,得到的lst_channel就可以调用 HandleEvent()进行事件处理,这里是可读事件触发所以调用_read_callback()(_event_callback为空)。

由于之前对监听套接字lst_channel设置的可读回调函数是Acceptor(),那么就会执行这个函数:

获取新链接,这里先不创建Socket套接字,直接创建通信channel对newfd描述符进行事件管理。同时对这个通信channel设置可读、可写、关闭、错误、任意事件的回调函数,并且启动可读事件监控,因为Poller的 Poll一直在不断监控,所以一旦客户端给服务端发送数据,这个通信channel的可读事件被触发,按照下面的tcp_svr.cc测试代码:服务端成功读取信息,就会开启可写事件监控,触发可写事件给客户端发送特定信息。////注意当客户端关闭时,是可写事件被触发,recv接口返回值为0,再调用HandleClose函数,移除这个通信channel的所有监控,释放channel。

Channel模块和Poller模块代码参考

class Poller;
//描述符事件管理
class Channel
{
private:
    int _fd;
    Poller *_poller;
    uint32_t _events; //当前需要监控的事件
    uint32_t _revents; //当前连接触发的事件
    using EventCallback = std::function;//由connection模块传入
    EventCallback _read_callback;   //可读事件被触发的回调函数
    EventCallback _write_callback;   //可写事件被触发的回调函数
    EventCallback _error_callback;   //错误事件被触发的回调函数
    EventCallback _close_callback;   //连接断开事件被触发的回调函数
    EventCallback _event_callback;   //任意事件被触发的回调函数
public:
    Channel(int fd, Poller *poller):_poller(poller), _fd(fd), _events(0), _revents(0) {}
    int Fd() { return _fd; }
    //哪些事件被触发
    void SetREvents(uint32_t events) { _revents = events; }
    //需要监控哪些事件:传给poller
    uint32_t Event() { return _events; }
    //设置回调函数
    void SetReadCallback(const EventCallback &cb) { _read_callback = cb; }
    void SetWriteCallback(const EventCallback &cb) { _write_callback = cb; }
    void SetErrorCallback(const EventCallback &cb) { _error_callback = cb; }
    void SetCloseCallback(const EventCallback &cb) { _close_callback = cb; }
    void SetEventCallback(const EventCallback &cb) { _event_callback = cb; }

    //当前是否监控了可读
    bool ReadAble() { return (_events & EPOLLIN); }
    //当前是否监控了可写
    bool WriteAble() { return (_events & EPOLLOUT); }

    //启动读事件监控
    void EnableRead() { _events |= EPOLLIN;  Update(); }
    //启动写事件监控
    void EnableWrite() { _events |= EPOLLOUT;  Update(); }
    //关闭读事件监控
    void DisableRead() { _events &= ~EPOLLIN; Update(); }
    //关闭写事件监控
    void DisableWrite() { _events &= ~EPOLLOUT; Update(); }
    //关闭所有事件监控
    void DisableAll() { _events = 0; Update(); }
    //移除监控
    void Remove();
    void Update();

    //事件处理,一旦连接触发了事件,就调用这个函数,自己触发了什么时间自己决定怎么处理
    void HandleEvent()
    {
        if((_revents & EPOLLIN) || (_revents & EPOLLRDHUP) || (_revents & EPOLLPRI))
        {
             /*不管任何事件,都调用的回调函数*/
            if(_event_callback) _event_callback(); 
            if(_read_callback) _read_callback();
        }
        //有可能会释放连接的操作事件,一次只处理一个
        if(_revents & EPOLLOUT)
        {
            if(_write_callback) _write_callback();
            if(_event_callback) _event_callback(); //放到事件处理完毕后调用,刷新活跃度;
            //如果先执行后,客户端断开,_write_callback认为还是有效,写会出错
        }
        else if(_revents & EPOLLERR)
        {
            if(_event_callback) _event_callback(); 
            if(_error_callback) _error_callback();//一旦出错就会释放连接,后面就不能再进行处理任意事件,所以放前面
        }
        else if(_revents & EPOLLHUP)
        {
            if(_event_callback) _event_callback(); 
            if(_close_callback) _close_callback();
        }
    }
};


//描述符IO事件监控
#define MAX_EPOLLEVENTS 1024 
class Poller
{
private:
    int _epfd; //epoll操作句柄
    struct epoll_event _evs[MAX_EPOLLEVENTS]; //监控时保存当前所有活跃事件
    std::unordered_map _channel;  //管理描述符和描述符对应的事件管理Channel对象 
private:
    //对epoll的直接操作
    void Update(Channel* channel, int op)
    {
        //int epoll_ctl(int epfd, int op, int fd,struct epoll_event *_Nullable event);
        struct epoll_event event;
        event.data.fd = channel->Fd();
        event.events = channel->Event();
        int ret = epoll_ctl(_epfd, op, channel->Fd(), &event);
        if(ret < 0)
        {
            ERR_LOG("EPOLLCTL FAILED!");
            abort();
        }
    }
    //判断一个Channel是否已经添加事件监控
    bool HasChannel(Channel* channel)
    {
        auto it = _channel.find(channel->Fd());
        if(it != _channel.end()) return true;
        return false;
    }
public:
    Poller()
    {
        _epfd = epoll_create(1);
        if(_epfd < 0)
        {
            ERR_LOG("EPOLL CREATE FAILED!");
            abort();
        }
    }
    //添加或修改监控事件
    void UpdateEvent(Channel* channel)
    {
        if (!HasChannel(channel))
        {
            //不在就EPOLL_CTL_ADD,并且加入_channel
            Update(channel, EPOLL_CTL_ADD);
            _channel[channel->Fd()] = channel;
        }
        //在就EPOLL_CTL_MOD
        else Update(channel, EPOLL_CTL_MOD);
    }
    //移除监控
    void RemoveEvent(Channel* channel)
    {
        auto it = _channel.find(channel->Fd());
        if (it != _channel.end())
        {
            _channel.erase(it);
        }
        Update(channel, EPOLL_CTL_DEL);
    }
    //开始监控,返回活跃链接
    void Poll(std::vector *active)
    {
        //int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
        int nfds = epoll_wait(_epfd, _evs, MAX_EPOLLEVENTS, -1);//-1:阻塞监控
        if(nfds < 0)
        {
            if(errno == EINTR)
            {
                return;
            }
            ERR_LOG("EPOLL WAIT ERROR:%s
", strerror(errno));
            abort();
        }
        for(int i = 0; i < nfds; i++)
        {
            auto it = _channel[_evs[i].data.fd];
            it->SetREvents(_evs[i].events);//设置实际就绪的事件
            active->push_back(it);
        }
    }
};

void Channel::Remove() { _poller->RemoveEvent(this); }
void Channel::Update() { _poller->UpdateEvent(this); }

注意:
void Channel::Remove() { _poller->RemoveEvent(this); }
void Channel::Update() { _poller->UpdateEvent(this); }
这里必须写在类外,如果写在Channel类内,由于编译器是从上到下编译,只声明了class Poller是不够的,因为Poller类的具体设计在下面,此时编译器是不知道_poller有哪些成员变量和成员函数的,这里会出错;只能先暂时声明void Remove(); void Update() 这两个函数,当走到最后才能对这两个函数的实现进行编译。

客户端和服务端测试代码

//tcp_srv.cc:服务端测试
#include "../source/server.hpp"

void HandleClose(Channel *channel)
{
    std::cout << "close: " << channel->Fd() << std::endl;
    channel->Remove();//移除监控
    delete channel;//释放Channel
}
void HandleWrite(Channel *channel)
{
    char *data = "你好bugubugu";
    int ret = send(channel->Fd(), data, strlen(data), 0);
    if(ret < 0) HandleClose(channel);
    else channel->DisableWrite();//回应对方一次就关闭写监控
}
void HandleError(Channel *channel)
{
    HandleClose(channel);
}
void HandleEvent(Channel *channel)
{
    std::cout << "有事件被触发" << std::endl;
}

void HandleRead(Channel *channel)
{
    char buf[1024] = {0};
    int ret = recv(channel->Fd(), buf, 1023, 0);
    if(ret <= 0) HandleClose(channel);
    else
    {
        channel->EnableWrite();
        std::cout << buf << std::endl;
    }
}
void Acceptor(Poller *poller, Channel* lis_channel)
{
    int newfd = accept(lis_channel->Fd(), nullptr, nullptr);
    if(newfd < 0) return;
    Channel* channel = new Channel(newfd, poller);
    channel->SetReadCallback(std::bind(HandleRead, channel));//为通信套接字设置可读事件的回调函数
    channel->SetWriteCallback(std::bind(HandleWrite, channel));//为通信套接字设置可写事件的回调函数
    channel->SetCloseCallback(std::bind(HandleClose, channel));//为通信套接字关闭可读事件的回调函数
    channel->SetErrorCallback(std::bind(HandleError, channel));//为通信套接字设置错误事件的回调函数
    channel->SetEventCallback(std::bind(HandleEvent, channel));//为通信套接字设置任意事件的回调函数
    channel->EnableRead();//启动可读事件监控
}
int main()
{
    Socket lis_sock;
    lis_sock.CreateServer(8080);
    Poller poller;
    //为监听套接字,创建Channel进行事件的管理和处理
    Channel channel(lis_sock.Fd(), &poller);
    //设置回调函数:获取新链接,为新连接创建Channel,设置回调函数并且添加进行监控
    channel.SetReadCallback(std::bind(Acceptor, &poller, &channel));
    channel.EnableRead();
    while(1)
    {
        std::vector actives;
        poller.Poll(&actives);
        for(auto & e : actives)
        {
            e->HandleEvent();
        }
    }
    lis_sock.Close();
    return 0;
}
-----------------------------------------------------------------------------------------
//tcp_cli.cc:客户端测试
#include "../source/server.hpp"
int main()
{
    Socket cli_sock;
    cli_sock.CreateClient(8080, "127.0.0.1");
    while (1)
    {
        std::string s = "我是bugubugu!";
        cli_sock.Send(s.c_str(), s.size());
        char buffer[1024] = {0};
        cli_sock.Recv(buffer, 1023);
        DBG_LOG("%s", buffer);
        sleep(1);
    }
    return 0;
}

二、EventLoop

Poller模块只是EventLoop的子模块,EventLoop才是对所有描述符进行事件监控和处理的模块。

eventfd

1、认识eventfd:是一种事件通知机制,可以创建一个描述符用于实现事件通知,本质就是创建eventfd就会在内核创建一个计数器的结构:每次向eventfd中写入(write)一个数值来表示事件通知次数,也可以使用read进行数据读取,读取到的数据就是通知的次数。这里和信号量不一样,信号量是加一再减一;而对于eventfd,假设每次给eventfd写入一个1,就表示通知一次,连续write三次之后,从read读取的数字是3,读取之后计数清0。
在EventLoop模块中实现线程间的事件通知功能
2、eventfd的使用:
#include
int eventfd(unsigned int initval, int flags);
功能:创建一个eventfd对象,实现事件通知
参数:initial:计算初值;flags:EFD_CLOEXEC -- 禁止进程复制;EFD_NONBLOCK -- 启动非阻塞属性
返回值:返回一个文件描述符用于操作(
eventfd通过read/write/close进行操作,read/write进行IO时的数据只能是一个8字节数据

EventLoop模块设计思想

1、eventloop和线程是一一对应的:即1 Thread = 1 Poller = N Channels,

  • 1 个线程 (Thread) 拥有 1 个 Poller (EventLoop)。
  • 1 个 Poller 管理 成千上万个 Channel。
  • 结论:1 个线程同时管理成千上万个 Channel(连接)

2、为什么是一一对应?EventLoop监控一个连接,这个连接一旦就绪,就要进行事件处理。但是如果这个描述符在多个线程中触发了事件,进行处理时,因为是多个线程对一个Fd进行操作,就会存在安全问题,因此需要将一个连接即一个Fd的事件监控、连接事件处理以及其他操作都放在同一个线程中进行。

3、如何保证一个连接的所有操作都在 EventLoop 对应的线程里?
        并不需要将对连接的所有操作一律加入任务队列,而是应该采用 runInLoop 的机制进行判断:每当要执行某个操作(如 send或 close)时,先检查当前调用线程是否就是该 EventLoop 所在线程

  • 如果是(InLoop):说明不存在跨线程竞争,直接执行该操作,无需入队(零开销)。
  • 如果不是(NotInLoop/外来任务):说明是其他线程(如业务线程池)想操作该连接,此时必须将操作封装成任务(Functor),加入任务队列并唤醒 EventLoop 线程,由 EventLoop 线程在稍后的阶段统一执行

举个例子:

(1)处理IO事件(InLoop):这里处理的是“来自客户的请求”(读数据、新连接)。这是由 epoll_wait 直接触发的,不经过任务队列,直接在Channel::HandleEvent 里执行。

(2)处理任务队列(NotInLoop/外来任务)
        假设你的服务器收到了一个客户端 Client-zhangsan 的连接,服务器会创建唯一的一个对象Channel 来代表 Client-zhangsan ,这个Channel 被分配给了 IO 线程 B(Poller) 管理;现在,外人线程 A(比如计算线程) 正在处理Client-zhangsan 发来的请求(比如计算1+1=2 ),计算完了,线程 A 拿着结果 2,想发回给 Client-zhangsan。但是,外人线程 A 手里并没有一个属于它自己的“Channel 副本”,它想发数据,必须找到唯一的那个属于线程 B 的 Channel
外人 A 把数据打包成一个任务, 把这个任务扔进 IO 线程 B 的任务队列;唤醒EventLoop 线程 B 醒来,由 B 自己调用 send

4、eventloop具体的处理流程:
(1) 等待事件:在线程中调用 epoll_wait监控描述符,等待事件就绪(此时线程可能阻塞)。
(2) 处理已就绪的 IO 事件:当描述符就绪(如可读/可写),立即在当前线程直接调用对应的事件处理函数HandleEvent()。(注意:这一步通常不进任务队列,因为这里本来就是 IO 线程自己,直接处理效率最高)
(3)执行任务队列(处理外来任务):IO 事件处理完毕后,检查任务队列。 如果有来自其他线程投递的任务(如业务线程计算完结果后的发送请求),则在当前线程依次取出并执行这些任务。

用一个图来表示就是

这样可以保证对于连接的所有操作,都在一个线程进行,不涉及安全问题(注意任务队列的入队和出队操作必须加互斥锁(Mutex))

接口设计功能

  1. 事件监控:使用Poller模块,有事件就绪则进行事件处理
  2. 执行任务队列中的任务:一个线程安全的任务队列

注意:有可能因为一直等待描述符IO事件就绪,导致执行线程阻塞,这时候任务队列中的任务将得不到执行,因此得有一个事件通知的东西,能够唤醒事件监控的阻塞
        当事件就绪,需要处理的时候,处理过程中,如果对连接要进行某些操作,这些操作必须在EventLoop对应的线程中执行,保证对连接的各项操作都是线程安全的。
1.如果执行的操作本就在线程中,不需要将操作压入队列了,可以直接执行
2.如果执行的操作不在线程中,才需要加入任务池,等到事件处理完了然后执行任务

代码分析

class EventLoop
{
private:
    std::thread::id _thread_id; //当前线程id:判断操作是否要加入任务队列还是直接执行
    Poller _poller;//进行所有描述符的事件监控
    using Functor = std::function;
    std::vector _tasks;//任务池
    std::mutex _mutex;//实现任务池操作的线程安全
    int _event_fd; //eventfd: 唤醒IO事件监控可能导致的阻塞
public:
    void RunAllTask()
    {
        std::vector tasks;
        {
            std::unique_lock _lock(_mutex);
            tasks.swap(_tasks);//共享资源
        }
        for(auto &f : tasks)//线程私有
            f();
    }
};

void RunAllTask() :用 task 变量 + swap 将共享资源转移为线程私有,有利于
(1)把持有锁的时间降到最低:假设把任务执行放到锁的作用域,_task 任务执行时间太长,在这段时间里其他线程想往 _task 塞任务,是不是就得一直等待锁,服务器效率低下。使用局部变量交换任务后,f() 就不是共享资源了,不影响其他线程对 _task 变量的访问。
(2)避免死锁:还是一样,如果全程加锁,当我执行任务A时,锁被 RunAllTask() 拿着,当A里面需要实现往_task塞任务函数时,这个函数也需要申请锁,就会造成 RunAllTask() 在等待A运行结束,而任务A里因为调用塞任务函数在等待  RunAllTask()  运行结束。

下面给的代码是EventLoop的部分实现代码

class EventLoop
{
private:
    std::thread::id _thread_id; //当前EventLoop线程id:判断操作是否要加入任务队列还是直接执行
    Poller _poller;//进行所有描述符的事件监控
    using Functor = std::function;
    std::vector _tasks;//任务池
    std::mutex _mutex;//实现任务池操作的线程安全
    int _event_fd; //eventfd: 唤醒IO事件监控可能导致的阻塞
    Channel _event_channel;
public:
    static int CreatEventFd()
    {
       int fd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
       if(fd < 0)
       {
            ERR_LOG("CreatEventFd fail");
            abort();
       }
       return fd;
    }
    void ReadEventFd()
    {
        uint64_t val = 0;
        int ret = read(_event_fd, &val, sizeof(val));
        if (ret <= 0)
        {
            if (errno == EAGAIN || errno == EINTR)
            {
                return;
            }
            ERR_LOG("Read EventFd Fail");
            abort();
        }
    }
    void WakeUpEventFd()
    {
        uint64_t val = 1;
        int ret = write(_event_fd, &val, sizeof(val));
        if (ret <= 0)
        {
            if (errno == EAGAIN || errno == EINTR)
            {
                return;
            }
            ERR_LOG("Write EventFd Fail");
            abort();
        }
    }
public:
    void RunAllTask()
    {
        std::vector tasks;
        {
            std::unique_lock _lock(_mutex);
            tasks.swap(_tasks);
        }
        for(auto &f : tasks)
            f();
    }
    EventLoop():_thread_id(std::this_thread::get_id()),
                _event_fd(CreatEventFd()),
                _event_channel(_event_fd, this)
    {
        //给eventfd添加可读事件回调函数,读取eventfd事件通知次数
        _event_channel.SetReadCallback(std::bind(&EventLoop::ReadEventFd, this));
        //启动EventFd的读事件监控
        _event_channel.EnableRead();
    } 

    //事件监控-->就绪事件处理-->执行任务
    void Start()
    {
        //事件监控
        std::vector actives;
        _poller.Poll(&actives);
        //就绪事件处理
        for(auto &e : actives)
        {
            e->HandleEvent();
        }
        //执行任务
        RunAllTask();
    }

    //判断要执行的任务是否处于EventLoop线程,是就执行,不是就加入任务队列
    void RunInLoop(const Functor & cb)
    {
        if(IsInLoop()) cb();
        else QueueInLoop(cb);
    }
    //判断当前线程是否是EventLoop对应线程
    bool IsInLoop()
    {
        return (std::this_thread::get_id() == _thread_id);
    }
    //将任务压入线程池
    void QueueInLoop(const Functor & cb)
    {
        {
            std::unique_lock lock(_mutex);
            _tasks.push_back(cb);
        }
        //有可能没有事件就绪,事件监控阻塞着,需要一个事件通知的函数,触发eventfd可读事件
        //给eventfd写一个数据触发
        WakeUpEventFd();
    }
    //添加、修改描述符的事件监控
    void UpdateEvent(Channel * channel)
    {
        _poller.UpdateEvent(channel);
    }
    //移除描述符的事件监控
    void RemoveEvent(Channel * channel)
    {
        _poller.RemoveEvent(channel);
    }
    
};

三、定时器模块的整合

整合思想

  • timerfd:实现内核每隔一段时间,给进程一次超时事件(timerfd可读)
  • timerwheel: 实现每次执行Runtimetask,都可以执行一波到期的定时任务
  • 要实现一个完整的秒级定时器,就需要将这两个功能整合到一起:timerfd设置为每秒钟触发一次定时事件,当事件被触发,则运行一次timerwheel的runtimertask,执行一下所有的过期定时任务

而timefd的事件监控与触发,可以融合 EventLoop 来实现:当 EventLoop 的 poll 监控到线程往timefd 写入时,就会触发可读事件处理,即读取 timefd 超时次数和执行 timerwheel 的 runtimertask。所以TimeWheel 模块需要引入 EventLoop 对timefd 进行监控,引入channel 对timefd 设置可读事件回调函数已经开启可读事件监控。

其实只要是描述符:定时器timerfd,监听fd,连接fd,eventfd都需要和 eventloop(对Poller的封装)糅合在一起才能监控,并且需要channel管理事件:设置可读回调函数等等。

具体步骤

  1. 在TimerWheel的初始化阶段,会执行 CreateTimerFd()创建和启动定时器。这里会调用 timerfd_settime (内核里的计时器就开始走字了,内核会往这个 timerfd 的文件缓冲区里写入数据),返回 timerfd
  2. 创建 _timer_channel 绑定这个 fd 并进行管理:设置可读回调函数并且对这个 fd 开启监控,此时_timer_channel 已经通过 void Channel::Update() { _loop->UpdateEvent(this); } 将 timerfd 可读监控加入epoll 的监控里
  3. 一旦poll 监控到 timerfd 有事件触发 (启动计时器那块就会触发可读事件),就会执行EvnetHandle(),即执行可读回调函数 ReadTimeFd() 和 RunTimerTask(),实现读取 timefd 超时次数和执行 timerwheel 的 RunTimerTask()

注意:定时器中有_timers, _wheel成员,定时器信息和_wheel的操作有可能在多线程中进行,因此需要考虑线程安全问题。 如果不想加锁,那就把对定时器的所有操作,都放到一个线程中进行,对于 RunTimerTask() ,与TimerAdd、TimerRefresh、TimerCancel相比,它是eventloop线程本身的,不需要像其他三个函数是外部线程委托的进行判断。

区分同步异步

TimerWheel类:HasTimerTask 不能用 RunInLoop 是因为HasTimerTask 可能是外来线程给的业务,那我塞到RunInLoop只能是加入到任务队列,并不能保证马上执行,但是代码继续往下走,外来线程需要知道任务是否存在。解决方法就是我们自己控制 HasTimerTask 只能让eventloop线程调用,实现串行执行。

底层:同步是需求要立马答复比如线程A需要马上知道hastimer的返回值,而异步适合的需求是不需要马上执行任务,RunInLoop 的设计初衷就是用来处理那些“不需要立刻知道结果”的事情。

下面是把定时器 TimerFd 加入到 TimerWheel模块,实现一个完整的秒级定时器

using TaskFunc = std::function;
using ReleaseFunc = std::function;
// 定时任务
class TimerTask
{
private:
    bool _cancel;
    uint64_t _id;         // 定时器任务对象ID
    uint32_t _timeout;    // 定时任务的超时时间
    TaskFunc _task_cb;    // 定时器任务对象要执行的定时任务
    ReleaseFunc _release; // 用于删除TimerWheel(_timers)中保存的定时器对象信息
public:
    TimerTask(uint64_t id, uint32_t timeout, const TaskFunc &task_cb)
        : _id(id), _timeout(timeout), _task_cb(task_cb), _cancel(false) {}
    ~TimerTask()
    {
        if (_cancel == false)
        {
            _task_cb();
        }
        _release();
    }
    void Cancel() { _cancel = true; }
    void SetRelease(const ReleaseFunc &release) { _release = release; }
    uint32_t DelayTime() { return _timeout; }
};

class TimerWheel
{
private:
    using PtrTask = std::shared_ptr;
    using WeakTask = std::weak_ptr;

    int _capacity; // 最大延迟时间
    std::vector> _wheel;
    int _tick;                                      // 当前秒针,走到哪里释放哪里的对象,相当于执行哪里的任务
    std::unordered_map _timers; // 定时器

    int _timerfd; // 定时器描述符:可读事件实现读取超时次数和执行定时任务
    EventLoop *_loop;
    std::unique_ptr _timer_channel;

private:
    void RemoveTimer(uint64_t id)
    {
        auto pos = _timers.find(id);
        if (pos != _timers.end())
        {
            _timers.erase(pos);
        }
    }
    int CreatTimerFd()
    {
        int fd = timerfd_create(CLOCK_MONOTONIC, 0);
        if (fd < 0)
        {
            ERR_LOG("timerfd_create error");
            abort();
        }
        struct itimerspec * itime = new itimerspec;
        //第一次超时时间
        itime->it_value.tv_sec = 1;
        itime->it_value.tv_nsec = 0;
        //第一次之后超时时间
        itime->it_interval.tv_sec = 1;
        itime->it_interval.tv_nsec = 0;
        //启动定时器
        timerfd_settime(fd, 0, itime, nullptr);        
        return fd;
    }
    void ReadTimeFd()
    {
        uint64_t times;
        int n = read(_timerfd, ×, 8);
        if(n < 0)
        {
            ERR_LOG("read errpr");
            abort();
        }       
    }
    void TimerAddInLoop(uint64_t id, uint32_t timeout, const TaskFunc &task_cb)
    {
        PtrTask pt(new TimerTask(id, timeout, task_cb)); // 这里不能用等号
        int pos = (_tick + timeout) % _capacity;
        _wheel[pos].push_back(pt);
        // 添加管理信息和定义删除管理信息的函数
        _timers[id] = WeakTask(pt);
        pt->SetRelease(std::bind(&TimerWheel::RemoveTimer, this, id));
    }
    void TimerRefreshInLoop(uint64_t id)
    {
        auto it = _timers.find(id);
        if (it == _timers.end())
        {
            return;
        }
        ////通过保存的定时器对象_timers的weak_ptr构造一个shared_ptr出来,添加到轮子中
        PtrTask pt = it->second.lock(); // lock获取weak_ptr管理的对象对应的shared_ptr
        int timeout = pt->DelayTime();
        int pos = (_tick + timeout) % _capacity;
        _wheel[pos].push_back(pt);
    }
    void TimerCancelInLoop(uint64_t id)
    {
        auto it = _timers.find(id);
        if (it == _timers.end())
        {
            return;
        }
        PtrTask pt = it->second.lock();
        pt->Cancel();
    }
    ////这个函数应该每秒钟被执行一次,相当于秒针向后走了一步
    //与TimerAdd、TimerRefresh、TimerCancel相比,它是eventloop线程本身的,不需要像其他三个业务一样进行判断
    void RunTimerTask()
    {
        _tick = (_tick + 1) % _capacity;
        _wheel[_tick].clear(); // 清空指定位置的数组,就会把数组中保存的所有管理 定时器任务对象 的shared_ptr释放掉
    }
    //读取定时器内容,执行任务
    void OnTime()
    {
        ReadTimeFd();
        RunTimerTask();
    }
public:
    TimerWheel(EventLoop *loop) : _tick(0), _capacity(60), _wheel(_capacity), _loop(loop),
                   _timerfd(CreatTimerFd()), _timer_channel(new Channel(_timerfd, loop))
    {
        _timer_channel->SetReadCallback(std::bind(&TimerWheel::OnTime, this));//
        _timer_channel->EnableRead();
    }

    //定时器中有_timers, _wheel成员,定时器信息和_wheel的操作有可能在多线程中进行,因此需要考虑线程安全问题
    //如果不想加锁,那就把对定时器的所有操作,都放到一个线程中进行
    // 添加定时任务
    void TimerAdd(uint64_t id, uint32_t timeout, const TaskFunc &task_cb);
    // 刷新/延迟定时任务
    void TimerRefresh(uint64_t id);
    //取消定时任务执行
    void TimerCancel(uint64_t id);
    //只能让loop线程自己执行这个接口(串行实现同步),不让其他线程调用
    bool HasTimerTask(uint64_t id)
    {
        auto it = _timers.find(id);
        if (it == _timers.end())
        {
            return false;
        }  
        return true;      
    }
};

注:我们需要将TimerWheel 加入EventLoop模块,让EventLoop提供添加定时任务、刷新定时任务、取消定时任务接口。在类TimerWheel里,添加定时任务、刷新定时任务、取消定时任务这里涉及对EventLoop 对象的成员函数访问,为避免编译报错,将它们的实现放在最后。

下面给出对EventLoop的修改代码

class EventLoop
{
private:
    ...
    TimerWheel _timerwheel;
public:
    void TimerAdd(uint64_t id, uint32_t timeout, const TaskFunc &task_cb)
    {
        _timerwheel.TimerAdd(id, timeout, task_cb);
    }
    // 刷新/延迟定时任务
    void TimerRefresh(uint64_t id)
    {
        _timerwheel.TimerRefresh(id);
    }
    //取消定时任务执行
    void TimerCancel(uint64_t id)
    {
        _timerwheel.TimerCancel(id);
    }  

    bool HasTimerTask(uint64_t id)
    {
        return _timerwheel.HasTimerTask(id);
    }
};

测试EventLoop 和 TimerWheel

将TimerWheel 加入EventLoop模块后进行联合调试,我们对非活跃连接的超时实现释放操作(//tcp_srv.cc:服务端测试 的HandleClose()函数),在获取到活跃连接后,需要先添加定时销毁任务,将任务Id 和 TimerWheel 的_timer 绑定,再启动通信 channel 的读事件监控
原因:因为每次通信 channel 事件触发都要在 HandleEvent 里对定时销毁任务进行更新操作,如果先启动监控,立即有事件,那执行更新操作时是需要访问 _timer 数组的,那我此时拿着任务 Id 也找不到TimerTask任务指针,不就出现逻辑问题了吗?任务并没有被加入到 _wheel 和 _timer。

下面给出tcp_srv.cc修改部分

void HandleClose(Channel *channel)
{
    DBG_LOG("%d",channel->Fd());
    channel->Remove();//移除监控
    delete channel;//释放Channel
}

void HandleEvent(Channel *channel, EventLoop *loop, uint64_t id)
{
    loop->TimerRefresh(id);
}
void Acceptor(EventLoop *loop, Channel* lis_channel)
{
    int newfd = accept(lis_channel->Fd(), nullptr, nullptr);
    if(newfd < 0) return;
    uint64_t task_id = rand() % 10000;
    Channel* channel = new Channel(newfd, loop);
    channel->SetReadCallback(std::bind(HandleRead, channel));//为通信套接字设置可读事件的回调函数
    channel->SetWriteCallback(std::bind(HandleWrite, channel));//为通信套接字设置可写事件的回调函数
    channel->SetCloseCallback(std::bind(HandleClose, channel));//为通信套接字关闭可读事件的回调函数
    channel->SetErrorCallback(std::bind(HandleError, channel));//为通信套接字设置错误事件的回调函数
    channel->SetEventCallback(std::bind(HandleEvent, channel, loop, task_id));//为通信套接字设置任意事件的回调函数
    //10s后对非活跃连接进行释放
    //释放任务的添加要放在监控之前,如果先开启监控,立即有事件,在刷新释放任务里找不到这个任务
    loop->TimerAdd(task_id, 10, std::bind(HandleClose, channel));
    channel->EnableRead();//启动可读事件监控
}

四、EventLoop模块简单流程图

//tcp_svr.cc
int main()
{
    Socket lis_sock;
    lis_sock.CreateServer(8080);
    EventLoop loop;
    //为监听套接字,创建Channel进行事件的管理和处理
    Channel lst_channel(lis_sock.Fd(), &loop);
    //设置回调函数:获取新链接,为新连接创建Channel,设置回调函数并且添加进行监控
    lst_channel.SetReadCallback(std::bind(Acceptor, &loop, &lst_channel));
    lst_channel.EnableRead();
    while(1)
    {
        loop.Start();
    }
    lis_sock.Close();
    return 0;
}

首先创建了一个监听套接字,再接着创建了一个loop 对象,这里需要明白loop 对象里面有什么:_poll 变量是进行监控描述符就绪事件的、_task 是存放外来线程的任务的任务池、_event_chanel是管理事件通知_event_fd 的、_timer_wheel就是一个定时器,用来执行定时任务的;我们创建了监听 lst_channel 为其设置Acceptor 可读回调函数,通过EnableRead() 将监听描述符挂到 _poller上,对监听描述符进行监听;当有新连接产生时,lst_channel的可读事件触发,执行Acceptor 函数:

void Acceptor(EventLoop *loop, Channel* lis_channel)
{
    int newfd = accept(lis_channel->Fd(), nullptr, nullptr);
    if(newfd < 0) return;
    uint64_t task_id = rand() % 10000;
    Channel* new_channel = new Channel(newfd, loop);
    new_channel->SetReadCallback(std::bind(HandleRead, new_channel));//为通信套接字设置可读事件的回调函数
    new_channel->SetWriteCallback(std::bind(HandleWrite, new_channel));//为通信套接字设置可写事件的回调函数
    new_channel->SetCloseCallback(std::bind(HandleClose, new_channel));//为通信套接字关闭可读事件的回调函数
    new_channel->SetErrorCallback(std::bind(HandleError, new_channel));//为通信套接字设置错误事件的回调函数
    new_channel->SetEventCallback(std::bind(HandleEvent, new_channel, loop, task_id));//为通信套接字设置任意事件的回调函数
    //10s后对非活跃连接进行释放
    //释放任务的添加要放在监控之前,如果先开启监控,立即有事件,在刷新释放任务里找不到这个任务
    loop->TimerAdd(task_id, 10, std::bind(HandleClose, new_channel));
    new_channel->EnableRead();//启动可读事件监控
}

为新连接的描述符创建一个new_channel,设置可读可写、错误、任意事件回调函数,还有添加定时销毁任务(对规定时间内非活跃连接的释放),通过new_channel->EnableRead() 将new_channel的描述符挂到 _poller上,对通信描述符进行监听,当可读事件被触发时,HandleEvent 和 HandleRead 被执行:进行定时销毁任务的刷新和读取数据,假设一段时间后,可读事件未触发,任务未刷新,那么就会执行销毁任务将 new_channel 释放。

_evnet_channel创建后也会给eventfd添加可读事件回调函数(读取eventfd事件通知次数)并启动EventFd的读事件监控;
_timer_wheel的初始化也会添加可读事件回调函数(读取定时器内容,执行任务)启动_timer_fd 的读事件监控(timerfd_settime(fd, 0, itime, nullptr)会一秒触发一次可读事件)。

     

   

本文地址:https://www.yitenyun.com/1134.html

搜索文章

Tags

#远程工作 #服务器 #python #pip #conda #ios面试 #ios弱网 #断点续传 #ios开发 #objective-c #ios #ios缓存 香港站群服务器 多IP服务器 香港站群 站群服务器 #kubernetes #笔记 #平面 #容器 #linux #学习方法 #运维 #Trae #IDE #AI 原生集成开发环境 #Trae AI #进程控制 #学习 #开发语言 #云原生 #iventoy #VmWare #OpenEuler #docker #后端 #数据库 #MobaXterm #ubuntu #数信院生信服务器 #Rstudio #生信入门 #生信云服务器 #内网穿透 #网络 #cpolar #人工智能 #node.js #fastapi #html #css #Conda # 私有索引 # 包管理 #低代码 #爬虫 #音视频 #物联网 #websocket #算法 #大数据 #github #git #vscode #mobaxterm #深度学习 #计算机视觉 #web安全 #安全 #kylin #vllm #大模型 #Streamlit #Qwen #本地部署 #AI聊天机器人 #nginx #tcp/ip #开源 #golang #java #redis #RTP over RTSP #RTP over TCP #RTSP服务器 #RTP #TCP发送RTP #分阶段策略 #模型协议 #缓存 #unity #c# #游戏引擎 #android #腾讯云 #需求分析 #Dell #PowerEdge620 #内存 #硬盘 #RAID5 #jvm #hadoop #hbase #hive #zookeeper #spark #kafka #flink #我的世界 #FTP服务器 #云计算 #ping通服务器 #读不了内网数据库 #bug菌问答团队 #银河麒麟高级服务器操作系统安装 #银河麒麟高级服务器V11配置 #设置基础软件仓库时出错 #银河麒高级服务器系统的实操教程 #生产级部署银河麒麟服务系统教程 #Linux系统的快速上手教程 #ssh #科技 #自然语言处理 #神经网络 #centos #ide #qt #c++ #架构 #面试 #http #fiddler #华为 #ModelEngine #银河麒麟 #系统升级 #信创 #国产化 #多个客户端访问 #IO多路复用 #回显服务器 #TCP相关API #C++ #凤希AI伴侣 #编辑器 #udp #研发管理 #禅道 #禅道云端部署 #json #jmeter #功能测试 #软件测试 #自动化测试 #职场和发展 #prometheus #gpu算力 #grafana #华为云 #部署上线 #动静分离 #Nginx #新人首发 #Harbor #ollama #ai #llm #性能优化 #todesk #asp.net大文件上传 #asp.net大文件上传下载 #asp.net大文件上传源码 #ASP.NET断点续传 #asp.net上传文件夹 #Spring AI #MCP服务器 #STDIO协议 #Streamable-HTTP #McpTool注解 #服务器能力 #Android #Bluedroid #flutter #数码相机 #windows #debian #改行学it #创业创新 #程序员创富 #asp.net #压力测试 #flask #openlayers #bmap #tile #server #vue #1024程序员节 #php #前端 #claude #arm开发 #c语言 #网络协议 #uni-app #小程序 #notepad++ #信令服务器 #Janus #MediaSoup #jenkins #jar #sqlserver #elasticsearch #版本控制 #Git入门 #开发工具 #代码托管 #个人博客 #课程设计 #n8n #cpp #项目 #高并发 #企业开发 #ERP #项目实践 #.NET开发 #C#编程 #编程与数学 #嵌入式编译 #ccache #distcc #gemini #gemini国内访问 #gemini api #gemini中转搭建 #Cloudflare #stm32 #screen 命令 #游戏 #智能路由器 #AI编程 #mvp #个人开发 #设计模式 #oracle #SA-PEKS # 关键词猜测攻击 # 盲签名 # 限速机制 #时序数据库 #我的世界服务器搭建 #minecraft #金融 #mcp #金融投资Agent #Agent #AI #大模型学习 #Ansible #Playbook #AI服务器 #javascript #流媒体 #NAS #飞牛NAS #监控 #NVR #EasyNVR #毕设 #RAID #RAID技术 #磁盘 #存储 #电脑 #自动化 #vue.js #高级IO #select #计算机网络 #SPA #单页应用 #django #web3.py #语音识别 #说话人验证 #声纹识别 #CAM++ #swagger #visual studio code #测试工具 #ansible #RustDesk #IndexTTS 2.0 #本地化部署 #AIGC #spring boot #麒麟OS #pycharm #gitea #macos #毕业设计 #车辆排放 #微服务 #p2p #Windows #网站 #截图工具 #批量处理图片 #图片格式转换 #图片裁剪 #智能手机 #单片机 #嵌入式硬件 #SSH #X11转发 #Miniconda #mcu #阿里云 #MCP #journalctl #epoll #SSE # AI翻译机 # 实时翻译 #sqlite #openresty #lua #wordpress #雨云 #电气工程 #C# #PLC #聊天小程序 #NFC #智能公交 #服务器计费 #数据挖掘 #FP-增长 #tdengine #制造 #涛思数据 #apache #libosinfo #Proxmox VE #虚拟化 #VMware #spring #交互 #GPU服务器 #8U #硬件架构 #NPU #CANN #TCP #客户端 #嵌入式 #DIY机器人工房 #maven #gitlab #cosmic #集成测试 #远程桌面 #远程控制 #游戏机 #JumpServer #堡垒机 #万悟 #联通元景 #智能体 #镜像 #振镜 #振镜焊接 #teamviewer #scala #测试用例 #webrtc #idm #bash #jupyter #YOLO # Triton # 目标检测 #微信小程序 #微信 #健身房预约系统 #健身房管理系统 #健身管理系统 #mysql #北京百思可瑞教育 #百思可瑞教育 #北京百思教育 #SRS #直播 #数据结构 #web server #请求处理流程 #鸭科夫 #逃离鸭科夫 #鸭科夫联机 #鸭科夫异地联机 #开服 #risc-v #deepseek #react.js #分布式 #SSH公钥认证 # PyTorch # 安全加固 #rocketmq #单元测试 #selenium #守护进程 #复用 #screen #网络安全 #系统架构 #部署 #Clawdbot #个人助理 #数字员工 #搜索引擎 #umeditor粘贴word #ueditor粘贴word #ueditor复制word #ueditor上传word图片 #pytorch #IPv6 #DNS #源码 #闲置物品交易系统 #transformer #chatgpt #东方仙盟 #API限流 # 频率限制 # 令牌桶算法 #iBMC #UltraISO #视频去字幕 #黑群晖 #虚拟机 #无U盘 #纯小白 #java-ee #蓝湖 #Axure原型发布 #prompt #计算机 #树莓派4b安装系统 #mamba #llama #opencv #语言模型 #DisM++ # GLM-4.6V # 系统维护 #京东云 #算力一体机 #ai算力服务器 #DeepSeek #蓝耘智算 #910B #昇腾 #ida #aws #MQTT协议 #深度优先 #DFS #webpack #svn #AI论文写作工具 #学术写作辅助 #论文创作效率提升 #AI写论文实测 #tomcat # 双因素认证 # TensorFlow #sql #intellij-idea #rustdesk #ssl #vuejs #YOLOFuse # Base64编码 # 多模态检测 #进程 #操作系统 #进程创建与终止 #shell #1panel #vmware #eBPF #unity3d #服务器框架 #Fantasy #C语言 #bootstrap #信息与通信 #信号处理 #tcpdump #PTP_1588 #gPTP #经验分享 #Termux #Samba #Linux #机器学习 #kmeans #聚类 #文件IO #输入输出流 #ci/cd #ms-swift # 大模型 # 模型训练 #ShaderGraph #图形 #VMware Workstation16 #服务器操作系统 #Java #rust #硬件工程 #fpga开发 #进程等待 #wait #waitpid #Smokeping #pdf #程序员 #大模型教程 #AI大模型 #pve #结构体 #LangGraph #CLI #Python #JavaScript #langgraph.json #harmonyos #鸿蒙PC #GPU #AutoDL ##租显卡 #paddleocr #企业级存储 #网络设备 #工具集 #大模型应用 #API调用 #PyInstaller打包运行 #服务端部署 #大语言模型 #能源 #Android16 #音频性能实战 #音频进阶 #zotero #WebDAV #同步失败 #代理模式 #H5网页 #网页白屏 #H5页面空白 #资源加载问题 #打包部署后网页打不开 #HBuilderX #扩展屏应用开发 #android runtime #postgresql #VMWare Tool #openEuler #欧拉 #无人机 #Deepoc #具身模型 #开发板 #未来 #插件 #开源软件 #麒麟 # IndexTTS 2.0 # 自动化运维 #心理健康服务平台 #心理健康系统 #心理服务平台 #心理健康小程序 #rdp #目标检测 #海外服务器安装宝塔面板 #学术论文创作 #论文效率提升 #MBA论文写作 #tensorflow #arm #业界资讯 #论文笔记 #langchain #大模型开发 #adb #大模型部署 #mindie #大模型推理 #系统安全 #数据分析 #SSH反向隧道 # Miniconda # Jupyter远程访问 #idea #intellij idea #简单数论 #埃氏筛法 #cursor #yum #ui #5G #windows11 #microsoft #系统修复 #codex #memcache #rtsp #转发 #C2000 #TI #实时控制MCU #AI服务器电源 #leetcode #Llama-Factory # 树莓派 # ARM架构 #H5 #跨域 #发布上线后跨域报错 #请求接口跨域问题解决 #跨域请求代理配置 #request浏览器跨域 #三维 #3D #三维重建 #银河麒麟操作系统 #openssh #华为交换机 #信创终端 # 大模型推理 #UDP的API使用 #处理器 #CVE-2025-61686 #漏洞 #路径遍历高危漏洞 #统信UOS #win10 #qemu #智能体来了 #智能体对传统行业冲击 #行业转型 #AI赋能 #Socket网络编程 #web服务器 #win11 #RAG #LLM #chat # GPU租赁 # 自建服务器 #远程开发 #YOLO26 #muduo #TcpServer #accept #高并发服务器 #MinIO服务器启动与配置详解 #安卓 #milvus #springboot #知识库 #DHCP #agent #ai大模型 #实时音视频 #postman #chrome #媒体 #GATT服务器 #蓝牙低功耗 #交通物流 #散列表 #哈希算法 #Host #web #渗透测试 #SSRF #手机h5网页浏览器 #安卓app #苹果ios APP #手机电脑开启摄像头并排查 #IO #硬件 #ArkUI #ArkTS #鸿蒙开发 #PyTorch #CUDA #Triton #政务 #Nacos #昇腾300I DUO #PowerBI #企业 #go #dify #puppeteer #KMS #slmgr #vnstat #文心一言 #AI智能体 #POC #问答 #交付 #xlwings #Excel #攻防演练 #Java web #红队 #运维开发 #WT-2026-0001 #QVD-2026-4572 #smartermail #翻译 #spring cloud #nfs #iscsi #飞牛nas #fnos #支付 #前端框架 #负载均衡 #系统管理 #服务 #YOLOv8 # Docker镜像 #Modbus-TCP #文件管理 #文件服务器 #jetty #管道Pipe #system V #scanf #printf #getchar #putchar #cin #cout #esp32教程 #SAP #ebs #metaerp #oracle ebs #muduo库 #uv #uvx #uv pip #npx #Ruff #pytest #cesium #可视化 #排序算法 #jdk #排序 #青少年编程 # 高并发部署 #ddos #laravel #vps #计算几何 #斜率 #方向归一化 #叉积 #aiohttp #asyncio #异步 #Anaconda配置云虚拟环境 #数据仓库 #软件 #本地生活 #电商系统 #商城 #vivado license #html5 #MC #https #LoRA # lora-scripts # 模型微调 #fabric #密码学 #可信计算技术 #openHiTLS #TLCP #DTLCP #商用密码算法 #.netcore #链表 #链表的销毁 #链表的排序 #链表倒置 #判断链表是否有环 #服务器繁忙 #CPU #reactjs #web3 #测评 #CCE #Dify-LLM #Flexus #Go并发 #高并发架构 #Goroutine #系统设计 #Dify #ARM架构 #鲲鹏 #.net #net core #kestrel #web-server #asp.net-core #连接数据库报错 #智能家居 #mybatis #集成学习 #EMC存储 #存储维护 #NetApp存储 #spine #ipmitool #BMC #C #MC群组服务器 #STDIO传输 #SSE传输 #WebMVC #WebFlux #企业微信 #云服务器 #个人电脑 #embedding #unix #C/C++ #编程 #c++高并发 #百万并发 #机器人 #CS2 #debian13 #mariadb #Spring AOP #程序人生 #uip #k8s #wsl #人大金仓 #Kingbase #信创国产化 #达梦数据库 #树莓派 #温湿度监控 #WhatsApp通知 #IoT #MySQL #word #MS #Materials #多进程 #python技巧 #推荐算法 #raid #raid阵列 #SMTP # 内容安全 # Qwen3Guard #黑客技术 #文件上传漏洞 #Kylin-Server #国产操作系统 #服务器安装 #平板 #零售 #智能硬件 #vncdotool #链接VNC服务器 #如何隐藏光标 #CTF #A2A #GenAI #Langchain-Chatchat # 国产化服务器 # 信创 #ue5 #numpy #bond #服务器链路聚合 #网卡绑定 #database #儿童AI #图像生成 #星图GPU #pjsip #LobeChat #vLLM #GPU加速 #r-tree #FHSS #SSH保活 #算力建设 #人脸识别sdk #视频编解码 #人脸识别 #开源工具 #服务器解析漏洞 #nodejs #SSH密钥 # CUDA #n8n解惑 #练习 #基础练习 #数组 #循环 #九九乘法表 #计算机实现 #dynadot #域名 #ETL管道 #向量存储 #数据预处理 #DocumentReader #esb接口 #走处理类报异常 #ffmpeg #ZooKeeper #ZooKeeper面试题 #面试宝典 #深入解析 #网路编程 #CosyVoice3 # 语音合成 #smtp #smtp服务器 #PHP #银河麒麟部署 #银河麒麟部署文档 #银河麒麟linux #银河麒麟linux部署教程 #serverless #agi #mongodb #广播 #组播 #并发服务器 #x86_64 #数字人系统 #信息可视化 #claude code #code cli #ccusage #uvicorn #uvloop #asgi #event #yolov12 #研究生life #eureka #大剑师 #nodejs面试题 #SSH免密登录 #ServBay #SFTP #gpu #nvcc #cuda #nvidia #其他 #AI 推理 #NV #RXT4090显卡 #RTX4090 #深度学习服务器 #硬件选型 #SQL注入主机 #neo4j #NoSQL #SQL #ESP32 # OTA升级 # 黄山派 #ansys #ansys问题解决办法 #HeyGem # WebUI # 网络延迟 #ARM服务器 # 多模态推理 #ranger #MySQL8.0 # 服务器IP # 端口7860 #RK3576 #瑞芯微 #硬件设计 #echarts #devops #戴尔服务器 #戴尔730 #装系统 #elk #sql注入 #junit #ThingsBoard MCP #LangFlow # 智能运维 # 性能瓶颈分析 #代理 #鸿蒙 # 服务器IP访问 # 端口映射 # 服务器配置 # GPU #遛狗 #bug #clickhouse #arm64 #自动化运维 #AI-native #excel # CosyVoice3 # 批量部署 #copilot #数据安全 #注入漏洞 #微PE #硬盘克隆 #DiskGenius # TTS服务器 # 键鼠锁定 #wpf #远程连接 #串口服务器 #Modbus #MOXA # 一锤定音 # 大模型微调 #WinSCP 下载安装教程 #FTP工具 #服务器文件传输 # ControlMaster #scrapy #AI写作 # RTX 3090 #node #le audio #蓝牙 #低功耗音频 #通信 #连接 #参数估计 #矩估计 #概率论 #分类 #LE Audio #BAP #powerbi #es安装 #Node.js # child_process #googlecloud #Qwen3-14B # 大模型部署 # 私有化AI #模型训练 #仙盟创梦IDE #GLM-4.6V-Flash-WEB # AI视觉 # 本地部署 #vp9 #动态规划 #dlms #dlms协议 #逻辑设备 #逻辑设置间权限 #scikit-learn #随机森林 # keep-alive #安全威胁分析 #GB28181 #SIP信令 #SpringBoot #视频监控 #SSH跳板机 # Python3.11 #LVDS #高速ADC #DDR #Minecraft #Minecraft服务器 #PaperMC #我的世界服务器 #驱动开发 #3d #kong #Kong Audio #Kong Audio3 #KongAudio3 #空音3 #空音 #中国民乐 #Gunicorn #WSGI #Flask #并发模型 #容器化 #性能调优 #Emby #视频 #screen命令 #小艺 #搜索 #门禁 #梯控 #智能一卡通 #门禁一卡通 #消费一卡通 #智能梯控 #一卡通 #源代码管理 #超时设置 #客户端/服务器 #网络编程 #挖矿 #Linux病毒 #ai编程 #ambari #产品经理 #就业 # 高并发 #数据恢复 #视频恢复 #视频修复 #RAID5恢复 #流媒体服务器恢复 #CMake #Make #diskinfo # 磁盘健康 #框架搭建 #V11 #kylinos #状态模式 #dba #Tokio #流量监控 #KMS激活 #国产化OS #react native #Java程序员 #Java面试 #后端开发 #Spring源码 #Spring #Gateway #认证服务器集成详解 #CPU利用率 # 批量管理 #CSDN #ASR #SenseVoice #glibc #中间件 #论文阅读 #软件工程 #CVE-2025-68143 #CVE-2025-68144 #CVE-2025-68145 #zabbix #证书 #winscp #ONLYOFFICE #MCP 服务器 #后端框架 # 自动化部署 # VibeThinker #Aluminium #Google # 数字人系统 # 远程部署 #数字化转型 #实体经济 #商业模式 #软件开发 #数智红包 #商业变革 #创业干货 #Tracker 服务器 #响应最快 #torrent 下载 #2026年 #Aria2 可用 #迅雷可用 #BT工具通用 #AI技术 #Zabbix #语音合成 #FASTMCP #高斯溅射 #运维工具 #网络攻击模型 #pyqt #产品运营 #Puppet # IndexTTS2 # TTS #联机教程 #局域网联机 #局域网联机教程 #局域网游戏 #KMS 激活 #AI智能棋盘 #Rock Pi S #边缘计算 # 黑屏模式 #领域驱动 #移动端h5网页 #调用浏览器摄像头并拍照 #开启摄像头权限 #拍照后查看与上传服务器端 #摄像头黑屏打不开问题 #身体实验室 #健康认知重构 #系统思维 #微行动 #NEAT效应 #亚健康自救 #ICT人 #云开发 #服务器IO模型 #非阻塞轮询模型 #多任务并发模型 #异步信号模型 #多路复用模型 #工业级串口服务器 #串口转以太网 #串口设备联网通讯模块 #串口服务器选型 #ceph #IndexTTS2 # 阿里云安骑士 # 木马查杀 #漏洞挖掘 #入侵 #日志排查 #SSH别名 #BoringSSL #Docker #云计算运维 #asp.net上传大文件 #群晖 # ARM服务器 # 鲲鹏 #http头信息 # 公钥认证 # 权限修复 #STUN #turn #ICE #生信 # HiChatBox # 离线AI #策略模式 #租显卡 #训练推理 #TCP服务器 #开发实战 #全文检索 #银河麒麟服务器系统 #蓝桥杯 #iot #Anything-LLM #IDC服务器 #私有化部署 #汽车 #java大文件上传 #java大文件秒传 #java大文件上传下载 #java文件传输解决方案 #可撤销IBE #服务器辅助 #私钥更新 #安全性证明 #双线性Diffie-Hellman #bigtop #hdp #hue #kerberos #pencil #pencil.dev #设计 #短剧 #短剧小程序 #短剧系统 #微剧 #轻量化 #低配服务器 #hibernate #nosql #docker安装seata #TLS协议 #HTTPS #漏洞修复 #运维安全 #PyCharm # 远程调试 # YOLOFuse #全链路优化 #实战教程 #CNAS #CMA #程序文件 #VS Code调试配置 #wireshark #网络安全大赛 #Syslog #系统日志 #日志分析 #日志监控 #matlab #生产服务器问题查询 #日志过滤 #Autodl私有云 #深度服务器配置 # 水冷服务器 # 风冷服务器 #VoxCPM-1.5-TTS # 云端GPU # PyCharm宕机 #outlook #错误代码2603 #无网络连接 #2603 #everything #实时检测 #卷积神经网络 #AI生成 # outputs目录 # 自动化 #DAG #stl #IIS Crypto #云服务器选购 #Saas #线程 #VibeVoice #ComfyUI # 推理服务器 #具身智能 #编程助手 #HarmonyOS APP #rabbitmq #esp32 arduino #决策树 #HistoryServer #Spark #YARN #jobhistory #sglang #SSH Agent Forwarding # 容器化 #spring ai #oauth2 #log #数据可视化 #内存接口 # 澜起科技 # 服务器主板 #rtmp # GLM-4.6V-Flash-WEB # 显卡驱动备份 #声源定位 #MUSIC #模拟退火算法 #计算机毕业设计 #程序定制 #毕设代做 #课设 #Hadoop #AI电商客服 #ROS # 局域网访问 # 批量处理 #浏览器自动化 #python #nacos #银河麒麟aarch64 # 服务器迁移 # 回滚方案 #大模型入门 #homelab #Lattepanda #Jellyfin #Plex #Kodi # 高温监控 #开关电源 #热敏电阻 #PTC热敏电阻 #文件传输 #电脑文件传输 #电脑传输文件 #电脑怎么传输文件到另一台电脑 #电脑传输文件到另一台电脑 #fs7TF # 远程访问 #性能 #优化 #RAM #npu #gpt #TensorRT # 推理优化 #企业存储 #RustFS #对象存储 #高可用 #Xshell #Finalshell #生物信息学 #组学 #远程软件 #音乐 #IntelliJ IDEA #Spring Boot #Coturn #TURN #内网 # 跳板机 #log4j #Jetty # 嵌入式服务器 #模块 #分布式数据库 #集中式数据库 #业务需求 #选型误 # Connection refused #WRF #WRFDA #建筑缺陷 #红外 #数据集 #SMARC #ARM #代理服务器 #rsync # 数据同步 # 代理转发 #blender #设计师 #图像处理 #游戏美术 #技术美术 #视觉检测 #visual studio # 云服务器 #多线程 #claudeCode #content7 #跳槽 #工作 #gRPC #注册中心 #odoo #HarmonyOS #Reactor #雨云服务器 #教程 #MCSM面板 #Apple AI #Apple 人工智能 #FoundationModel #Summarize #SwiftUI #空间计算 #原型模式 #数据访问 #gateway #Comate # 串口服务器 # NPort5630 #appche #I/O模型 #并发 #水平触发、边缘触发 #多路复用 #c #ftp #sftp #YOLO识别 #YOLO环境搭建Windows #YOLO环境搭建Ubuntu #SSH复用 # 远程开发 # 轻量化镜像 # 边缘计算 #OpenHarmony #磁盘配额 #存储管理 #形考作业 #国家开放大学 #系统运维 #Python办公自动化 #Python办公 #C++ UA Server #SDK #跨平台开发 #eclipse #servlet #mssql #opc #opc ua #opc模拟服务器 #lucene #cpu #工程设计 #预混 #扩散 #燃烧知识 #层流 #湍流 #机器视觉 #6D位姿 #量子计算 #UOS #海光K100 #统信 #语音生成 #TTS #Keycloak #Quarkus #AI编程需求分析 #nas #Fun-ASR # 语音识别 #AI部署 # ms-swift #PN 结 #密码 #firefox #safari #服务器线程 # SSL通信 # 动态结构体 #RWK35xx #语音流 #实时传输 #b树 #超算中心 #PBS #lsf #报表制作 #职场 #用数据讲故事 #lvs #adobe #memory mcp #Cursor #nmodbus4类库使用教程 #docker-compose #目标跟踪 #数据迁移 #测速 #iperf #iperf3 #windbg分析蓝屏教程 #express #cherry studio #Buck #NVIDIA #算力 #交错并联 #DGX #gmssh #宝塔 #Exchange #内存治理 #宝塔面板部署RustDesk #RustDesk远程控制手机 #手机远程控制 #系统安装 #铁路桥梁 #DIC技术 #箱梁试验 #裂纹监测 #四点弯曲 #IFix #c++20 #可再生能源 #绿色算力 #风电 # 远程连接 #麦克风权限 #访问麦克风并录制音频 #麦克风录制音频后在线播放 #用户拒绝访问麦克风权限怎么办 #uniapp 安卓 苹果ios #将音频保存本地或上传服务器 # 环境迁移 #matplotlib #安全架构 #AI应用编程 # REST API #gerrit #若依 # GLM-TTS # 数据安全 #前端开发 #EN4FE #xshell #host key #TTS私有化 # IndexTTS # 音色克隆 #自由表达演说平台 #演说 #AI Agent #开发者工具 #指针 #anaconda #虚拟环境 #okhttp #范式 #计算机外设 #iphone #Karalon #AI Test #ip #流程图 #图论 #国产开源制品管理工具 #Hadess #一文上手 #模版 #函数 #类 #笔试 #健康医疗 #OPCUA #网安应急响应 # GLM # 服务连通性 #azure #ET模式 #非阻塞 #remote-ssh #工程实践 #WEB #AI应用 #图像识别 #高考 #多模态 #微调 #超参 #LLamafactory #服务器开启 TLS v1.2 #IISCrypto 使用教程 #TLS 协议配置 #IIS 安全设置 #服务器运维工具 #uniapp #合法域名校验出错 #服务器域名配置不生效 #request域名配置 #已经配置好了但还是报错 #uniapp微信小程序 #华为od #华为机试 #API #taro #SSH跳转 #wps #Linux多线程 # GPU集群 #Beidou #北斗 #SSR #samba #RSO #机器人操作系统 #simulink #mtgsig #美团医药 #美团医药mtgsig #美团医药mtgsig1.2 #信息安全 #信息收集 #poll #Socket #套接字 #I/O多路复用 #字节序 #weston #x11 #x11显示服务器 #传统行业 # AI部署 # TURN # NAT穿透 #材料工程 #智能电视 #MCP服务器注解 #异步支持 #方法筛选 #声明式编程 #自动筛选机制 #AB包 #VMware创建虚拟机 #远程更新 #缓存更新 #多指令适配 #物料关联计划 #挖漏洞 #攻击溯源 #warp #JNI #pxe #Ubuntu服务器 #硬盘扩容 #命令行操作 #防毒面罩 #防尘面罩 #free #vmstat #sar #sentinel #m3u8 #HLS #移动端H5网页 #APP安卓苹果ios #监控画面 直播视频流 #Prometheus #MinIO #DooTask #UEFI #BIOS #Legacy BIOS #Discord机器人 #云部署 #程序那些事 #r语言 #交换机 #三层交换机 #TRO #TRO侵权 #TRO和解 #模型上下文协议 #MultiServerMCPC #load_mcp_tools #load_mcp_prompt #思爱普 #SAP S/4HANA #ABAP #NetWeaver #文生视频 #WAN2.2 #AI视频生成 #游戏策划 #游戏程序 #用户体验 #markdown #建站 #数据报系统 #数学建模 #2026年美赛C题代码 #2026年美赛 #esp32 #mosquito #智能制造 #供应链管理 #工业工程 #库存管理 #智慧城市 #WinDbg #Windows调试 #内存转储分析 #运维 #AI视频创作系统 #AI视频创作 #AI创作系统 #AI工具 #AI创作工具 #华为od机试 #华为od机考 #华为od最新上机考试题库 #华为OD题库 #华为OD机试双机位C卷 #od机考题库 #Ascend #MindIE #FRP #上下文工程 #langgraph #意图识别 #数据采集 #浏览器指纹 #自动化巡检 #Ubuntu #CA证书 #UDP套接字编程 #UDP协议 #网络测试 #webgl #区块链 #反向代理 #分子动力学 #化工仿真 #期刊 #SCI #面向对象 #基础语法 #标识符 #常量与变量 #数据类型 #运算符与表达式 #session #游戏服务器断线 #静脉曲张 #腿部健康 #运动 #主板 #总体设计 #电源树 #框图 #数模美赛 #边缘AI # Kontron # SMARC-sAMX8 #duckdb #OpenAI #故障 #阿里云RDS #coffeescript #二值化 #Canny边缘检测 #轮廓检测 #透视变换