TIPC入门实战:从零跑通Linux集群消息收发
写这篇的时候我刚从一个集群调测现场回来趁着热乎劲把TIPC入门阶段该知道的东西整理出来。这个系列做到第六篇前几篇要么在讲原理要么在讲内核实现不少读者反馈说看得懂但上手还是懵。所以这篇定位很明确不整高深的内核源码就讲怎么从零把一个TIPC消息收发程序跑通包括环境怎么准备、概念怎么理解、代码怎么写、坑怎么避。适合有Linux基础、想搞懂集群节点间通信是怎么回事的开发者和运维同学。TIPC全称是Transparent Inter Process Communication透明进程间通信协议。它最初是为集群环境设计的解决的是在一个集群内部多个节点上的进程之间如何高效、可靠地交换消息。和TCP/IP不同它不是用来跨互联网通信的而是专注于局域网内的集群节点通信。入门阶段你不需要理解它的全部机制但要抓住几个关键点它是基于消息的它有自己的寻址方式它的API走的是socket接口但和TCP/UDP的习惯差异很大。这篇文章就围绕这些展开。1. TIPC到底解决什么问题为什么入门要先看这里1.1 TIPC是什么一个集群通信协议的“长话短说”TIPC最早是集群环境下的产物设计的初衷是给集群内部节点上的进程提供一种“透明”的通信方式。透明这个词很关键意思是进程之间通信的时候不需要关心对方到底在哪台机器上就像在同一台主机上通信一样简单。举个例子你在两台服务器上分别部署了A服务和B服务A需要向B发一条消息。如果用TCP你得知道B的IP地址和端口号还得处理连接建立、断线重连、粘包拆包。但用TIPCA只需要知道B对外提供的“服务名”即可底层走哪条链路、节点之间怎么建立连接都是协议栈自动搞定的事情。这种设计让TIPC特别适合集群控制面通信、分布式锁协调、状态同步这类场景。很多高性能计算领域的集群管理软件节点之间的心跳和状态上报就是走TIPC完成的。入门的时候不要求你马上玩得多深但理解这个定位能帮你避开一个常见误区不要把TIPC当成TCP的替代品去用它是解决集群内部通信问题的一套专门方案。1.2 和TCP/IP比TIPC赢在哪些场景很多人第一次接触TIPC都会问用TCP不就行了吗这个问题的答案决定了你后面怎么学。看一组对比你就能理解两者的定位差异。TCP是面向连接的字节流协议你得先建立连接然后像读写文件一样读写数据。它工作在IP层之上需要IP地址和端口号来定位目标。TIPC则不同它直接在链路层之上运行有一套自己的地址和命名机制发消息的时候指定对方提供的服务名不需要维护连接状态也不需要关心对方节点的IP。从性能角度看TIPC省去了TCP/IP协议栈的很多开销比如三次握手、拥塞控制、拆包组包这些环节。在集群内部这种低延迟高可靠的网络环境里TIPC的收发延迟和CPU开销都比TCP有优势。但要注意TIPC不是万能的。它不会替你解决跨网段通信的问题也不能替代公网场景下的传输协议。入门阶段你只需要记住TIPC诞生于集群内部通信它的优势集中在低延迟、可扩展、高可靠这几个方向。如果你的场景是跨地域或者公网环境老老实实用TCP不需要商量。1.3 入门需要具备的知识底子入门TIPC之前你不需要是内核专家但有几个底子建议先有至少熟悉Linux基本操作会装内核模块能看懂C语言的socket编程基础最好还写过一个简单的TCP服务端和客户端。如果没写过socket程序也不慌。TIPC的API虽然细节和TCP不太一样但整体结构还是socket那套框架socket创建、bind绑定、send/recv收发、close关闭。你把TCP的socket写法理解了再来看TIPC就是换几个结构体和参数的事。另外建议准备一个能跑起来的最小实验环境。两台Linux虚拟机就够了或者一台机器上起两个命名空间也行。我自己的经验是不要在单机上只用回环接口测试因为TIPC的一个核心特性是跨节点通信单机测试很多问题根本暴露不出来。2. 入门前的环境准备和概念对齐2.1 内核模块与工具链TIPC的主干实现是作为内核模块存在的。第一步先确认你的内核有没有这个模块。执行下面这条命令modprobe tipc如果没有任何输出说明模块加载成功。然后看模块确实加载进来没有lsmod | grep tipc正常情况下能看到一行tipc模块记录。这里有个小细节很多发行版默认没有自动加载TIPC模块所以每次重启后需要重新modprobe。如果嫌麻烦可以把tipc写进/etc/modules-load.d/下的配置里开机自动加载。然后确认用户态工具是否安装。TIPC的管理命令是tipc它用来配置网络、查看链路状态、查看服务表。执行which tipc看路径如果提示找不到装一下对应的工具包。装好后先看下版本和帮助tipc --help到这里环境就基本准备好了。我自己刚开始学的时候还犯过一个低级错误只在一台机器上加载了模块另一台没加载结果两边怎么都建不了链路折腾了半天才意识到是环境没齐。入门阶段先把两台机器的模块和工具都装好再往下走。2.2 TIPC的四个基本概念很多入门者卡住不是因为代码难而是因为TIPC的概念和TCP/IP对不上号。我建议先把四个基本概念搞清楚节点node、承载介质bearer、链路link和服务service。节点是参与TIPC通信的“机器”每台机器对应一个节点每个节点有一个唯一的节点ID和地址。承载介质是节点之间实际传输数据的物理通道最常见的就是以太网接口比如eth0。链路则是两个节点之间建立起来的数据通路链路建立后两个节点才能通信。服务可以理解为节点上的进程对外提供的通信入口别的节点上的进程可以通过服务名来找到它。你把这些概念想象成一个小型办公园区节点是园区里的一栋栋楼承载介质是连接各栋楼的马路链路是楼和楼之间的一条专用通道服务则是每栋楼里对外公布的“接待窗口”。一个进程想联系另一个进程不需要知道它在哪栋楼哪个房间只需要知道它的接待窗口编号就行了剩下的路由和通道选择都由园区系统搞定。2.3 先跑通一张精简的集群网络道环境准备好后第一步不是在IDE里写代码而是先在两台机器上把TIPC集群跑通看到链路起来再做应用开发。这一步能帮你排除掉大部分环境层面的问题。在每台机器上执行同样的命令设置集群网络IDtipc net set netid 12345网络ID的值可以自定义但你集群里所有节点必须一致。它的作用是给集群一个标识类似于给整个园区起个名字防止不同组网之间的TIPC报文串扰。接着启用承载介质。假设物理网卡是eth0tipc bearer enable media eth dev eth0这条命令是让节点从eth0这个接口上收发TIPC报文。注意如果集群里有多台机器每台都要执行这两步而且netid和承载接口必须一致。然后查看链路是否建立。在其中一台机器上执行tipc link list如果输出里能看到对端节点的信息和“UP”状态就说明链路已经建起来了。这时候你再看一下节点列表tipc node list看到两个节点都在基本信息就通了。我第一次搭的时候弄完这些看到UP状态说实话心里踏实了一半后面写代码调试的底气也足。3. 第一个TIPC程序从零跑通消息收发3.1 用C语言写一个最简服务端概念通了、链路起来了就可以动手写第一个TIPC程序了。这里我用C语言做一个最简服务端示例作用是等待客户端发消息并回显。TIPC的socket使用AF_TIPC地址族这是它和TCP最直观的区别。看下面这个服务端核心代码#include stdio.h #include string.h #include unistd.h #include sys/socket.h #include linux/tipc.h int main() { int fd socket(AF_TIPC, SOCK_RDM, 0); if (fd 0) { perror(socket); return 1; } struct sockaddr_tipc addr; memset(addr, 0, sizeof(addr)); addr.family AF_TIPC; addr.addrtype TIPC_ADDR_NAME; addr.addr.name.name.type 18888; addr.addr.name.name.instance 1; addr.scope TIPC_CLUSTER_SCOPE; if (bind(fd, (struct sockaddr *)addr, sizeof(addr)) 0) { perror(bind); return 1; } char buf[1024]; memset(buf, 0, sizeof(buf)); int len recv(fd, buf, sizeof(buf) - 1, 0); if (len 0) { printf(received: %s\n, buf); } close(fd); return 0; }这段代码有几个关键点。addr.addr.name.name.type是服务类型你可以理解成一个“业务编号”比如18888代表回显业务。instance是服务实例同一个业务可以启动多个实例客户端发送时可以指定要发给哪个实例。scope设置的是服务的可见范围集群范围内可见用TIPC_CLUSTER_SCOPE。这里最容易搞混的地方是TIPC的bind绑定的不是IP和端口而是服务名。服务端不需要关心自己机器的IP是什么也不需要关心客户端从哪里来只需要声明“我叫什么”其他节点就能通过这个名字找到它。3.2 用C语言写一个最简客户端服务端准备好了客户端就更简单了。客户端不需要bind直接创建一个socket然后向目标服务名发送消息。核心代码如下#include stdio.h #include string.h #include unistd.h #include sys/socket.h #include linux/tipc.h int main() { int fd socket(AF_TIPC, SOCK_RDM, 0); if (fd 0) { perror(socket); return 1; } struct sockaddr_tipc addr; memset(addr, 0, sizeof(addr)); addr.family AF_TIPC; addr.addrtype TIPC_ADDR_NAME; addr.addr.name.name.type 18888; addr.addr.name.name.instance 1; addr.scope TIPC_CLUSTER_SCOPE; const char *msg hello tipc; int ret sendto(fd, msg, strlen(msg) 1, 0, (struct sockaddr *)addr, sizeof(addr)); if (ret 0) { perror(sendto); return 1; } close(fd); return 0; }注意这里用的是sendto比TCP的send多了一个目标地址参数。因为在TIPC里socket本身并不维护到对端的连接每次发送消息的时候都要指定目标是哪个服务。跑的时候先起服务端再起客户端。服务端接收成功后终端会打印出received: hello tipc。这时候你就已经完成了第一个TIPC跨节点通信程序后面再往里加业务逻辑都是在这个框架上扩展。3.3 用Python快速体验如果不想一开始就碰C语言编译也可以用Python先快速验证环境。Python的标准socket库同样支持AF_TIPC只要内核已经加载了TIPC模块。最简服务端大概是这个样子import socket fd socket.socket(socket.AF_TIPC, socket.SOCK_RDM) addr socket.sockaddr_tipc() addr.addrtype socket.TIPC_ADDR_NAME addr.addr.name.name.type 18888 addr.scope socket.TIPC_CLUSTER_SCOPE fd.bind(addr) data, src fd.recvfrom(1024) print(received:, data) fd.close()客户端对应地写import socket fd socket.socket(socket.AF_TIPC, socket.SOCK_RDM) addr socket.sockaddr_tipc() addr.addrtype socket.TIPC_ADDR_NAME addr.addr.name.name.type 18888 addr.scope socket.TIPC_CLUSTER_SCOPE fd.sendto(bhello from python, addr) fd.close()用Python的好处是调试速度快不需要考虑编译链接的问题适合第一轮验证。但正式项目里如果需要极致性能还是建议用C或Go这类编译型语言。Python版本验证通过后再迁到C也不迟。4. 入门阶段最容易踩的坑4.1 服务发现与地址解析陷阱入门阶段踩得最多的坑就是服务名和寻址的问题。TIPC的寻址方式和TCP/IP完全不同TCP/IP靠IP和端口定位TIPC靠服务名。刚上手的人往往会下意识地去找“对方的IP填哪里”结果发现根本没有这个字段然后就容易填错。常见的一个错误是scope设置不对。TIPC有三种scopeTIPC_NODE_SCOPE只在节点内可见TIPC_CLUSTER_SCOPE在集群内可见TIPC_ZONE_SCOPE跨集群可见。如果你客户端和服务端在同一个集群但不同节点上服务端绑定用的是TIPC_NODE_SCOPE客户端自然找不到它。入门阶段建议无脑用TIPC_CLUSTER_SCOPE等理解清楚再调。另一个容易疏忽的点是服务类型type和实例instance的配合。同一个type下如果起了多个instance比如type18888有两个实例instance1和instance2客户端发给instance1只会到那个实例不会自动负载均衡。如果想让TIPC自动分发给同类型的多个实例需要把instance设成TIPC_ANY_INSTANCE这时候TIPC会在多个实例之间做分发。4.2 消息类型选择与包长上限TIPC的socket类型比TCP多入门者经常在这上面迷惑。我最常用的两种是SOCK_STREAM和SOCK_RDM。SOCK_STREAM是流式套接字语义和TCP类似保证字节流的可靠有序传输。SOCK_RDM是可靠数据报保证消息边界每条send对应一条recv不会出现TCP那种粘包问题而且消息本身是可靠的。很多写TCP的人刚上手会自然选SOCK_STREAM结果发现TIPC的流式套接字在某些行为上和TCP还是不太一样比如处理消息边界很别扭。我的建议是如果业务逻辑天然是“一条一条消息”的直接用SOCK_RDM如果确实需要流式传输大量数据再考虑SOCK_STREAM。还有一个坑和包长有关。TIPC对单条消息长度有限制超过上限会返回EMSGSIZE。我遇到过一个场景业务方要传一个几十KB的配置块直接用SOCK_RDM发结果一直失败。当时查了半天才发现是消息超过限制。解决思路有两个要么业务层做分片要么用SOCK_STREAM这种流式方式分多次发送。入门阶段写测试程序时先把消息控制在几百字节内先把通道跑通再谈大消息。4.3 多网卡、域配置与扩容问题服务器一般都有一张以上的网卡这在TIPC里也会带来麻烦。多个网卡意味着你可以启用多个bearer但如果配置不对节点之间可能走了一条不期望的链路。最典型的问题是集群里不同机器各有多个网卡结果TIPC在两台机器之间建立了多条不必要的链路管理起来很乱。入门阶段最省心的做法是只启用一张网卡作为TIPC承载。等通信逻辑稳定了再根据实际网络拓扑考虑多承载的冗余方案。不要一开始就搞多bearer否则你排查链路问题时会把大量时间耗在物理网络配置上。集群扩容也值得提一句。新节点加入集群时要保证netid一致、承载介质一致。如果新节点配置的netid和已有集群不一致链路根本起不来。实际排查的时候用tipc link list和tipc node list先看状态通常能快速定位是配置问题还是物理网络问题。4.4 新手常问的几个问题速查我把新手问得最多的问题整理成一张表方便对照查阅。问题原因与对策socket(AF_TIPC, ...)失败内核模块未加载先执行modprobe tipcbind总是返回地址已被占用同一个节点上服务typeinstance组合已经被人占用换一个实例号客户端sendto成功但服务端收不到检查scope设置、检查链路是否UP、确认两台节点netid一致服务端能收但回显发送失败回显给客户端时要注意目标地址是收到消息的来源地址不是原始服务名消息接收不完整检查包长是否超过TIPC限制或者用了SOCK_STREAM却没处理流边界在单机上测试无法通信TIPC的设计目标是多节点集群环境不要用单机回环做跨节点验证至少起两个命名空间或虚拟机上面这些问题的排查顺序我建议先看链路再查服务最后抓网络报文。链路都没建立后面的一切都是空的。命令行界面用tipc工具逐步排查比直接看代码高效得多。5. 入门之后的路怎么走以及我个人踩坑后的体会如果你跟着上面的示例跑通了第一个TIPC程序恭喜你入门算是完成了。接下来值得深入的方向有三个一是研究TIPC的link状态机理解链路建立、断开、恢复的机制这在做高可用集群时非常重要二是搞懂TIPC的服务表同步机制服务名在集群内是怎么广播、怎么发现、怎么失效的三是读一读TIPC的socket实现源码理解SOCK_RDM和SOCK_STREAM在底层是怎么处理缓冲和重传的这在调优时能帮上大忙。最后说点我自己的体会。我第一次跑通TIPC的时候犯的错误几乎都集中在“用TCP的思维写TIPC”找IP、填端口、bind到地址、按字节流处理。后来想明白了一件事TIPC最核心的理念是让通信对位置无关地址是协议栈维护的概念不是应用层该操心的事。你越是能放下TCP的习惯越是能体会到TIPC在处理集群内服务通信时的顺手程度。建议入门阶段不要指望看文档就能学会。文档只能告诉你接口长什么样真正的手感是在把服务端和客户端分别部署到两台机器、看着tipc link list显示UP、把第一条消息从A节点发到B节点的时候建立起来的。如果你在动手过程中也踩了什么有趣的坑那是好事带着问题回头看协议和源码理解会深很多。

