项目架构与业务逻辑全解

TCP Server 项目架构与业务逻辑全解


目录


一、项目整体架构

三层架构

复制代码
┌─────────────────────────────────────────────────────────────┐
│                     业务层 (Application Layer)                │
│       ┌──────────────┐           ┌──────────────┐            │
│       │  EchoServer  │           │  HttpServer  │            │
│       └──────────────┘           └──────────────┘            │
└─────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│                  网络框架层 (Framework Layer)                 │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐       │
│  │TcpServer │ │Connection│ │EventLoop │ │TimerWheel│       │
│  └──────────┘ └──────────┘ └──────────┘ └──────────┘       │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐       │
│  │ Acceptor │ │  Channel │ │LoopThread│ │   Any    │       │
│  └──────────┘ └──────────┘ └──────────┘ └──────────┘       │
└─────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│                系统调用封装层 (System Call Layer)              │
│       ┌──────────┐  ┌──────────┐  ┌──────────┐              │
│       │  Socket  │  │  Poller  │  │  Buffer  │              │
│       └──────────┘  └──────────┘  └───���──────┘              │
└─────────────────────────────────────────────────────────────┘

模块持有关系总图

复制代码
HttpServer / EchoServer
  │ 持有
  ↓
TcpServer
  ├─── _baseloop (EventLoop)         ← 主线程事件循环
  │      ├─── _poller (Poller)       ← 封装 epoll
  │      │      ├─── _epfd           ← epoll 文件描述符
  │      │      └─── _channels       ← fd→Channel 映射表
  │      ├─── _event_fd + _event_channel  ← eventfd 跨线程唤醒
  │      ├─── _timer_wheel (TimerWheel)   ← 时间轮定时器
  │      │      ├─── _timerfd + _timer_channel
  │      │      ├─── _wheel[60]      ← 60 格时间轮
  │      │      └─── _timers         ← id→weak_ptr 映射
  │      ├─── _tasks[]               ← 任务队列
  │      └─── _mutex                 ← 保护任务队列
  │
  ├─── _acceptor (Acceptor)
  │      ├─── _socket (Socket)       ← 监听套接字
  │      ├─── _channel (Channel)     ← 监听 fd 的事件管理
  │      └─── _accept_callback       ← = TcpServer::NewConnection
  │
  ├─── _pool (LoopThreadPool)
  │      ├─── _threads[] (LoopThread)
  │      │      └─── _thread + _loop (EventLoop)
  │      └─── _loops[] (EventLoop*)  ← 工作线程的 EventLoop
  │
  └─── _conns (map<id, shared_ptr<Connection>>)
         └─── Connection
               ├─── _socket (Socket)      ← 客户端套接字
               ├─── _channel (Channel)    ← 客户端 fd 事件管理
               ├─── _in_buffer (Buffer)   ← 输入缓冲区
               ├─── _out_buffer (Buffer)  ← 输出缓冲区
               ├─── _context (Any)        ← 存放 HttpContext
               └─── _loop (EventLoop*)    ← 绑定的工作线程

主从 Reactor 线程模型

复制代码
┌──────────────────────────────────────────────────────┐
│  主线程 (Main Reactor)                                │
│  ┌────────────────────────────────────────┐           │
│  │  _baseloop (EventLoop)                  │           │
│  │    epoll 监控:                           │           │
│  │    ┌──────────┐ ┌──────────┐            │           │
│  │    │listen_fd │ │ eventfd  │            │           │
│  │    │(Acceptor)│ │(唤醒用)  │            │           │
│  │    └──────────┘ └──────────┘            │           │
│  │  职责: 只负责 accept 新连接             │           │
│  └────────────────────────────────────────┘           │
│                     │ 新连接 fd 轮询分配               │
│          ┌──────────┼──────────┐                      │
│          ↓          ↓          ↓                      │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐              │
│  │ 工作线程1 │ │ 工作线程2 │ │ 工作线程3 │              │
│  │EventLoop │ │EventLoop │ │EventLoop │              │
│  │ epoll:   │ │ epoll:   │ │ epoll:   │              │
│  │ conn_fds │ │ conn_fds │ │ conn_fds │              │
│  │ eventfd  │ │ eventfd  │ │ eventfd  │              │
│  │ timerfd  │ │ timerfd  │ │ timerfd  │              │
│  └──────────┘ └──────────┘ └──────────┘              │
└──────────────────────────────────────────────────────┘

模块调用关系

复制代码
Channel::EnableRead()
  → Channel::Update()
    → EventLoop::UpdateEvent()
      → Poller::UpdateEvent()
        → epoll_ctl(ADD / MOD)

EventLoop::Start() 主循环
  ├→ Poller::Poll()           ← epoll_wait
  ├→ Channel::HandleEvent()   ← 分发事件回调
  │    ├→ Connection::HandleRead/Write/Close/Error/Event
  └→ EventLoop::RunAllTask()  ← 执行跨线程任务

回调链路: epoll_wait → Poller::Poll → Channel::HandleEvent
  → Connection::HandleRead → _message_callback
    → HttpServer::OnMessage → 解析→路由→响应→conn->Send

二、核心模块详解

1. Buffer - 应用层缓冲区

复制代码
Buffer 内存布局:
┌────────────────────────────────────────────────────────┐
│  已读区域  │    可读数据区域    │    可写空闲区域    │
│ (废弃空间) │                    │                    │
└────────────────────────────────────────────────────────┘
      ↑              ↑                    ↑
  Begin()      _reader_idx           _writer_idx
               ReadPosition()        WritePosition()

Buffer 是应用层缓冲区的核心实现,底层使用 vector 存储数据,通过两个索引指针 _reader_idx 和 _writer_idx 来管理读写位置。这种设计避免了频繁的内存拷贝和移动。当业务层从 Buffer 中读取数据时,只需要移动 _reader_idx 读指针向后偏移,而不需要真正删除或移动内存中的数据。同样,写入数据时只需要追加到 _writer_idx 位置并更新写指针。

ReadAbleSize 方法返回当前可读数据的大小,计算方式是 _writer_idx 减去 _reader_idx,这个区间就是有效数据区域。EnsureWriteSpace 方法用于确保有足够的写入空间,它的策略是先检查尾部剩余空间是否足够,如果足够就直接返回;如果尾部不够但头部已读区域加上尾部空间足够,就把有效数据整体搬移到 vector 开头,回收已读空间;如果总空间都不够,就调用 vector 的 resize 进行扩容。这种策略在大多数情况下避免了频繁的内存分配。

为什么需要 Buffer 这一层抽象?因为 TCP 是面向字节流的协议,它不保证消息边界。应用层调用一次 recv 可能只读到半个 HTTP 请求,也可能一次读到多个请求粘连在一起,这就是所谓的半包和粘包问题。如果直接把 recv 的数据当作完整消息处理就会出错。Buffer 的作用就是在应用层积累数据,让业务层可以按照协议规则(比如 HTTP 的请求行、请求头、请求体)逐步解析,解析多少就消费多少,剩余数据留在 Buffer 中等待下次继续处理。同样,send 系统调用也不保证一次能把所有数据发送完,没发完的数据需要暂存在输出 Buffer 中,等 socket 可写时继续发送。

2. Socket - 套接字封装

Socket 类是对 Linux socket 系统调用的面向对象封装,采用 RAII 资源管理方式。构造函数中通过 Create 方法调用 socket 系统调用创建套接字文件描述符,析构函数中自动调用 close 关闭文件描述符,避免了资源泄漏。这种设计让使用者不需要手动管理 fd 的生命周期,大大降低了出错概率。

Socket 类提供了一系列方法对应不同的系统调用。Create 方法调用 socket 创建 TCP 套接字;Bind 方法调用 bind 绑定本地地址和端口;Listen 方法调用 listen 将套接字设置为监听状态;Accept 方法调用 accept 接受新连接并返回客户端 fd。对于数据收发,NonBlockRecv 和 NonBlockSend 分别调用 recv 和 send,并且都传入 MSG_DONTWAIT 标志实现非阻塞 IO,即使 socket 本身没有设置非阻塞模式,这次调用也不会阻塞。

ReuseAddress 方法通过 setsockopt 设置 SO_REUSEADDR 和 SO_REUSEPORT 选项,允许端口复用。SO_REUSEADDR 让处于 TIME_WAIT 状态的端口可以立即重新绑定,避免服务器重启时出现"地址已被使用"的错误;SO_REUSEPORT 允许多个进程或线程绑定同一端口,由内核做负载均衡。NonBlock 方法通过 fcntl 系统调用给 fd 添加 O_NONBLOCK 标志,让所有 IO 操作都变成非阻塞模式。

在错误处理上,NonBlockRecv 和 NonBlockSend 对 EAGAIN 和 EINTR 这两个特殊错误码返回 0 表示暂时无数据或被信号中断,调用者可以稍后重试;对于其他错误返回 -1 表示真正的错误发生,需要关闭连接。这种统一的错误处理简化了上层代码的逻辑。

3. Channel - fd 事件管理器

Channel 是事件分发的核心组件,每个需要监控的文件描述符都对应一个 Channel 对象。Channel 的职责是管理这个 fd 上关注的事件类型以及事件发生时的回调函数。它不拥有 fd 本身,只是作为 fd 和 EventLoop 之间的桥梁。

Channel 内部维护两个重要的事件字段。_events 表示用户想要监控的事件类型,比如 EPOLLIN 可读事件、EPOLLOUT 可写事件等;_revents 是 epoll_wait 返回时内核设置的实际就绪事件。当业务层想要开始监控某个事件时,调用 EnableRead 或 EnableWrite 方法,这些方法会修改 _events 字段,然后调用 Update 方法通知 EventLoop 更新 epoll 的监控状态,最终走到 Poller 的 UpdateEvent 方法调用 epoll_ctl 系统调用。

HandleEvent 方法是事件分发的核心逻辑。当 epoll_wait 返回某个 fd 就绪时,Poller 会找到对应的 Channel 并调用它的 HandleEvent 方法。HandleEvent 根据 _revents 中的事件类型分别调用不同的回调函数。如果是 EPOLLIN、EPOLLRDHUP 或 EPOLLPRI 这些可读相关的事件,就调用 _read_callback 读回调;如果是 EPOLLOUT 可写事件,就调用 _write_callback 写回调;如果是 EPOLLERR 错误事件,调用 _error_callback;如果是 EPOLLHUP 挂断事件,调用 _close_callback。最后无论什么事件都会调用 _event_callback,这个回调通常用于刷新连接的活跃时间,防止非活跃连接超时被释放。

这种设计的好处是把事件监控和事件处理解耦。Channel 只负责事件的注册和分发,具体的处理逻辑由外部设置的回调函数决定。对于监听 socket,读回调是 accept 新连接;对于客户端 socket,读回调是接收数据。同一套 Channel 机制可以适配不同的业务场景。

Channel 事件注册流程:

复制代码
业务层调用 EnableRead/EnableWrite
  ↓
修改 _events 字段 (添加 EPOLLIN/EPOLLOUT)
  ↓
Channel::Update()
  ↓
EventLoop::UpdateEvent(channel)
  ↓
Poller::UpdateEvent(channel)
  ↓
检查 fd 是否已注册?
  ├─ 是 → epoll_ctl(EPOLL_CTL_MOD) 修改事件
  └─ 否 → epoll_ctl(EPOLL_CTL_ADD) 添加监控
           同时保存到 _channels[fd] = channel

Channel 事件分发流程:

复制代码
epoll_wait 返回就绪 fd
  ↓
Poller::Poll() 遍历就绪事件
  ↓
通过 fd 从 _channels 找到 Channel*
  ↓
channel->SetREvents(实际就绪事件)
  ↓
加入 active 列表返回
  ↓
EventLoop 遍历 active 列表
  ↓
channel->HandleEvent()
  ↓
根据 _revents 分发到不同回调:
  EPOLLIN/EPOLLRDHUP/EPOLLPRI → _read_callback()
  EPOLLOUT → _write_callback()
  EPOLLERR → _error_callback()
  EPOLLHUP → _close_callback()
  最后 → _event_callback() (任意事件都触发)

4. Poller - epoll 封装

Poller 类是对 Linux epoll 机制的完整封装,它隐藏了 epoll 的底层细节,对外提供简洁的接口。Poller 内部维护三个关键成员:_epfd 是 epoll 实例的文件描述符,通过 epoll_create 创建;_evs 是一个大小为 1024 的 epoll_event 数组,用于接收 epoll_wait 返回的就绪事件;_channels 是一个 map 容器,建立了 fd 到 Channel 指针的映射关系,这样当某个 fd 就绪时可以快速找到对应的 Channel 对象。

