一、整体架构:为什么是 Reactor 模式?
在传统的网络编程中,常用的模型有:
- 阻塞IO + 多线程/多进程:每个连接一个线程,连接数上来线程极多
- 非阻塞IO + select/poll/epoll:单线程循环等待事件,但业务处理会阻塞IO
Reactor 模式的核心思想是:有事件来了才处理,没事就阻塞等着。整个服务器就是一个事件循环,谁有数据来了就调对应的回调函数处理。
muduo 是陈硕写的 C++ 网络库,它的经典设计是 One Loop One Thread:
- 主线程(mainReactor)只负责监听新连接
- 子线程池(subReactors)负责已连接的IO处理
- 新连接来了通过轮询(Round-Robin)分配给某个子线程
- 每个线程内部是一个独立的事件循环,线程之间通过 eventfd 唤醒,几乎无锁
本项目 server.hpp 复刻了这个架构,让我们从最底层的工具类开始,一层层往上拆解。
二、工具基础层
2.1 日志模块
cpp
#define INF 0, DBG 1, ERR 2
#define LOG_LEVEL DBG
#define LOG(level, format, ...) do{
if (level < LOG_LEVEL) break;
time_t t = time(NULL);
struct tm *ltm = localtime(&t);
char tmp[32] = {0};
strftime(tmp, 31, "%H:%M:%S", ltm);
fprintf(stdout, "[%p %s %s:%d] " format "\n",
(void*)pthread_self(), tmp, __FILE__, __LINE__, ##__VA_ARGS__);
}while(0)
#define INF_LOG(format, ...) LOG(INF, format, ##__VA_ARGS__)
#define DBG_LOG(format, ...) LOG(DBG, format, ##__VA_ARGS__)
#define ERR_LOG(format, ...) LOG(ERR, format, ##__VA_ARGS__)
设计要点:
- 用宏定义实现,零运行时开销
- 自动打印:当前线程ID、时间、文件名、行号
##__VA_ARGS__处理可变参数为空的情况do{...}while(0)宏封装经典写法,避免 if 语句不加大括号导致的问题
这是一个生产环境可用的极简日志,不用依赖任何第三方库。
2.2 Buffer:双指针实现的高效缓冲区
网络编程中,读数据不一定一次读完,写数据也不一定一次发完,所以必须要有缓冲区。
cpp
class Buffer {
private:
std::vector<char> _buffer;
uint64_t _reader_idx; // 读偏移
uint64_t _writer_idx; // 写偏移
// ...
};
内存布局图:
┌─────────────────────────────────────────────────────────┐
│ 已读空闲区 │ 可读数据区 │ 可写空闲区 │
│ (HeadIdle) │ (ReadAbleSize) │ (TailIdle) │
└─────────────────────────────────────────────────────────┘
↑ ↑ ↑ ↑
0 _reader_idx _writer_idx size()
核心操作逻辑:
- 写入数据:从
_writer_idx开始写,写完移动写指针 - 读取数据:从
_reader_idx开始读,读完移动读指针 - 空间不足怎么办?
EnsureWriteSpace()负责判断空间容量:- 如果尾部空闲够,直接写
- 尾部不够,但头部+尾部加起来够:把数据搬移到开头,连续空间就出来了
- 还是不够:扩容 resize
实用API:
WriteAndPush/ReadAndPop:写入/读取后自动移动偏移量FindCRLF()/GetLine():找\n,按行读取,为HTTP协议解析准备Clear():只需要把偏移量归0,不用清空vector数据,非常高效
这就是 muduo 中 Buffer 类的简化实现,双指针设计避免了频繁的内存分配和数据拷贝。
2.3 Socket:RAII封装系统调用
原生的 socket API 是C风格的,很容易忘记 close,这个类把它封装成了 C++ 对象。
cpp
class Socket {
private:
int _sockfd;
public:
bool Create();
bool Bind(const std::string &ip, uint16_t port);
bool Listen(int backlog = MAX_LISTEN);
int Accept();
bool Connect(const std::string &ip, uint16_t port);
ssize_t Recv(void *buf, size_t len, int flag = 0);
ssize_t Send(const void *buf, size_t len, int flag = 0);
void Close();
// 高层封装
bool CreateServer(uint16_t port, const std::string &ip = "0.0.0.0", bool block_flag = false);
bool CreateClient(uint16_t port, const std::string &ip);
void ReuseAddress(); // 地址复用,解决TIME_WAIT问题
void NonBlock(); // 设置非阻塞
};
关键细节:
- RAII :析构函数自动调用
Close(),对象销毁自动关闭fd - 非阻塞Recv/Send:对 EAGAIN/EINTR 做了特殊处理,返回0表示"现在没数据,不是错误"
- 地址复用顺序 :
SO_REUSEADDR必须在bind()之前调用,否则不生效! CreateServer()一站式完成:创建 → 非阻塞 → 绑定 → 监听 → 地址复用
有了这个类,我们再也不用跟原生的 sockaddr_in 打交道了。
2.4 Any:自己实现一个 std::any
C++17 才有的 std::any 可以装任意类型的数据,这里用类型擦除技术实现了一个,用于保存协议上下文。
cpp
class Any {
private:
class holder { // 抽象基类
public:
virtual ~holder() {}
virtual const std::type_info& type() = 0;
virtual holder *clone() = 0;
};
template<class T>
class placeholder : public holder { // 具体类型持有者
public:
placeholder(const T &val): _val(val) {}
virtual const std::type_info& type() { return typeid(T); }
virtual holder *clone() { return new placeholder(_val); }
T _val;
};
holder *_content;
public:
template<class T>
T *get() {
assert(typeid(T) == _content->type()); // 类型安全检查
return &((placeholder<T>*)_content)->_val;
}
// ... 构造、赋值运算符
};
类型擦除原理:
- 基类
holder定义虚函数接口 - 模板子类
placeholder<T>保存具体类型数据 - 外面Any类只存基类指针
_content,需要的时候用typeid检查类型,向下转型 - 赋值时实现拷贝并交换 ,创建临时对象
Any(val)与自身交换数据指针,函数结束时临时对象自动析构并带走原数据
为什么需要它?服务器要支持多协议(TCP Echo、HTTP等),每个连接需要保存自己协议的解析上下文,用 Any 就不需要用 void*,保证类型安全。
三、Reactor核心层
3.1 Channel:事件通道
每个文件描述符(不管是监听socket、客户端socket、还是timerfd/eventfd)在内核中只是一个整数,我们需要一个东西来管理:这个fd关心什么事件?事件来了该调用什么函数?------这就是 Channel。
cpp
class Channel {
private:
int _fd;
EventLoop *_loop; // 所属的事件循环
uint32_t _events; // 想要监控的事件(EPOLLIN/EPOLLOUT)
uint32_t _revents; // 实际触发的事件
using EventCallback = std::function<void()>;
EventCallback _read_callback; // 读事件回调
EventCallback _write_callback; // 写事件回调
EventCallback _error_callback; // 错误事件回调
EventCallback _close_callback; // 关闭事件回调
EventCallback _event_callback; // 任意事件回调(刷新活跃度)
public:
// 事件启停控制
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(); }
// 事件分发:revents是啥就调啥回调
void HandleEvent() {
if ((_revents & EPOLLIN) || (_revents & EPOLLRDHUP) || (_revents & EPOLLPRI)) {
if (_read_callback) _read_callback();
}
if (_revents & EPOLLOUT) {
if (_write_callback) _write_callback();
} else if (_revents & EPOLLERR) {
if (_error_callback) _error_callback();
} else if (_revents & EPOLLHUP) {
if (_close_callback) _close_callback();
}
if (_event_callback) _event_callback();
}
void Update(); // 调用loop更新epoll监控
void Remove(); // 移除监控
};
Channel的核心理念:
- 它不拥有fd,fd的生命周期由Socket/Connection管理,Channel只是"借"来用
- 它把"fd + 回调函数 + 事件监控"打包在一起
Update()/Remove()不是自己操作epoll,而是通过所属的EventLoop去操作(跨线程安全考虑)
一个Channel对应一个fd,这是Reactor模式最基础的单元。
3.2 Poller:epoll的简单封装
Poller 就是对 epoll 的薄封装,它的职责很单一:
- 维护 fd → Channel 的映射
- 调用
epoll_wait等待事件 - 返回活跃的 Channel 列表
cpp
class Poller {
private:
int _epfd;
struct epoll_event _evs[MAX_EPOLLEVENTS];
std::unordered_map<int, Channel *> _channels; // fd -> Channel*
void Update(Channel *channel, int op) {
struct epoll_event ev;
ev.data.fd = channel->Fd();
ev.events = channel->Events();
epoll_ctl(_epfd, op, channel->Fd(), &ev);
}
public:
void UpdateEvent(Channel *channel); // ADD/MOD
void RemoveEvent(Channel *channel); // DEL
// 等待事件,把活跃的Channel填到active数组里
void Poll(std::vector<Channel*> *active) {
int nfds = epoll_wait(_epfd, _evs, MAX_EPOLLEVENTS, -1);
for (int i = 0; i < nfds; i++) {
auto it = _channels.find(_evs[i].data.fd);
assert(it != _channels.end());
it->second->SetREvents(_evs[i].events); // 设置实际触发的事件
active->push_back(it->second);
}
}
};
注意:Poller 不拥有Channel,只是存指针,Channel的生命周期在外层管理。
3.3 TimerWheel:时间轮定时器,O(1)
服务器需要定时功能,比如:非活跃连接30秒没通信就断开。如果用定时器队列/最小堆,每次刷新活跃时间都是O(logN),连接多了性能不行。这里用了时间轮算法。
想象一个钟表:
- 表盘有60个格子(默认60秒超时)
- 有一个秒针
_tick,每秒走一格 - 添加一个30秒超时的任务,就放在"当前位置+30格"的格子里
- 秒针走到哪里,就把那个格子里的任务全部执行(释放shared_ptr即可)
cpp
class TimerTask { // 定时任务对象
uint64_t _id;
uint32_t _timeout;
bool _canceled;
TaskFunc _task_cb; // 要执行的任务
ReleaseFunc _release; // 析构时从时间轮map中移除自己
public:
~TimerTask() {
if (!_canceled) _task_cb(); // 析构时自动执行任务
_release();
}
void Cancel() { _canceled = true; }
};
class TimerWheel {
int _tick; // 当前秒针位置
int _capacity; // 表盘大小=最大超时时间
std::vector<std::vector<PtrTask>> _wheel; // 表盘:每个格子是一个任务数组
std::unordered_map<uint64_t, WeakTask> _timers; // id -> weak_ptr(用于刷新/取消)
int _timerfd; // 用timerfd_create创建,每秒触发一次读事件
std::unique_ptr<Channel> _timer_channel;
// 每秒被调用一次:秒针走一步,释放该格子任务
void RunTimerTask() {
_tick = (_tick + 1) % _capacity;
_wheel[_tick].clear(); // 释放shared_ptr,TimerTask析构自动执行任务
}
public:
// 添加定时任务
void TimerAddInLoop(uint64_t id, uint32_t delay, const TaskFunc &cb) {
PtrTask pt(new TimerTask(id, delay, cb));
pt->SetRelease(std::bind(&TimerWheel::RemoveTimer, this, id));
int pos = (_tick + delay) % _capacity;
_wheel[pos].push_back(pt);
_timers[id] = WeakTask(pt);
}
// 刷新活跃:把任务再"挂"到新位置,相当于延长寿命
void TimerRefreshInLoop(uint64_t id) {
auto it = _timers.find(id);
if (it == _timers.end()) return;
PtrTask pt = it->second.lock(); // weak_ptr -> shared_ptr
int pos = (_tick + pt->DelayTime()) % _capacity;
_wheel[pos].push_back(pt);
}
};
TimerTask析构执行任务
- 任务对象是
shared_ptr,当格子clear时引用计数减1 - 如果该任务被刷新过(shared_ptr又被放到了新格子),引用计数不为0,不会析构
- 如果该任务没被刷新,引用计数归0,析构自动执行回调
- 不需要"到时了去取任务执行",释放就是执行
为什么用 weak_ptr?
_wheel里存shared_ptr(拥有对象)_timers里存weak_ptr(不拥有,只是查找用)- 这样不会因为
_timers也存了shared_ptr导致任务永远不释放
定时器怎么驱动?
- 用 Linux 的
timerfd_create创建一个fd,设置成每秒超时一次 - 把这个fd封装成Channel加到epoll里
- 每秒fd读事件就绪 → 读timerfd → RunTimerTask → 秒针走一步
定时器也是一个fd,也走epoll事件循环,和网络IO统一处理
3.4 EventLoop:事件循环
EventLoop 就是那个"无限循环等事件"的东西,每个线程有且只有一个。它是整个框架最核心的类,组合了Poller、TimerWheel,还负责跨线程任务调度。
cpp
class EventLoop {
std::thread::id _thread_id; // 记录自己属于哪个线程
int _event_fd; // eventfd,用于跨线程唤醒
std::unique_ptr<Channel> _event_channel;
Poller _poller; // epoll监控
TimerWheel _timer_wheel; // 定时器
std::vector<Functor> _tasks; // 跨线程投递的任务队列
std::mutex _mutex; // 保护_tasks的锁
public:
// 核心:三步走的事件循环
void Start() {
while(1) {
// 1. 阻塞等待事件
std::vector<Channel *> actives;
_poller.Poll(&actives);
// 2. 处理所有就绪事件
for (auto &channel : actives) {
channel->HandleEvent();
}
// 3. 执行跨线程投递给我的任务
RunAllTask();
}
}
// 判断当前线程是不是我自己的线程
bool IsInLoop() { return _thread_id == std::this_thread::get_id(); }
void AssertInLoop() { assert(_thread_id == std::this_thread::get_id()); }
// 在loop线程中执行cb:如果已在loop线程,直接执行;否则投递到队列
void RunInLoop(const Functor &cb) {
if (IsInLoop()) return cb();
return QueueInLoop(cb);
}
// 把任务投递给这个loop
void QueueInLoop(const Functor &cb) {
{
std::unique_lock<std::mutex> lock(_mutex);
_tasks.push_back(cb);
}
WeakUpEventFd(); // 关键!给eventfd写个数据,唤醒epoll_wait
}
// eventfd相关
static int CreateEventFd() {
return eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
}
void ReadEventfd() {
uint64_t res; read(_event_fd, &res, sizeof(res));
}
void WeakUpEventFd() {
uint64_t val = 1; write(_event_fd, &val, sizeof(val));
}
};
最关键的设计:跨线程唤醒机制
假设子线程A正在epoll_wait阻塞着,主线程想让子线程A干点活(比如给新连接启动读监控),怎么办?
- 不能直接调用,因为Channel/epoll操作不是线程安全的
- 给任务队列加锁?子线程在epoll_wait里阻塞着根本不会去看队列
- 解决方案:eventfd
eventfd是Linux提供的一个专门用于事件通知的fd:
- EventLoop构造时创建一个eventfd,也封装成Channel加到epoll里,监控读事件
- 别的线程想给我派活:加锁把任务放队列 → 给eventfd写个8字节整数 → eventfd读事件就绪 → epoll_wait返回
- 我(EventLoop线程)被唤醒:读掉eventfd的数据 → 处理IO事件 → RunAllTask执行队列里的任务
这样就实现了线程安全的跨线程调度,而且整个epoll里只有很短的时间加锁(只锁任务队列的push/swap),性能极高。
四、网络连接层
4.1 LoopThread / LoopThreadPool:线程池管理
我们要多个线程跑多个EventLoop,怎么管理?
cpp
class LoopThread {
std::mutex _mutex;
std::condition_variable _cond;
EventLoop *_loop;
std::thread _thread;
// 线程入口函数
void ThreadEntry() {
EventLoop loop; // 注意:loop是栈上对象!
{
std::unique_lock<std::mutex> lock(_mutex);
_loop = &loop;
_cond.notify_all(); // 通知GetLoop:我创建好了
}
loop.Start(); // 进入事件循环,直到线程退出
}
public:
LoopThread() : _loop(NULL), _thread(std::thread(&LoopThread::ThreadEntry, this)) {}
EventLoop *GetLoop() {
EventLoop *loop = NULL;
{
std::unique_lock<std::mutex> lock(_mutex);
_cond.wait(lock, [&](){ return _loop != NULL; }); // 等loop创建好
loop = _loop;
}
return loop;
}
};
class LoopThreadPool {
int _thread_count;
int _next_idx; // 轮询索引
EventLoop *_baseloop;
std::vector<LoopThread*> _threads;
std::vector<EventLoop *> _loops;
public:
void Create() {
if (_thread_count > 0) {
_threads.resize(_thread_count);
_loops.resize(_thread_count);
for (int i = 0; i < _thread_count; i++) {
_threads[i] = new LoopThread();
_loops[i] = _threads[i]->GetLoop(); // 这里会阻塞等待线程创建好loop
}
}
}
// 轮询选择下一个EventLoop(Round-Robin)
EventLoop *NextLoop() {
if (_thread_count == 0) return _baseloop; // 没子线程就用主线程
_next_idx = (_next_idx + 1) % _thread_count;
return _loops[_next_idx];
}
};
条件变量的作用:
- 线程创建了,但EventLoop是在线程函数里栈上构造的
- 如果外部线程马上调用GetLoop,很可能loop还是NULL
- 用条件变量wait:loop构造好才notify,GetLoop拿到的一定是有效的loop指针
这就是多线程编程中的"线程安全初始化"经典写法。
4.2 Acceptor:专门处理新连接
Acceptor 很简单,它只干一件事:监听端口,有新连接来了就accept,然后回调给上层。
cpp
class Acceptor {
Socket _socket; // 监听套接字
EventLoop *_loop; // 固定跑在baseloop(主线程)
Channel _channel; // 监听fd的事件通道
using AcceptCallback = std::function<void(int)>;
AcceptCallback _accept_callback; // 新连接回调:参数是newfd
void HandleRead() {
int newfd = _socket.Accept();
if (newfd < 0) return;
if (_accept_callback) _accept_callback(newfd); // 新fd交给上层
}
public:
Acceptor(EventLoop *loop, int port)
: _socket(CreateServer(port)), _loop(loop), _channel(loop, _socket.Fd()) {
_channel.SetReadCallback(std::bind(&Acceptor::HandleRead, this));
}
void SetAcceptCallback(const AcceptCallback &cb) { _accept_callback = cb; }
void Listen() { _channel.EnableRead(); } // 构造完设置好回调,再启动读监控!
};
重要细节 :为什么不在构造函数里直接
EnableRead()?因为构造时
_accept_callback还没设置,如果启动监控后马上有连接进来,HandleRead里回调是空的,新连接就丢了。所以要:
- 构造对象
- 外部调用SetAcceptCallback设置回调
- 再调用Listen()启动读监控
这是"二阶段初始化"思想,避免对象没构造完就被回调。
4.3 Connection:客户端连接的完整生命周期管理
这是最复杂的一个类,一个Connection对象代表一个已建立的TCP客户端连接。
cpp
typedef enum { DISCONNECTED, CONNECTING, CONNECTED, DISCONNECTING } ConnStatu;
using PtrConnection = std::shared_ptr<Connection>;
class Connection : public std::enable_shared_from_this<Connection> {
uint64_t _conn_id;
int _sockfd;
bool _enable_inactive_release;
EventLoop *_loop; // 这个连接属于哪个EventLoop
ConnStatu _statu; // 连接状态机
Socket _socket; // 套接字操作
Channel _channel; // 事件通道
Buffer _in_buffer; // 输入缓冲区
Buffer _out_buffer; // 输出缓冲区
Any _context; // 协议上下文(任意类型)
// 两组回调:用户设置的 + 服务器内部的
ConnectedCallback _connected_callback;
MessageCallback _message_callback; // 用户业务处理回调!
ClosedCallback _closed_callback;
AnyEventCallback _event_callback;
ClosedCallback _server_closed_callback; // TcpServer用来从_conns移除自己
public:
// 五个Channel事件回调函数
void HandleRead() {
char buf[65536];
ssize_t ret = _socket.NonBlockRecv(buf, 65535);
if (ret < 0) return ShutdownInLoop();
_in_buffer.WriteAndPush(buf, ret);
if (_in_buffer.ReadAbleSize() > 0) {
// 关键:shared_from_this() 把自己的shared_ptr传给用户回调
return _message_callback(shared_from_this(), &_in_buffer);
}
}
void HandleWrite() {
ssize_t ret = _socket.NonBlockSend(
_out_buffer.ReadPosition(), _out_buffer.ReadAbleSize());
if (ret < 0) {
if (_in_buffer.ReadAbleSize() > 0)
_message_callback(shared_from_this(), &_in_buffer);
return Release();
}
_out_buffer.MoveReadOffset(ret);
if (_out_buffer.ReadAbleSize() == 0) {
_channel.DisableWrite(); // 发完了就关掉写监控,不然一直触发
if (_statu == DISCONNECTING) return Release();
}
}
void HandleClose() {
if (_in_buffer.ReadAbleSize() > 0)
_message_callback(shared_from_this(), &_in_buffer);
return Release();
}
void HandleEvent() {
// 任意事件都刷新活跃度(重置超时定时器)
if (_enable_inactive_release) _loop->TimerRefresh(_conn_id);
if (_event_callback) _event_callback(shared_from_this());
}
// 供用户使用的API:线程安全
void Send(const char *data, size_t len) {
Buffer buf;
buf.WriteAndPush(data, len);
_loop->RunInLoop(std::bind(&Connection::SendInLoop, this, std::move(buf)));
}
void Shutdown() {
_loop->RunInLoop(std::bind(&Connection::ShutdownInLoop, this));
}
void EnableInactiveRelease(int sec) {
_loop->RunInLoop(std::bind(&Connection::EnableInactiveReleaseInLoop, this, sec));
}
// 协议升级:HTTP升级WebSocket之类的可以用
void Upgrade(const Any &context, const ConnectedCallback &conn,
const MessageCallback &msg, const ClosedCallback &closed,
const AnyEventCallback &event);
};
Connection的状态机设计:
DISCONNECTED ──创建──> CONNECTING ──Established()──> CONNECTED
↑ │
│ │ Shutdown()
│ ▼
RELEASE <──Release()─────── DISCONNECTING
CONNECTING:刚创建,fd拿到了,但还没启动读监控CONNECTED:正式建立,可以收发数据DISCONNECTING:用户调用Shutdown,但可能还有数据没发完,等发完再关DISCONNECTED:最终关闭状态
为什么要继承 enable_shared_from_this?
- 回调函数需要传
shared_ptr<Connection>给用户 - 但在类成员函数里,this是裸指针
shared_from_this()可以从this安全地创建出一个shared_ptr,保证引用计数正确- 注意:不能在构造函数里用shared_from_this,那时候对象还没被shared_ptr管理
发送数据的"假写"设计:
- 用户调用Send,不是直接send(),而是:
- 把数据拷贝到局部Buffer buf
- 通过RunInLoop把SendInLoop任务投递给所属EventLoop
- 为什么要拷贝一份?因为用户传的data可能是栈上临时变量,任务执行的时候可能已经释放了
- SendInLoop里:数据写到_out_buffer → EnableWrite
- 真正的send在HandleWrite里:epoll通知可写了才发
- 发完了立刻DisableWrite,不然水平触发的epoll会一直报可写事件,busy loop
4.4 TcpServer:顶层入口,用户唯一接触的类
用户用这个库只需要:
- 创建TcpServer对象
- 设置回调函数(连接建立、收到消息、连接关闭)
- 设置线程数
- 调用Start()
cpp
class TcpServer {
uint64_t _next_id;
int _port;
int _timeout;
bool _enable_inactive_release;
EventLoop _baseloop; // 主线程事件循环:只负责accept
Acceptor _acceptor; // 监听管理
LoopThreadPool _pool; // IO线程池
std::unordered_map<uint64_t, PtrConnection> _conns; // 所有连接
// 用户设置的四个回调
ConnectedCallback _connected_callback;
MessageCallback _message_callback;
ClosedCallback _closed_callback;
AnyEventCallback _event_callback;
// 新连接到来时的处理
void NewConnection(int fd) {
_next_id++;
// 轮询选一个子线程的EventLoop
EventLoop *sub_loop = _pool.NextLoop();
// 创建Connection对象
PtrConnection conn(new Connection(sub_loop, _next_id, fd));
// 设置回调
conn->SetMessageCallback(_message_callback);
conn->SetClosedCallback(_closed_callback);
conn->SetConnectedCallback(_connected_callback);
conn->SetAnyEventCallback(_event_callback);
// 内部回调:关闭时从_conns移除
conn->SetSrvClosedCallback(std::bind(&TcpServer::RemoveConnection, this, _1));
if (_enable_inactive_release)
conn->EnableInactiveRelease(_timeout);
conn->Established(); // 启动读监控,进入CONNECTED状态
_conns.insert(std::make_pair(_next_id, conn));
}
void RemoveConnectionInLoop(const PtrConnection &conn) {
int id = conn->Id();
auto it = _conns.find(id);
if (it != _conns.end()) _conns.erase(it);
// 从map中erase,shared_ptr引用计数减1,没人用就自动析构
}
public:
TcpServer(int port)
: _port(port), _next_id(0), _enable_inactive_release(false),
_acceptor(&_baseloop, port), _pool(&_baseloop) {
_acceptor.SetAcceptCallback(
std::bind(&TcpServer::NewConnection, this, std::placeholders::_1));
_acceptor.Listen();
}
void SetThreadCount(int count) { _pool.SetThreadCount(count); }
void SetConnectedCallback(const ConnectedCallback&cb) { _connected_callback = cb; }
void SetMessageCallback(const MessageCallback&cb) { _message_callback = cb; }
void SetClosedCallback(const ClosedCallback&cb) { _closed_callback = cb; }
void SetAnyEventCallback(const AnyEventCallback&cb) { _event_callback = cb; }
void EnableInactiveRelease(int timeout) { _timeout = timeout; _enable_inactive_release = true; }
void Start() {
_pool.Create(); // 创建IO线程池
_baseloop.Start(); // 主线程进入事件循环
}
};
五、完整工作流程:一次请求的旅行
现在把所有模块串起来,看看一个客户端从连接到发送数据到服务器,发生了什么:
TcpServer server(8080);
server.SetThreadCount(4);
server.SetMessageCallback(OnMessage);
server.Start();
阶段1:服务器启动
- TcpServer构造函数:
- 实例化
_baseloop(主线程EventLoop) - 构造
_acceptor:创建监听socket,绑定8080,监听 - 设置Acceptor回调为TcpServer::NewConnection
- Acceptor::Listen() 给监听fd启动读监控(挂在baseloop)
- 实例化
- Start():
- _pool.Create():创建4个LoopThread,每个线程内部跑一个EventLoop
- _baseloop.Start():主线程进入while循环,epoll_wait等待
阶段2:新连接到来
- 客户端 connect(8080)
- 主线程epoll_wait返回,监听fd读事件就绪
- Acceptor::HandleRead() 调用 → accept()拿到newfd
- 回调 TcpServer::NewConnection(newfd)
- _pool.NextLoop() 轮询选一个子线程的EventLoop(比如子线程2)
- 创建Connection对象,设置好所有回调,包括业务的OnMessage
- conn->Established() → RunInLoop给子线程2投递任务:EnableRead
- eventfd唤醒子线程2的epoll,子线程2执行EnableRead:把conn的fd加到子线程2的epoll
- 新连接正式进入CONNECTED状态,可以收发数据
阶段3:客户端发送数据
- 客户端send数据
- 子线程2的epoll_wait返回,该conn的fd读事件就绪
- Channel::HandleEvent() → Connection::HandleRead()
- NonBlockRecv把数据读到_in_buffer
- 回调用户的 OnMessage(conn, &_in_buffer),业务处理
- HandleEvent刷新定时器:_conn_id的定时任务再延长_timeout秒
阶段4:服务器回复
- 用户业务处理中调用 conn->Send(response)
- Send() → RunInLoop投递SendInLoop任务(如果是当前线程直接执行)
- SendInLoop:数据写入_out_buffer,EnableWrite启动写监控
- 下一次epoll_wait返回写事件 → HandleWrite()
- NonBlockSend把_out_buffer的数据发出去
- 发完了 → DisableWrite,避免busy loop
- 如果是半关闭状态(DISCONNECTING),发完直接Release
阶段5:连接断开/超时
- 如果客户端正常close:
- EPOLLRDHUP/EPOLLHUP事件触发 → HandleClose() → Release()
- 如果客户端长时间没通信:
- 每次事件都TimerRefresh,没事件就不刷新
- 时间轮秒针走到任务所在格子 → shared_ptr释放
- TimerTask析构 → 执行Release任务
- ReleaseInLoop:
- 状态设为DISCONNECTED
- Channel::Remove() 从epoll移除
- Socket::Close() 关闭fd
- 取消定时器
- 调用用户_closed_callback
- 调用_server_closed_callback → TcpServer::RemoveConnection → 从_conns erase
- 最后一个shared_ptr释放,Connection对象析构
六、设计思想总结
6.1 线程模型:One Loop Per Thread
- 主线程只做accept,IO密集型场景下accept不会被业务处理阻塞
- 连接轮询分配给子线程,负载均衡
- 每个线程独立EventLoop,线程之间不共享Channel/Connection,几乎不需要加锁
- 唯一需要加锁的地方:跨线程投递任务队列(非常短的临界区)
6.2 RAII与智能指针
- Socket析构自动close,Channel跟随Connection,Connection用shared_ptr管理
- 生命周期问题是网络编程最头疼的,智能指针 + enable_shared_from_this 解决了"回调时对象已经被释放"的经典bug
6.3 一切皆文件描述符
在Linux世界里,一切皆文件。这个框架把:
- 网络socket
- timerfd(定时器)
- eventfd(跨线程唤醒)
全部封装成Channel,统一在epoll里等待,全部走事件回调。没有特殊情况,没有多余的wait/notify,代码高度统一。
6.4 回调的分层设计
回调不是一层,而是分了两层:
- 底层回调:Channel给Connection的(HandleRead/Write/Close/Error),框架内部设置
- 上层回调:Connection给用户的(MessageCallback/ClosedCallback等),用户设置
- 内部管理回调:Connection给TcpServer的(ServerClosedCallback),框架内部用来从map移除
每一层只关心自己该做的事,边界清晰。