相关新闻

重读RFC 793:TCP协议底层行为与状态机实现精要

重读RFC 793:TCP协议底层行为与状态机实现精要

1. 为什么今天还要亲手翻RFC 793?不是有现成译文吗?“RFC 793翻译(TCP的主体内容)”——看到这个标题,很多刚接触网络协议的人第一反应是:网上早就有中文版了,GitHub上搜一搜、CSDN点开就看&…

2026/10/18 4:38:32 阅读更多 →
图像相似度匹配实战:从特征向量到向量检索的完整方案

图像相似度匹配实战:从特征向量到向量检索的完整方案

我第一次被图像相似度匹配这个技术按在地上摩擦,是在一个UI自动化测试项目里。当时需求很简单:把待测页面截图和历史基线截图丢给程序,告诉我这两个页面是不是同一个页面。我第一反应是直接逐像素对比,结果被动态文案、加载动画、…

2026/10/18 4:37:32 阅读更多 →
UE5 UPROPERTY meta元数据:显示名、条件编辑与提示

UE5 UPROPERTY meta元数据:显示名、条件编辑与提示

做项目的时候,肯定都遇到过这类场景:C 里变量名为了见名知义,写得又长又规范,比如 WeaponAmmoCapacity、bEnableScatter;结果到了蓝图侧,策划看到的就是一串英文驼峰加下划线,还经常要反复问“这…