UpdateEvent 方法负责向 epoll 注册或修改事件监控。它首先通过 HasChannel 方法检查这个 fd 是否已经在 _channels 中存在,如果存在说明之前已经注册过,就调用 epoll_ctl 传入 EPOLL_CTL_MOD 操作码修改监控的事件类型;如果不存在说明是第一次注册,就调用 epoll_ctl 传入 EPOLL_CTL_ADD 操作码添加到 epoll 监控集合,同时把 Channel 指针保存到 _channels 映射表中。RemoveEvent 方法则调用 epoll_ctl 传入 EPOLL_CTL_DEL 从 epoll 中删除这个 fd 的监控,并从 _channels 中移除映射关系。

Poll 方法是事件等待的核心,它调用 epoll_wait 系统调用阻塞等待事件就绪。epoll_wait 的参数包括 epoll 实例 _epfd、用于接收就绪事件的数组 _evs、数组大小 1024、以及超时时间。项目中超时时间设置为 -1 表示永久阻塞,直到有事件就绪或被 eventfd 唤醒才返回。epoll_wait 返回后,Poll 方法遍历所有就绪的事件,通过 fd 从 _channels 中找到对应的 Channel 对象,调用 Channel 的 SetREvents 方法设置实际就绪的事件类型,然后把这个 Channel 加入到 active 活跃列表中返回给 EventLoop。EventLoop 拿到活跃列表后逐个调用 Channel 的 HandleEvent 方法完成事件分发。

这种设计把 epoll 的三个系统调用 epoll_create、epoll_ctl、epoll_wait 完整封装起来,上层代码不需要直接操作 epoll,只需要通过 Poller 提供的接口就能实现事件驱动的 IO 多路复用。

5. EventLoop - 事件循环核心

EventLoop 是整个网络框架的心脏,实现了 Reactor 模式中的事件循环。项目采用 One Loop Per Thread 设计,每个线程拥有一个独立的 EventLoop 对象,线程之间互不干扰。EventLoop 的核心职责是不断循环执行三个步骤:等待事件就绪、处理就绪事件、执行跨线程任务。

Start 方法是事件循环的入口,它包含一个无限 while 循环。第一步调用 _poller.Poll 方法,这会阻塞在 epoll_wait 系统调用上等待 IO 事件就绪或被 eventfd 唤醒。Poll 返回后得到一个活跃 Channel 列表,第二步遍历这个列表逐个调用 Channel 的 HandleEvent 方法,触发对应的读写回调函数,完成实际的业务处理。第三步调用 RunAllTask 方法执行任务队列中的所有跨线程任务。这三步构成一个完整的事件循环周期,然后继续下一轮循环。

跨线程任务投递是 EventLoop 的重要功能。RunInLoop 方法接收一个回调函数,它首先判断当前调用线程是否就是 EventLoop 所在的线程,如果是就直接执行回调函数,避免不必要的线程切换开销;如果不是,说明是跨线程调用,就调用 QueueInLoop 方法把回调函数加入任务队列。QueueInLoop 内部先加锁保护任务队列,把回调函数 push_back 到 _tasks 向量中,然后向 eventfd 写入一个 8 字节整数。写入 eventfd 会让它变成可读状态,触发 EPOLLIN 事件,正在 epoll_wait 中阻塞的 EventLoop 线程会被唤醒。唤醒后 EventLoop 会调用 ReadEventfd 读取并清空 eventfd,然后在 RunAllTask 中加锁取出所有任务逐个执行。

这种设计保证了线程安全。每个 Connection 的所有操作都在它绑定的 EventLoop 线程中执行,不需要对 Connection 的成员变量加锁。外部线程想要操作 Connection 时,不是直接调用而是通过 RunInLoop 把操作投递到对应线程,由该线程自己执行。唯一需要加锁的地方就是任务队列的 push 和 pop 操作,锁的粒度非常小,不会成为性能瓶颈。

EventLoop 事件循环流程:

复制代码
EventLoop::Start()
  ↓
┌─────────────────────────────────────┐
│  while(1) 无限循环                   │
│  ┌───────────────────────────────┐  │
│  │ 1. _poller.Poll(&actives)     │  │
│  │    ↓ epoll_wait 阻塞等待       │  │
│  │    ↓ IO 事件就绪或 eventfd 唤醒│  │
│  │    ↓ 返回活跃 Channel 列表     │  │
│  └───────────────────────────────┘  │
│  ┌───────────────────────────────┐  │
│  │ 2. for (ch : actives)         │  │
│  │      ch->HandleEvent()        │  │
│  │    ↓ 分发事件到回调函数        │  │
│  │    ↓ 处理读写业务逻辑          │  │
│  └───────────────────────────────┘  │
│  ┌───────────────────────────────┐  │
│  │ 3. RunAllTask()               │  │
│  │    ↓ 加锁取出 _tasks 队列      │  │
│  │    ↓ 逐个执行跨线程任务        │  │
│  └───────────────────────────────┘  │
│  ↓ 继续下一轮循环                   │
└─────────────────────────────────────┘

跨线程任务投递流程:

复制代码
线程 A 调用 RunInLoop(callback)
  ↓
判断: 当前线程 == EventLoop 线程?
  ├─ 是 → 直接执行 callback()
  │       (避免不必要的线程切换)
  │
  └─ 否 → QueueInLoop(callback)
           ↓
        加锁 _mutex
           ↓
        _tasks.push_back(callback)
           ↓
        解锁 _mutex
           ↓
        write(eventfd, 1)  唤醒 EventLoop
           ↓
        ┌──────────────────────────┐
        │ EventLoop 线程            │
        │  epoll_wait 返回          │
        │  ↓ eventfd EPOLLIN 触发   │
        │  ↓ ReadEventfd() 清空计数 │
        │  ↓ RunAllTask()           │
        │    ├─ 加锁取出所有任务     │
        │    ├─ 逐个执行 callback    │
        │    └─ 解锁                │
        └──────────────────────────┘

6. eventfd - 跨线程唤醒

eventfd 是 Linux 提供的一种轻量级线程间通信机制,在本项目中专门用于唤醒阻塞在 epoll_wait 中的 EventLoop 线程。每个 EventLoop 在创建时都会调用 eventfd 系统调用创建一个 eventfd 实例,传入 EFD_CLOEXEC 标志防止子进程继承,传入 EFD_NONBLOCK 标志设置为非阻塞模式。这个 eventfd 也被封装成一个 Channel 对象,注册到 epoll 中监控可读事件。

跨线程唤醒的完整流程是这样的。EventLoop 线程正常情况下阻塞在 epoll_wait 系统调用上,等待 IO 事件就绪。此时如果另一个线程想要让这个 EventLoop 执行某个任务,它会调用 EventLoop 的 QueueInLoop 方法,把任务回调函数加锁放入 _tasks 任务队列,然后调用 write 系统调用向 eventfd 写入一个 8 字节的整数(通常是 1)。写入操作会让 eventfd 的内部计数器增加,状态变为可读。内核检测到 eventfd 可读后,会将其加入 epoll 的就绪链表并唤醒阻塞在 epoll_wait 上的线程。

EventLoop 线程被唤醒后,epoll_wait 返回就绪事件列表,其中包含 eventfd 的 EPOLLIN 事件。EventLoop 找到 eventfd 对应的 Channel 并调用其读回调函数 ReadEventfd,这个函数调用 read 系统调用读取 eventfd 的值并清空计数器,让 eventfd 恢复到不可读状态。然后 EventLoop 继续处理其他就绪事件,最后在 RunAllTask 阶段加锁取出任务队列中的所有回调函数逐个执行。

为什么不用条件变量或信号量?因为 EventLoop 已经在使用 epoll 监控 IO 事件,如果用条件变量就需要在 epoll_wait 和条件变量之间做选择,无法统一管理。eventfd 的优势是它本身就是一个文件描述符,可以像普通 socket 一样注册到 epoll 中,和 IO 事件使用同一套机制,代码更简洁统一。而且 eventfd 的开销非常小,一次 write 和 read 操作都是 O(1) 时间复杂度,不涉及复杂的内核数据结构。

eventfd 跨线程唤醒时序图:

复制代码
EventLoop 线程                      其他线程
     │                                 │
     │ epoll_wait() 阻塞               │
     │ 等待 IO 事件...                 │
     │                                 │
     │                                 │ QueueInLoop(task)
     │                                 │   ↓
     │                                 │ 加锁 push_back 到 _tasks
     │                                 │   ↓
     │                                 │ write(eventfd, 1)
     │ ← eventfd 可读 ─────────────────┤
     │ EPOLLIN 触发                    │
     │                                 │
     │ epoll_wait 返回                 │
     │   ↓                             │
     │ ReadEventfd()                   │
     │ read(eventfd) 清空计数器        │
     │   ↓                             │
     │ 处理其他就绪事件                │
     │   ↓                             │
     │ RunAllTask()                    │
     │   ├─ 加锁取出 _tasks            │
     │   ├─ 执行 task()                │
     │   └─ 解锁                       │
     │   ↓                             │
     │ 继续 epoll_wait                 │
     │                                 │

7. TimerWheel - 时间轮定时器

时间轮是一种高效的定时器实现方式,本项目使用 60 格的时间轮,每格代表 1 秒的时间跨度,可以处理 1 到 60 秒范围内的定时任务。时间轮的核心数据结构是一个大小为 60 的数组 _wheel,每个槽位存放一个 vector,里面保存该时刻应该触发的所有定时任务的 shared_ptr。_tick 成员变量是时间轮的秒针,指向当前时刻对应的槽位索引。

时间轮的驱动依赖 timerfd 这个 Linux 特有的定时器文件描述符。EventLoop 创建 TimerWheel 时会调用 timerfd_create 创建一个 timerfd,然后通过 timerfd_settime 设置为每秒触发一次的周期性定时器。这个 timerfd 也被封装成 Channel 注册到 epoll 中监控可读事件。每当 1 秒时间到达,timerfd 变为可读状态触发 EPOLLIN 事件,EventLoop 调用 TimerWheel 的 OnTime 回调函数。

OnTime 函数首先调用 read 读取 timerfd,返回值表示自上次读取以来超时了多少次(通常是 1,如果系统繁忙可能累积多次)。然后让秒针 _tick 前进相应的步数,对于走过的每个槽位,取出其中的所有 TimerTask 的 shared_ptr 并清空槽位。清空槽位后,如果某个 TimerTask 的 shared_ptr 引用计数变为零,说明没有其他地方持有它,就会触发析构。TimerTask 的析构函数中会检查 _canceled 标志,如果没有被取消就执行 _task_cb 回调函数,这个回调通常是关闭超时的连接。

添加定时任务通过 TimerAdd 方法实现。它接收延迟时间 delay 和回调函数,创建一个 TimerTask 对象的 shared_ptr,计算应该放入的槽位 pos = (_tick + delay) % 60,把 shared_ptr 放入 _wheel[pos]。同时在 _timers 映射表中保存定时器 ID 到 weak_ptr 的映射,weak_ptr 不增加引用计数但可以用来检测对象是否还存活。

刷新定时器通过 TimerRefresh 方法实现,这是时间轮的精妙之处。当连接有活动时,需要延长它的超时时间。TimerRefresh 通过定时器 ID 从 _timers 中找到对应的 weak_ptr,调用 lock 方法尝试提升为 shared_ptr。如果提升成功说明定时器还没有被释放,就把这个 shared_ptr 重新放入未来的槽位 (_tick + delay) % 60。这样旧槽位中的 shared_ptr 在时间到达时被清空,但因为新槽位还持有 shared_ptr,引用计数不为零,所以不会触发析构,定时器被成功延期。

取消定时器通过 TimerCancel 方法实现。它同样通过 weak_ptr 找到 TimerTask 对象,调用其 Cancel 方法设置 _canceled 标志为 true。这样即使时间到达触发析构,析构函数中检测到已取消就不会执行回调。这种设计避免了从时间轮中查找和删除定时器的开销,只需要设置一个标志位即可。

时间轮相比红黑树或最小堆的优势在于添加、删除、刷新定时器都是 O(1) 时间复杂度,只需要计算槽位索引并操作 vector。缺点是精度受限于槽位数量和时间粒度,本项目的 60 格 1 秒精度只适合处理连接超时这种秒级定时任务。如果需要更高精度或更长时间范围,可以使用多级时间轮或改用红黑树实现。

TimerWheel 工作流程图:

复制代码
时间轮结构 (_wheel[60]):
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ 0 │ 1 │ 2 │ 3 │ 4 │...│57 │58 │59 │ 0 │ (循环)
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
  ↑
