【网络精讲6】muduo 的组件与接口:可视化了解muduo库

【网络精讲6】muduo 的组件与接口:可视化了解muduo库

写在前面

上一篇把 muduo 的定位说清楚了:它是 IO 底座,不碰业务。定位清楚之后,下一个问题立刻浮上来------真动手写项目时,到底该调哪个类、哪个函数。

这个问题不好答,原因是 muduo 的类太多,而且分布在不同层:有的在 base,有的在 net,有的只活在库内部、外面根本碰不到。看官方示例能跑起来,但换成自己的业务,往往卡在"这个回调是谁调谁"、"这个缓冲区该从哪拿"上。

换一个视角会清楚很多:把这个库当成一块电路板。

  • 芯片就是类,引脚就是公开接口;
  • 引脚分方向:有的是外部往里灌信号的(调用接口),有的是芯片往外吐信号的(回调);
  • 板上还有电源轨和地线(base 层的日志、时间、线程、锁),所有芯片都挂在上面;
  • 有一条总线在不停扫描(EventLoop 的 poll),谁有信号就把谁的事件推出去。

按这个视角把整库摊开,写项目时需要的只有一件事:看准引脚,焊对线。

引脚签名全部来自实际安装的 /usr/local/include/muduo 头文件,不是凭记忆复述。


一、主板总览:整库只有六层

先看整块板子的分区。从上到下不是随意排的,而是严格的调用方向:上层只用得到下层的公开接口,反过来不行。
#mermaid-svg-oP9NvfX4TMcke8j8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-oP9NvfX4TMcke8j8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-oP9NvfX4TMcke8j8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-oP9NvfX4TMcke8j8 .error-icon{fill:#552222;}#mermaid-svg-oP9NvfX4TMcke8j8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-oP9NvfX4TMcke8j8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-oP9NvfX4TMcke8j8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-oP9NvfX4TMcke8j8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-oP9NvfX4TMcke8j8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-oP9NvfX4TMcke8j8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-oP9NvfX4TMcke8j8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-oP9NvfX4TMcke8j8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-oP9NvfX4TMcke8j8 .marker.cross{stroke:#333333;}#mermaid-svg-oP9NvfX4TMcke8j8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-oP9NvfX4TMcke8j8 p{margin:0;}#mermaid-svg-oP9NvfX4TMcke8j8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-oP9NvfX4TMcke8j8 .cluster-label text{fill:#333;}#mermaid-svg-oP9NvfX4TMcke8j8 .cluster-label span{color:#333;}#mermaid-svg-oP9NvfX4TMcke8j8 .cluster-label span p{background-color:transparent;}#mermaid-svg-oP9NvfX4TMcke8j8 .label text,#mermaid-svg-oP9NvfX4TMcke8j8 span{fill:#333;color:#333;}#mermaid-svg-oP9NvfX4TMcke8j8 .node rect,#mermaid-svg-oP9NvfX4TMcke8j8 .node circle,#mermaid-svg-oP9NvfX4TMcke8j8 .node ellipse,#mermaid-svg-oP9NvfX4TMcke8j8 .node polygon,#mermaid-svg-oP9NvfX4TMcke8j8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-oP9NvfX4TMcke8j8 .rough-node .label text,#mermaid-svg-oP9NvfX4TMcke8j8 .node .label text,#mermaid-svg-oP9NvfX4TMcke8j8 .image-shape .label,#mermaid-svg-oP9NvfX4TMcke8j8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-oP9NvfX4TMcke8j8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-oP9NvfX4TMcke8j8 .rough-node .label,#mermaid-svg-oP9NvfX4TMcke8j8 .node .label,#mermaid-svg-oP9NvfX4TMcke8j8 .image-shape .label,#mermaid-svg-oP9NvfX4TMcke8j8 .icon-shape .label{text-align:center;}#mermaid-svg-oP9NvfX4TMcke8j8 .node.clickable{cursor:pointer;}#mermaid-svg-oP9NvfX4TMcke8j8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-oP9NvfX4TMcke8j8 .arrowheadPath{fill:#333333;}#mermaid-svg-oP9NvfX4TMcke8j8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-oP9NvfX4TMcke8j8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-oP9NvfX4TMcke8j8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oP9NvfX4TMcke8j8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-oP9NvfX4TMcke8j8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oP9NvfX4TMcke8j8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-oP9NvfX4TMcke8j8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-oP9NvfX4TMcke8j8 .cluster text{fill:#333;}#mermaid-svg-oP9NvfX4TMcke8j8 .cluster span{color:#333;}#mermaid-svg-oP9NvfX4TMcke8j8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-oP9NvfX4TMcke8j8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-oP9NvfX4TMcke8j8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-oP9NvfX4TMcke8j8 .icon-shape,#mermaid-svg-oP9NvfX4TMcke8j8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oP9NvfX4TMcke8j8 .icon-shape p,#mermaid-svg-oP9NvfX4TMcke8j8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-oP9NvfX4TMcke8j8 .icon-shape .label rect,#mermaid-svg-oP9NvfX4TMcke8j8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oP9NvfX4TMcke8j8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-oP9NvfX4TMcke8j8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-oP9NvfX4TMcke8j8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 系统调用层
事件层
连接层
接口层
应用层
base 基础设施
Timestamp / Logging / AsyncLogging
Thread / ThreadPool / ThreadLocal
Mutex / Condition / BlockingQueue
业务代码