2026/10/18 4:37:32 阅读更多 →

最新新闻

国庆AI行业盘点:模型更新、开源生态与智能体落地加速

国庆AI行业盘点:模型更新、开源生态与智能体落地加速

国庆长假这两天,AI圈跟过年似的。10月1日到10月8日,前后八天,不断有模型更新、开源仓库爆点、智能体应用密集落地。作为一个长期跟踪这条赛道的人,我几乎每天都要刷好几遍测试榜单和社区讨论串,很多信息还没来得及沉淀…

2026/10/18 5:37:42 阅读更多 →
程序员必备20个网站:技术阅读、刷题、接单与效率工具盘点

程序员必备20个网站:技术阅读、刷题、接单与效率工具盘点

程序员必备网站这个老话题,几乎每隔几个月就会被重新拿出来聊一轮。技术、接单、刷题这三类网站,每个开发者的收藏夹里都躺着不止一个,但真到要用的时候,很多人会发现:收藏了 300 个网站,能稳定打开、准时解…

2026/10/18 5:37:42 阅读更多 →
全大写提示词触发AI风控?解析Claude互审封禁真相与安全写法

全大写提示词触发AI风控?解析Claude互审封禁真相与安全写法

有意思。“让Claude管Claude”本身没问题,问题是这句提示词是用全大写敲出来的。我是在一个技术讨论群里看到这件事的:某人做了个自动化流程,让一个Claude会话去审核另一个Claude会话生成的文案,这本来是个很常见的质检思路&#…