_tick (秒针,每秒前进一格)

每格存储: vector<shared_ptr<TimerTask>>

timerfd 每秒触发一次
  ↓
OnTime() 回调
  ↓
read(timerfd) 获取超时次数 n
  ↓
for (i = 0; i < n; i++)
  ├─ _tick = (_tick + 1) % 60
  ├─ 取出 _wheel[_tick] 的所有 TimerTask
  ├─ 清空 _wheel[_tick]
  └─ shared_ptr 引用计数减少
       ↓
     引用计数 == 0?
       ├─ 是 → TimerTask 析构
       │        ↓
       │      _canceled == false?
       │        ├─ 是 → 执行 _task_cb() 回调
       │        │       (通常是关闭超时连接)
       │        └─ 否 → 不执行 (已取消)
       │
       └─ 否 → 不析构 (其他槽位还持有)

定时器操作流程:

复制代码
1. 添加定时器 TimerAdd(delay, callback):
   ↓
   创建 TimerTask(callback)
   ↓
   pos = (_tick + delay) % 60
   ↓
   shared_ptr 放入 _wheel[pos]
   ↓
   weak_ptr 保存到 _timers[id]
   ↓
   返回 id

2. 刷新定时器 TimerRefresh(id):
   ↓
   从 _timers[id] 获取 weak_ptr
   ↓
   weak_ptr.lock() 提升为 shared_ptr
   ↓
   成功? (对象还存活)
   ├─ 是 → pos = (_tick + delay) % 60
   │       ↓
   │       shared_ptr 重新放入 _wheel[pos]
   │       (旧槽位的 shared_ptr 会在时间到达时被清空)
   │
   └─ 否 → 定时器已过期,不处理

3. 取消定时器 TimerCancel(id):
   ↓
   从 _timers[id] 获取 weak_ptr
   ↓
   weak_ptr.lock() 提升为 shared_ptr
   ↓
   成功? 调用 Cancel() 设置 _canceled = true
   (析构时检查标志,不执行回调)

非活跃连接释放机制:

复制代码
连接建立时:
  EnableInactiveRelease(10秒)
    ↓
  TimerAdd(10, [conn]{ conn->Release(); })
    ↓
  定时器放入 _wheel[(_tick+10)%60]

每次有事件发生:
  HandleEvent() → TimerRefresh(timer_id)
    ↓
  定时器从旧槽位移到新槽位 (_tick+10)
    ↓
  超时时间延后 10 秒

10 秒内无任何事件:
  秒针走到该槽位
    ↓
  清空槽位,shared_ptr 引用计数归零
    ↓
  TimerTask 析构,执行 conn->Release()
    ↓
  连接被关闭释放

8. Connection - TCP 连接管理

Connection 类是对单个 TCP 连接的完整封装,它管理连接的整个生命周期从建立到关闭。每个 Connection 对象绑定到一个固定的 EventLoop 线程,所有对这个连接的操作都在该线程中执行,避免了多线程竞争。Connection 内部维护客户端 Socket、事件管理 Channel、输入输出 Buffer、以及用户自定义上下文 Any 对象。

连接状态通过一个状态机管理,包含五个状态。DISCONNECTED 表示连接已断开或尚未建立;CONNECTING 表示连接对象已创建但还未完成初始化;CONNECTED 表示连接已建立并正常工作;DISCONNECTING 表示连接正在关闭过程中,可能还有数据待发送;最后回到 DISCONNECTED 状态。状态转换通过 Established、Shutdown、Release 等方法驱动。

Connection 定义了五大事件回调函数来处理不同的事件。HandleRead 处理 EPOLLIN 可读事件,它调用 Socket 的 NonBlockRecv 方法从内核接收缓冲区读取数据到应用层输入 Buffer,然后调用用户设置的 _message_callback 消息回调,让业务层处理接收到的数据。HandleWrite 处理 EPOLLOUT 可写事件,它调用 NonBlockSend 尽量发送输出 Buffer 中的数据,发送成功后移动读指针消费已发送的数据,如果全部发完就调用 DisableWrite 关闭写事件监控避免 CPU 空转,如果连接状态是 DISCONNECTING 且数据已发完就调用 Release 释放连接。HandleClose 处理 EPOLLHUP 挂断事件,先处理输入缓冲区中可能残留的数据,然后调用 Release 释放连接。HandleError 处理 EPOLLERR 错误事件,处理方式和 HandleClose 相同。HandleEvent 是一个特殊的回调,它在任何事件发生时都会被调用,用于刷新连接的活跃时间,防止非活跃连接超时被释放。

Send 方法是业务层发送数据的接口。它接收数据指针和长度,首先把数据拷贝到一个临时 Buffer 对象中,这是因为 Send 不是立即发送而是通过 RunInLoop 异步投递到 EventLoop 线程执行,如果不拷贝,等到真正执行时原始数据可能已经被释放。然后调用 RunInLoop 投递 SendInLoop 方法,SendInLoop 在 EventLoop 线程中执行,把临时 Buffer 的数据追加到输出 Buffer,然后调用 EnableWrite 开启 EPOLLOUT 监控,等 socket 可写时 HandleWrite 会被触发完成实际发送。

Shutdown 方法用于主动关闭连接。它首先把连接状态改为 DISCONNECTING,然后调用 QueueInLoop 投递 ShutdownInLoop 到 EventLoop 线程。ShutdownInLoop 中先处理输入缓冲区可能残留的数据,然后检查输出缓冲区是否还有待发送的数据,如果有就等待 HandleWrite 发完后再释放,如果没有就直接调用 Release。

Release 方法是连接释放的核心流程。它通过 QueueInLoop 投递 ReleaseInLoop 确保在 EventLoop 线程中执行。ReleaseInLoop 首先把连接状态改为 DISCONNECTED,然后调用 Channel 的 Remove 方法从 epoll 中删除 fd 的监控,调用 Socket 的 Close 方法关闭文件描述符,调用 CancelInactiveRelease 取消定时器防止重复释放,调用用户设置的 _closed_callback 关闭回调让业务层做清理工作,最后调用 _server_closed_callback 通知 TcpServer 从连接表 _conns 中删除这个连接的 shared_ptr。当 shared_ptr 被删除后引用计数归零,Connection 对象自动析构,完成整个生命周期。

这种设计保证了连接的线程安全和资源安全。所有状态修改都在单线程中进行,不需要加锁;通过 shared_ptr 管理生命周期,不会出现悬空指针;通过 RAII 管理 Socket 和 Channel,不会泄漏文件描述符;通过回调机制解耦业务逻辑和网络框架,让框架保持通用性。

Connection 状态机转换图:

复制代码
                    NewConnection
                         ↓
                  ┌─────────────┐
                  │ CONNECTING  │ (初始状态)
                  └─────────────┘
                         ↓ Established()
                  ┌─────────────┐
            ┌────→│  CONNECTED  │←────┐
            │     └─────────────┘     │ (正常工作)
            │            ↓            │
            │      Shutdown()         │
            │            ↓            │
            │     ┌─────────────┐    │
            │     │DISCONNECTING│    │ (等待数据发完)
            │     └─────────────┘    │
            │            ↓            │
            │      Release()          │
            │            ↓            │
            │     ┌─────────────┐    │
            └─────│DISCONNECTED │────┘ (连接已关闭)
                  └─────────────┘
                         ↓
                  对象析构释放

异常路径: CONNECTED → HandleClose/HandleError → Release → DISCONNECTED

Connection 事件处理流程:

复制代码
epoll_wait 返回 fd 就绪
  ↓
Channel::HandleEvent() 检查 _revents
  ↓
┌─────────────┬─────────────┬─────────────┬─────────────┐
│  EPOLLIN    │  EPOLLOUT   │  EPOLLHUP   │  EPOLLERR   │
│  可读事件    │  可写事件    │  挂断事件    │  错误事件    │
└─────────────┴─────────────┴─────────────┴─────────────┘
      ↓              ↓              ↓              ↓
 HandleRead     HandleWrite    HandleClose    HandleError
      ↓              ↓              ↓              ↓
 NonBlockRecv   NonBlockSend   处理残留数据   处理残留数据
      ↓              ↓              ↓              ↓
 写入 _in_buffer 发送 _out_buffer  Release()     Release()
      ↓              ↓
 _message_callback  发完? DisableWrite
      ↓              ↓
 业务层处理      DISCONNECTING? Release
      ↓
 最后统一调用 HandleEvent → TimerRefresh (刷新活跃时间)

9. Acceptor / TcpServer / LoopThreadPool

这三个类协同工作实现主从 Reactor 模式的服务器架构。

Acceptor 是新连接接收器,它持有监听 Socket 和对应的 Channel。在 TcpServer 创建 Acceptor 时,Acceptor 内部创建监听 socket,调用 Bind 绑定服务器地址和端口,调用 Listen 进入监听状态,然后把监听 socket 的 fd 封装成 Channel 注册到主线程的 EventLoop。Channel 的读回调被设置为 Acceptor 的 HandleRead 方法。当有客户端发起连接时,监听 socket 触发 EPOLLIN 事件,HandleRead 被调用,它循环调用 accept 系统调用接受所有等待的连接,每接受一个新连接就调用 _accept_callback 回调函数,这个回调在 TcpServer 中被设置为 NewConnection 方法。

TcpServer 是服务器的核心管理类,它持有主线程的 EventLoop、Acceptor、LoopThreadPool 线程池、以及连接表 _conns。NewConnection 方法是新连接建立的入口,它接收 Acceptor 传来的客户端 fd,首先自增连接 ID 生成唯一标识,然后调用 _pool.NextLoop 从线程池中轮询选择一个工作 EventLoop,创建 Connection 对象并传入选中的 EventLoop 和客户端 fd。接着设置 Connection 的各种回调函数,包括用户设置的消息回调、连接建立回调、关闭回调,以及 TcpServer 自己的 server_closed_callback 用于从连接表中删除连接。如果启用了非活跃释放,就调用 EnableInactiveRelease 给连接添加定时任务。然后调用 Connection 的 Established 方法完成连接初始化,把连接状态从 CONNECTING 改为 CONNECTED,开启 EPOLLIN 读事件监控,调用用户的连接建立回调。最后把 Connection 的 shared_ptr 保存到 _conns 连接表中,key 是连接 ID,value 是 shared_ptr,这样可以通过 ID 快速查找连接,同时 shared_ptr 保证连接对象不会被提前释放。

LoopThreadPool 是工作线程池,它管理多个 LoopThread 对象。每个 LoopThread 在构造时创建一个 std::thread 线程,线程函数中创建 EventLoop 对象并调用 Start 进入事件循环。为了保证主线程能拿到子线程创建的 EventLoop 指针,LoopThread 使用条件变量做线程同步。子线程创建 EventLoop 后通过条件变量通知主线程,主线程等待通知后才返回 EventLoop 指针。LoopThreadPool 的 Create 方法根据设置的线程数量创建相应数量的 LoopThread,并把每个 LoopThread 的 EventLoop 指针保存到 _loops 数组中。NextLoop 方法实现简单的轮询负载均衡,通过 _next_idx 索引循环遍历 _loops 数组,每次返回一个 EventLoop 指针并让索引加一,这样新连接会均匀分配到各个工作线程。

这种架构的优势在于职责分离和并发处理。主线程只负责 accept 新连接,不处理具体的读写业务,避免被某个慢速连接阻塞。工作线程各自独立处理分配给自己的连接,充分利用多核 CPU。每个连接绑定到固定线程,避免了锁竞争。线程数量可以根据 CPU 核心数灵活配置,通常设置为核心数或核心数的两倍。

10. Any - 类型擦除容器

Any 类是一个类型擦除容器,它可以存储任意类型的对象,同时保持类型安全。这种技术在 C++11 中通过基类指针和模板子类实现,类似于 std::any 的简化版本。Any 的设计目的是让 Connection 可以携带用户自定义的上下文数据,而不需要修改 Connection 类的定义。

Any 的实现基于两个关键类。holder 是一个抽象基类,它只定义了虚析构函数,不包含任何数据成员。placeholder 是一个模板子类,继承自 holder,它有一个类型为 T 的成员变量 _val 用于实际存储数据。Any 类内部持有一个 holder 类型的基类指针 _content,这个指针可以指向任意类型的 placeholder 对象。

当用户调用 Connection 的 SetContext 方法传入一个对象时,Any 的构造函数被调用,它创建一个 placeholder 对象来存储这个对象,然后把 placeholder 的指针赋值给 _content 基类指针。由于使用了模板,编译器会为每种具体类型生成对应的 placeholder 特化版本,但对外都是通过 holder 基类指针访问,实现了类型擦除。