new TcpServer + setXxxCallback + loop.loop
TcpServer

一个端口一个实例
EventLoopThreadPool

子 Reactor 线程池
Acceptor

只做 accept
TcpConnection

每条连接一个
Connector

只做 connect
Channel

一个 fd 一个
Poller / EPollPoller

epoll 抽象
TimerQueue

定时器
wakeupChannel

跨线程唤醒
epoll_create1 / epoll_ctl / epoll_wait
accept4 / readv / write / close
timerfd_settime / eventfd

完整版布局图(含每层内部的组件划分与连线标注)放在文末附录。

每一层的职责和"要不要碰",可以列成一张表:

层 代表组件 职责 写业务时是否直接操作
应用层 自己的 ChatServer 之类 协议解析、业务分发、数据落库 全部自己写
接口层 TcpServer、EventLoopThreadPool 把监听、线程、回调三件事打包好 只调 setXxx 和 start()
连接层 Acceptor、TcpConnection、Connector 收连接、管单条连接、发连接 只通过回调拿到 conn
事件层 Channel、Poller、TimerQueue fd 事件注册、epoll 等待、定时 基本不碰
系统调用层 epoll_wait、accept4、timerfd 真正的内核交互 完全不碰
base 层 Timestamp、Logging、Thread、Mutex 时间、日志、线程、锁、队列 日志和计时会用到

一个关键事实:中间四层的组装全部由库自己完成 。TcpServer 构造时会自己 new Acceptor、new EventLoopThreadPool;Acceptor 会自己造 Channel;TcpConnection 会自己造 Channel 和两个 Buffer。业务代码一个都不需要 new。


二、TcpServer:一颗双面芯片

TcpServer 是应用唯一直接持有并配置的对象。把它看成一颗粒子,左边是灌进去的配置,右边是吐出来的回调。
#mermaid-svg-DqeuAIEzFjZ24Cyp{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-DqeuAIEzFjZ24Cyp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DqeuAIEzFjZ24Cyp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DqeuAIEzFjZ24Cyp .error-icon{fill:#552222;}#mermaid-svg-DqeuAIEzFjZ24Cyp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DqeuAIEzFjZ24Cyp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DqeuAIEzFjZ24Cyp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DqeuAIEzFjZ24Cyp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DqeuAIEzFjZ24Cyp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DqeuAIEzFjZ24Cyp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DqeuAIEzFjZ24Cyp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DqeuAIEzFjZ24Cyp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DqeuAIEzFjZ24Cyp .marker.cross{stroke:#333333;}#mermaid-svg-DqeuAIEzFjZ24Cyp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DqeuAIEzFjZ24Cyp p{margin:0;}#mermaid-svg-DqeuAIEzFjZ24Cyp .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-DqeuAIEzFjZ24Cyp .cluster-label text{fill:#333;}#mermaid-svg-DqeuAIEzFjZ24Cyp .cluster-label span{color:#333;}#mermaid-svg-DqeuAIEzFjZ24Cyp .cluster-label span p{background-color:transparent;}#mermaid-svg-DqeuAIEzFjZ24Cyp .label text,#mermaid-svg-DqeuAIEzFjZ24Cyp span{fill:#333;color:#333;}#mermaid-svg-DqeuAIEzFjZ24Cyp .node rect,#mermaid-svg-DqeuAIEzFjZ24Cyp .node circle,#mermaid-svg-DqeuAIEzFjZ24Cyp .node ellipse,#mermaid-svg-DqeuAIEzFjZ24Cyp .node polygon,#mermaid-svg-DqeuAIEzFjZ24Cyp .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-DqeuAIEzFjZ24Cyp .rough-node .label text,#mermaid-svg-DqeuAIEzFjZ24Cyp .node .label text,#mermaid-svg-DqeuAIEzFjZ24Cyp .image-shape .label,#mermaid-svg-DqeuAIEzFjZ24Cyp .icon-shape .label{text-anchor:middle;}#mermaid-svg-DqeuAIEzFjZ24Cyp .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-DqeuAIEzFjZ24Cyp .rough-node .label,#mermaid-svg-DqeuAIEzFjZ24Cyp .node .label,#mermaid-svg-DqeuAIEzFjZ24Cyp .image-shape .label,#mermaid-svg-DqeuAIEzFjZ24Cyp .icon-shape .label{text-align:center;}#mermaid-svg-DqeuAIEzFjZ24Cyp .node.clickable{cursor:pointer;}#mermaid-svg-DqeuAIEzFjZ24Cyp .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-DqeuAIEzFjZ24Cyp .arrowheadPath{fill:#333333;}#mermaid-svg-DqeuAIEzFjZ24Cyp .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-DqeuAIEzFjZ24Cyp .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-DqeuAIEzFjZ24Cyp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DqeuAIEzFjZ24Cyp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-DqeuAIEzFjZ24Cyp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DqeuAIEzFjZ24Cyp .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-DqeuAIEzFjZ24Cyp .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-DqeuAIEzFjZ24Cyp .cluster text{fill:#333;}#mermaid-svg-DqeuAIEzFjZ24Cyp .cluster span{color:#333;}#mermaid-svg-DqeuAIEzFjZ24Cyp div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-DqeuAIEzFjZ24Cyp .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-DqeuAIEzFjZ24Cyp rect.text{fill:none;stroke-width:0;}#mermaid-svg-DqeuAIEzFjZ24Cyp .icon-shape,#mermaid-svg-DqeuAIEzFjZ24Cyp .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DqeuAIEzFjZ24Cyp .icon-shape p,#mermaid-svg-DqeuAIEzFjZ24Cyp .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-DqeuAIEzFjZ24Cyp .icon-shape .label rect,#mermaid-svg-DqeuAIEzFjZ24Cyp .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DqeuAIEzFjZ24Cyp .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-DqeuAIEzFjZ24Cyp .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-DqeuAIEzFjZ24Cyp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 构造 TcpServer(loop, addr, name)
TcpServer

