细解muduo中的每个核心类
核心类:
channel:代表一个事件,打包起来,包含fd,event,以及回调函数
EpollPoller:继承抽象类Poller,事件监听器
EventLoop:IO多路复用,事件循环.一个EventLoop有一个EpollPoller:,负责监听多个channel,对于EventLoop中监听到的activeChannel进行回调(需要协调多线程)
在主线程(baseLoop)中,TcpServer通过Acceptor,有一个新用户连接,通过accept函数拿到connfd(通讯fd) ---> 打包TcpConnection设置回调,再把回调设置给channel,channel注册到poller上,poller监听到事件会就会调用channel的回调操作
1.Logger类
就是很简单的定义了日志类
类的成员有级别,
方法有获取唯一实例对象(因为是单例类),设置日志级别以及写日志
值得学习的是,在这里还定义了宏,四种分别根据Log级别定义(INFO, // 普通信息 ERROR, // 错误信息 FATAL, // core信息 DEBUG, // 调试信息)。这样四个宏里面可以自动设置级别,和填入log信息(只需在用宏时填入要填入的信息,就像函数调用样,只不过说这个函数可以帮你自动做些事)
2.Timestamp类
在这个时间戳类中,
成员有
microSecondsSinceEpoch_(核心时间数据),意思是
存储从 Unix 纪元(1970年1月1日 00:00:00 UTC) 开始到当前时刻经过的微秒数。
方法有
Timestamp()
默认构造函数 ,初始化变量,创建表示1970年1月1日零点的时间戳
带参构造函数 explicit Timestamp(int64_t),
从其他时间源转换
静态方法 static Timestamp now(),
获取当前系统时间
格式化方法 std::string toString() const
将时间戳转换为易读字符串
3.Channel类
channel类是“事件通道”,把文件描述符(比如TCP连接fd)和IO事件(比如“可读”、“可写”)以及回调函数整合到一起
成员有
*EventLoop loop_
- 这个Channel属于哪个事件循环(线程)。
- 事件循环对象,负责调用本Channel的回调。
const int fd_
- 代表关心的文件描述符,通常是 socket fd。
- 比如一个客户端连接,fd_就是它的fd。
int events_
- 描述这个fd当前“感兴趣”的IO事件(比如读、写)。
- 会被epoll等IO复用器使用,比如
EPOLLIN
、EPOLLOUT
。
int revents_
- poller返回的、fd刚刚发生的事件。
- 由epoll等IO复用库返回,表示究竟发生了什么事件。
int index_
- 在Poller里的状态记录
- 通常用于标记当前Channel在对应Poller数组里的状态,便于管理。
//感兴趣事件状态信息的描述
index_ 状态含义:
值 常量名 含义 -1 kNew 尚未添加到Poller 1 kAdded 已添加到Poller 2 kDeleted 已从Poller删除(但未移除)
std::weak_ptr tie_
- 用weak_ptr观测一个对象(通常是TcpConnection),这样Channel不会导致资源循环引用和内存泄漏。
- 用于跨线程时保证被观测对象没被提前销毁。
//防止Channel被手动remove后,还在使用Channel,所以进行了一个跨线程的对象的生存状态的鉴定
//shared_ptr和weak_ptr配合使用即可解决只使用shared_ptr的循环引用问题,
//还可以用weak_ptr在多线程里监听它所观察的资源的生存状态
//使用的时候可以把这个弱智能指针提升为强智能指针,提升失败说明观察的资源以及释放掉了
bool tied_
- 标记是否“绑过”对应对象(tie_)来做生存状态检测。
事件类型常量(static const int)
KNoneEvent
:不关心任何事件。KReadEvent
:关心“可读”事件(数据可读)。KWriteEvent
:关心“可写”事件。
回调函数对象定义
using EventCallback = std::function<void()>;
//事件的回调using ReadEventCallback = std::function<void(Timestamp)>;
//只读事件的回调- 这俩类型,分别用于“无参数事件回调”与“带时间戳的读事件回调”(方便统计业务延迟等)。
各事件回调对象
ReadEventCallback readCallback_
EventCallback writeCallback_
EventCallback closeCallback_
EventCallback errorCallback_
- 由业务层配置(注册),事件发生时调用。
方法有
一、构造和析构
*Channel(EventLoop loop, int fd)
- 构造函数:新建一个Channel,指定它归属哪个事件循环、对应哪个fd。
- 一般在 TcpConnection 初始化时创建。
~Channel()
- 析构函数:销毁一个Channel对象
二、设置回调、关注/取消事件
setReadCallback, setWriteCallback, setCloseCallback, setErrorCallback
- 设置对应事件发生时要调用的函数。
- 业务开发者写好回调后,通过这些设置进去。
tie(const std::shared_ptr&)
- 与一个对象(一般是TcpConnection)“绑定”,用weak_ptr监控它的生命周期,防止它还没处理完事件就被销毁。
三、事件注册、取消和状态控制
enableReading, disableReading, enableWriting, disableWriting, disableAll
- 关心/取消关注 某种事件(读/写等),并自动更新底层poller注册。
- 例如,
enableReading()
就是在感兴趣事件里标上“可读”,并通知epoll/poller关注。 - 典型用途:需要关注某个fd上新的事件时调用。
一个EventLoop里有一个poller,要更新EventLoop里的channel得通过poller的方法,比如这里的update()函数,本质上是通过EventLoop调用poller的相应方法epoll_ctl
update()
- 通知底层Poller(epoll等),根据当前events_设置,重新注册fd事件。
- 这是接口对下层epoll_ctl的透明封装。
remove()
- 把这个Channel从poller(IO复用器)里去除,不再监听。
- 典型用途:连接关闭或对象即将销毁时。
四、事件处理核心
handleEvent(Timestamp receiveTime)
- 这个是“事件分发入口”。当fd有读/写/错误等事件发生,poller通知上来的时候调用,由它决定该调用哪种回调。
- 如果tied_为true,先检测下被监控对象还在不在(guard),避免已经销毁;没问题再
handleEventWithGuard
。 - //实际上是用来保证channel对象是否还存活,若存活则调用handleEventWithGuard(Timestamp)
handleEventWithGuard(Timestamp)
真正干活的分发函数。根据revents_内容,依次判断
- 有挂断 -> 调用closeCallback_
- 有错误 -> 调用errorCallback_
- 有数据可读 -> 调用readCallback_
- 可写 -> 调用writeCallback_
业务写的事件逻辑最终都在这些回调里实现,保证线程安全和资源不提前释放。
五、辅助函数(getter/setter)
fd()
- 返回Channel对应的fd。
events()、set_revents()
- 获取当前关心/设置刚发生的事件。
index()、set_index()
- 管理在poller里的索引/状态。
ownerLoop()
- 找到归属的EventLoop(事件循环)、便于跨对象回调。
isNoneEvent()、isWriting()、isReading()
- 查询当前感兴趣/被激活的事件类型。
4.poller抽象类,抽象层
Poller
是“多路IO”复用器的抽象基类,屏蔽不同操作系统IO多路复用实现的差异。
- 常见实现有:
select
、poll
、epoll
,在linux里一般用epoll
。
它的职责:
- **登记:**记录所有想要监听的事件和fd。
- **监听:**高效检测这些fd上面是否有“就绪事件”发生。
- **分发:**把发生事件的fd(和对应的信息)通知上层(即Channel、EventLoop)。
你可以把它理解为:“操作系统级的事件收发转运中心”,负责采集、上报所有网络或定时事件。
二、成员变量(属性)
1. EventLoop* ownerLoop_
- 含义: 这个 Poller 属于哪个事件循环(EventLoop)?
- 作用场景:
- 保证线程安全
- 未来可以通过这个指针找到归属它的EventLoop对象
2. using ChannelMap = std::unordered_map<int, Channel*>;
- 含义: 映射表
- key:文件描述符(fd或sockfd)
- value:对应fd的Channel对象指针
- 变量名:
channels_
- 维护所有被监控的通道(即所有被注册监听的fd及其对应的“管理者”Channel)
- 生活类比:
- 就像“班主任登记表”:每个学生学号(fd)都挂有自己的档案(Channel*)。
- 作用:
- 高效查找、插入、删除Channel
- 检查某个fd是否已被监控
三、成员方法(接口)
1. 构造和析构
- Poller(EventLoop loop)*
- 构造函数,初始化Poller,标记归属的EventLoop
- virtual ~Poller()=default;
- 虚析构,确保多态删除安全,便于扩展(如epoll实现析构时回收资源)
2. 三大核心纯虚函数(必须由子类实现)
a) virtual Timestamp poll(int timeoutMs, ChannelList* activeChannels)=0;
- 作用:
- “等事件发生”主接口。阻塞等待事件发生,返回活跃的 Channel 列表
- 在timeoutMs毫秒内等待所有注册过的fd发生感兴趣的事件,发生了就填充到activeChannels列表,返回本次poll调用的时间(Timestamp)。
- 生活类比:
- 像是“班主任”定时巡视教室,看哪些学生(fd)举手(事件发生),把名单记录(activeChannels)交给上层处理。
- 应用场景:
- EventLoop::loop 主循环里会反复调用它,等待事件。
b) virtual void updateChannel(Channel* channel)=0;
- 作用:
- 把一个Channel对象对应fd感兴趣的事件注册到Poller里(比如添加或变更事件类型)。
- 如果已经加过,可能只是更新事件类型(如从可读变成可写)。
- 场景:
- 新连接接入时注册;业务逻辑变更后需要更新(比如发完包可以暂停监听写事件)。
- 生活类比:
- 有学生(fd+Channel)需要登记新的兴趣爱好(事件类型),更新到名册(Poller/操作系统)。
c) virtual void removeChannel(Channel* channel)=0;
- 作用:
- 从Poller(操作系统的epoll/poll/等)注销一个fd和其所有事件,不再监听这个fd。
- 通常在连接关闭、事件不再关心时调用。
- 场景:
- 连接关闭或对象销毁时调用。
- 生活类比:
- 学生毕业或者转学(fd注销),把他从名册里删了,不再关注。
3. 辅助接口
d) bool hasChannel(Channel* channel) const
- 作用:
- 检查Poller当前是否已登记(监听)了某个fd/Channel。
- 通过查找channels_哈希表,看是否存在。
- 场景:
- 用于断言、调试、容错,避免重复操作。
- 通俗类比:
- “查查名册里有没有XXX这个同学的档案”。
- 实现小建议:
- 原代码此处命名有误,
channel_
应为channels_
。
- 原代码此处命名有误,
e) static Poller* newDefaultPoller(EventLoop* loop);
- 作用:
- 拓展点,工厂函数,创建特定平台的最佳Poller实现(如Linux一般返回epoll实现)。
- 代码里可以只依赖基类,但底层实际用的是“最合适的派生类”。
- 创建平台特定的 Poller 实例
提供统一的接口创建不同实现的 Poller
自动选择最佳 I/O 复用机制(Linux 用 epoll,macOS 用 kqueue)
隐藏具体实现细节
- 通俗说法:
- “老板你说’我要一个能干活的Poller’,系统自动根据环境选给你最优的。”
5.EPollPoller类
一、类的整体定位
EPollPoller
是Poller
的具体实现(派生类)。- 核心任务:利用
epoll
接口高效监听许多文件描述符(socket等)是否有事件(读、写、异常等)产生。 - 适用场景:高性能网络服务器,支持大量连接。
二、成员变量(属性)
1. int epollfd_;
- 含义: epoll实例的文件描述符。
- 作用: 通过这个fd调用
epoll_ctl
和epoll_wait
进行事件管理; - 创建方式: 在构造时,调用
::epoll_create1(EPOLL_CLOEXEC)
创建。
2. EventList events_;
- 定义:
using
EventList=std::vector<epoll_event>;
,即定义一个vector<epoll_event>
类型的成员。 - 作用: 存储
epoll_wait()
返回的“就绪事件数组”。 - 特点: 初始容量为
kInitEventListSize=16
,会根据需要动态扩大。
总结:
epollfd_
管理epoll队列,events_
数组存放等待通知的事件,是缓冲区。
想象 epollfd 就像一家快递公司的总控制台:
通过它可以管理所有快递柜(socket)
可以监控哪些柜子有新快递(可读事件)
可以监控哪些柜子有空位(可写事件)
三、常量定义(全局静态常量)
kInitEventListSize=16
:初始监听数组大小(初始缓冲区大小)。kNew=-1
:表示Channel还未加入到Poller(未注册)。kAdded=1
:表示Channel已注册到epoll中(已监听)。kDeleted=2
:表示Channel曾被删除(不再监听)。
这些常量用来管理Channel的状态(对应index_
成员)。
四、关键成员函数(方法)
1. 构造函数:EPollPoller(EventLoop *loop);
- 作用: 初始化epoll实例和事件数组。
- 重点:
epollfd_
用::epoll_create1()
创建;events_
初始化容量;- 若创建失败,使用
LOG_FATAL
打印错误。
2. 析构函数:~EPollPoller()
- 作用: 关闭epoll文件描述符,释放系统资源。
3. 核心工作:poll
Timestamp poll(int timeoutMs, ChannelList *activeChannels);
产生作用: 等待事件发生。
流程:
- 调用
::epoll_wait()
,等待最多timeoutMs
毫秒。 - 将就绪的事件存入
events_
数组。 - 根据事件数,调用
fillActiveChannels()
把事件关联的Channel
指针装入activeChannels
。 - 若事件数等于数组容量,动态扩容,提高效率。
- 遇到错误:保存errno,打印日志。
- 调用
返回值:
- 当前时间戳(
Timestamp
对象),标志事件的时间点。
- 当前时间戳(
4. 注册/修改/删除:updateChannel,用于更新channel状态,调用底层update
updateChannel
决定 "需要做什么"update
负责 "具体怎么做"方法名 作用 调用场景 主要职责 updateChannel()
高层操作:根据Channel状态,决定注册、变更或删除epoll监控 由 EventLoop
或其他代码调用,管理Channel的注册状态判断Channel当前状态,调用 update()
完成具体的epoll_ctl
操作update()
低层操作:封装 epoll_ctl()
系统调用updateChannel()
内部调用,用于实际执行注册/修改/删除操作根据参数设置 epoll_event
结构体,调用epoll_ctl()
执行操作
void updateChannel(Channel *channel);
作用:
- 根据Channel状态,调用
epoll_ctl()
将Channel对应fd注册、变更或删除监听。
- 根据Channel状态,调用
细节流程:
- 判断Channel的权限状态(index_)
- 新增(kNew)或已删除(kDeleted)
- 调用
epoll_ctl(ADD)
注册; - 以
channels_[fd]=channel
存入映射; - 改变Channel状态为
kAdded
。
- 调用
- 已注册(kAdded):
- 如果没有感兴趣的事件(
isNoneEvent()
),调用epoll_ctl(DEL)
注销; - 如果有事件,调用
epoll_ctl(MOD)
变更。
- 如果没有感兴趣的事件(
- 新增(kNew)或已删除(kDeleted)
- 调用底层
update()
完成epoll_ctl()
实际操作。
- 判断Channel的权限状态(index_)
5. 删除Channel:removeChannel
void removeChannel(Channel* channel);
- 作用:
- 从
channels_
哈希表中删除。 - 若在监听(kAdded),调用
epoll_ctl(DEL)
取消。 - 将Channel状态置为
kNew
。
- 从
6. 填充活跃通道:fillActiveChannels
void fillActiveChannels(int numEvents, ChannelList* activeChannels) const;
作用:
- 遍历
epoll_event
数组。 - 通过
data.ptr
找到对应的Channel
,设置事件类型revents
。 - 把
Channel*
加入到activeChannels
,告诉EventLoop
哪些通道有事件。
- 遍历
注意:
epoll_event.data.ptr
存的就是Channel*
,这是关键数据。
7. 底层epoll_ctl
封装:update
void update(int operation, Channel *channel);
- 作用:
- 根据
operation
(EPOLL_CTL_ADD
、EPOLL_CTL_MOD
、EPOLL_CTL_DEL
)调用::epoll_ctl()
。 - 设置
epoll_event
的感兴趣事件和指向Channel的指针。 - 失败时,输出日志。
- 根据
五、总结
成员/方法 | 作用 | 关键点/细节 |
---|---|---|
epollfd_ | epoll实例的文件描述符 | 初始化调用epoll_create1() |
events_ | 存放就绪事件数组 | 初始容量16,动态增长 |
构造函数 | 创建epoll对象,初始化events_ | 若创建失败,输出严重错误 |
poll() | 等待事件,获取就绪的Channel列表 | 调用epoll_wait() ,扩容、日志、时间戳返回 |
updateChannel() | 增删改Channel对应的fd在epoll中的状态 | 依据index_ 值,决定注册、变更或删除 |
removeChannel() | 移除Channel,注销epoll上的监听 | 从映射中删除,操作epoll_ctl(DEL) |
fillActiveChannels() | 把发生事件的Channel放入活动队列 | 枚举epoll_event 数组,提取Channel* |
update() | 调用epoll_ctl() 封装底层epoll操作 | 设置事件类型,处理错误 |
6.DefaultPoller类
这里提供的代码片段其实是一个工厂函数(newDefaultPoller()
),用于根据环境变量决定创建哪种Poller
实例(使用poll
还是epoll
)。
你这个函数实现的逻辑是:
- 如果环境变量
"MUDUO_USE_POLL"
被设置(存在环境变量),- 就不创建
EPollPoller
,可能会使用poll
技术实现的Poller(暂未在这个代码里显示,但逻辑上应存在)
- 就不创建
- 否则,
- 创建
EPollPoller
(基于epoll
的高效事件轮询机制)
- 创建
这实际上是多态工厂的一种应用:根据配置选择不同的事件通知机制(poll或epoll)。
Poller
很可能是一个抽象基类(接口类),规定了一组虚函数来管理“事件监听”,比如注册、修改、删除事件的操作。EPollPoller
是继承自Poller
的具体实现,专门利用epoll
系统调用实现这些操作。- 其他可能存在的子类(不是在你的代码片段中)还可能有
PollPoller
(用poll()
实现)等等。
7.EventLoop类
EventLoop
类是一个网络多路复用的“核心调度器”,它负责管理事件监听、调度回调和协调多线程操作。
一、EventLoop
的功能概述
- 事件检测:通过
Poller
检测文件描述符(如:socket)是否有事件发生(读写、连接等) - 事件处理:调用对应
Channel
的回调函数处理事件 - 线程安全调度:支持在不同线程中安全地加入任务(回调)
- 跨线程通知:使用
wakeup
机制(如eventfd
)唤醒事件循环,处理新加入的任务 - 管理生命周期:启动、退出事件循环
二、成员变量详解(通俗版)
1. 控制变量
std::atomic_bool looping_
:这个EventLoop
是否在运行(循环在跑)std::atomic_bool quit_
:是否要退出循环(关闭事件循环)
2. 线程标识
const pid_t threadId_
:标记这是在哪个线程创建的EventLoop
,保证每个线程只能有一个EventLoop
3. 时间信息
Timestamp pollReturnTime_
:上一次poll()
调用返回的时间点,用来判断事件发生时间- (类里的其他时间相关变量)
4. 事件轮询机制
std::unique_ptr<Poller> poller_
:封装poll
/epoll
等检测事件的对象(接口抽象)ChannelList activeChannels_
:刚检测到事件的通道(socket连接等)
5. 跨线程唤醒
int wakeupFd_
:用来唤醒事件循环的文件描述符(通常是eventfd
)std::unique_ptr<Channel> wakeupChannel_
:监听wakeupFd_
的Channel
6. 任务队列
std::vector<Functor> pendingFunctors_
:待执行的任务(回调函数)队列std::mutex mutex_
:保护pendingFunctors_
的锁std::atomic_bool callingPendingFunctors_
:在执行回调时,避免并发导致重复调用
三、成员方法详解(通俗版)
生命周期管理
EventLoop()
:构造函数,初始化所有东西,并绑定当前线程~EventLoop()
:析构函数,清理资源(关闭fd、移除channel)
核心控制(运行/退出)
void loop()
:启动事件轮询和处理void quit()
:请求退出事件循环
事件检测与处理
void handleRead()
:wakeupFd_
读事件的处理函数(被唤醒时调用)Timestamp pollReturnTime() const
:获取上次检测到事件的时间
任务调度(跨线程执行回调)
void runInLoop(Functor cb)
:在当前线程立即执行回调(如果在自己线程)void queueInLoop(Functor cb)
:把回调放到队列,异步在自己线程执行void doPendingFunctors()
:执行队列中的任务(在循环中调用)bool isInLoopThread() const
:判断当前是否在自己创建的EventLoop
所在的线程
通信机制
void wakeup()
:用eventfd
通知EventLoop
有新任务(唤醒事件循环)
Channel管理
void updateChannel(Channel *channel)
:注册或修改某个通道的事件void removeChannel(Channel *channel)
:取消某个通道bool hasChannel(Channel *channel)
:检测是否管理某个通道
四、工作流程简要(通俗版)
- 创建
EventLoop
实例时,初始化通知唤醒机制、Poller
,确保每个线程只有一个EventLoop
。 - 调用
loop()
,开始无限循环等待事件:- 使用
poller_
检测文件描述符是否有事件 - 如果有,调用对应的
Channel
处理 - 还会处理
pendingFunctors_
队列中的任务(来自其他线程)
- 使用
- 跨线程调度任务:
- 其他线程调用
queueInLoop()
,加入任务队列 - 通过
wakeup()
唤醒自己,及时执行新加入的任务
- 其他线程调用
- 退出循环:
- 调用
quit()
,设置退出标志,下一轮检测到退出,安全退出。
- 调用
五、总结(简洁版)
成员类型 | 作用 |
---|---|
looping_ | 控制是否在运行的标志 |
quit_ | 控制是否退出的标志 |
threadId_ | 创建时的线程ID,保证每个线程只有一个EventLoop |
poller_ | 事件检测(poll/epoll)封装对象 |
activeChannels_ | 发生事件后,存放激活中的Channel 列表 |
wakeupFd_ | 用于跨线程通知EventLoop 有新任务 |
wakeupChannel_ | 监听wakeupFd_ ,当有通知时会触发回调 |
pendingFunctors_ | 存放待执行任务的队列 |
mutex_ | 保护pendingFunctors_ 的线程安全,确保多线程安全操作 |
8.thread类
Thread 类是 muduo 网络库中线程管理的核心组件,它封装了线程的生命周期管理、线程标识和线程函数执行。
Thread
类是对C++标准库线程std::thread
的封装,让多线程编程更易用、易管理、更安全。
- 它可以方便地在C++程序中启动一个新的线程来运行一个函数。
- 并且可以给线程命名,拿到线程号,管理线程的生命周期(启动/等待结束)。
- 防止错误复制(通过继承
noncopyable
禁止拷贝)。
二、成员变量详解
成员变量 | 含义与作用 | 通俗解释 |
---|---|---|
ThreadFunc func_ | 线程要执行的函数 | 你在线程中想执行的代码(比如:服务器主循环、任务逻辑等) |
std::shared_ptr<std::thread> thread_ | 线程对象指针 | 底层真实的一条C++线程 |
bool started_ | 已启动标记 | 线程是否启用过(是否start过) |
bool joined_ | 已回收标记 | 线程是否已经join(回收)过 |
pid_t tid_ | 线程ID | 子线程自己的ID号(系统分配的),便于调试 |
std::string name_ | 线程名字 | 给线程起的名字(方便排查、多线程调试日志时看得懂) |
static std::atomic_int numCreated_ | 已创建的线程数 | 全局的线程计数器,每创建一个累加一次 |
三、核心成员方法详细解释
1. 构造与析构
Thread(ThreadFunc func, const std::string &name)
- 构造函数,传入线程要执行的函数,以及线程名称(可选)。
- 只做“准备”,不会马上启动线程。
- 会给
name_
设一个默认名字(比如Thread1
,Thread2
等),如果没给的话。
~Thread()
- 析构函数,如果线程已start但没join,会把此线程设成“分离”线程(daemon/守护线程,主线程结束它也跟着结束),防止资源泄漏。
2. 线程控制
void start()
- 真正启动线程(只会启动一次)。
- 内部用lambda表达式,将线程主函数包装进一个
std::thread
并启动。 - 新线程会保存自己的线程id(tid_),并通过
sem_t
信号量同步回主线程,确保主线程可以拿到此id。 - 线程启动后会立即执行你传进来的
func_
任务。
void join()
- 等待线程运行结束,相当于“回收线程”。
- 防止主线程太快退出导致子线程没做完。
3. 线程信息&工具方法
bool started() const
- 当前线程是否已启动过。
pid_t tid() const
- 返回线程id。
const std::string& name() const
- 返回线程名字。
static std::atomic_int32 numCreated_()
- 返回创建过的线程总数。
void setDefaultName()
- 若name_为空的话,自动生成一个如“Thread1”名字。
四、Thread类的使用举例(通俗表述)
1. 如何用
void myTask() {// do something
}Thread t(myTask, "worker1");
t.start(); // 开启新线程,执行myTask
t.join(); // 等待myTask执行完毕
2. 你可以得到什么?
- 每个Thread能执行你给他的函数
- 每个Thread会被命名,log和调试很友好
- Thread对象管理资源,用起来很安全,不会泄漏
9.EventLoopThread
一、类的设计理念
EventLoopThread 的作用:
- 用于在单独的线程中创建和管理一个独立的 EventLoop(事件循环)对象。
- 典型用途是网络编程中“一个IO线程管理一个事件循环”(one loop per thread)的模式,解耦多线程下的事件驱动机制,让每个线程专心干自己的事。
打个通俗的比方:
EventLoopThread 就好像是“线程+事件循环”的捆绑套餐,一旦启动就创建一个线程,线程内专心跑一个事件循环。
二、成员变量剖析
成员 | 类型 | 作用/通俗解释 |
---|---|---|
EventLoop* loop_ | 指针,指向线程内的EventLoop对象 | 指向自己线程里那个“主循环”的地址;主线程可通过它和该子线程的事件循环通信 |
bool exiting_ | 布尔值 | 标记是否要退出(析构清理用) |
Thread thread_ | Thread类对象 | 真正的工作线程(管理线程的启动与停止) |
std::mutex mutex_ | 互斥锁 | 用于多线程安全;主线程和子线程间同步数据用 |
std::condition_variable cond_ | 条件变量 | 配合mutex,让主线程等子线程做好准备(如eventloop创建好) |
ThreadInitCallback callback_ | 回调函数类型 | 可选:线程初始化时可执行自定义操作(如设置优先级、绑定资源等),函数签名是void(EventLoop*) |
三、构造与析构
1. 构造函数
EventLoopThread(const ThreadInitCallback &cb = ThreadInitCallback(), const std::string &name = std::string());
- 可以传一个初始化回调(
cb
),和线程名(name
)。 - 内部会把线程主函数绑定为自己类的
threadFunc
这个成员函数。 - 此时只是准备“套餐”,线程和EventLoop都还没真正启动。
2. 析构函数
~EventLoopThread();
- 如果事件循环存在,调用它的
quit()
让loop干净退出。 - 并确保线程通过join被正确“回收”,防资源泄漏。
四、核心方法详解
1. EventLoop* startLoop()
作用:
- 启动专属线程,并在其中创建并运行一个新的EventLoop。
- 整个过程主线程会一直等待,直到子线程的loop对象创建好并赋给
loop_
,保证线程和loop都准备好了之后才返回。
流程简述:
thread_.start()
启动线程(threadFunc开始运行)。- 主线程用 mutex+cond_ 一直等,直到
loop_
被子线程线程内正确赋值。 - 返回loop指针(主线程可通过它和事件循环通信)。
通俗解释:
就像启动一个外包小队(新线程),你(主线程)必须等小队长(loop对象)向你报平安(cond_),你才用它去接任务。
2. void threadFunc()
作用:
- 真正线程里要做的事,全在这里!
主要流程:
- 在线程内栈上创建一个独立的
EventLoop
对象。 - 如果有初始化回调(
callback_
),就拿EventLoop*
给它调用一下,便于用户做些特殊初始化。 - 把
loop_
赋值为新建的loop地址,唤醒主线程(通知cond_条件变量)。 - 进入事件循环:
loop.loop()
,开始处理IO和事件。 - loop退出后,清空
loop_
指针(生命周期彻底结束)。
通俗解释:
threadFunc就像让新员工(新线程)“入职”:布置好办公桌(创建EventLoop)、调用上岗前培训(callback_)、汇报工号(loop_赋值+通知),然后开始正式上班(loop.loop),下班再交接走人。
五、整体使用场景说明
举例,如果你想给每个网络连接分配一个专用线程和事件循环,可以创建多组 EventLoopThread(比如 N 个 IO线程池),让每个 EventLoopThread 独立管理自己的 loop,轻松做到多线程高并发处理。
EventLoopThread *worker = new EventLoopThread();
EventLoop *loop = worker->startLoop();
// 现在 loop 已经在专门线程里跑起来了,你可以通过 loop 在线程间传递任务了。
六、总结优势
- 隔离线程和事件循环:每个EventLoop专属一个线程,不串台,线程安全。
- 流程清晰:主线程等子线程准备完毕再进行后续操作,满足并发语境下严格的同步要求。
- 扩展友好:支持初始化回调,易于做定制扩展。
- 用起来简单:整体接口高度抽象,开发者仅需关心入口输出,线程和事件循环的细节自动管理。
10.EventLoopThreadPool类
一、类的设计定位
EventLoopThreadPool 类的主要作用是:
- 用于网络编程中,自动化管理多个事件循环(
EventLoop
)和线程,提升服务器并发能力。 - 让你“一下子”轻松拥有多个 I/O 线程,每个线程管理一个 loop,能轮询分配新连接或任务。
可以直观比喻它:像前台小经理,统筹安排多个工位(线程)和员工(loop)轮流上岗。
二、成员变量详解
变量名 | 类型 | 作用 | 通俗解释 |
---|---|---|---|
baseLoop_ | EventLoop* | 主 loop,通常属于主线程,用于没有子线程时的工作 | “自己亲自干活”时用的 loop |
name_ | std::string | 线程池名字,调试或日志有用 | 给整个线程池起个好听的名 |
started_ | bool | 标记线程池是否已经启动 | 线程池开张了吗? |
numThreads_ | int | 池内有几个 EventLoopThread | 我手下有几位员工? |
next_ | int | 记录下次该把任务给哪个 loop | 排班轮到谁了? |
threads_ | std::vector<std::unique_ptr<EventLoopThread>> | 保存每一个线程与其管理对象 | 手下所有 IO 员工 threading 的指针集合 |
loops_ | std::vector<EventLoop*> | 保存每一个线程里的 EventLoop 指针 | 每个员工的工位(事件循环)集合 |
三、构造与析构
构造函数
EventLoopThreadPool(EventLoop *baseLoop, const std::string &nameArg)
- 由外部传入主线程的
EventLoop
对象指针和线程池名字。 - 初始并不创建线程或 loop,仅记录参数。
析构函数
~EventLoopThreadPool()
- 线程池析构时,智能指针
unique_ptr
会自动释放每个 EventLoopThread。线程和loop都能被安全自动清理。
四、主要方法逐个解释
1. void setThreadNum(int numThreads)
设置线程数(工位数)。
- 在
start
之前调用。 - 例如传 4 就表示准备起 4 个专属线程(和 loop)。
2. void start(const ThreadInitCallback &cb = ThreadInitCallback())
正式启动线程池,创建所有线程和它们的 EventLoop。
底层流程:
- 启动标记:
started_ = true
,以后不再重复启动。 - 循环 n 次:为每名工人(线程)分配:
- 一个 EventLoopThread 对象
- (用 unique_ptr 管理,自动释放)
- 调用它的
startLoop()
方法,实际启动线程+创建 loop,并保存 loop 指针。
- 特殊情况:如果线程数是0,只用主线程(
baseLoop_
)处理所有事件。这时如果传了回调,主线程的 loop 也做必要初始化。
通俗解释:
老板说“大家开始上班!”,于是经理把所有工位打开,每个员工(线程+loop)都 ready 了。如果人力不足(线程数0),老板亲自上!
3. EventLoop* getNextLoop()
作用:轮询分配下一个 loop。
底层流程:
- 若没有线程池(loops为空),一直返回主 loop(单线程)。
- 有子线程时,采用“轮流值班”,把事件分配给队列中的下一个 loop。
通俗解释:
前台经理安排客户,每来一个就交给下一个工位,公平轮换(负载均衡)。
4. std::vector<EventLoop*> getAllLoops()
返回所有(活着的)loop指针。
- 如果没有子线程,则只返回主线程的 base loop;
- 有子线程,则返回所有子线程的 loop 指针数组。
通俗解释:
想批量通知所有员工或广播任务,就找经理(getAllLoops)问一声,他能告诉你每个人的位置。
5. 其他接口
bool started() const
:线程池是否已运行。const std::string name() const
:返回线程池名字。
五、典型使用场景与好处
- 适合高并发 server:每个连接或任务,可以均衡轮询分发到各个独立 thread+loop(不会“挤死”某一线程)。
- 方便多线程通信:有统一的接口分配和回收 worker loop。
- 支持回调自定义初始化:后期扩展时可以灵活调整 loop/thread 初始化细节。
六、例子通俗比喻梳理
假设你要开一个烧烤摊,平时只有你一个人(baseLoop_),忙不过来时就招了几个小工(EventLoopThread)。顾客(channel/任务)来了,你轮流安排各个小工接待,大家互不影响,非常高效!
七、可能的扩展说明
- 线程数支持动态设置(
setThreadNum
)。 - 可以提供更多定制的线程初始化操作(
start
的回调)。 - 这个池子适合做 TCP server、网络代理等高并发服务的 event-loop 后台。
八、总结
EventLoopThreadPool 是线程+事件循环“打包发”的智能调度中心。
- 不用手动管理每个线程和loop。
- 能自动做并发负载均衡,节约开发精力。
- 提供取下一个“工位”、取全部工位的工具接口。
11.CurrentThread类
一、整体概览
这个CurrentThread
模块的主要目的是:
- 快速获取当前线程的ID(tid)
- 避免每次都调用系统级的
syscall
,提升效率(通过缓存)
简而言之,就是帮你“偷懒”,每次用到线程ID时,一次性“记下来”,后续再用就不用调用啥系统级别的繁琐操作。
二、成员和方法详细分析
1. __thread int t_cachedTid
- 类型:
__thread int
- 作用:存储当前线程的ID(
tid
),这是一个线程本地存储(thread-local storage),每个线程都拥有自己的副本。 - 通俗比喻:想象每个工人在不同工厂(线程里)都拥有一个“记事本”,写上自己ID,避免别的工厂记错。
2. void cacheTid()
- 作用:给当前线程的
t_cachedTid
赋值,记录这个线程的真实ID。 - 实现方式:调用 Linux 系统调用
syscall(SYS_gettid)
获取当前线程的唯一ID。
通俗理解:
- 这就像给每个工人(线程)“拍照”记录他们的身份证(ID),以后不用再照相。
3. inline int tid()
- 作用:快速返回当前线程的ID (
tid
)。 - 实现流程:
- 首先检查
t_cachedTid
是否为0(还没获取过)。 - 如果为0,则调用
cacheTid()
,执行“拍照”动作,存下来。 - 最后返回
t_cachedTid
。
- 首先检查
为什么要这样设计?
- 调用系统
syscall
获取ID比较慢(因为陷入内核态)。 - 通过在第一次调用的时候缓存(拍照)下来,以后都直接用
缓存
的值,提高效率。
特殊点:
__builtin_expect()
是一个 GCC 内置函数,用于优化预测(告诉编译器:t_cachedTid=0
的概率很低),以优化分支预测,提升运行速度。
通俗理解:
- 就像第一次你去银行取ID(花时间),之后每次用ID只需要看记事本(缓存)就行了。
三、代码结构
你的代码实际上分成两个文件,但都属于命名空间CurrentThread
,内容如下:
1. 头文件 (声明部分,带namespace
)
复制代码
namespace CurrentThread
{extern __thread int t_cachedTid; // 声明void cacheTid(); // 声明inline int tid(); // 声明
}
2. 源文件(定义部分)
// 定义静态线程局部变量
__thread int CurrentThread::t_cachedTid = 0;// 定义缓存函数
void CurrentThread::cacheTid()
{if(t_cachedTid == 0){t_cachedTid = static_cast<pid_t>(::syscall(SYS_gettid));}
}
注意:extern
声明是为了让其他文件引用这个__thread
变量。
四、详细总结:它的“成员”和“方法”都在干啥?
组件 | 类型 | 作用 | 通俗比喻 |
---|---|---|---|
__thread int t_cachedTid | 线程局部变量 | 用于存放当前线程的ID,确保每个线程自己的“身份证”独立存放 | 每个工厂有自己的“身份证本子” |
void cacheTid() | 函数 | 用系统调用获取当前线程的ID,存到t_cachedTid 里 | 让工人“拍照”并存下身份证号码 |
inline int tid() | 内联函数 | 快速返回当前线程ID,如果没拍照就调用cacheTid() 拍照存下来 | 工人用“身份证本子”查身份证号码,没有就“让工人拍照” |
五、它的作用总结
- 效率:第一次调用
tid()
时会很慢(因为调用系统调用),但之后就直接用缓存,速度快很多。- 简单方便:你不用每次都去操作复杂的系统调用,只需调用
CurrentThread::tid()
即可直接拿到线程ID。- 适用场景:多线程程序中,需要知道当前在哪个线程执行(比如日志输出带线程ID,或调试追踪)。
六、建议与扩展
- 这个设计很经典,常用于网络框架、调试信息、日志系统中。
- 你也可以扩展,加入
thread_name()
等信息,丰富调试信息。
12.Socket类
这个Socket
类是个封装网络编程中socket
文件描述符(fd)的工具类,旨在简化和管理TCP连接相关的操作。让我们逐一介绍它的成员变量和成员函数,力求清楚、详细、通俗易懂。
一、成员变量
private:const int sockfd_;
sockfd_
:这是Socket
类中的核心成员,是封装的“套接字文件描述符”。- 作用:标识一个具体的网络连接或监听端口的数字编号(由系统分配,类似“房间编号”)。
const
修饰:表示一旦创建,不能更改,确保每个Socket
对象对应一个唯一的fd。
二、构造函数
explicit Socket(int sockfd): sockfd_(sockfd)
{}
- 作用:创建一个
Socket
实例,绑定给定的socket fd。 - 参数:
sockfd
:已创建的socket(比如socket()
调用返回的fd或accept()
返回的连接fd)。
explicit
:避免隐式类型转换,保证传入参数明确。
三、析构函数
~Socket();
- 作用:当
Socket
对象销毁时,自动关闭对应的socket资源。
void Socket::~Socket()
{close(sockfd_);
}
- 说明:调用系统API
close()
关闭fd,释放资源。
四、核心成员方法(操作socket)
1. bindAddress
void bindAddress(const InetAddress &localaddr);
- 作用:将这个socket绑定到某个特定的IP地址和端口(设为监听端口)。
- 具体操作:调用系统
bind()
。 - 参数:
localaddr
:封装的网络地址(IP、端口)。
2. listen
void listen();
- 作用:将socket设置为监听状态,等待远端连接请求。
- 调用系统API:
listen()
,参数1024表示允许的连接队列最大长度。 - 注意:在调用
listen()
之前要确保socket已经绑定。
3. accept
int accept(InetAddress *peeraddr);
- 作用:接受客户端的连接请求,返回连接的socket fd(新连接文件描述符)。
- 两个关键点:
- 它会阻塞直到有连接到来(除非设置为非阻塞)。
- 如果成功,会把客户端的地址信息(IP、端口)存入
peeraddr
。
- 参数:
peeraddr
:传入一个指针,调用成功会填充远端客户的地址信息。
- 特殊点:
- 使用
accept4()
,结合标志SOCK_NONBLOCK | SOCK_CLOEXEC
:SOCK_NONBLOCK
:让新连接的fd是非阻塞的,避免阻塞调用。SOCK_CLOEXEC
:确保在后续exec()
调用时自动关闭这个fd。
- 使用
- 返回值:新连接的socket fd,如果出错返回
-1
。
4. shutdownWrite
void shutdownWrite();
- 作用:优雅地关闭连接的写端。
- 调用系统API:
shutdown(sockfd_, SHUT_WR)
。 - 作用:告诉对端“我不再发数据了,你可以读完”。
- 出错处理:如果失败,用
LOG_ERROR
记录。
5. 设置socket的各种属性(用于调优和优化)
setTcpNoDelay(bool on)
禁用Nagle算法(关掉
TCP_DELAY
),减少延迟,适合实时通信。setReuseAddr(bool on)
允许快速重用地址,避免端口占用问题。
setReusePort(bool on)
允许多个socket绑定到同一个端口(多核负载均衡场景常用)。
setKeepAlive(bool on)
设置保持连接的检测机制,避免死连接。
五、总结:这个Socket
类的作用和用途
- 封装底层操作:把底层的
socket()
、bind()
、listen()
、accept()
、shutdown()
和setsockopt()
封装成易用的成员函数。 - 使用简易:调用者只需创建
Socket
对象,然后调用相关成员函数,不必重复写繁琐的系统调用,减少出错。 - 资源管理:在对象销毁时自动关闭fd,保证资源不泄露。
- 配置参数:提供方便的方法,支持设置TCP参数(如禁用Nagle算法、开启地址重用等)。
贴心总结
成员/方法 | 作用 | 简单理解 |
---|---|---|
sockfd_ | 管理的socket编号(文件描述符) | “我的房间号” |
构造函数 | 初始化Socket 对象,将已有socket fd绑定到对象上 | “进入房间” |
~Socket() | 析构函数,关闭socket资源 | “离开房间” |
bindAddress() | 给socket绑定IP和端口 | “在房子门口挂牌” |
listen() | 设置socket为监听模式 | “开门迎客” |
accept() | 接受一个客户端连接,返回新连接的fd和客户端地址 | “迎接客人” |
shutdownWrite() | 关闭连接的写端,结束发送数据 | “说完话,不发了” |
其他set...() | 设置一些网络参数,优化连接性能或行为 | “调教网络” |
13. Acceptor 类
一、总体作用
Acceptor 的职责就是监听(listen)端口,一旦有新客户端“敲门”(即有新连接请求),它负责接受这个连接,并把这个新连接交给后续的处理(比如,分配给工作线程)。
它解耦了“侦听新连接”和“业务处理/读写”的职责,让整个TCP服务器结构更清晰,也支持高性能的事件驱动(Reactor)模型。
二、成员变量详细解释
1. loop_
EventLoop *loop_;
- 指向“主事件循环”(mainLoop),主线程用来管理监听socket和接收新连接事件。
- 每个 Acceptor 只在一个 EventLoop(主循环)中活跃。
2. acceptSocket_
Socket acceptSocket_;
- 用来监听的 socket(服务端的 listenfd)。
- 专门用于接受新客户端连接。
3. acceptChannel_
Channel acceptChannel_;
- 事件通道对象,把 acceptSocket_ 的“读事件”注册到 EventLoop。
- 当有新连接到来,这个 Channel 会捕捉到事件,从而调用回调函数。
4. newConnectionCallback_
NewConnectionCallback newConnectionCallback_;
- 新连接到来时要执行的回调(functor):
void(int sockfd, const InetAddress&)
- 通常,这个回调函数会把新连接(包括fd和客户端地址)交给 TcpServer/线程池 的具体某个“子Loop”继续处理。
5. listenning_
bool listenning_;
- 表示当前是否处于监听状态(即 server 是否已主动“开门迎客”)。
三、成员方法详细解释
1. 构造函数
Acceptor(EventLoop *loop, const InetAddress &listenAddr, bool reuseport);
- 主要做了以下几件事:
- 创建一个非阻塞的 listen socket。
- 设置 socket 选项(地址复用、端口复用)。
- 绑定地址 listenAddr。
- 创建 Channel,绑定到主 EventLoop。
- 给 Channel 设置“读事件回调”——即有新连接时调用 handleRead。
- 通俗理解:像物业经理一样,拿到自己的大门(socket),贴好门牌号(bind),告知保安(Channel)哪怕半夜有人敲门都通知自己。
2. 析构函数
~Acceptor();
- 资源清理,关闭 Channel 对事件的监听并从 EventLoop 解除注册。
3. setNewConnectionCallback
void setNewConnectionCallback(const NewConnectionCallback &cb);
- 设置新连接到来时的响应函数(回调)。
- 外部(比如 TcpServer)可将自己的处理逻辑交给 Acceptor。
4. listenning
bool listenning() const;
- 返回当前 Acceptor 是否处于“监听”状态,可用于状态判断。
5. listen
void listen();
- 真正让 socket 进入监听状态,告诉内核开始处理客户端连接。
- 同时让 Channel 检查“读事件”,即有人连上就立刻捕捉到并回调。
6. handleRead(核心!)
void handleRead();
- 职责:
- 有新连接到来时,被 EventLoop/Channel 调用。
- 调 accept,获得新连接的 fd 及对方地址。
- 如果设置了 newConnectionCallback_,就把新连接交给注册方处理。
- 若回调未设置,则立即关闭这个新fd,避免泄露。
- Resilience:
- 错误处理(比如 EMFILE:文件描述符已满)。
- LOG 错误信息。
7. createNonblocking(静态函数)
static int createNonblocking();
- 工厂方法,辅助生成一个“非阻塞+close on exec”的 TCP socket fd。
四、通俗理解——为什么要这样设计?
- 分工明确: Acceptor 只负责“开门、验票”,新客人的后续处理(比如对话、聊天)由其他模块负责。
- 事件驱动: Channel 负责监听事件,EventLoop 负责事件循环,Acceptor 只实现回调。
- 高性能:使用非阻塞fd,配合Reactor模式,可以单线程高效管理海量连接。
- 能扩展:后续可以配合线程池,把新连接均衡分配给多个Worker线程处理。
总结表格
成员/方法 | 作用/用途 | 通俗解释 |
---|---|---|
loop_ | 指哪一个主事件循环用来监听“门铃” | 谁来守大门 |
acceptSocket_ | 封装的服务端监听 socket(listenfd) | 大门(监听入口) |
acceptChannel_ | 事件通道,捕捉新连接事件并分发 | “门铃”与安保 |
newConnectionCallback_ | 有新连接就调用此回调 | “有客人了,你来接待吧” |
listenning_ | 当前是否“开门迎客” | 显示“营业中” |
Acceptor(), ~Acceptor() | 构造/析构函数,初始化/资源回收 | 房子装修/拆除 |
setNewConnectionCallback() | 设置“新客人”到来时的接待处理方式 | 用户自定义“怎么接待客人” |
listen() | 进入正式“营业”状态,开始监听 | 掀开帘子,开门迎客 |
handleRead() | 有新连接自动处理(收下新fd/分发回调/关门拒接等) | 门铃一响立刻开门 |
总结一句话:
Acceptor 就像TCP服务器中的前台/门卫,它只负责等人敲门,一旦有新客户,它就招呼对方进门,并把客人交给内部工作人员继续服务!
14.TcpServer类
这个 TcpServer 类是一个高层次封装的网络服务器类,负责整体管理新连接、连接维护以及调度工作。下面我会用通俗易懂的方式,详细介绍它的所有成员变量和方法。
一、TcpServer 类的主要作用
- 创建并管理监听端口(主端口)
- 支持多线程(线程池)处理多个客户端
- 管理所有活跃连接(TcpConnection)
- 在连接建立、消息到达、连接关闭等场景下提供回调函数接口
- 让用户简单快捷地写出高性能的TCP服务器程序
二、成员变量详细介绍
成员变量 | 作用/描述 | 通俗解释 |
---|---|---|
EventLoop *loop_ | 指向主事件循环(主线程的事件循环) | 服务器核心循环,用来管理所有事件 |
const std::string ipPort_ | 绑定监听的IP和端口字符串 | 比如 “192.168.1.100:8888”,知道服务器在哪个地址监听 |
const std::string name_ | 服务器的名字 | 方便识别和日志记录 |
std::unique_ptr<Acceptor> acceptor_ | 监听新连接的“守门员”,实际做监听工作 | 让它负责处理“谁来报到” |
std::shared_ptr<EventLoopThreadPool> threadPool_ | 线程池,管理多个子事件循环(多线程处理) | 支持多核CPU,提升性能,让多连接同时处理无压力 |
ConnectionCallback connectionCallback_ | 新连接建立时的回调函数 | 用户可以定义“客户端连接来了,要干啥” |
MessageCallback messageCallback_ | 收到消息后的回调 | 用户定义“收到消息后,怎么处理” |
WriteCompleteCallback writeCompleteCallback_ | 消息发送完毕的回调 | 用于处理消息被成功发出的逻辑(比如确认通知) |
ThreadInitCallback threadInitCallback_ | 线程初始化(启动子Loop线程时调用) | 用户可以设置每个线程的初始化逻辑 |
std::atomic<int> started_ | 记录服务器是否已启动(防止重复启动) | 多线程安全,控制start只调用一次 |
int nextConnId_ | 用于生成唯一连接名的编号 | 每个连接一个编号,确保名字不重复 |
ConnectionMap connections_ | 存放所有活跃的连接对象(映射:连接名 -> TcpConnection) | 管理当前所有在线上的客户端连接 |
三、关键方法逐一讲解
1. 构造函数 TcpServer()
TcpServer(EventLoop *loop, const InetAddress &listenAddr, const std::string &nameArg, Option option);
- 作用:初始化服务器对象
- 做的事情:
- 检查
loop
不为空(用CheckLoopNotNull()
) - 创建监听 socket(Acceptor)
- 设置监听的地址和端口
- 创建线程池(用
EventLoopThreadPool
) - 设置
Acceptor
的新连接回调为类中的newConnection()
- 通俗:搭建好“门”和“工作团队(线程池)”。
- 检查
2. 析构函数 ~TcpServer()
~TcpServer();
- 作用:清理资源
- 会逐个关闭所有的连接(调用
connectDestroyed()
) - 通俗:像收摊,逐个打扫“流水线”上的摊位,确保不留下脏东西。
3. 设置各类回调接口
void setConnectionCallback(const ConnectionCallback &cb);
void setMessageCallback(const MessageCallback &cb);
void setWriteCompleteCallback(const WriteCompleteCallback &cb);
void setThreadInitCallback(const ThreadInitCallback &cb);
- 用户可以通过这些接口定义自己处理“新连接”、“消息到达”、“消息发送完毕”和“线程初始化”的逻辑。
4. setThreadNum(int numThreads)
void setThreadNum(int numThreads);
- 设置工作线程池中的线程数量
- 只有调用后,线程池才会启动,支持多线程处理
5. start()
void start();
- 启动服务器模型
- 作用包括:
- 防止重复调用(用
started_
) - 启动线程池(调用
threadPool_->start()
) - 开始监听(调用
acceptor_->listen()
)
- 防止重复调用(用
6. newConnection()
void newConnection(int sockfd, const InetAddress &peerAddr);
- 核心功能:当acceptor检测到有新客户端连接时调用
- 作业:
- 选择一个子Loop(用线程池)
- 创建唯一名字,
name_ + "-ip-port-#id"
- 创建
TcpConnection
对象(代表这条连接) - 保存到
connections_
(方便管理和关闭) - 设置该连接的各种回调(处理消息、连接建立、关闭)
- 让子Loop调用
connectEstablished()
,正式开始处理
- 通俗:新客户来了,找一个“服务员(线程)”来陪他聊天,记下他的“档案”。
7. removeConnection()
和 removeConnectionInLoop()
void removeConnection(const TcpConnectionPtr &conn);
void removeConnectionInLoop(const TcpConnectionPtr &conn);
- 作用:关闭连接
- 通常由
TcpConnection
在检测到连接关闭后调用 - 处理:
- 从
connections_
删除 - 调用
connectDestroyed()
,清理资源
- 从
- 通俗:跟客人说再见,清理“档案袋”,让连接端正退出。
四、核心流程总结(用户场景)
- 用户创建
TcpServer
- 配置回调(连接、消息等)
- 设置线程池大小
- 调用
start()
开始服务 - 服务器开始监听端口
- 一旦有客户端连接,
acceptor
触发newConnection()
newConnection()
找个子Loop创建TcpConnection
,并激活它- 用户自定义的回调(比如处理数据)被调用
- 客户端关闭时,
TcpConnection
检测到,通知TcpServer
清理连接
五、总结:它像什么?
TcpServer 就像一个大型的“接待大厅”,
- acceptor是“门卫”负责迎客;
- 线程池是“多个服务员”同时服务多客;
- **connections_**是“在场的客人”名单;
- 回调是“提前安排好的接待流程”。
14.TcpConnection类
这个TcpConnection类是一个关键的网络连接类,它封装了一条实际的TCP连接(socket),管理这条连接的所有细节,比如数据的收发、连接的建立和关闭,以及各种事件的回调。下面我会以通俗易懂的方式,详细介绍它的成员变量和方法。
一、TcpConnection类的作用
- 表示一次TCP连接
- 管理连接的状态(是否连接上)
- 负责数据的读取和写入
- 提供接口设置用户的回调(连接事件、消息事件、写完事件等)
- 正确处理连接的建立和关闭流程
二、成员变量详细介绍
成员变量 | 作用/描述 | 通俗解释 |
---|---|---|
EventLoop *loop_ | 所属的事件循环(子loop),不是主loop(用来处理这个连接的事件) | 负责管理这个连接的事件(读写等),不是全局的主环 |
const std::string name_ | 连接的名字(标识符) | 方便日志和管理 |
int state_ | 当前连接状态(连接、等待、断开等) | 用枚举值表示:已连接、连接中、断开中等。 |
bool reading_ | 是否正在读取数据 | 用于控制读取状态(比如暂停读取等) |
std::unique_ptr<Socket> socket_ | socket封装对象,操作底层socket | 实现对socket的封装(如设置socket选项,关闭等) |
std::unique_ptr<Channel> channel_ | 事件通道,管理事件(读写等)交给poller检测 | 连接的各种事件(可读、可写、异常)都由Channel负责监控 |
const InetAddress localAddr_ | 本地IP地址信息 | 连接端的本机地址 |
const InetAddress peerAddr_ | 对端(客户端)地址信息 | 客户端的地址信息 |
ConnectionCallback connectionCallback_ | 连接状态变化的回调(连接建立或断开) | 用户定义的,连接成功或断开后要做的事 |
MessageCallback messageCallback_ | 收到消息时的回调 | 用户定义,处理收到的数据 |
WriteCompleteCallback writeCompleteCallback_ | 发送完整后的回调 | 用户定义,数据全部发出后处理 |
HighWaterMarkCallback highWaterMarkCallback_ | 水位回调(缓冲区达到阈值触发) | 发送缓冲区太大时通知用户 |
CloseCallback closeCallback_ | 连接关闭时的回调 | 用户定义的关闭处理,比如从Server中移除连接 |
size_t highWaterMark_ | 缓冲区水位线(阈值) | 大小设为64MB,超过会触发高水位回调 |
Buffer inputBuffer_ | 接收缓冲区,用于存放从socket读到的数据 | 方便处理数据(存放临时数据) |
Buffer outputBuffer_ | 发送缓冲区,用于存放待发出去的数据 | 只要还没发出去的数据都放这里 |
三、主要方法逐一讲解
1. 构造函数 TcpConnection()
TcpConnection(EventLoop *loop, const std::string &name, int sockfd, const InetAddress& localAddr, const InetAddress& peerAddr);
- 作用:建立连接对象,初始化相关资源
- 内容:
- 将socket封装到
socket_
- 创建对应的Channel(事件管理器)
- 设置Channel对应的事件和回调(读写、异常、关闭)
- 设置socket的一些基本参数(如KeepAlive)
- 将socket封装到
- 通俗:像“注册”这条连接,让它知道什么时候可以读、写,准备好与客户端通信。
2. 析构函数 ~TcpConnection()
~TcpConnection();
- 作用:清理资源
- 主要是输出日志,告诉我们这条连接销毁了。
3. send()
void send(const std::string &buf);
- 作用:对外提供接口,将数据发出去
- 细节:
- 判断当前是否在该连接所属的
loop_
线程中 - 如果在,直接调用
sendInLoop()
; - 如果不在,跨线程调用
runInLoop()
在正确线程中发数据
- 判断当前是否在该连接所属的
- 通俗:对外发消息的主入口,自动处理跨线程问题。
4. sendInLoop()
void sendInLoop(const void* message, size_t len);
- 作用:实际在连接的事件循环中发送数据
- 细节:
- 先尝试直接写(
write()
)数据到socket - 如果不能全部写完,把剩余数据放到缓冲区(
outputBuffer_
) - 如果缓冲区积累到一定大小(高水位),通知用户
- 如果需要,就注册写事件,让缓冲区在可写时继续写
- 先尝试直接写(
- 通俗:保证数据可靠发出,遇到写不完的,缓存在缓冲区中等待下一次发出。
5. shutdown()
void shutdown();
- 作用:优雅关闭连接
- 改变状态,调用
shutdownInLoop()
在子loop中关闭写端,通知对端断开。
6. shutdownInLoop()
void shutdownInLoop();
- 作用:实际关闭socket的写端,确保数据都发完后关闭
7. connectEstablished()
void connectEstablished();
- 作用:连接建立时调用
- 设置状态为已连接
- 注册读事件到poller
- 调用用户定义的连接建立回调
8. connectDestroyed()
void connectDestroyed();
- 作用:连接断开
- 设置状态为已断开
- 移除注册的事件
- 调用用户定义的断开回调
9. handleRead()
void handleRead(Timestamp receiveTime);
- 作用:处理读事件(收到数据)
- 从socket读数据到
inputBuffer_
- 调用用户定义的消息处理回调
- 如果对端关闭,调用
handleClose()
- 发生错误,调用
handleError()
10. handleWrite()
复制代码
void handleWrite();
- 作用:处理写事件
- 将缓冲区中的数据写出
- 写完后,取消写事件注册
- 调用写完成回调,通知用户
11. handleClose()
void handleClose();
- 作用:连接关闭
- 改变状态
- 取消所有事件
- 调用连接关闭回调,让上层知道连接关闭(比如Server移除)
12. handleError()
void handleError();
- 作用:处理socket错误
- 获取具体错误,输出日志,方便排查问题
四、总结:它像什么?
TcpConnection 就像一辆车,
- 启动(connectEstablished):车子启动,准备好出发
- 发消息(send, sendInLoop):车子行驶,发出货物
- 接受消息(handleRead):收到车载信息
- 完成发货(writeCompleteCallback):货物全发完
- 关闭(shutdown, handleClose):停车熄火,关闭车辆
- 异常(handleError):路上出问题,报警处理
15.Buffer类
这个Buffer类是一个高性能的内存缓冲区,为网络编程中的读写操作提供方便和高效的支持。它的作用就是临时存储接收到的数据或者待发送的数据,避免频繁的系统调用开销。下面我用通俗易懂的语言详细介绍它的成员变量和成员方法。
一、Buffer类的作用和背景
- 在网络编程中,数据是通过读写socket的文件描述符(fd)来传输的。
- 但socket的数据不是一次传完的(不确定接收数据的大小);需要一个缓冲区存放临时的数据。
- Buffer类管理一块内存区域,支持高效读取和写入,方便上层代码处理网络数据。
二、成员变量详细介绍
成员变量 | 作用/描述 | 简单理解 |
---|---|---|
static const size_t kCheapPrepend | 预留的前导空间(8个字节) | 让你可以在前面空出空间,方便以后添加内容或协议处理 |
static const size_t kInitialSize | 初始缓冲区大小(1024字节) | 缓冲区的起始容量大小 |
std::vector<char> buffer_ | 实际存放数据的内存区域 | 真正存储数据的容器 |
size_t readerIndex_ | 读指针(读取位置的索引) | 读到哪个位置了,指示可以开始读取的数据起点 |
size_t writerIndex_ | 写指针(写入位置的索引) | 数据写到哪了,指示可以开始写入数据的起点 |
简单理解:
这块缓冲区用一个动态数组(vector)作为底层存储空间,readerIndex_
和 writerIndex_
标志着数据的“开始”位置和“结束”位置。
三、静态常量定义
kCheapPrepend
:8字节的空间,允许事先在缓冲区前面插入少量数据(比如协议头部等)kInitialSize
:缓冲区初始化大小1KB(1024字节)
四、成员方法详细介绍
1. 构造函数和基础访问方法
explicit Buffer(size_t initialSize = kInitialSize);
- 初始化缓冲区容量,读写指针都指向预留空间(8字节处)
用例:
创建一个缓冲区,默认大小1024字节,提前预留一些空间。
2. readableBytes()
size_t readableBytes() const
- 作用:计算这块缓冲区中可以读取的数据长度
- 原理:
writerIndex_ - readerIndex_
比喻:就像你书架上已经摆好书的数量。
3. writableBytes()
size_t writableBytes() const
- 作用:还能写入多少数据(还留有多大空间)
- 原理:
buffer_.size() - writerIndex_
比喻:书架上剩余多少空间可以放书。
4. prependableBytes()
size_t prependableBytes() const
- 作用:缓冲区前面可用空间(可以用来插入数据)
- 原理:
readerIndex_
比喻:书架前面空余的空间,方便插入内容。
5. peek()
const char* peek() const
- 作用:返回当前可读数据的起始地址
- 原理:
begin() + readerIndex_
比喻:看见书架上待领的书的起点。
6. retrieve()
void retrieve(size_t len)
- 作用:标记已读取的字节数,相当于“丢弃”这部分数据
- 逻辑:
- 如果只读部分少于剩余数据,就把
readerIndex_
向后移动 - 如果读取了全部剩余数据,就调用
retrieveAll()
复位到初始状态
- 如果只读部分少于剩余数据,就把
比喻:拿走一部分书,调节指针。
7. retrieveAll()
void retrieveAll()
- 作用:把缓冲区中的所有数据都“取走”,指针回到起始位置
- 效果:使缓冲区空了,准备重新使用
8. retrieveAllAsString()
std::string retrieveAllAsString()
- 作用:把缓冲区所有可读数据转成字符串返回
- 用法:上层应用可以方便地处理数据
9. retrieveAsString(size_t len)
std::string retrieveAsString(size_t len)
- 作用:取出指定长度的数据作为字符串,内部会调用
retrieve(len)
更新指针
10. ensureWriteableBytes()
void ensureWriteableBytes(size_t len)
- 作用:确保缓冲区有足够空间来写入len字节
- 逻辑:
- 如果空间不够,就调用
makeSpace()
扩容或移动数据到缓冲头
- 如果空间不够,就调用
11. append()
复制代码
void append(const char *data, size_t len)
- 作用:把外部数据拷贝到缓冲区
- 过程:
- 先确保空间够
- 拷贝数据到缓冲区末尾
- 更新写指针
比喻:把新书放到书架上。
12. beginWrite()
复制代码
char* beginWrite()
复制代码
const char* beginWrite() const
- 作用:返回写入位置的指针(当前可写的起点)
13. readFd()
复制代码
ssize_t readFd(int fd, int* saveErrno)
- 作用:从文件描述符(socket)读取数据,存入缓冲区
- 技术点:
- 使用
readv()
实现先尝试直接写入缓冲区 - 如果空间不足,就用
extrabuf
临时缓冲,再合并到缓冲区
- 使用
通俗点:像在过河,先尽量用主路(缓冲区),没有走完就用临时桥(临时缓冲区)补充。
14. writeFd()
复制代码
ssize_t writeFd(int fd, int* saveErrno)
- 作用:把缓冲区中的数据写到文件描述符(socket)
- 过程:直接调用
write()
,写出可读数据
四、私有辅助方法
1. begin()
char* begin()
- 获取底层数组起始地址(const和非const两个版本)
2. makeSpace(size_t len)
void makeSpace(size_t len)
- 作用:
- 如果剩余空间不够,直接resize扩大buffer
- 如果前面有空余(因为已读取过的部分),就把剩余未读的数据移动到缓冲区头部(节省空间)
比喻:整理书架,把剩余书搬到前面,再继续放新书。
五、整体总结
- Buffer 就像一个灵活的“组装箱”
- 通过读指针和写指针,动态管理数据存入和取出
- 支持高效读取(读File描述符)、写入(写到socket)
- 提供方便的接口处理数组转字符串、扩容、移动数据