当用户需要取出数据时,调用 Any 的 get 方法。这个方法首先通过 dynamic_cast 把 holder 基类指针转换为 placeholder 子类指针,如果转换失败说明类型不匹配返回空指针,如果成功就返回 placeholder 中存储的 _val 的地址。这样就实现了类型安全的取出操作,如果用户传入错误的类型参数,get 会返回空指针而不是导致未定义行为。

在 HTTP 服务器中,每个连接需要保存 HttpContext 对象来记录 HTTP 请求的解析状态。Connection 通过 _context 成员变量(类型是 Any)存储 HttpContext。连接建立时调用 SetContext 把 HttpContext 对象放入 Any,每次收到数据时调用 GetContext()->get() 取出 HttpContext 指针,继续解析 HTTP 请求。这种设计让网络框架层的 Connection 不需要知道 HttpContext 的存在,保持了框架的通用性,业务层可以存储任意类型的上下文数据。

Any 的析构函数会自动 delete _content 指针,释放 placeholder 对象,placeholder 的析构又会释放其中存储的实际对象,整个生命周期管理是自动的。这种设计虽然涉及虚函数和动态内存分配,有一定的性能开销,但对于连接级别的上下文存储来说,这点开销完全可以接受,换来的是极大的灵活性和类型安全。

Any 类型擦除流程图:

复制代码
SetContext(HttpContext ctx)
  ↓
创建 placeholder<HttpContext>
  _val = ctx (拷贝构造)
  ↓
_content = new placeholder<HttpContext>
  (holder* 基类指针指向子类对象)

GetContext()->get<HttpContext>()
  ↓
dynamic_cast<placeholder<HttpContext>*>(_content)
  ↓
成功 → 返回 &_val
失败 → 返回 nullptr (类型不匹配)

11. NetWork - SIGPIPE 处理

NetWork 是一个简单但重要的工具类,它的唯一职责是在程序启动时忽略 SIGPIPE 信号。在 Linux 系统中,当进程向一个已经关闭的 socket 写入数据时,内核会向进程发送 SIGPIPE 信号。这个信号的默认处理方式是终止进程,这对于网络服务器来说是不可接受的,因为客户端随时可能断开连接,服务器不应该因为向一个已关闭的连接写数据就崩溃。

NetWork 类的实现非常简洁,它只有一个构造函数,在构造函数中调用 signal 系统调用,把 SIGPIPE 信号的处理方式设置为 SIG_IGN 忽略。然后在全局作用域定义一个 NetWork 类型的静态变量 nw,这样在 main 函数执行之前,C++ 运行时会先初始化全局静态对象,NetWork 的构造函数被调用,SIGPIPE 信号被忽略。

忽略 SIGPIPE 后,当进程向已关闭的 socket 写数据时,send 或 write 系统调用不会触发信号,而是返回 -1 并设置 errno 为 EPIPE 错误码。这样程序可以通过检查返回值和 errno 来判断连接已关闭,然后调用 Connection 的关闭流程清理资源,而不是整个进程崩溃。

这种处理方式是网络编程的标准做法。几乎所有的网络服务器都需要忽略 SIGPIPE 信号,否则在高并发场景下,客户端频繁断开连接会导致服务器不稳定。通过一个简单的全局静态对象,项目在程序启动时就完成了这个必要的初始化,使用者不需要关心这个细节,避免了遗漏导致的问题。


三、Epoll 深度剖析

epoll 三板斧

epoll 是 Linux 内核提供的高效 IO 多路复用机制,它通过三个系统调用实现对大量文件描述符的监控。相比 select 和 poll,epoll 在处理大量连接时性能优势明显,是高性能网络服务器的标准选择。

1. epoll_create - 创建 epoll 实例

epoll_create 或 epoll_create1 系统调用在内核中创建一个 eventpoll 结构体,返回一个文件描述符作为 epoll 实例的句柄。这个 eventpoll 结构体是 epoll 的核心数据结构,它包含两个关键组件:一个红黑树和一个双向链表。

红黑树(rbr)用于存储所有被监控的文件描述符及其关注的事件类型。红黑树是一种自平衡二叉搜索树,插入、删除、查找操作的时间复杂度都是 O(log n)。每个节点对应一个被监控的 fd,节点中保存了 fd 的值、关注的事件类型(EPOLLIN、EPOLLOUT 等)、以及用户数据指针。当需要添加、修改、删除监控的 fd 时,都是对这棵红黑树进行操作。

就绪链表(rdllist)用于存储当前就绪的文件描述符。这是一个双向链表,初始为空。当某个被监控的 fd 状态变化(比如 socket 接收缓冲区有数据到达),内核会调用之前注册的回调函数,把这个 fd 对应的节点从红黑树中找出来,加入到就绪链表的尾部。epoll_wait 调用时直接从就绪链表中取出就绪的 fd 返回给用户,不需要遍历所有监控的 fd,这是 epoll 高效的关键。

项目中 Poller 的构造函数调用 epoll_create(1024) 创建 epoll 实例,参数 1024 在新版本内核中已经被忽略,只要大于 0 即可。返回的 epoll fd 保存在 _epfd 成员变量中,后续所有 epoll 操作都通过这个 fd 进行。

2. epoll_ctl - 操作监控集合

epoll_ctl 系统调用用于向 epoll 实例中添加、修改、删除监控的文件描述符。它接收四个参数:epoll 实例的 fd、操作类型 op、目标文件描述符 fd、以及 epoll_event 结构体指针。

操作类型 op 有三种。EPOLL_CTL_ADD 表示添加一个新的 fd 到监控集合,内核会在红黑树中插入一个新节点,保存 fd、事件类型、用户数据等信息,同时在内核中注册回调函数,当这个 fd 状态变化时回调函数会被触发。EPOLL_CTL_MOD 表示修改已存在的 fd 的监控事件类型,内核在红黑树中找到对应节点,更新其事件字段。EPOLL_CTL_DEL 表示从监控集合中删除一个 fd,内核从红黑树中删除对应节点,注销回调函数。

epoll_event 结构体包含两个字段。events 字段是一个位掩码,表示关注的事件类型,可以是 EPOLLIN、EPOLLOUT、EPOLLERR、EPOLLHUP 等的组合。data 字段是一个联合体,可以存储用户自定义数据,通常存储 fd 的值或者指向用户数据结构的指针,这样 epoll_wait 返回时可以通过 data 字段快速找到对应的上下文。

项目中 Poller 的 UpdateEvent 方法封装了 epoll_ctl 的调用。它首先通过 HasChannel 检查这个 fd 是否已经在 _channels 映射表中,如果存在说明之前已经添加过,就调用 epoll_ctl 传入 EPOLL_CTL_MOD 修改事件类型;如果不存在说明是第一次添加,就调用 epoll_ctl 传入 EPOLL_CTL_ADD 添加到 epoll,同时把 Channel 指针保存到 _channels 中。RemoveEvent 方法调用 epoll_ctl 传入 EPOLL_CTL_DEL 删除监控,并从 _channels 中移除映射。

3. epoll_wait - 等待事件就绪

epoll_wait 系统调用阻塞等待监控的文件描述符中有事件就绪。它接收四个参数:epoll 实例的 fd、用于接收就绪事件的数组、数组大小、以及超时时间。

调用 epoll_wait 时,内核首先检查就绪链表是否为空。如果就绪链表中有节点,说明有 fd 已经就绪,内核直接把就绪链表中的节点信息拷贝到用户提供的数组中,返回就绪 fd 的数量。每个数组元素是一个 epoll_event 结构体,包含就绪的事件类型和用户数据。

如果就绪链表为空,说明当前没有 fd 就绪,进程进入休眠状态,加入等待队列。当某个被监控的 fd 状态变化时,比如 socket 接收缓冲区有数据到达,内核的网络子系统会调用之前注册的回调函数。回调函数把这个 fd 对应的节点加入就绪链表,然后唤醒等待队列中的进程。进程被唤醒后,从就绪链表中取出就绪的 fd,拷贝到用户数组,返回给用户。

超时时间参数控制等待行为。如果设置为 -1 表示永久阻塞,直到有事件就绪或被信号中断才返回。如果设置为 0 表示非阻塞,立即返回当前就绪的 fd,如果没有就绪的就返回 0。如果设置为正数表示等待指定的毫秒数,超时后返回。

项目中 Poller 的 Poll 方法调用 epoll_wait,传入 _epfd、_evs 数组、数组大小 1024、超时时间 -1。这意味着 EventLoop 会永久阻塞在 epoll_wait,直到有 IO 事件就绪或被 eventfd 唤醒。epoll_wait 返回后,Poll 方法遍历返回的就绪事件数组,通过 fd 从 _channels 中找到对应的 Channel,调用 SetREvents 设置实际就绪的事件类型,把 Channel 加入 active 列表返回给 EventLoop。

epoll 的高效性体现在几个方面。首先,监控的 fd 集合通过 epoll_ctl 一次性注册到内核,不需要像 select 那样每次调用都把整个 fd 集合从用户态拷贝到内核态。其次,就绪 fd 通过回调机制加入就绪链表,epoll_wait 只返回就绪的 fd,不需要遍历所有监控的 fd。最后,红黑树的查找、插入、删除都是 O(log n),就绪链表的操作是 O(1),整体性能随连接数增长缓慢。当连接数达到万级甚至十万级,而活跃连接只有少数时,epoll 的优势最为明显。

事件类型

事件 含义 处理
EPOLLIN 可读 监听 socket: accept / 客户端 socket: recv
EPOLLOUT 可写 发送缓冲区可写,用于发数据
EPOLLRDHUP 对端关闭 归入读回调,通过 recv 返回值判断
EPOLLERR 错误 调用错误回调 → 关闭连接
EPOLLHUP 挂断 调用关闭回调 → 释放连接

LT vs ET

特性 LT (项目使用) ET
触发条件 条件满足就一直通知 状态变化时通知一次
读写要求 可以不一次读完 必须循环到 EAGAIN
非阻塞 IO 可选 必须
编程难度 简单 复杂

项目用 LT:注册事件没加 EPOLLET,HandleRead/Write 只读写一次,没有 while 循环到 EAGAIN。

EPOLLOUT 按需开启

socket 发送缓冲区大部分时间可写,一直监控会导致 epoll_wait 频繁返回、CPU 空转。

复制代码
平时: 只监控 EPOLLIN
  ↓ 业务调 Send()
数据写入 _out_buffer → EnableWrite() 开启 EPOLLOUT
  ↓ EPOLLOUT 触发
HandleWrite() 发送数据 → 发完 → DisableWrite() 关闭 EPOLLOUT

select / poll / epoll 对比

特性 select poll epoll
fd 上限 1024 无限制 无限制
数据拷贝 每次全量 每次全量 epoll_ctl 一次注册
就绪查找 O(n) 遍历 O(n) 遍历 O(1) 就绪链表
返回结果 所有 fd 所有 fd 只返回就绪 fd

连接数多、活跃连接少时,epoll 优势最明显。


四、完整业务流程

1. 服务器启动

服务器启动是整个系统初始化的过程,涉及多个组件的创建和配置。main 函数中首先创建 HttpServer 或 EchoServer 对象,传入监听端口号。以 HttpServer 为例,它的构造函数内部会创建 TcpServer 对象,TcpServer 的构造函数完成核心组件的初始化。

TcpServer 构造时首先创建主线程的 EventLoop 对象 _baseloop,这个 EventLoop 会在主线程中运行,专门负责接受新连接。然后创建 Acceptor 对象,Acceptor 内部创建监听 socket,调用 Bind 方法绑定服务器地址和端口,调用 Listen 方法将 socket 设置为监听状态,backlog 队列长度设置为 1024。Acceptor 把监听 socket 的 fd 封装成 Channel 对象,设置读回调为 Acceptor::HandleRead,但此时还没有注册到 epoll,需要等 Start 方法调用时才注册。Acceptor 的 _accept_callback 被设置为 TcpServer::NewConnection 方法,这样当新连接到来时就会调用 TcpServer 的逻辑。

TcpServer 创建完成后,业务层可以调用 SetThreadCount 设置工作线程数量,通常设置为 CPU 核心数或核心数的两倍。调用 SetMessageCallback 设置消息回调函数,这个回调会在每次收到数据时被调用,业务层在这里实现协议解析和业务逻辑。调用 EnableInactiveRelease 启用非活跃连接释放机制,传入超时时间(比如 10 秒),超过这个时间没有任何活动的连接会被自动关闭释放资源。