net/TcpServer.h

内部持有 Acceptor + 线程池 + 连接表
setThreadNum(n)
setThreadInitCallback(cb)
setConnectionCallback(cb)
setMessageCallback(cb)
setWriteCompleteCallback(cb)
start()
onConnection(conn)
onMessage(conn, buf, t)
onWriteComplete(conn)

左侧引脚,逐个看签名:

引脚 签名 说明
构造 TcpServer(EventLoop* loop, const InetAddress& listenAddr, const string& nameArg, Option option = kNoReusePort) nameArg 会拼进每条连接的名字,日志里靠它区分
线程数 void setThreadNum(int numThreads) 子 Reactor 个数,0 表示不用线程池
线程初始化 void setThreadInitCallback(const ThreadInitCallback& cb) 参数是 void(EventLoop*),每个 IO 线程启动时执行一次
连接回调 void setConnectionCallback(const ConnectionCallback& cb) 上下线都会进来,靠 conn->connected() 区分
消息回调 void setMessageCallback(const MessageCallback& cb) 业务的真正入口
写完成回调 void setWriteCompleteCallback(const WriteCompleteCallback& cb) 输出缓冲被清空时触发
启动 void start() 构造时不监听,start() 才 listen

三个容易被忽略的点:

第一个,构造函数的第四个参数。 绝大多数教程只写前三个参数,实际上有 Option option = kNoReusePort,取值 kNoReusePort / kReusePort。要开 SO_REUSEPORT(多进程共用一个端口做负载分摊)必须显式传 kReusePort,光加 SO_REUSEADDR 是不够的。

第二个,start() 之前什么都不会发生。 TcpServer 的构造函数只保存参数、创建 Acceptor 与线程池对象,不 bind、不 listen。忘了调 start() 的表现是:进程正常跑着,端口根本不存在,客户端连接直接 Connection refused。

第三个,三个回调一个都不设也能跑。 Callbacks.h 里定义了兜底实现 defaultConnectionCallback 与 defaultMessageCallback,TcpServer 成员初始就指向它们。兜底版本只打一行日志,并且把收到的数据直接丢弃。所以"服务能起来但收不到数据"这个现象,第一嫌疑永远是 setMessageCallback 没接上。

右侧三个回调的取值时刻:

回调 触发条件 典型用途
onConnection 连接建立完成、连接销毁前 上线打日志、初始化会话、下线清理
onMessage inputBuffer_ 里可读数据到达 协议解析、业务分发
onWriteComplete outputBuffer_ 被写空 大文件分块续传、关闭输出缓冲监听

三、TcpConnection:最常焊的那颗

连接建立之后,TcpServer 会把每个连接包成 TcpConnectionPtr 交出来。之后所有针对单条连接的操作,都在这个对象上。

先说生命周期,这是最重要的一条。

TcpConnection 继承 enable_shared_from_this,由 shared_ptr 管理。绝不能把 TcpConnection* 或 TcpConnectionPtr 的引用长期保存下来------连接断开即析构,保存的指针随即悬空。所有对连接的操作,只用回调参数传进来的那个 TcpConnectionPtr。库里有一个专门的机制保护这一点:Channel::tie() 持有一个 weak_ptr,回调执行期间临时升级为 shared_ptr,保证这几十微秒内对象不会被析构。