2026/10/18 5:37:42 阅读更多 →
Image GPS Editor实战:照片GPS数据的读取、修改与批量处理

Image GPS Editor实战:照片GPS数据的读取、修改与批量处理

前阵子帮朋友整理一趟长途自驾的素材,几百张照片导进电脑,结果发现相机里明明开了GPS记录,出来的照片却有一大半坐标信息是空的。另一批用手机拍的照片倒是带位置,但其中有几张因为当时在船上漂着,定位偏到了海里。用看…

2026/10/18 5:37:42 阅读更多 →
变量之路:理清编译与链接中的符号、地址与重定位机制

变量之路:理清编译与链接中的符号、地址与重定位机制

先问一个问题:你在 IDE 里写了int counter 0;,点一下运行按钮,程序跑起来了。可是那个叫counter的变量,它的地址到底是谁给它的?它为什么能从一个名字变成一个真实存在于内存里的地址?我在带新人的时候发现…

2026/10/18 5:37:42 阅读更多 →
律所案件管理系统设计与落地:角色权限、流程引擎与文书协同实践

律所案件管理系统设计与落地:角色权限、流程引擎与文书协同实践

1. 这个系统到底在解决什么问题做律师的朋友应该都有过这种体验:一个案子从收到材料到最终归档,中间要经过立案、排期、取证、文书起草、审核、盖章、送达、开庭、归档这么一串流程。传统做法是什么?微信群接龙、Excel表格、共享文件夹、Outl…

2026/10/18 5:36:42 阅读更多 →

日新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 0:55:02 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 14:23:36 阅读更多 →