配置完成后调用 Start 方法启动服务器。Start 方法首先调用 _pool.Create 创建工作线程池,根据设置的线程数量创建相应数量的 LoopThread 对象,每个 LoopThread 在独立的线程中创建 EventLoop 并进入事件循环。然后调用 _acceptor.Listen 把监听 socket 的 Channel 注册到主线程的 epoll,开启 EPOLLIN 读事件监控。最后调用 _baseloop.Start 让主线程进入事件循环,开始 while 无限循环调用 epoll_wait 等待事件。

至此服务器启动完成,主线程阻塞在 epoll_wait 等待客户端连接,工作线程也各自阻塞在 epoll_wait 等待被分配连接。整个系统进入就绪状态,可以处理客户端请求。

2. 新连接建立

新连接建立是客户端和服务器完成 TCP 三次握手后,服务器接受连接并创建 Connection 对象的过程。当客户端调用 connect 向服务器发起连接时,TCP 三次握手在内核中完成,监听 socket 的接收队列中会有一个已完成连接等待被 accept。此时监听 socket 变为可读状态,触发 EPOLLIN 事件。

主线程的 EventLoop 正阻塞在 epoll_wait,监听 socket 就绪后 epoll_wait 返回,Poller 从就绪事件中找到监听 socket 对应的 Channel,调用其 HandleEvent 方法。HandleEvent 检测到是 EPOLLIN 事件,调用 Channel 的读回调,也就是 Acceptor 的 HandleRead 方法。

HandleRead 方法内部是一个循环,不断调用 accept 系统调用接受新连接。每次 accept 成功返回一个客户端 socket 的 fd,HandleRead 就调用 _accept_callback 回调函数,传入这个 fd。循环直到 accept 返回 -1 且 errno 为 EAGAIN,说明当前所有等待的连接都已接受完毕。这种循环 accept 的方式可以一次性处理多个同时到达的连接,提高效率。

_accept_callback 实际上是 TcpServer 的 NewConnection 方法。NewConnection 首先自增 _next_id 生成一个唯一的连接 ID,然后调用 _pool.NextLoop 从工作线程池中轮询选择一个 EventLoop。NextLoop 通过一个循环递增的索引 _next_idx 遍历 _loops 数组,实现简单的轮询负载均衡,让新连接均匀分配到各个工作线程。

选定 EventLoop 后,NewConnection 创建 Connection 对象,传入选中的 EventLoop 指针、连接 ID、客户端 fd。然后设置 Connection 的各种回调函数。_message_callback 设置为用户提供的消息处理回调,_connected_callback 设置为用户提供的连接建立回调,_closed_callback 设置为用户提供的连接关闭回调,_server_closed_callback 设置为 TcpServer 自己的 RemoveConnection 方法用于从连接表中删除连接。如果启用了非活跃释放,调用 EnableInactiveRelease 给连接添加定时任务,超时后自动关闭。

接下来调用 Connection 的 Established 方法完成连接初始化。Established 通过 RunInLoop 把 EstablishedInLoop 投递到连接绑定的工作 EventLoop 线程执行。EstablishedInLoop 中首先检查连接状态,如果是 CONNECTED 说明已经初始化过直接返回,避免重复初始化。然后把状态从 CONNECTING 改为 CONNECTED,调用 Channel 的 EnableRead 开启 EPOLLIN 读事件监控,这会触发 epoll_ctl 把客户端 fd 注册到工作线程的 epoll。最后调用用户的 _connected_callback 连接建立回调,让业务层可以做一些初始化工作,比如发送欢迎消息。

最后 NewConnection 把 Connection 的 shared_ptr 保存到 _conns 连接表中,key 是连接 ID,value 是 shared_ptr。这样 TcpServer 持有所有活跃连接的引用,可以通过 ID 查找连接,同时保证连接对象不会在事件处理过程中被意外释放。

至此新连接建立完成,客户端 fd 已经在工作线程的 epoll 中监控,等待客户端发送数据。主线程继续回到 epoll_wait 等待下一个新连接,工作线程也回到 epoll_wait 等待这个连接的读写事件。

3. 数据接收

数据接收流程从客户端发送数据开始。当客户端调用 send 或 write 向服务器发送数据时,数据首先到达服务器内核的 socket 接收缓冲区。内核检测到接收缓冲区有数据后,将这个 socket 标记为可读状态,如果这个 fd 在 epoll 中监控了 EPOLLIN 事件,内核就会把它加入 epoll 的就绪链表。

工作线程的 EventLoop 正阻塞在 epoll_wait,客户端 socket 就绪后 epoll_wait 返回就绪事件列表。Poller 的 Poll 方法遍历就绪事件,通过 fd 从 _channels 映射表中找到对应的 Channel 对象,调用 SetREvents 设置实际就绪的事件类型,把 Channel 加入 active 活跃列表。Poll 方法返回后,EventLoop 遍历 active 列表,逐个调用 Channel 的 HandleEvent 方法。

HandleEvent 方法检测到 _revents 中包含 EPOLLIN 事件,调用 Channel 的读回调,也就是 Connection 的 HandleRead 方法。HandleRead 首先在栈上分配一个 65536 字节的临时缓冲区,然后调用 Socket 的 NonBlockRecv 方法从内核接收缓冲区读取数据到临时缓冲区。NonBlockRecv 内部调用 recv 系统调用并传入 MSG_DONTWAIT 标志实现非阻塞读取,即使 socket 本身是阻塞模式,这次调用也不会阻塞。

recv 返回实际读取的字节数,HandleRead 把这些数据通过 WriteBuffer 方法追加到 Connection 的输入 Buffer _in_buffer 中。WriteBuffer 内部调用 EnsureWriteSpace 确保有足够的写入空间,如果空间不够会先尝试回收已读区域,实在不够就 resize 扩容。数据写入后更新 _writer_idx 写指针。

数据追加到 Buffer 后,HandleRead 调用用户设置的 _message_callback 消息回调函数,传入 Connection 的 shared_ptr 和输入 Buffer 的指针。这个回调是业务层实现的,比如 HttpServer 的 OnMessage 方法或 EchoServer 的 OnMessage 方法。业务层从 Buffer 中按照协议规则解析数据,解析多少就通过 MoveReadOffset 消费多少,移动读指针。如果数据不完整,比如 HTTP 请求只收到一半,业务层就不消费,等待下次数据到来后继续解析。

HandleEvent 方法在调用完读回调后,还会调用 _event_callback 事件回调。这个回调通常设置为 Connection 的 HandleEvent 方法,它会调用 TimerRefresh 刷新连接的定时器。如果启用了非活跃释放,每次有事件发生都会刷新定时器,把连接的超时时间延后,防止活跃连接被误判为非活跃而释放。

整个接收流程是异步非阻塞的。recv 不会阻塞,如果内核缓冲区没有数据就返回 EAGAIN。业务层解析不完整也不会阻塞,数据留在 Buffer 中等待下次继续。这样一个工作线程可以同时处理多个连接的读写事件,不会因为某个连接的数据未到达而阻塞其他连接的处理。

数据接收完整流程图:

复制代码
客户端 send() 发送数据
  ↓
数据到达服务器内核 socket 接收缓冲区
  ↓
内核标记 fd 为可读,加入 epoll 就绪链表
  ↓
工作线程 epoll_wait 返回
  ↓
Poller::Poll() 遍历就绪事件
  ↓
找到 fd 对应的 Channel
  ↓
channel->SetREvents(EPOLLIN)
  ↓
加入 active 列表
  ↓
EventLoop 遍历 active
  ↓
channel->HandleEvent()
  ↓
检测到 EPOLLIN → 调用 _read_callback
  ↓
Connection::HandleRead()
  ├─ 栈上分配 65536 字节临时缓冲区
  ├─ NonBlockRecv(buf, 65536)
  │    ↓ recv(fd, buf, 65536, MSG_DONTWAIT)
  │    ↓ 返回实际读取字节数 n
  ├─ _in_buffer.WriteBuffer(buf, n)
  │    ↓ EnsureWriteSpace(n) 确保空间
  │    ↓ memcpy 追加数据
  │    ↓ _writer_idx += n
  └─ _message_callback(conn, &_in_buffer)
       ↓
     业务层处理 (HttpServer::OnMessage)
       ├─ 从 Buffer 按协议解析
       ├─ 解析成功 → MoveReadOffset 消费数据
       └─ 解析不完整 → 不消费,等待下次
  ↓
HandleEvent() 最后调用 _event_callback
  ↓
TimerRefresh() 刷新连接活跃时间

Buffer 数据管理示意:

复制代码
初始状态:
┌────────────────────────────────────────┐
│        │                               │
└────────────────────────────────────────┘
  ↑      ↑
  0      _reader_idx = _writer_idx = 0

第一次接收 100 字节:
┌────────────────────────────────────────┐
│        │ 100 字节数据 │                │
└────────────────────────────────────────┘
  ↑      ↑              ↑
  0      _reader=0      _writer=100

业务层消费 50 字节:
┌────────────────────────────────────────┐
│        │已读50│剩余50│                 │
└────────────────────────────────────────┘
  ↑             ↑       ↑
  0             _reader=50  _writer=100

第二次接收 80 字节:
┌────────────────────────────────────────┐
│        │已读50│剩余50│新80字节│        │
└────────────────────────────────────────┘
  ↑             ↑                ↑
  0             _reader=50       _writer=180

业务层消费剩余 130 字节:
┌────────────────────────────────────────┐
│        │      已读 180 字节      │      │
└────────────────────────────────────────┘
  ↑                                ↑
  0                                _reader=_writer=180

下次写入时空间不够,回收已读空间:
┌────────────────────────────────────────┐
│                                        │
└────────────────────────────────────────┘
  ↑
  _reader=_writer=0 (重置到开头)

4. 数据发送

数据发送流程从业务层调用 Connection 的 Send 方法开始。业务层处理完请求后,比如 HTTP 服务器构造好响应内容,调用 conn->Send 传入数据指针和长度。Send 方法不是立即发送数据,而是采用异步发送的方式,这样可以避免阻塞业务逻辑,也能更好地处理跨线程调用。

Send 方法首先把传入的数据拷贝到一个临时 Buffer 对象中。为什么要拷贝?因为 Send 可能在任意线程中被调用,而实际发送必须在 Connection 绑定的 EventLoop 线程中执行。如果不拷贝,外部传入的数据可能是栈上的临时变量或临时 string 对象,等到 EventLoop 线程真正执行发送时,原始数据可能已经被释放,导致访问野指针。拷贝到临时 Buffer 后,通过 std::move 把 Buffer 对象的所有权转移给 lambda 表达式,避免额外的拷贝开销。

然后 Send 调用 RunInLoop 把 SendInLoop 方法投递到 Connection 绑定的 EventLoop 线程。RunInLoop 会判断当前线程是否就是 EventLoop 线程,如果是就直接执行 SendInLoop,如果不是就通过 QueueInLoop 加锁放入任务队列并通过 eventfd 唤醒 EventLoop 线程。

SendInLoop 在 EventLoop 线程中执行,它首先检查连接状态,如果不是 CONNECTED 状态说明连接已关闭,直接返回不发送。然后把临时 Buffer 中的数据通过 WriteBuffer 方法追加到 Connection 的输出 Buffer _out_buffer 中。WriteBuffer 内部确保有足够的写入空间,把数据拷贝到 _out_buffer 并更新写指针。

数据追加到输出 Buffer 后,SendInLoop 调用 Channel 的 EnableWrite 方法开启 EPOLLOUT 写事件监控。EnableWrite 修改 Channel 的 _events 字段添加 EPOLLOUT 标志,然后调用 Update 方法通知 EventLoop 更新 epoll 监控状态,最终调用 epoll_ctl 传入 EPOLL_CTL_MOD 修改这个 fd 的监控事件。

开启 EPOLLOUT 后,当 socket 的发送缓冲区有空间可写时,内核会触发 EPOLLOUT 事件。工作线程的 epoll_wait 返回,找到对应的 Channel 调用 HandleEvent,检测到 EPOLLOUT 事件后调用写回调,也就是 Connection 的 HandleWrite 方法。

HandleWrite 方法调用 Socket 的 NonBlockSend 尽量发送输出 Buffer 中的数据。NonBlockSend 内部调用 send 系统调用并传入 MSG_DONTWAIT 标志实现非阻塞发送。send 返回实际发送的字节数,可能小于请求发送的字节数,因为内核发送缓冲区可能已满。HandleWrite 根据返回值调用 MoveReadOffset 移动输出 Buffer 的读指针,消费已发送的数据。