状态机只有四个状态:
#mermaid-svg-lfU2dPa3q2juY5vH{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-lfU2dPa3q2juY5vH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-lfU2dPa3q2juY5vH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-lfU2dPa3q2juY5vH .error-icon{fill:#552222;}#mermaid-svg-lfU2dPa3q2juY5vH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lfU2dPa3q2juY5vH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-lfU2dPa3q2juY5vH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lfU2dPa3q2juY5vH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lfU2dPa3q2juY5vH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-lfU2dPa3q2juY5vH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lfU2dPa3q2juY5vH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lfU2dPa3q2juY5vH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lfU2dPa3q2juY5vH .marker.cross{stroke:#333333;}#mermaid-svg-lfU2dPa3q2juY5vH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lfU2dPa3q2juY5vH p{margin:0;}#mermaid-svg-lfU2dPa3q2juY5vH .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-lfU2dPa3q2juY5vH .cluster-label text{fill:#333;}#mermaid-svg-lfU2dPa3q2juY5vH .cluster-label span{color:#333;}#mermaid-svg-lfU2dPa3q2juY5vH .cluster-label span p{background-color:transparent;}#mermaid-svg-lfU2dPa3q2juY5vH .label text,#mermaid-svg-lfU2dPa3q2juY5vH span{fill:#333;color:#333;}#mermaid-svg-lfU2dPa3q2juY5vH .node rect,#mermaid-svg-lfU2dPa3q2juY5vH .node circle,#mermaid-svg-lfU2dPa3q2juY5vH .node ellipse,#mermaid-svg-lfU2dPa3q2juY5vH .node polygon,#mermaid-svg-lfU2dPa3q2juY5vH .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-lfU2dPa3q2juY5vH .rough-node .label text,#mermaid-svg-lfU2dPa3q2juY5vH .node .label text,#mermaid-svg-lfU2dPa3q2juY5vH .image-shape .label,#mermaid-svg-lfU2dPa3q2juY5vH .icon-shape .label{text-anchor:middle;}#mermaid-svg-lfU2dPa3q2juY5vH .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-lfU2dPa3q2juY5vH .rough-node .label,#mermaid-svg-lfU2dPa3q2juY5vH .node .label,#mermaid-svg-lfU2dPa3q2juY5vH .image-shape .label,#mermaid-svg-lfU2dPa3q2juY5vH .icon-shape .label{text-align:center;}#mermaid-svg-lfU2dPa3q2juY5vH .node.clickable{cursor:pointer;}#mermaid-svg-lfU2dPa3q2juY5vH .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-lfU2dPa3q2juY5vH .arrowheadPath{fill:#333333;}#mermaid-svg-lfU2dPa3q2juY5vH .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-lfU2dPa3q2juY5vH .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-lfU2dPa3q2juY5vH .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lfU2dPa3q2juY5vH .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-lfU2dPa3q2juY5vH .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lfU2dPa3q2juY5vH .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-lfU2dPa3q2juY5vH .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-lfU2dPa3q2juY5vH .cluster text{fill:#333;}#mermaid-svg-lfU2dPa3q2juY5vH .cluster span{color:#333;}#mermaid-svg-lfU2dPa3q2juY5vH div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-lfU2dPa3q2juY5vH .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-lfU2dPa3q2juY5vH rect.text{fill:none;stroke-width:0;}#mermaid-svg-lfU2dPa3q2juY5vH .icon-shape,#mermaid-svg-lfU2dPa3q2juY5vH .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lfU2dPa3q2juY5vH .icon-shape p,#mermaid-svg-lfU2dPa3q2juY5vH .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-lfU2dPa3q2juY5vH .icon-shape .label rect,#mermaid-svg-lfU2dPa3q2juY5vH .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lfU2dPa3q2juY5vH .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-lfU2dPa3q2juY5vH .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-lfU2dPa3q2juY5vH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} kConnecting
kConnected
kDisconnecting
kDisconnected

kConnecting 只在客户端侧短暂存在;服务端 onConnection 拿到连接时,状态已经是 kConnected。

主动操作引脚:

引脚 签名 注意事项
发送 void send(const void* message, int len) 最底层的一版,二进制安全
发送 void send(const StringPiece& message) 传字符串最方便
发送 void send(Buffer* message) 会 swap 数据,传进来的 Buffer 立刻变空
优雅关闭 void shutdown() 源码注释明确写 NOT thread safe,只在所属 loop 线程调
强制关闭 void forceClose() / void forceCloseWithDelay(double seconds) 不等输出缓冲,直接关
读开关 void startRead() / void stopRead() 做流量控制用,不读时自动摘掉 EPOLLIN
读状态 bool isReading() const 注释同样标了 NOT thread safe
粘包控制 void setTcpNoDelay(bool on) 关 Nagle,低延迟场景必开
挂业务状态 void setContext(const boost::any&) / const boost::any& getContext() / boost::any* getMutableContext() 会话对象挂这里,别自己维护 map
缓冲区 Buffer* inputBuffer() / Buffer* outputBuffer() 直接拿库的缓冲区,注意有效期
水位回调 void setHighWaterMarkCallback(const HighWaterMarkCallback& cb, size_t highWaterMark) 两个参数:回调 + 阈值

