本机通信选型:回环IP与Unix域套接字的性能与原理剖析
做后端服务这几年我越来越觉得“本机通信”这件事被很多人低估了。大家写微服务、写中间件天天跟端口、IP 打交道但一说到本机进程间通信往往默认走127.0.0.1:8080这种形式很少有人仔细琢磨过同样是两个进程在同一个内核里交换数据走“本机 IP”和走“域套接字”底层收发包流程到底差在哪性能差多少选型时又该看什么。今天这篇文章就围绕这个主题展开把“本机 IP 收发包”和“域套接字收发包”两条路径从内核到用户态完整拆开对比一下各自的开销、适用场景和排查手段。内容偏底层但我会尽量用通俗的方式讲清楚适合对网络编程、中间件原理、性能调优感兴趣的同学阅读。1. 先搞清楚本机通信为什么有两条主流路径1.1 两种收发包路径的本质差异先说结论本机通信有两条完全不同的路径。第一条是TCP/IP 127.0.0.1回环。它本质上还是走完整的网络协议栈只不过所有数据都停留在内核里物理网卡根本不参与。另一条是Unix 域套接字也叫 Unix Domain Socket通常用AF_UNIX表示它把通信双方直接挂在内核的 socket 机制上像读写管道一样交换数据完全绕开 IP、路由、校验和这些网络层的麻烦事。很多同学会觉得“本机走 localhost 很快”其实真不是。回环路径虽然没有网卡延迟但协议栈该走的步骤一步不少TCP 要握手、要维护序列号、要算校验和IP 层要查路由链路层要经过lo这个虚拟网卡接口。而域套接字就没这么多破事数据从进程 A 的发送缓冲区直接进到内核里一个 skb再挂到进程 B 的接收队列中间不经过任何“网络”语义。1.2 为什么这个问题值得深挖搞清楚这两种路径的价值不只是性能对比更重要的是影响你的架构决策。比如你在设计一个本地 IPC 组件既要支撑高并发小报文又要保证数据不丢、顺序不乱那域套接字几乎是首选。但如果你希望业务代码未来能平滑迁移到跨主机部署或者团队已经基于 HTTP/TCP 封装了大量工具链那这时候用127.0.0.1回环反而更省事。另外排查问题的时候这两种路径的“可观测性”完全不同。127.0.0.1上的流量可以用 tcpdump 看到而域套接字的流量在 tcpdump 里完全是透明的。这个差异我后面会专门展开实战中出现过不少次因为不了解这个特性导致的误判。2. 本机 127.0.0.1 收发包数据在内核里走了一条完整的“长征路”2.1 从 send 到 recv 的完整路径解剖假设进程 A 要向进程 B 发一段数据用的是socket(AF_INET, SOCK_STREAM)建立的 TCP 回环连接内核里大体要经过这么一串环节进程A用户态 send() → 内核 socket 层 → TCP 发送路径 tcp_sendmsg → 把用户数据拷贝到 skb 并挂到发送队列 → IP 层 ip_queue_xmit查路由表 → 路由发现目标 127.0.0.1 属于 lo 接口 → 链路层 dev_queue_xmit 交给 loopback 网卡驱动 → loopback_xmit 不写硬件直接把 skb 喂回接收路径 → netif_rx / ip_rcv → TCP 接收路径 tcp_v4_rcv → 把 skb 放进进程B的socket接收队列 → 唤醒进程B → 进程B用户态 recv() 拷贝数据乍一看链路也不算长但每一层都有实际成本。TCP 层要处理序列号、确认号、窗口管理、可能的重传和拥塞控制IP 层要查路由表、设置 IP 头链路层要经过一个“虚拟网卡”。这些工作全部由 CPU 完成而且是实打实的内核态开销。值得一提的是回环接口lo的 MTU 通常默认是65536所以本机 TCP 传输基本不需要 IP 分片但 TCP 分段Segmentation的逻辑仍然参与工作。早期内核里所有分段在 CPU 里做现在虽然有一些 offload 机制但 loopback 本身没有硬件最终还是 CPU 在算。2.2 为什么回环 IP 比想象的慢三个隐藏开销第一个隐藏开销是内存拷贝。send()时用户态数据要拷贝到内核 skbrecv()时 skb 数据要拷贝回用户态缓冲区。如果传输大块数据内核还可能因为 skb 组织方式产生额外拷贝。内存拷贝在本地通信里往往是最大开销来源之一真不是网络栈那点协议逻辑。第二个隐藏开销是上下文切换和唤醒。进程 A 调用send()是一次用户态到内核态的切换进程 B 收到数据后从睡眠中唤醒、执行recv()又是一次上下文切换。一来一回至少 4 次切换。高频小报文时这个开销会非常扎眼CPU 占用高才是真凶。第三个隐藏开销是协议栈状态机的“例行公事”。比如 TCP 建立连接要三次握手。虽然 loopback 没有 RTT握手过程在几十微秒内完成但状态机还是要老老实实走一遍。再比如每个 TCP 段都要算校验和本机场景虽然无需网卡校验但 Linux 在软件层还是会处理这些 CPU 周期都是白花花的成本。2.3 loopback 内核处理路径里的优化措施为了弥补无硬件网卡的尴尬Linux 对 loopback 做过一些优化。最典型的是loopback_xmit后不再走硬中断而是直接进入netif_rx让接收路径尽可能在当前进程上下文里执行。这意味着发送进程在系统调用返回前接收路径可能已经跑了一部分而不是像真实网卡那样要等异步中断。此外现代内核还针对本机 TCP 做了一些 fast path 优化比如跳过部分拥塞控制逻辑或者利用SO_BUSY_POLL让接收方忙等减少唤醒延迟。但必须提醒这些优化改变不了协议栈的整体开销量级。127.0.0.1确实比跨机网络快几个数量级但它和域套接字比仍然是“重量级选手”。3. 域套接字收发包脱掉网络外套的轻装快跑3.1 域套接字与网络套接字的根本区别域套接字和 TCP/UDP 套接字最重要的区别是它压根不基于“网络”概念设计。数据从进程 A 到进程 B不经过 IP 层、不查路由、不依赖网卡接口CPU 不需要组装 IP 头也不需要校验和。在 Linux 上域套接字有两种寻址方式文件系统路径名和抽象命名空间。用文件路径时bind()会在文件系统里创建一个 socket 文件例如常见的/var/run/docker.sock、/tmp/php-fpm.sock。用抽象命名空间时路径以\0开头不产生任何文件系统痕迹适合一些不想留下文件、不想被权限约束的敏感场景。从编程模型看域套接字同样支持SOCK_STREAM流式和SOCK_DGRAM数据报流式接口语义类似 TCP但不具备 TCP 的重传、拥塞控制、keepalive 等机制它的可靠性来源是“同一个内核内传递不会从物理链路上丢失”。数据报接口则保留了消息边界跟 UDP 一样一次sendmsg对应一次recvmsg读取完整消息但不会像 UDP 那样乱序或丢包。3.2 流式域套接字的完整收发流程我用AF_UNIX, SOCK_STREAM为例描述一条完整的发送-接收链进程 A 通过socket(AF_UNIX, SOCK_STREAM, 0)创建套接字进程 B 监听一个 sock 文件路径。A 调用connect()。这里没有三次握手内核只需要找对监听套接字双方就建立连接速度极快。A 调用send()或write()。内核执行unix_stream_sendmsg()将用户数据拷贝进一个 skb然后把这个 skb 追加到对端 socket 的接收队列。内核唤醒正在recv()上睡眠的进程 B。B 调用recv()或read()内核从接收队列剥离 skb把数据拷贝到 B 的用户态缓冲区。这个链路上没有 TCP 层、没有 IP 路由、没有虚拟网卡数据从 A 的用户态进入内核再从内核进入 B 的用户态总共就两次拷贝。协议开销被压缩到极致。相比回环 TCP 少了一大堆“纯消耗”的环节性能自然好得多。为了更直观我给一条实测路线的量级作为参考在主流 Linux 内核、普通 x86 服务器上小报文的回环 TCP 单次往返延迟通常在 20-50 微秒而域套接字多在 5-15 微秒。CPU 占用上域套接字往往能省一半甚至更多。不同的机器和内核版本结果会有浮动但量级差距是公认的。3.3 数据报与文件描述符传递域套接字还有一个让网络套接字羡慕的能力通过sendmsg()的辅助数据ancillary data在进程之间传递文件描述符。比如父进程可以把自己的日志文件 fd 传给子进程或者让一个服务把自己的监听 socket fd 传给另一个进程这在 Java、Go、C 的进程管理器、服务热升级场景里非常常见。这个能力是 TCP/UDP 做不到的。另外SOCK_DGRAM的域套接字每个报文使用独立 skb天然保留消息边界。在同一个机器上做“消息队列式”通信时很合适。需要注意的是虽然是数据报但不会丢包。如果接收缓冲区满发送方会被阻塞或者返回EAGAIN这取决于 socket 是否设置成非阻塞。这种语义有利于可靠本地通信。3.4 域套接字的一些“坑”域套接字虽然轻快但不是没有短板。首先基于文件路径的域套接字受文件系统权限控制如果目录权限设置错误可能连接不上另外 socket 文件是持久化在磁盘上的如果程序异常退出可能残留一个“幽灵”sock 文件下次 bind 时会报“Address already in use”需要手动清理。其次域套接字的路径长度上限是108字节sun_path数组长度限制路径别设计得太深太长。最后由于不经过 TCP/IP你无法直接利用tcpdump -i lo这类工具抓包排查手段和网络套接字完全不一样。4. 对比带着需求做选型什么时候用域套接字什么时候用本机 IP4.1 从性能与功能两个维度对比做一个简洁的对照方便读者直接参考维度127.0.0.1 回环 TCP域套接字 AF_UNIX协议栈开销完整 TCP/IP 协议栈重极简仅 socket 收发队列数据拷贝用户态↔内核态约 2 次以上通常 2 次流程更短小报文延迟几十微秒量级个位数十微秒量级CPU 占用高协议处理密集低很适合高频小包跨主机能力支持可直接扩展不支持只能本机抓包观测tcpdump 可直接抓 lo 流量tcpdump 不可见需用 ss/lsof权限控制基于端口与防火墙可基于文件系统权限额外能力无支持传递 fd、抽象命名空间性能之外还有一个容易被忽略的点回环 TCP 有完整的端口生命周期管理。如果服务重启端口可能进入TIME_WAIT影响新连接绑定。域套接字则没有这个烦恼但反过来如果进程 crash 导致 sock 文件残留又会产生另一个问题。所以选型的时候不要只看峰值速度工程上的可维护性同样重要。4.2 场景化选型建议如果你要设计的是高性能本地 IPC比如收集 agent 和主进程之间的遥测数据通道、本地缓存服务的请求管道、开发工具里父子进程通信我一般直接建议用域套接字流式接口。延迟低、开销小而且不占端口、不容易被扫描到在单机场景几乎是天然最优解。如果你的代码将来要变成跨主机服务或者团队已经积累了基于 TCP 的协议封装、连接池、重试框架那还是老老实实走127.0.0.1回环。虽然牺牲一点本机性能但换来的是部署形态的可迁移性这个权衡在架构里经常是对的。还有一类场景建议用回环 TCP需要利用 TCP 连接状态探测对端是否存活时。本机场景下进程 crash内核会立刻关闭 fd这个优势其实没那么明显但如果涉及代理、网关等网络中间件保持“协议一致”会减少很多麻烦。我自己的习惯是凡是确定只在本机通信的优先域套接字然后在配置里把 sock 路径统一管理凡是有远程化演进可能或者需要和外部系统无缝对接的走 TCP loopback。这些年用过 nginx 的 fastcgi、redis 的 unix socket 配置都是这个思路。4.3 内存拷贝不是唯一因素后面我会详细说观测方法但选型时千万别只盯着“内存拷贝次数”。域套接字省下的是协议栈逻辑、校验和计算、路由查找、虚拟网卡调度等成本这些在长连接大流量场景下体现得非常明显。但反过来如果业务是低频少量请求比如每分钟几次健康检查那选哪种方案在性能上毫无差别此时反而应该优先考虑代码复用和运维习惯。5. 实际观测与问题排查别靠猜直接看内核表现5.1 用 strace 观察收发包系统调用排查本机通信最快的方法是strace。分别跟踪一个用回环 TCP 的进程和一个用域套接字的进程系统调用序列差异很直观。对于回环 TCP你会看到socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) 3 connect(3, {sa_familyAF_INET, sin_porthtons(8080), sin_addrinet_addr(127.0.0.1)}, 16) 0 sendto(3, hello, 5, 0, NULL, 0) 5对于域套接字你会看到socket(AF_UNIX, SOCK_STREAM, 0) 3 connect(3, {sa_familyAF_UNIX, sun_path/tmp/app.sock}, 110) 0 sendto(3, hello, 5, 0, NULL, 0) 5注意AF_INET和AF_UNIX的差别以及目标地址形式的不同。如果你发现某个进程意外阻塞在sendto或connect上下一步就该查接收端是不是没在recv或者接收队列是不是满了。5.2 查看套接字状态的正确姿势查看 TCP 回环连接可以用ss -tnp比如ss -tnp | grep 127.0.0.1。看域套接字则要用ss -xnp输出里会列出路径、uid、inode 等信息。举个例子$ ss -xnp | grep app.sock u_str ESTAB 0 0 /tmp/app.sock 12345 * 12346这里u_str表示流式域套接字ESTAB表示连接建立。如果你看到大量套接字处于非 ESTAB 状态或者 inode 对应的进程已经不存在就要警惕残留问题。要特别说明的是抓包工具tcpdump -i lo只能看到127.0.0.1的流量看不到AF_UNIX的数据。因为 tcpdump 基于网络协议栈的 hook 点工作而域套接字数据根本不经过 ARP、IP、TCP/UDP 这些层。很多人第一次排查域套接字时拿着tcpdump抓了半天什么都抓不到怀疑网络有问题实际上方向就错了。正确做法是用strace看系统调用、用ss -x看内核 socket 状态、用lsof -U看哪个进程占用了 sock 文件。5.3 常见问题速查现象排查方向常见解决办法connect 域套接字报 No such file or directorysock 文件不存在或路径错误检查程序 bind 和 connect 的路径是否一致bind 报 Address already in use上次退出残留了 sock 文件删除残留文件或用抽象命名空间服务端 recv 一直阻塞客户端未发送或发送队列异常strace 客户端检查 sendto 返回值客户端 send 阻塞服务端未及时 recv接收队列占满调大 SO_SNDBUF/SO_RCVBUF优化消费速度抓不到域套接字流量域套接字不经过 IP 协议栈改用 ss -x / lsof -U / strace回环 TCP 出现大量 TIME_WAIT短连接过多端口回收慢开启 tcp_tw_reuse或改用长连接 / 域套接字一个额外的小技巧是用抽象命名空间的时候路径以\0开头ss -x的输出会显示成name的形式。抽象 socket 不会在文件系统里留文件所以不存在残留文件导致 bind 失败的问题但可观测性更弱。如果你在/tmp下找不到 sock 文件却能在ss -x里看到开头的条目那就是抽象套接字。6. 一些实操中的心得体会这几年代码写下来我最大的感受是技术选型没有绝对的优劣域套接字和本机 IP 的关系也不是“谁替代谁”而是一对互补方案。它们之间的分界线很清晰难的是搞明白自己到底需不需要那份极致性能以及有没有能力承担相应的运维负担。另外我还想强调“调试思维”的重要性。遇到本机通信变慢第一反应不该是怀疑协议栈而是先用strace看系统调用时间用ss看队列积压。很多时候问题出在应用层接收方消费不及时导致队列堆积发送方在缓冲区写满后被动阻塞这种场景下换域套接字只能缓解症状根本解法是调整消费速率。最后分享一个具体经验在 Linux 上不要把基于文件路径的域套接字放在/tmp这种全局可写的目录里因为权限太宽泛容易被其他用户干扰。我通常会在/var/run/app/下单独建目录设置好属主和权限。这个习惯帮我避免过不少诡异问题。本机收发包流程看起来是底层的小事但细节里的坑往往最耗时值得多花点心思。