如果输出 Buffer 中的数据全部发送完毕,ReadAbleSize 返回 0,HandleWrite 调用 Channel 的 DisableWrite 关闭 EPOLLOUT 监控。这一步非常重要,因为 socket 的发送缓冲区大部分时间都是可写的,如果一直监控 EPOLLOUT,epoll_wait 会频繁返回写事件,即使没有数据要发送,也会不断触发 HandleWrite 回调,造成 CPU 空转。按需开启和关闭 EPOLLOUT 是高性能网络编程的关键技巧。

关闭写事件后,HandleWrite 还会检查连接状态。如果状态是 DISCONNECTING 且输出 Buffer 已空,说明连接正在关闭过程中,数据已经全部发送完毕,此时调用 Release 方法释放连接。这种设计保证了即使业务层调用 Shutdown 主动关闭连接,也会等待输出缓冲区的数据发送完毕后再真正关闭,避免数据丢失。

如果输出 Buffer 还有数据没发完,HandleWrite 不会关闭 EPOLLOUT,等待下次 socket 可写时继续发送。这样即使一次发不完,也能通过多次 EPOLLOUT 事件逐步发送完所有数据。整个发送流程是完全异步的,不会阻塞业务逻辑,也不会阻塞其他连接的处理。

数据发送完整流程图:

复制代码
业务层调用 conn->Send(data, len)
  ↓
拷贝数据到临时 Buffer tmp_buf
  ↓
RunInLoop(SendInLoop)
  ↓
判断: 当前线程 == EventLoop 线程?
  ├─ 是 → 直接执行 SendInLoop
  └─ 否 → QueueInLoop + eventfd 唤醒
  ↓
SendInLoop(tmp_buf) 在 EventLoop 线程执行
  ├─ 检查连接状态 != CONNECTED? 返回
  ├─ _out_buffer.WriteBuffer(tmp_buf)
  │    ↓ 追加数据到输出缓冲区
  │    ↓ _writer_idx 后移
  └─ EnableWrite()
       ↓ _events |= EPOLLOUT
       ↓ epoll_ctl(EPOLL_CTL_MOD)
  ↓
socket 发送缓冲区有空间
  ↓
EPOLLOUT 事件触发
  ↓
epoll_wait 返回
  ↓
Channel::HandleEvent() 检测到 EPOLLOUT
  ↓
调用 _write_callback
  ↓
Connection::HandleWrite()
  ├─ NonBlockSend(_out_buffer.ReadPosition(), ReadAbleSize())
  │    ↓ send(fd, data, len, MSG_DONTWAIT)
  │    ↓ 返回实际发送字节数 n (可能 < len)
  ├─ _out_buffer.MoveReadOffset(n)
  │    ↓ _reader_idx += n (消费已发送数据)
  ├─ 检查: _out_buffer.ReadAbleSize() == 0?
  │    ├─ 是 → 数据全部发完
  │    │    ├─ DisableWrite()
  │    │    │    ↓ _events &= ~EPOLLOUT
  │    │    │    ↓ epoll_ctl(EPOLL_CTL_MOD)
  │    │    └─ 检查: 状态 == DISCONNECTING?
  │    │         ├─ 是 → Release() 关闭连接
  │    │         └─ 否 → 继续保持连接
  │    │
  │    └─ 否 → 还有数据未发完
  │         └─ 保持 EPOLLOUT,等待下次触发
  └─ 返回

EPOLLOUT 按需开启机制:

复制代码
平时状态:
  Channel._events = EPOLLIN (只监控读事件)
  socket 发送缓冲区可写,但不触发事件
  CPU 不会空转

业务层调用 Send():
  ↓
  数据追加到 _out_buffer
  ↓
  EnableWrite() 开启 EPOLLOUT
  ↓
  _events |= EPOLLOUT
  ↓
  epoll_ctl(EPOLL_CTL_MOD)

socket 可写时:
  ↓
  EPOLLOUT 触发
  ↓
  HandleWrite() 发送数据
  ↓
  发送完毕?
  ├─ 是 → DisableWrite() 关闭 EPOLLOUT
  │       ↓ _events &= ~EPOLLOUT
  │       ↓ epoll_ctl(EPOLL_CTL_MOD)
  │       ↓ 回到平时状态
  │
  └─ 否 → 保持 EPOLLOUT
          ↓ 下次继续发送

如果不按需开启:
  一直监控 EPOLLOUT
  ↓
  socket 大部分时间可写
  ↓
  epoll_wait 频繁返回 EPOLLOUT
  ↓
  HandleWrite 被频繁调用
  ↓
  但 _out_buffer 为空,无数据可发
  ↓
  CPU 空转,浪费资源

5. 连接关闭

连接关闭分为主动关闭和异常关闭两种情况,但最终都会走到 Release 方法完成资源释放。

主动关闭流程通过 Shutdown 方法触发。业务层处理完请求后,如果判断需要关闭连接(比如 HTTP 短连接或客户端请求断开),就调用 conn->Shutdown。Shutdown 方法首先把连接状态从 CONNECTED 改为 DISCONNECTING,表示连接正在关闭过程中。然后通过 QueueInLoop 把 ShutdownInLoop 投递到 Connection 绑定的 EventLoop 线程执行,确保关闭操作在正确的线程中进行。

ShutdownInLoop 在 EventLoop 线程中执行,它首先检查输入 Buffer 是否还有残留数据。如果有,调用 _message_callback 让业务层处理这些数据,避免数据丢失。然后检查输出 Buffer 是否还有待发送的数据。如果输出 Buffer 不为空,说明还有响应数据没发完,此时不能立即关闭连接,而是等待 HandleWrite 把数据发送完毕。HandleWrite 在发现输出 Buffer 为空且连接状态是 DISCONNECTING 时,会自动调用 Release 完成关闭。如果输出 Buffer 已经为空,ShutdownInLoop 直接调用 Release 立即释放连接。

异常关闭流程通过 HandleClose 或 HandleError 触发。当客户端主动断开连接或网络异常时,服务器端的 socket 会触发 EPOLLHUP 挂断事件或 EPOLLERR 错误事件。Channel 的 HandleEvent 检测到这些事件后,分别调用 _close_callback 或 _error_callback,它们都被设置为 Connection 的 HandleClose 或 HandleError 方法。这两个方法的处理逻辑相同,都是先处理输入 Buffer 中可能残留的数据,然后直接调用 Release 释放连接。异常关闭不需要等待输出 Buffer 发送完毕,因为连接已经断开,发送也会失败。

Release 方法是连接释放的核心流程,它通过 QueueInLoop 把 ReleaseInLoop 投递到 EventLoop 线程执行。ReleaseInLoop 首先把连接状态改为 DISCONNECTED,表示连接已完全断开。然后调用 Channel 的 Remove 方法从 epoll 中删除这个 fd 的监控,Remove 内部调用 EventLoop 的 RemoveEvent,最终调用 epoll_ctl 传入 EPOLL_CTL_DEL 操作码。

从 epoll 删除后,调用 Socket 的 Close 方法关闭文件描述符。Socket 的析构函数中会自动调用 close 系统调用,但这里显式调用 Close 可以更早释放 fd 资源。关闭 fd 后,调用 CancelInactiveRelease 取消连接的定时任务,防止定时器触发时连接已经被释放导致访问野指针。

接下来调用用户设置的 _closed_callback 关闭回调,让业务层可以做一些清理工作,比如记录日志、更新统计信息等。最后调用 _server_closed_callback,这个回调被设置为 TcpServer 的 RemoveConnection 方法。RemoveConnection 通过 RunInLoop 投递到主线程的 EventLoop,从 _conns 连接表中 erase 这个连接的 shared_ptr。

当 _conns 中的 shared_ptr 被删除后,如果没有其他地方持有这个 Connection 的 shared_ptr,引用计数就会归零,Connection 对象自动析构。Connection 的析构函数会依次析构其成员变量,包括 Socket、Channel、Buffer 等,这些类都采用 RAII 设计,析构时自动释放资源。整个生命周期管理是自动的,不需要手动 delete,也不会出现内存泄漏或悬空指针。

这种设计保证了连接关闭的安全性和完整性。主动关闭会等待数据发送完毕,避免数据丢失;异常关闭会处理残留数据,避免数据遗漏;Release 流程清理所有资源,避免资源泄漏;通过 shared_ptr 管理生命周期,避免悬空指针;所有操作都在正确的线程中执行,避免线程安全问题。

连接关闭完整流程图:

复制代码
主动关闭路径:
  业务层调用 conn->Shutdown()
    ↓
  状态 → DISCONNECTING
    ↓
  QueueInLoop(ShutdownInLoop)
    ↓
  ShutdownInLoop() 在 EventLoop 线程执行
    ├─ 处理 _in_buffer 残留数据
    │    ↓ _message_callback(conn, &_in_buffer)
    ├─ 检查 _out_buffer.ReadAbleSize() > 0?
    │    ├─ 是 → 有待发送数据
    │    │    └─ 等待 HandleWrite 发完
    │    │         ↓ HandleWrite 检测到:
    │    │         ↓ 数据发完 && 状态==DISCONNECTING
    │    │         ↓ 调用 Release()
    │    │
    │    └─ 否 → 无待发送数据
    │         └─ 直接调用 Release()
    └─ 进入 Release 流程

异常关闭路径:
  客户端断开 / 网络异常
    ↓
  触发 EPOLLHUP 或 EPOLLERR
    ↓
  Channel::HandleEvent()
    ├─ EPOLLHUP → _close_callback()
    └─ EPOLLERR → _error_callback()
    ↓
  Connection::HandleClose/HandleError()
    ├─ 处理 _in_buffer 残留数据
    └─ 直接调用 Release()
    ↓
  进入 Release 流程

Release 统一释放流程:
  QueueInLoop(ReleaseInLoop)
    ↓
  ReleaseInLoop() 在 EventLoop 线程执行
    ↓
  ┌─────────────────────────────────────┐
  │ 1. 状态 → DISCONNECTED              │
  └─────────────────────────────────────┘
    ↓
  ┌─────────────────────────────────────┐
  │ 2. Channel::Remove()                │
  │    ↓ EventLoop::RemoveEvent()       │
  │    ↓ Poller::RemoveEvent()          │
  │    ↓ epoll_ctl(EPOLL_CTL_DEL)       │
  │    ↓ 从 _channels 中删除映射        │
  └─────────────────────────────────────┘
    ↓
  ┌─────────────────────────────────────┐
  │ 3. Socket::Close()                  │
  │    ↓ close(fd) 关闭文件描述符       │
  └─────────────────────────────────────┘
    ↓
  ┌─────────────────────────────────────┐
  │ 4. CancelInactiveRelease()          │
  │    ↓ TimerCancel(timer_id)          │
  │    ↓ 防止定时器触发重复释放         │
  └─────────────────────────────────────┘
    ↓
  ┌─────────────────────────────────────┐
  │ 5. _closed_callback()               │
  │    ↓ 用户自定义关闭回调             │
  │    ↓ 业务层清理工作                 │
  └─────────────────────────────────────┘
    ↓
  ┌─────────────────────────────────────┐
  │ 6. _server_closed_callback()        │
  │    ↓ TcpServer::RemoveConnection()  │
  │    ↓ RunInLoop 投递到主线程         │
  │    ↓ _conns.erase(id)               │
  │    ↓ 删除 shared_ptr                │
  └─────────────────────────────────────┘
    ↓
  shared_ptr 引用计数归零
    ↓
  Connection 对象析构
    ├─ Socket 析构 (RAII)
    ├─ Channel 析构
    ├─ Buffer 析构
    └─ 其他成员析构
    ↓
  资源完全释放

五、HTTP 业务流程

HttpServer 结构

复制代码
HttpServer
  ├── _server (TcpServer)
  ├── _basedir (string)          ← 静态资源根目录
  ├── _get_route (Handlers)      ← GET 路由表 [正则→handler]
  ├── _post_route (Handlers)     ← POST 路由表
  ├── _put_route (Handlers)      ← PUT 路由表
  └── _delete_route (Handlers)   ← DELETE 路由表

HTTP 请求解析状态机

复制代码
请求示例:
POST /api/login?redirect=/home HTTP/1.1\r\n
Host: localhost:8085\r\n
Content-Length: 42\r\n
Connection: keep-alive\r\n
\r\n
{"username":"admin","password":"123456"}

阶段1: RECV_HTTP_LINE - 解析请求行

复制代码
正则: (GET|HEAD|POST|PUT|DELETE) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])
  matches[1] = "POST"           → _method
  matches[2] = "/api/login"     → _path (URL 解码)
  matches[3] = "redirect=/home" → 按 & 和 = 拆分到 _params
  matches[4] = "HTTP/1.1"       → _version