三个 send 重载的区别值得单独说,因为这里出过太多问题:

send(const void*, int) 和 send(const StringPiece&) 都是拷贝 语义,数据复制进 outputBuffer_(或直接 writev 出去),调用方立刻可以释放自己的内存。

send(Buffer*) 是移动 语义,源码里就一行 std::swap(*message, outputBuffer_)。传进去的 Buffer 内容被换走,出来时是空的。这个设计是为了避免大块数据再拷贝一次,但如果调用方不知道这条,后面继续从那个 Buffer 读数据会读到空------表现是"转发大量数据时偶发丢包"。

查询引脚:

引脚 签名 用途
名字 const string& name() 形如 ChatServer-0.0.0.0:6000#3,日志定位用
对端地址 const InetAddress& peerAddress() 配合 toIpPort() 打印 1.2.3.4:5678
本端地址 const InetAddress& localAddress() 多网卡环境判断从哪个地址进来的
连接状态 bool connected() / bool disconnected() onConnection 里区分上下线
TCP 信息 bool getTcpInfo(struct tcp_info*) / string getTcpInfoString() 取 RTT、重传数、拥塞窗口
所属循环 EventLoop* getLoop() 判断当前线程是否连接所属线程

getTcpInfoString() 返回的是一行现成的可读文本,排查网络抖动时直接打进日志即可,不用自己解析 struct tcp_info 的几十个字段。


四、回调接线:std::bind 是焊锡

库和业务之间只有一个连接点:把库定义的回调类型,焊到自己的成员函数上。这一步焊接动作就是 std::bind。
EventLoop TcpConnection Acceptor TcpServer 业务代码 EventLoop TcpConnection Acceptor TcpServer 业务代码 #mermaid-svg-UKqiWxBNA9anU9Ld{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-UKqiWxBNA9anU9Ld .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-UKqiWxBNA9anU9Ld .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-UKqiWxBNA9anU9Ld .error-icon{fill:#552222;}#mermaid-svg-UKqiWxBNA9anU9Ld .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-UKqiWxBNA9anU9Ld .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-UKqiWxBNA9anU9Ld .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-UKqiWxBNA9anU9Ld .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-UKqiWxBNA9anU9Ld .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-UKqiWxBNA9anU9Ld .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-UKqiWxBNA9anU9Ld .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-UKqiWxBNA9anU9Ld .marker{fill:#333333;stroke:#333333;}#mermaid-svg-UKqiWxBNA9anU9Ld .marker.cross{stroke:#333333;}#mermaid-svg-UKqiWxBNA9anU9Ld svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-UKqiWxBNA9anU9Ld p{margin:0;}#mermaid-svg-UKqiWxBNA9anU9Ld .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-UKqiWxBNA9anU9Ld text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-UKqiWxBNA9anU9Ld .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-UKqiWxBNA9anU9Ld .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-UKqiWxBNA9anU9Ld .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-UKqiWxBNA9anU9Ld .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-UKqiWxBNA9anU9Ld #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-UKqiWxBNA9anU9Ld .sequenceNumber{fill:white;}#mermaid-svg-UKqiWxBNA9anU9Ld #sequencenumber{fill:#333;}#mermaid-svg-UKqiWxBNA9anU9Ld #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-UKqiWxBNA9anU9Ld .messageText{fill:#333;stroke:none;}#mermaid-svg-UKqiWxBNA9anU9Ld .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-UKqiWxBNA9anU9Ld .labelText,#mermaid-svg-UKqiWxBNA9anU9Ld .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-UKqiWxBNA9anU9Ld .loopText,#mermaid-svg-UKqiWxBNA9anU9Ld .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-UKqiWxBNA9anU9Ld .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-UKqiWxBNA9anU9Ld .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-UKqiWxBNA9anU9Ld .noteText,#mermaid-svg-UKqiWxBNA9anU9Ld .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-UKqiWxBNA9anU9Ld .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-UKqiWxBNA9anU9Ld .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-UKqiWxBNA9anU9Ld .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-UKqiWxBNA9anU9Ld .actorPopupMenu{position:absolute;}#mermaid-svg-UKqiWxBNA9anU9Ld .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-UKqiWxBNA9anU9Ld .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-UKqiWxBNA9anU9Ld .actor-man circle,#mermaid-svg-UKqiWxBNA9anU9Ld line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-UKqiWxBNA9anU9Ld :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} setMessageCallback(onMessage)start()listen()listenfd 可读handleRead 产出新连接创建 TcpConnectiononConnection(conn)连接 fd 可读readFd 写入 inputBuffer_onMessage(conn, buf, t)send(reply)sendInLoop 或直接 writev