相关新闻

自演化具身智能实战:核心矛盾、技术路线与工程闭环

自演化具身智能实战:核心矛盾、技术路线与工程闭环

一个能在围棋棋盘上碾压世界冠军的AI,放进真实厨房里连煎个鸡蛋都搞不定——这不是段子,而是过去几年"具身人工智能"(Embodied AI)领域最扎心的事实。所谓具身,就是要让智能体拥有身体,能感知、能…

2026/10/9 8:35:28 阅读更多 →
Node.js高并发优化:文件描述符(FD)排查与调优实战

Node.js高并发优化:文件描述符(FD)排查与调优实战

1. 先搞清楚:文件描述符到底是什么,为什么能卡住高并发 很多人写Node.js服务,压测的时候遇到瓶颈,第一反应就是查CPU、查内存、查数据库慢查询,很少有人会第一时间想到文件描述符(File Descriptor&#xff…

2026/10/9 8:34:26 阅读更多 →
Docker + Ollama + Open WebUI + Qwen3:本地私有化大模型部署全指南

Docker + Ollama + Open WebUI + Qwen3:本地私有化大模型部署全指南

先说结论:如果你想在本地或内网环境里,用 Docker 一键把开源大模型跑起来,再做一套像 ChatGPT 一样的网页聊天界面,那 Docker Ollama Open WebUI Qwen3 这套组合,是当前最省心、迁移成本最低的方案之一。Ollama 负责…

2026/10/9 8:34:26 阅读更多 →

最新新闻

cmux多路复用调度工具:连接池、路由匹配与配置管理实战

cmux多路复用调度工具:连接池、路由匹配与配置管理实战

1. 从“cmux”这个名字说起:它到底想解决什么问题第一次看到“cmux”这个词,很多人会下意识地把它拆成“c”和“mux”两部分。mux 在工程领域是个老面孔,multiplexer 的缩写,意思是多路复用器——把多路信号合并到一条通道上传输&…

2026/10/10 12:41:24 阅读更多 →
系统架构师备考:用形象记忆法巧记11个高频网络端口号

系统架构师备考:用形象记忆法巧记11个高频网络端口号

备考系统架构师的时候,最让人头疼的往往不是那些宏观的架构设计原则,反而是网络基础里一堆零散的数字。协议端口号就是典型代表。上午的客观题里经常出现,案例分析里也可能冷不丁问一句“某服务默认端口是多少”,偏偏这玩意儿死记…

2026/10/10 12:41:23 阅读更多 →
Talend开源版实战指南:从安装到源码构建的ETL数据集成之路

Talend开源版实战指南:从安装到源码构建的ETL数据集成之路

简介:这份资源是一份与 Talend Open Studio 相关的源码包,面向希望了解或部署这款开源数据集成 ETL 工具的开发者与初学者。Talend 以可视化拖拽方式降低数据处理门槛,支持多数据库同步、数据清洗、筛选、Java 代码处理及导入导出&#xff0c…

2026/10/10 12:41:23 阅读更多 →
OpenClaw安装避坑指南:依赖锁定与CMake配置全解析

OpenClaw安装避坑指南:依赖锁定与CMake配置全解析

说实话,openclaw这个项目,我前前后后折腾了快两天。第一次编译失败是在周五晚上,屏幕上刷过一屏红色报错信息时,我差点把陪伴多年的老键盘拍进显示器里。第二次失败在周六凌晨一点,链接器告诉我缺少某个动态库的某个符…

2026/10/10 12:41:23 阅读更多 →
生产二进制与源码不一致?从反汇编到git历史还原一段被遗忘的代码

生产二进制与源码不一致?从反汇编到git历史还原一段被遗忘的代码

1. 引子:凌晨三点十七分的陌生告警凌晨 3 点 17 分 28 秒,几乎每隔一天,归档日志里就会出现一次同样的告警。时间稳定得吓人,前后误差不超过两秒。我在做历史日志迁移时接触到这批 2010 年的数据——sess_srv 是当时线上运行的会话…

2026/10/10 12:41:22 阅读更多 →
Spring Boot微服务与数据库:Java大厂面试全流程实战指南

Spring Boot微服务与数据库:Java大厂面试全流程实战指南

每次面试季结束,我都会收到很多类似的私信:Spring Boot背了一堆面试题,微服务的概念也说得头头是道,数据库索引原理张口就来,可一到真正的面试现场就卡壳。尤其是那种三轮起步的大厂Java面试,第一轮基础八股…

2026/10/10 12:40:19 阅读更多 →

日新闻

卫星轨道分类全解析:从LEO到GEO的选型逻辑与工程实践

卫星轨道分类全解析:从LEO到GEO的选型逻辑与工程实践

1. 从“卫星轨道分类”这个标题说起:为什么值得花时间搞懂第一次接触“卫星轨道分类”这个概念,很多人会觉得它离自己很远——不就是天上的星星怎么转吗?但如果你正在做航天任务规划、遥感数据接收、星座设计,甚至只是准备一场航天…

2026/10/10 0:00:39 阅读更多 →
Spring AOP 核心原理与实战:从概念到日志切面落地

Spring AOP 核心原理与实战:从概念到日志切面落地

1. 从一个真实痛点说起:为什么你的代码里到处都是重复逻辑刚入行那会儿,我写过一个用户管理模块,注册、登录、改密码、注销四个接口。每个接口里都塞了几乎一样的日志打印、参数校验、事务开启和提交。当时觉得没什么,能跑就行。直…

2026/10/10 0:00:40 阅读更多 →
Python招聘数据采集与分析可视化:从采集清洗到薪资技能城市可视化全链路

Python招聘数据采集与分析可视化:从采集清洗到薪资技能城市可视化全链路

简介:这是一套面向计算机相关专业学生与项目实战学习者的Python数据采集与分析可视化完整项目,以Boss直聘岗位数据为对象,适合用作毕业设计、课程设计或期末大作业。资源包共38个文件,约246KB,以13个py源码文件为核心&…

2026/10/10 0:00:40 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

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

2026/10/10 11:14:25 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

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

2026/10/10 1:36:08 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

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

2026/10/10 11:14:58 阅读更多 →

月新闻

我发现了一个新思路:用 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/10 5:23:50 阅读更多 →
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/9 21:32:20 阅读更多 →
黑夜航拍船只数据集训练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/10 10:38:42 阅读更多 →