阶段2: RECV_HTTP_HEAD - 逐行解析请求头

复制代码
每行按 ": " 分割为 key/value → _headers
遇到空行 "\r\n" → 头部结束

阶段3: RECV_HTTP_BODY - 按 Content-Length 读正文

复制代码
Buffer 数据 >= 剩余长度 → 读取,状态 → RECV_HTTP_OVER
Buffer 数据 < 剩余长度  → 读已有的,等下次数据到来

阶段4: RECV_HTTP_OVER - 解析完成,进入路由

半包处理: 每个连接绑定 HttpContext 记录当前解析阶段。Buffer 数据不足则 return 等待,下次数据到来后从当前阶段继续。

HTTP 请求解析状态机流程图:

复制代码
连接建立时:
  SetContext(HttpContext())
  ↓ 初始状态: RECV_HTTP_LINE

每次收到数据:
  OnMessage(conn, buffer)
    ↓
  HttpContext *ctx = conn->GetContext()->get<HttpContext>()
    ↓
  while (buffer->ReadAbleSize() > 0)  // 长连接循环处理
    ↓
  ┌──────────────────────────────────────────┐
  │ 状态机: 根据 ctx->_state 分发            │
  └──────────────────────────────────────────┘
    ↓
  ┌─────────────────────────────────────────────────┐
  │ RECV_HTTP_LINE (解析请求行)                     │
  │   ↓                                             │
  │ GetLine(buffer) 获取一行                        │
  │   ├─ 找不到 \r\n → return (等待更多数据)        │
  │   └─ 找到完整行 → 继续                          │
  │   ↓                                             │
  │ 正则匹配请求行:                                 │
  │ (GET|POST|...) ([^?]*)(?:\?(.*))? (HTTP/1\.[01])│
  │   ├─ 匹配失败 → 状态=ERROR, return              │
  │   └─ 匹配成功 → 提取 method/path/params/version │
  │   ↓                                             │
  │ 状态 → RECV_HTTP_HEAD                           │
  └─────────────────────────────────────────────────┘
    ↓
  ┌─────────────────────────────────────────────────┐
  │ RECV_HTTP_HEAD (解析请求头)                     │
  │   ↓                                             │
  │ while (true)                                    │
  │   ↓                                             │
  │ GetLine(buffer) 获取一行                        │
  │   ├─ 找不到 \r\n → return (等待更多数据)        │
  │   └─ 找到完整行 → 继续                          │
  │   ↓                                             │
  │ 检查是否空行 "\r\n"?                            │
  │   ├─ 是 → 头部结束                              │
  │   │    ↓ 检查 Content-Length?                   │
  │   │    ├─ 有 → 状态=RECV_HTTP_BODY              │
  │   │    └─ 无 → 状态=RECV_HTTP_OVER              │
  │   │                                             │
  │   └─ 否 → 按 ": " 分割为 key/value              │
  │        ↓ _headers[key] = value                  │
  │        ↓ 继续循环读下一行                       │
  └─────────────────────────────────────────────────┘
    ↓
  ┌─────────────────────────────────────────────────┐
  │ RECV_HTTP_BODY (解析请求体)                     │
  │   ↓                                             │
  │ 计算剩余需要读取的长度:                         │
  │ need = Content-Length - _body.size()            │
  │   ↓                                             │
  │ 检查 buffer->ReadAbleSize() >= need?            │
  │   ├─ 是 → 读取 need 字节追加到 _body            │
  │   │    ↓ buffer->MoveReadOffset(need)           │
  │   │    ↓ 状态 → RECV_HTTP_OVER                  │
  │   │                                             │
  │   └─ 否 → 读取 buffer 中所有数据                │
  │        ↓ 追加到 _body                           │
  │        ↓ buffer->MoveReadOffset(all)            │
  │        ↓ return (等待更多数据)                  │
  └─────────────────────────────────────────────────┘
    ↓
  ┌─────────────────────────────────────────────────┐
  │ RECV_HTTP_OVER (请求解析完成)                   │
  │   ↓                                             │
  │ Route(ctx->_request, &ctx->_response)           │
  │   ↓ 路由分发处理请求                            │
  │   ↓                                             │
  │ WriteReponse(conn, ctx->_response)              │
  │   ↓ 发送 HTTP 响应                              │
  │   ↓                                             │
  │ 检查 Connection 头?                             │
  │   ├─ keep-alive → ctx->ReSet()                  │
  │   │               ↓ 状态重置为 RECV_HTTP_LINE   │
  │   │               ↓ 继续 while 循环处理下一请求 │
  │   │                                             │
  │   └─ close → conn->Shutdown()                   │
  │              ↓ 关闭连接                         │
  │              ↓ break 退出循环                   │
  └─────────────────────────────────────────────────┘

ERROR 状态:
  解析失败 → 返回 400 Bad Request → 关闭连接

路由分发 (Route)

复制代码
1. 先判断是否静态资源请求 IsFileHandler():
   - 设置了 _basedir?
   - GET/HEAD 方法?
   - ValidPath 路径合法? (防目录穿越: 按 / 分割,遇 .. 则 level--,< 0 拒绝)
   - 是普通文件?
   → 是: FileHandler() 读文件内容 + 设置 MIME

2. 否则按 method 查路由表 Dispatcher():
   → 正则匹配 path → 命中则调用 handler
   → 都不匹配 → 404

3. 方法不支持 → 405

路由分发流程图:

复制代码
Route(request, response)
  ↓
┌────────────────────────────────────────┐
│ 1. 检查是否静态文件请求                │
│    IsFileHandler(request)?             │
└────────────────────────────────────────┘
  ├─ 是 → FileHandler(request, response)
  │        ↓
  │      检查 _basedir 是否设置?
  │        ├─ 否 → 返回 false
  │        └─ 是 → 继续
  │      ↓
  │      检查 method == GET/HEAD?
  │        ├─ 否 → 返回 false
  │        └─ 是 → 继续
  │      ↓
  │      ValidPath(request._path)?
  │        ├─ 否 → 返回 false (防目录穿越)
  │        └─ 是 → 继续
  │      ↓
  │      realpath = _basedir + request._path
  │      ↓
  │      检查是否普通文件?
  │        ├─ 否 → 返回 false
  │        └─ 是 → 继续
  │      ↓
  │      读取文件内容到 response._body
  │      ↓
  │      根据扩展名设置 Content-Type
  │      (.html→text/html, .jpg→image/jpeg...)
  │      ↓
  │      response._status = 200
  │      ↓
  │      返回 true (处理完成)
  │
  └─ 否 → 动态路由处理
           ↓
         ┌────────────────────────────────┐
         │ 2. 根据 method 选择路由表      │
         │    Dispatcher(request, response)│
         └────────────────────────────────┘
           ↓
         根据 request._method 选择:
           ├─ GET    → _get_route
           ├─ POST   → _post_route
           ├─ PUT    → _put_route
           ├─ DELETE → _delete_route
           └─ 其他   → 返回 405 Method Not Allowed
           ↓
         遍历路由表 (vector<pair<regex, handler>>)
           ↓
         for (auto &route : routes)
           ↓
         regex_match(request._path, route.first)?
           ├─ 匹配成功 → route.second(request, response)
           │             ↓ 调用用户 handler
           │             ↓ 返回 true (处理完成)
           │
           └─ 不匹配 → 继续下一个路由
           ↓
         所有路由都不匹配
           ↓
         返回 404 Not Found

最后:
  检查 response._status?
    ├─ 已设置 → 使用该状态码
    └─ 未设置 → 默认 200 OK

ValidPath 目录穿越防护:

复制代码
ValidPath("/../../etc/passwd")
  ↓
按 '/' 分割路径: ["", "..", "..", "etc", "passwd"]
  ↓
level = 0
  ↓
遍历每个部分:
  ""   → 跳过
  ".." → level-- = -1 (向上超出根目录)
         ↓ 返回 false (拒绝)

ValidPath("/api/../admin/users")
  ↓
分割: ["", "api", "..", "admin", "users"]
  ↓
level = 0
  ""     → level = 0
  "api"  → level = 1
  ".."   → level = 0 (回到根)
  "admin"→ level = 1
  "users"→ level = 2
  ↓ level >= 0
  ↓ 返回 true (允许)

响应发送

复制代码
WriteReponse():
  补齐 Connection / Content-Length / Content-Type / Location
  拼装: "HTTP/1.1 200 OK\r\nheaders\r\n\r\nbody"
  conn->Send(响应)

长连接 / 短连接

复制代码
Connection: keep-alive → 长连接: ReSet HttpContext,while 循环继续解析
否则 → 短连接: conn->Shutdown()

六、关键设计总结

线程安全

操作 方案
Connection 读写 绑定固定 EventLoop,单线程操作
跨线程任务 RunInLoop/QueueInLoop + eventfd 唤醒
任务队列 std::mutex
定时器/连接表 通过 RunInLoop 投递到对应线程执行

生命周期管理

对象 方式
Connection shared_ptr,TcpServer::_conns 持有
TimerTask shared_ptr (时间轮) + weak_ptr (映射表)
Channel Connection/Acceptor/TimerWheel 直接持有
EventLoop LoopThread 线程栈上创建
Socket RAII,析构自动 close

七、关键参数速查

参数 说明
BUFFER_DEFAULT_SIZE 1024 字节 Buffer 初始大小,可动态扩容
MAX_EPOLLEVENTS 1024 epoll_wait 一次最多返回的事件数
时间轮格数 60 每格 1 秒,最大延迟 60 秒
DEFALT_TIMEOUT 10 秒 HTTP 默认非活跃超时
MAX_LINE 8192 HTTP 请求行/头部单行最大长度
MAX_LISTEN 1024 listen backlog 队列长度
HandleRead 缓冲区 65536 字节 每次 NonBlockRecv 最多读取
epoll_wait timeout -1 永久阻塞,靠 eventfd 唤醒

八、面试高频问题

Q1: 为什么用 epoll 而不是 select/poll?

服务器要同时处理大量连接,如果一个连接一个线程,线程切换和内存开销都很大;如果用阻塞 IO,一个线程只能等一个 fd。epoll 是 Linux 下高效的 IO 多路复用机制,内核用红黑树管理监控的 fd,用就绪链表存放已就绪的 fd,epoll_wait 只返回就绪的 fd,不需要像 select 那样每次把整个 fd 集合从用户态拷贝到内核态,也不需要线性扫描所有 fd。当连接数很多但活跃连接不多时,epoll 的效率远高于 select 和 poll。select 还有 1024 个 fd 的限制,而 epoll 没有。

Q2: LT 和 ET 的区别?项目用的哪个?

我项目用的是 LT 水平触发,注册事件时没有加 EPOLLET 标志。LT 的特点是只要 fd 状态满足条件就会持续通知,比如接收缓冲区里还有数据没读完,下次 epoll_wait 还会返回可读事件。ET 是边缘触发,只在状态变化时通知一次,比如从无数据变成有数据时通知一次,如果这次没读完,后面不会再通知,所以 ET 必须配合非阻塞 IO 并且循环读写到 EAGAIN。我的项目读写没有做 while 循环读到 EAGAIN,而是每次只读写一次,依赖 LT 在数据未处理完时继续通知,编程更简单也更不容易出错。对于万级并发,LT 的性能完全够用。

Q3: EPOLLOUT 为什么不能一直开着?

socket 的发送缓冲区大部分时间都是可写的,如果一直监控 EPOLLOUT,epoll_wait 会频繁返回写事件,即使没有业务数据要发送,也会不断触发回调,造成 CPU 空转。所以我的做法是平时只关注 EPOLLIN 读事件,当业务层调用 Send 把数据放入输出缓冲区后,才通过 EnableWrite 开启 EPOLLOUT。HandleWrite 回调中尽量发送输出缓冲区的数据,发完之后立刻调用 DisableWrite 关闭写事件监控。这样写事件只在有待发送数据时开启,避免了空转。

Q4: 为什么需要应用层 Buffer?

TCP 是字节流协议,没有消息边界。一次 recv 可能只读到半个请求,也可能读到好几个请求粘在一起。如果直接把 recv 的数据当作一个完整请求处理就会出错。所以我在 Connection 里维护了输入 Buffer,每次 recv 到的数据追加进去,业务层从 Buffer 中按协议解析,解析多少就消费多少,剩下的留到下次继续处理。输出 Buffer 也是同理,NonBlockSend 不一定能一次把数据全发完,没发完的数据留在输出缓冲区等下次 EPOLLOUT 继续发送。Buffer 内部用 vector 加上读写双指针管理,消费数据时只移动读指针,空间不够时先尝试把已读空间回收,实在不够才 resize 扩容,减少频繁的内存移动。