上面的时序图讲的是执行顺序;静态的接线关系------五个回调类型各自焊到哪个业务函数------见文末附录。

五种回调类型的完整定义(net/Callbacks.h):

类型 定义 由谁调用 什么时候
ConnectionCallback std::function<void(const TcpConnectionPtr&)> TcpConnection 连接建立完成、销毁前
MessageCallback std::function<void(const TcpConnectionPtr&, Buffer*, Timestamp)> TcpConnection 数据到达并写入输入缓冲后
WriteCompleteCallback std::function<void(const TcpConnectionPtr&)> TcpConnection 输出缓冲写空
HighWaterMarkCallback std::function<void(const TcpConnectionPtr&, size_t)> TcpConnection 输出缓冲超过阈值,参数是超阈前的积压字节数
CloseCallback std::function<void(const TcpConnectionPtr&)> TcpConnection 连接销毁,库内部用
TimerCallback std::function<void()> EventLoop 定时到期

Callbacks.h 顺手把 _1、_2、_3 三个占位符 using 到了 muduo::net 命名空间,所以写 std::bind(&ChatServer::onMessage, this, _1, _2, _3) 不需要自己再 using namespace std::placeholders。

接线写法只有一条原则:成员函数必须用 std::bind 绑 this,或者直接用 lambda 捕获 this 。回调函数签名里的参数个数必须和 std::function 完全对上,MessageCallback 就是三个参数,少写一个编译期报错,多写一个也报错。


五、引脚总表

写代码时对着这张表查即可。

组件 关键接口 干什么用
TcpServer TcpServer(loop, addr, name, option) · setThreadNum(n) · setThreadInitCallback · setConnectionCallback · setMessageCallback · setWriteCompleteCallback · start() · ipPort() · name() · getLoop() · threadPool() 服务端总装,一个端口一个
TcpConnection send(void*, int) · send(StringPiece) · send(Buffer*) · shutdown() · forceClose() · forceCloseWithDelay(s) · startRead() · stopRead() · isReading() · setTcpNoDelay(bool) · setContext/getContext · inputBuffer()/outputBuffer() · setHighWaterMarkCallback(cb, n) · name() · localAddress() · peerAddress() · connected()/disconnected() · getTcpInfoString() 单条连接,最常用
EventLoop loop() · quit() · runInLoop(cb) · queueInLoop(cb) · runAt/runAfter/runEvery · cancel(TimerId) · isInLoopThread() · assertInLoopThread() · setContext/getContext 事件循环,跨线程调度
Channel enableReading() · enableWriting() · disableReading() · disableWriting() · disableAll() · isWriting() · handleEvent(t) · set{Read,Write,Close,Error}Callback · tie(shared_ptr) · fd() · events() 一个 fd 的事件封装
Poller poll(timeoutMs, activeChannels) · updateChannel(ch) · removeChannel(ch) · hasChannel(ch) epoll 的抽象基类,EPollPoller 是实现
Buffer readableBytes() · writableBytes() · prependableBytes() · peek() · retrieve(n) · retrieveAll() · retrieveAsString(n) · retrieveAllAsString() · append(str, len) · findCRLF() · readFd(fd, &err) 用户态读写缓冲
InetAddress InetAddress(port) · InetAddress(ip, port) · toIp() · toIpPort() · port() 地址端口封装
EventLoopThreadPool setThreadNum(n) · start(cb) · getNextLoop() · getLoopForHash(h) · getAllLoops() 子 Reactor 线程池
EventLoopThread EventLoopThread(cb, name) · startLoop() 把一个 loop 绑到新线程上跑
TcpClient connect() · disconnect() · enableRetry() · setConnectionCallback · setMessageCallback 客户端侧,连别的服务
TimerId 拷贝、比较、赋值 定时器句柄,用于 cancel
Timestamp now() · addTime(ts, s) · timeDifference(hi, lo) · toFormattedString() · microSecondsSinceEpoch() 微秒时间戳
Logging LOG_INFO · LOG_WARN · LOG_ERROR · LOG_DEBUG · LOG_FATAL · CHECK_EQ/CHECK_NE/CHECK_LT · setLogLevel() 同步日志
AsyncLogging AsyncLogging(baseName, rollSize, flushInterval) · start() · stop() · append(line, len) 异步日志,后台线程落盘
Thread Thread(func, name) · start() · join() · tid() · cachedTid() 线程封装
ThreadPool ThreadPool(name) · start(n) · run(Task) · setMaxQueueSize(n) · stop() 计算任务池,业务侧用
Mutex / Condition MutexLockGuard(lock) · wait() · notify() · notifyAll() 锁与条件变量
BlockingQueue put(x) · take() · size() 无界线程安全队列
BoundedBlockingQueue put(x) · take() · size() · capacity() 有界队列,内部是 boost::circular_buffer
Singleton Singleton<T>::instance() 全局单例

七、两个实测出来的坑

这两条都能复现,且都跟"头文件"有关。

7.1 内部头文件根本没装

按官方 make install 装完之后,/usr/local/include/muduo/net/ 下只有 12 个平铺头文件,而源码里这个目录有 21 个。差出来的 9 个是:

Acceptor.h、Connector.h、Poller.h、Socket.h、SocketsOps.h、Timer.h、TimerQueue.h,外加 boilerplate.h(空占位类)和 ZlibStream.h(没有参与编译)。

子目录同样不全:poller/ 下的 EPollPoller.h、PollPoller.h,inspect/ 下的 PerformanceInspector.h、ProcessInspector.h、SystemInspector.h,都没有装。

原因在 muduo/net/CMakeLists.txt。它的 install 规则是显式白名单,不是 glob 全量:

cmake 复制代码
set(HEADERS
  Buffer.h
  Callbacks.h
  Channel.h
  Endian.h
  EventLoop.h
  EventLoopThread.h
  EventLoopThreadPool.h
  InetAddress.h
  TcpClient.h
  TcpConnection.h
  TcpServer.h
  TimerId.h
  )
install(FILES ${HEADERS} DESTINATION include/muduo/net)

这 12 个是"对外面够用"的集合,内部类没有列进去。但它们的 .cc 全都编译进了 libmuduo_net.a------实现是齐的,只缺声明文件。

后果很直接:按某些教程写 #include <muduo/net/Acceptor.h> 会编译失败,报 No such file or directory。遇到这种情况不必怀疑装错了,补头文件就行:

bash 复制代码
# 在 muduo 源码目录下,把缺失的内部头文件补进去
cd ~/muduo/muduo/net

# 补齐 net/ 下的平铺头文件
sudo install -m 644 Acceptor.h Connector.h Poller.h Timer.h TimerQueue.h \
    Socket.h SocketsOps.h /usr/local/include/muduo/net/

# poller/ 子目录不存在,先建目录再拷
sudo install -d /usr/local/include/muduo/net/poller
sudo install -m 644 poller/EPollPoller.h poller/PollPoller.h \
    /usr/local/include/muduo/net/poller/

这里有个细节值得注意:install -D 看起来正好用于"目标目录不存在时自动创建",但它只接受单个源文件 。install -m 644 -D a.h b.h dir/ 会直接报 目标不是目录。多文件场景必须先 install -d 把目录建好,再批量拷进去。

补完不需要重新编译库,也不需要 ldconfig(静态库本来就不需要)。补完后的验证方式是:#include 一遍所有头文件,再实际构造一个 Acceptor 并 listen() 一个端口------能编过、能链上、能跑通,说明声明与实现版本一致。

有两个头文件即使补了也不该用:

  • net/ZlibStream.h:ZlibStream.cc 不在 net_SRCS 列表里,只被单元测试引用,补上也链接不到实现。
  • net/protobuf/、net/protorpc/:只有检测到 protobuf 才会生成对应库文件,环境里没有 libmuduo_protobuf.a 时装了也链接失败。

7.2 boost 没有被"彻底移除"

上一篇提到过,muduo 2.0 迁移到 C++11 之后不再需要链接 boost,这一点在链接命令上完全成立-------lmuduo_net -lmuduo_base -lpthread 三个就够了,.a 里查不到任何 boost 未定义符号。

但要补一句更精确的:boost 从链接依赖降级成了头文件依赖,没有被彻底移除。实际安装的头文件里仍有四处引用:

头文件 引用内容 影响面
base/Timestamp.h boost/operators.hpp,继承 equality_comparable / less_than_comparable 几乎每个程序都会碰到 ,日志、定时器全靠 Timestamp
net/EventLoop.h boost::any,用于 setContext 想给 loop 挂上下文就会触发
net/TcpConnection.h boost::any,用于 setContext 想给连接挂会话对象就会触发
base/BoundedBlockingQueue.h boost/circular_buffer.hpp 只有用这个模板类才触发

所以 libboost-dev 是编译期必需依赖,缺了会在 #include <muduo/base/Timestamp.h> 这一步直接失败:

复制代码
fatal error: boost/operators.hpp: No such file or directory

安装方式只有头文件,不需要运行库:

bash 复制代码
sudo apt install libboost-dev

这也是 setContext 的签名写的是 boost::any 而不是 std::any 的原因------库里这块一直没换。


速查与常见追问

为什么 send 有三个重载

底层那版 send(const void*, int) 最通用;send(const StringPiece&) 是为了不用写 strlen;send(Buffer*) 是为大块数据省一次拷贝。前两版是拷贝语义,第三版是交换语义,传进去的 Buffer 会变空。批量转发场景用第三版最省内存,但必须知道它把数据"拿走"了。