Q5: Reactor 模型在项目里怎么体现的?

我这个项目是典型的主从 Reactor 模型。EventLoop 就是 Reactor,负责事件循环;Poller 是 Demultiplexer,底层封装了 epoll;Channel 是 Event Handler,绑定 fd 和回调;Connection 和 Acceptor 是 Concrete Handler。主线程的 EventLoop 只负责监听 socket 的事件,新连接到来后由 Acceptor accept 出客户端 fd,再由 TcpServer 创建 Connection 并分配给某个工作 EventLoop。之后这个连接的所有读写事件都在绑定的工作 EventLoop 线程里处理。这样主线程不会被某个连接的业务处理阻塞,工作线程之间互不干扰,利用了多核能力又避免了同一连接被多个线程同时操作。

Q6: 一个连接从建立到关闭的完整流程?

建立: 客户端 connect 后,监听 socket 触发 EPOLLIN,主线程 epoll_wait 返回,Acceptor 的读回调执行 accept 拿到客户端 fd,然后调用 TcpServer::NewConnection。NewConnection 中自增连接 ID,通过 LoopThreadPool::NextLoop 轮询选一个工作 EventLoop,创建 Connection 对象,设置消息回调、关闭回调等,如果启用了非活跃释放就添加定时任务,然后调用 Established 把连接状态从 CONNECTING 改成 CONNECTED,对客户端 fd 开启 EPOLLIN 读事件监控,最后把 Connection 的 shared_ptr 保存到 _conns 连接表。

读写: 客户端发数据后,客户端 fd 触发 EPOLLIN,工作线程的 epoll_wait 返回,找到对应 Channel 调用 HandleRead,Connection 从 socket 非阻塞读取数据到输入 Buffer,然后调用业务层 message_callback。业务层如果要响应就调用 Send,Send 把数据拷贝到临时 Buffer 然后通过 RunInLoop 投递 SendInLoop,数据追加到输出缓冲区并开启 EPOLLOUT,等 socket 可写时 HandleWrite 尽量发送,发完就关闭写事件。

关闭: 主动关闭调用 Shutdown,连接状态变成 DISCONNECTING,如果输出缓冲区还有数据就等发完再释放;异常关闭(读写出错、EPOLLHUP、EPOLLERR)直接进入 Release。Release 中从 epoll 移除 fd、关闭 socket、取消定时器、调用用户关闭回调、最后通过 server_closed_callback 从 _conns 中删除 shared_ptr。

Q7: eventfd 的作用是什么?

eventfd 是用来跨线程唤醒 EventLoop 的。因为 EventLoop 可能阻塞在 epoll_wait,如果只是把任务放进队列,EventLoop 不一定能马上醒来处理。每个 EventLoop 创建时会创建一个 eventfd,把它也封装成 Channel 注册到 epoll 监控可读事件。当其他线程调用 QueueInLoop 把回调任务放入任务队列后,紧接着向 eventfd 写入一个 8 字节整数,eventfd 就变成可读状态触发 EPOLLIN,epoll_wait 被唤醒返回。EventLoop 读取 eventfd 清除可读状态后,在本轮事件处理结束后调用 RunAllTask 执行队列里的所有任务。这样就实现了线程间的任务投递和唤醒。

Q8: timerfd 和时间轮怎么做的?

我用 timerfd 实现每秒一次的定时触发,timerfd 也注册到 epoll 里统一管理,不需要单独开线程 sleep。TimerWheel 内部维护一个 60 格的数组,_tick 是秒针位置,每秒 timerfd 可读一次,读出超时次数,秒针往前走对应步数,清空走过的槽位。清空时槽位里的 shared_ptr 被释放,如果某个 TimerTask 的 shared_ptr 引用计数归零就会析构,析构时如果没被 cancel 就执行任务回调。添加定时任务时根据延迟时间算出槽位 (_tick + delay) % 60 放入。刷新定时器时通过 weak_ptr 拿到 shared_ptr 再放入未来的槽位,这样旧槽位被清空时因为新槽位还持有 shared_ptr,引用计数不为零所以不会析构。用于非活跃连接释放时,连接每次有事件就刷新定时器,超过指定时间没活动就释放连接。

Q9: 连接的生命周期怎么保证?

Connection 用 shared_ptr 管理,TcpServer 的 _conns 保存所有活跃连接的 shared_ptr,保证连接对象不会在事件处理中被提前释放。事件回调里通过 shared_from_this 获取当前连接的 shared_ptr,确保回调执行期间对象有效。定时器中也通过 shared_ptr/weak_ptr 配合,weak_ptr 不增加引用计数但能检测对象是否存活。连接关闭时先从 epoll 删除 fd、关闭 socket、取消定时器,调用业务关闭回调,最后通知 TcpServer 从 _conns 中 erase,引用计数归零后自动析构。整个生命周期清晰可控,不会出现悬空指针。

Q10: 如何处理跨线程操作?

每个 Connection 都绑定到一个固定的 EventLoop,外部线程不能直接改它的 Channel 或 Buffer,而是通过 RunInLoop 或 QueueInLoop 把操作投递到它所属的 EventLoop。RunInLoop 会先判断调用线程是否就是 EventLoop 所在线程,如果是就直接执行,否则加锁放入任务队列并通过 eventfd 唤醒。这样连接的大部分状态只在一个线程内修改,唯一需要加锁的地方就是任务队列的 push_back 操作,用一把 mutex 保护,锁粒度很小。定时器的操作也走同样的路径投递到对应 EventLoop 线程执行,连接表 _conns 的增删投递到主线程执行,避免了复杂的锁竞争。

Q11: HTTP 半包怎么处理?

HTTP 请求可能分多次到达,所以我给每个连接保存一个 HttpContext 作为解析上下文,它记录当前解析到哪个阶段。状态机分为四个阶段:RECV_HTTP_LINE 解析请求行、RECV_HTTP_HEAD 解析请求头、RECV_HTTP_BODY 解析正文、RECV_HTTP_OVER 表示请求完整。每次有数据到来就从 Buffer 中按当前状态继续解析。比如正在解析请求行但 Buffer 里还没有完整一行(找不到换行符),函数就直接返回等待下次数据到来。请求头也是一行一行解析,遇到空行表示头部结束。正文长度通过 Content-Length 判断,如果 Buffer 中的数据还不够就把已有数据追加到 body,等下次继续补齐。这样不管数据怎么拆分到达,都能正确解析出完整的 HTTP 请求。

Q12: HTTP 长连接怎么处理?

每个请求处理完后,我会根据请求头的 Connection 字段判断是否是 keep-alive。如果是短连接,就发送响应后调用 Shutdown 等数据发完再关闭。如果是长连接,就重置 HttpContext 状态机回到 RECV_HTTP_LINE 初始状态,然后继续处理 Buffer 中可能已经到达的下一个请求。OnMessage 回调里用了 while 循环,只要 Buffer 里还有可读数据,就尝试解析下一个请求。这样一个 TCP 连接上可以连续处理多个 HTTP 请求,减少了频繁建立和关闭连接的开销。

Q13: 为什么 Send 要先拷贝数据?

因为 Send 不是直接发送,而是通过 RunInLoop 异步投递到 EventLoop 线程执行。外界传入的 data 可能是栈上的临时变量或者临时 string,投递后任务不一定立刻执行,等到真正执行时原始 data 指向的内存可能已经被释放了。所以 Send 里先把数据拷贝到一个临时 Buffer 对象,再通过 std::move 传递给 SendInLoop,保证异步执行时数据仍然有效。


九、性能分析与优化方向

当前瓶颈

  1. 单进程: 主线程是单点,无法利用多机多进程的扩展能力。可以用多进程 + SO_REUSEPORT 让多个进程监听同一端口,由内核做负载均衡。
  2. 时间轮精度: 容量 60 格、1 秒精度,只适合 60 秒以内的定时任务。更长时间或更高精度需要多级时间轮或最小堆。
  3. 内存分配: 频繁 new/delete Connection 和 Buffer。可以引入对象池和内存池复用。
  4. 数据拷贝: Send 时拷贝数据,recv 先到栈缓冲区再拷贝到 Buffer。可以用 readv/writev scatter-gather IO 或 sendfile 零拷贝优化。

可扩展方向

  • 支持 chunked 传输编码
  • 支持 WebSocket 协议升级
  • 异步日志系统 (避免 IO 阻塞工作线程)
  • 配置文件系统 (端口、线程数、超时时间等)
  • 优雅退出 (信号处理 + 等待连接关闭)
  • 压测数据 (wrk/ab)

十、使用示例

Echo Server

cpp 复制代码
#include "server.hpp"

class EchoServer {
    TcpServer _server;
    void OnMessage(const PtrConnection &conn, Buffer *buf) {
        conn->Send(buf->ReadPosition(), buf->ReadAbleSize());
        buf->MoveReadOffset(buf->ReadAbleSize());
        conn->Shutdown();
    }
public:
    EchoServer(int port) : _server(port) {
        _server.SetThreadCount(2);
        _server.EnableInactiveRelease(10);
        _server.SetMessageCallback(
            std::bind(&EchoServer::OnMessage, this,
                      std::placeholders::_1, std::placeholders::_2));
    }
    void Start() { _server.Start(); }
};

int main() {
    EchoServer server(8080);
    server.Start();
}

HTTP Server

cpp 复制代码
#include "http/http.hpp"

int main() {
    HttpServer server(8085);
    server.SetBaseDir("./http/wwwroot");  // 静态资源
    server.SetThreadCount(4);

    server.Get("/hello", [](const HttpRequest &req, HttpResponse *rsp) {
        rsp->SetContent("Hello World", "text/plain");
    });

    server.Post("/api/login", [](const HttpRequest &req, HttpResponse *rsp) {
        // req._body 包含 POST 正文
        rsp->SetContent("{\"code\":0}", "application/json");
    });

    server.Listen();  // 阻塞运行
}

十一、调试技巧

常见问题

现象 原因 排查方法
连接建立后立即断开 Established 未调用或 EnableRead 失败 在 EstablishedInLoop 加日志
数据发不出去 忘记 EnableWrite 或 MoveReadOffset 检查 SendInLoop 和 HandleWrite
内存泄漏 _conns 未 erase 或循环引用 valgrind --leak-check=full
程序卡死 持锁时调用 RunInLoop 导致死锁 gdb attach + bt 查看调用栈
HTTP 返回 400 请求格式不符合正则 打印 Buffer 内容和解析状态
连接超时被断开 非活跃释放触发 检查 TimerRefresh 是否被调用

编译与压测

bash 复制代码
# 编译
g++ -std=c++11 -O2 -o http_server http/main.cc -lpthread

# curl 测试
curl http://localhost:8085/hello
curl -X POST -d '{"key":"val"}' http://localhost:8085/api/login

# ab 压测
ab -n 10000 -c 100 http://localhost:8085/

# 查看连接数
netstat -an | grep 8085 | grep ESTABLISHED | wc -l

# 文件描述符限制 (压测前调大)
ulimit -n 65535

十二、与 muduo 对比

特性 本项目 muduo
定时器 时间轮 (60 格,秒级) 红黑树 (std::set,微秒级)
线程模型 主从 Reactor 主从 Reactor
Buffer 双指针 vector readIndex/writeIndex vector
跨线程唤醒 eventfd eventfd
日志 简单宏打印 异步日志库
代码量 ~1500 行 ~5000 行
特点 简洁易学,核心概念完整 功能完善,生产级品质

两者设计思想一致 (One Loop Per Thread、Reactor、RAII、回调),本项目可以看作是 muduo 的精简教学版。

相关推荐
SmartBrain2 小时前
AI技术演进与实战路径洞察
人工智能·架构·aigc
ZGi.ai2 小时前
ZGI四层能力架构:一个企业AI底座的设计逻辑
人工智能·架构
400分2 小时前
LangChain 与大模型技术全链路详解
算法·架构
墨者阳2 小时前
可观・可控・可治:DB运维平台架构设计与实践
运维·数据库·架构·自动化·数据可视化
skilllite作者3 小时前
SkillLite Rust 沙箱与 AI Agent 自进化实战指南
开发语言·人工智能·后端·架构·rust
剩下了什么3 小时前
微服务入门介绍
微服务·云原生·架构
heimeiyingwang3 小时前
【架构实战】微前端架构设计与落地
前端·架构
星浩AI3 小时前
基于知识图谱的多模态 GraphRAG 项目实战,系统架构详解[附源码]
架构·langchain·agent
张忠琳3 小时前
【vllm】(六)vLLM v1 Sample — 模块超深度分析之一
ai·架构·vllm