shutdown() 和 forceClose() 有什么区别

shutdown() 是优雅关闭:先关写端,等对端也关,等输出缓冲写完,走完四次挥手再销毁。forceClose() 直接关,输出缓冲里没发完的数据丢弃。业务里正常下线用 shutdown(),心跳超时、协议违规这种要立刻断开用 forceClose()。forceCloseWithDelay(seconds) 是延迟一段时间再强关,给对端一点时间收到最后的数据。

为什么 shutdown() 标了 NOT thread safe,send() 却可以从任意线程调

send() 内部判断了当前线程,不在所属 loop 线程就把任务 queueInLoop 转发过去,是线程安全的。shutdown() 直接改 Channel 的注册状态,没有做转发,跨线程调用会让 epoll_ctl 和事件派发打架。同理 isReading() 也标了不线程安全,因为它在读 reading_ 这个普通 bool。

回调里能不能做耗时操作

不能。所有回调都在 IO 线程里执行,一个回调卡住,该线程上所有连接全部停摆。数据库查询、磁盘写、加解密这类活要丢给 ThreadPool,算完再用 loop->runInLoop() 把结果送回 IO 线程发出去。

setThreadNum(0) 是什么效果

不启用子 Reactor 线程池,所有连接全部挂在主线程的 loop 上。单线程也能扛几千连接,前提是回调足够快。适合调试,不适合生产。

为什么要用 setContext 而不是自己维护 map<string, Session>

自己维护 map 要处理两件事:一是并发,回调在多个 IO 线程里跑,map 必须加锁;二是生命周期,连接销毁时得记得擦掉,漏一处就是内存泄漏。setContext 把会话对象挂在连接自己身上,连接死了对象跟着死,不需要锁,也不需要清理。

inputBuffer() 拿到的指针能存下来吗

不能。这个 Buffer 属于 TcpConnection 内部,retrieveAll 之后下标会重置,缓冲区扩容时内存可能重新分配。回调结束前把数据拷走或解析完,不要把指针带出去。

主 Reactor 和子 Reactor 分别在哪

主 Reactor 是 main 里那个 EventLoop,只负责 accept。子 Reactor 是 EventLoopThreadPool 里的 N 个 EventLoop,各自跑一个线程,负责已建立连接的读写。TcpServer 在 start() 时把线程池拉起来,之后每来一条新连接,按轮询分给其中一个 loop,这条连接之后的所有事件都归那个线程。


附一:三层文件对照

层 头文件 是否可直接 include
对外接口 TcpServer.h、TcpClient.h、TcpConnection.h、EventLoop.h、EventLoopThread.h、EventLoopThreadPool.h、Channel.h、Buffer.h、InetAddress.h、TimerId.h、Callbacks.h、Endian.h 官方 install 自带
内部实现 Acceptor.h、Connector.h、Poller.h、poller/EPollPoller.h、poller/PollPoller.h、Socket.h、SocketsOps.h、Timer.h、TimerQueue.h 需手动补装,见 7.1
无实现 ZlibStream.h、protobuf/*、protorpc/* 不建议使用

附二:完整电路板图

四张图按调用方向自上而下排布:应用层 → 接口层 → 连接层 → 事件层 → 系统调用层 → base 基础设施。

相比正文的流程图,这组图多出三类信息:每层内部的组件划分(比如 TcpConnection 里并排的 inputBuffer_、outputBuffer_、Channel、Socket、context)、连线上的动作标注(配置回调 / 内部持有 / 新建连接 / 注册 Channel / epoll_ctl)、以及双面引脚的左右分栏布局。

相关推荐
泡海椒3 小时前
深度解析JQuick-SQL SQL解析与AST生成机制|分布式SQL原理
数据库·分布式·sql
SelectDB3 小时前
订单表改成实时更新后,我把主键模型的坑踩了一遍
大数据·数据库·数据分析
devilnumber3 小时前
Redis 数据类型 + 项目落地场景(面试版,全面、实用,后端常用)
数据库·redis·面试
惠惠软件3 小时前
PDF加盖骑缝章标注工具,PDF 文档上批量添加骑缝章或盖章的小工具
数据库·pdf·办公文件
SelectDB3 小时前
压完一轮 TPC-H SF100,我才学会怎么读基准数字
大数据·数据库
鸢尾掠地平4 小时前
MySQL建库命令总结(含练习)
数据库·oracle
ReStock活库4 小时前
Zephyr异步SPI调用返回后,为什么不能立即复用发送缓冲区?
java·前端·网络
山雀~4 小时前
WebSocket 断连之后:一次手机采集偶现卡死的排查与修复
网络·websocket·网络协议
Novproxy_Z4 小时前
机房 IP 和住宅 IP 怎么区分?IP ASN、归属信息检测脚本
linux·网络·tcp/ip