HTTP协议实战避坑指南:从请求超时到连接池调优

1. 从一个“请求超时”的深夜说起

做后端开发这些年,跟 HTTP 打交道的时间比跟家人说话都多。白天写业务代码,晚上查线上问题,十次有八次最后都落到 HTTP 这一层。印象最深的是有一回凌晨两点被告警叫醒,说某个接口大面积超时,我爬起来查了半天,最后发现是上游服务返回的响应头里Connection字段配错了,导致连接池里的连接被提前关掉,后面复用的请求全在等一个已经死掉的 socket。那一刻我才真正意识到,HTTP 这东西看着简单,真出问题的时候,每一个字段、每一个状态码背后都有它的脾气。

这篇东西不是教科书,也不是 RFC 的翻译。我想聊的是这些年我在实际项目里踩过的 HTTP 相关的坑,以及那些文档里不会写、但线上环境一定会教你的经验。不管你是刚入行的新手,还是写了几年 CRUD 想往深了走的老手,只要你的系统还在用 HTTP 通信——几乎所有的 Web 系统都是——这些内容迟早会碰上。我会从协议本身的设计取舍讲起,一路聊到抓包排查、性能调优、缓存策略、连接管理这些实操层面的东西,尽量把“为什么这么设计”和“实际怎么用”串起来讲清楚。

2. HTTP 协议设计的核心取舍与版本演进

2.1 为什么 HTTP 是“无状态”的,这到底意味着什么

很多人背过“HTTP 是无状态协议”这句话,但真正理解它的人不多。无状态的意思是,服务器不会记住你上一次请求是谁、干了什么。每个请求都是独立的,服务器处理完就忘。这个设计在早期是为了让服务器实现简单、容易扩展——你加一台机器,不需要同步任何会话信息,直接就能扛流量。

但问题很快就来了。用户登录之后,下一个请求服务器怎么知道你是谁?于是有了 Cookie 和 Session。Cookie 是存在客户端的一小段数据,每次请求自动带上;Session 是存在服务端的用户状态,通过一个 Session ID 关联。这里有个关键取舍:Session 存在服务端,意味着你要么用粘性会话把同一个用户的请求都打到同一台机器,要么把 Session 集中存储(比如 Redis)。前者扩展性差,后者多了一次网络调用。

后来 JWT 流行起来,思路是把状态编码到 Token 里,服务端不存,每次请求带着 Token 自己验签。这又回到了“无状态”的路子上,但代价是 Token 一旦签发就无法主动失效,除非再引入一个黑名单——你看,状态这东西,你不想存,它也会以另一种形式回来。

实操心得:如果你的系统 QPS 不高、机器不多,Session 存 Redis 是最省心的方案。别一上来就上 JWT,JWT 的坑(续期、注销、体积大)比你想的多。

2.2 HTTP/1.1 的队头阻塞问题到底卡在哪

HTTP/1.1 默认开启持久连接,一个 TCP 连接可以发多个请求。但问题是,同一个连接上的请求必须串行处理——前一个响应没回来,后一个请求就只能等着。这就是所谓的“队头阻塞”(Head-of-Line Blocking)。

浏览器为了绕过这个限制,会对同一个域名开 6 个左右的并发连接。但连接本身有开销,而且服务端也要为每个连接维护状态。更麻烦的是,如果某个请求特别慢,它后面的请求全被堵住。我见过一个案例,某个页面加载慢,排查半天发现是一个统计上报的接口卡住了,把后面真正重要的业务请求全堵在连接里。

HTTP/2 的多路复用就是来解决这个问题的。它在同一个 TCP 连接上把请求拆成帧,不同请求的帧可以交错传输,互不阻塞。听起来很美好,但 HTTP/2 有个新的问题:TCP 层面的队头阻塞。因为所有请求都跑在一个 TCP 连接上,一旦发生丢包,TCP 的重传机制会让整个连接上的所有流都停下来等。这个问题要到 HTTP/3 用 QUIC(基于 UDP)才真正解决。

版本连接复用队头阻塞典型场景
HTTP/1.0每次新建无(因为串行)早期网页
HTTP/1.1持久连接应用层队头阻塞绝大多数现有系统
HTTP/2多路复用TCP 层队头阻塞现代 CDN、大厂内部
HTTP/3多路复用基本解决部分海外服务、新项目

2.3 从 HTTP/1.1 升级到 HTTP/2 的实际收益与代价

我在几个项目里推动过 HTTP/1.1 到 HTTP/2 的升级,说点实在的。收益方面,最明显的是请求数多的页面加载时间下降,因为多路复用省掉了连接建立和排队的时间。头部压缩(HPACK)对那种 Cookie 很大的站点效果显著,一个请求头从几 KB 压到几百字节,省下的带宽很可观。

但代价也有。首先,服务端和客户端都要支持,Nginx 要开http2,客户端库要跟上。其次,HTTP/2 的调试比 HTTP/1.1 麻烦,抓包工具对二进制帧的解析不如文本协议直观。还有一点容易被忽略:如果你的服务端是长连接、高并发,HTTP/2 的单连接特性可能让某个慢请求拖累整个连接,这时候反而需要做连接数的调优。

注意:升级 HTTP/2 之前,先确认你的负载均衡器和反向代理是否支持。有些老旧的硬件设备对 HTTP/2 的支持不完整,可能出现握手失败或降级。

3. 请求与响应里那些容易被忽略的细节

3.1 请求方法的选择不只是语义问题

GET、POST、PUT、DELETE、PATCH,这些方法在语义上有明确区分:GET 是安全且幂等的,POST 不安全也不幂等,PUT 和 DELETE 是幂等的。但实际开发中,很多人把所有写操作都用 POST,理由是“浏览器表单只支持 GET 和 POST”。这个理由在前后端分离的架构下已经不成立了,前端用 fetch 或 axios 可以发任何方法。

为什么要在意这个?因为幂等性直接影响你的重试策略。如果一个接口是幂等的,客户端超时后可以放心重试;如果不是,重试可能导致重复下单、重复扣款。我见过一个支付系统,因为把扣款接口设计成非幂等的 POST,结果网络抖动时客户端重试了三次,用户被扣了三笔钱。后来他们加了一个唯一的请求 ID 做去重,才解决这个问题。

所以我的建议是:写操作尽量设计成幂等的,用 PUT 代替 POST 做更新,用 DELETE 做删除。如果实在做不到幂等(比如创建订单),就在请求里带一个客户端生成的唯一 ID,服务端用它做去重。

3.2 状态码用对了,排查问题能省一半时间

状态码是 HTTP 里最被低估的部分。很多人返回 200 然后在 body 里写{"code": 500, "msg": "error"},这种做法在监控和排查时简直是灾难。因为你的网关、负载均衡、APM 工具都是按 HTTP 状态码来判断成功失败的,你返回 200,它们就认为一切正常,告警不会触发,链路追踪也看不出问题。

正确的做法是:能用状态码表达的就用状态码。400 表示客户端参数错误,401 表示未认证,403 表示无权限,404 表示资源不存在,429 表示限流,500 表示服务端内部错误,502 表示上游返回异常,504 表示上游超时。每个码都有明确的语义,网关和监控工具能直接识别。

状态码含义常见误用
200成功把错误也包在 200 里
301/302重定向该用 308 保持方法时用了 302
400请求错误参数校验失败返回 500
401未认证和 403 混用
403无权限未登录也返回 403
429限流限流时返回 503
502上游异常上游返回错误时直接透传 500
504上游超时超时和连接失败不区分

3.3 请求头和响应头里的关键字段

Content-Type决定了 body 怎么解析,application/json和application/x-www-form-urlencoded的处理方式完全不同。我见过前端发了 JSON 但没设Content-Type,后端按表单解析,结果所有字段都是 null,排查了半天。

Content-Length和Transfer-Encoding: chunked是二选一的。如果响应是流式的、长度不确定,就用 chunked。但有些老旧的客户端不支持 chunked,这时候要么缓冲全部内容算出长度,要么就分块发送。

Cache-Control控制缓存行为,no-cache不是不缓存,而是每次都要去服务端验证;no-store才是完全不缓存。这个区别很多人搞混,导致该缓存的没缓存,不该缓存的缓存了。

ETag和Last-Modified用于协商缓存。客户端带上If-None-Match或If-Modified-Since,服务端判断资源没变就返回 304,省掉 body 传输。对于静态资源,这个机制能大幅减少带宽。

实操心得:给静态资源加 hash 指纹(比如app.a1b2c3.js),然后设置Cache-Control: max-age=31536000,这样一年内浏览器直接用本地缓存,不用发请求。文件内容变了 hash 就变,自然失效。这是目前最省事的缓存策略。

4. 连接管理与性能调优的实战经验

4.1 连接池到底该设多大

连接池的大小是个经典问题。设小了,并发上来请求排队;设大了,服务端连接数爆掉,内存和文件描述符都不够用。我见过一个服务把连接池设成 500,结果数据库那边最大连接数只有 200,一压测就报“too many connections”。

一个经验公式是:连接数 = ((核心数 * 2) + 有效磁盘数)。但这个公式是针对数据库连接的,对于 HTTP 客户端连接池,还要考虑目标服务的处理能力和网络延迟。如果目标服务响应快(比如 10ms),那少量连接就能扛高 QPS;如果响应慢(比如 500ms),就需要更多连接来填满等待时间。

我的做法是先压测,从 20 个连接开始,逐步增加,观察 QPS 和响应时间的变化。当 QPS 不再上升、响应时间开始变长时,说明连接数已经够了,再加只会增加服务端压力。

4.2 超时设置:连接超时、读超时、写超时

超时设置是另一个容易踩坑的地方。很多人只设一个总的超时,但连接、读、写三个阶段的问题性质不同。连接超时通常是网络不通或服务没起来;读超时是服务端处理慢;写超时是发送请求体慢(比如大文件上传)。

我的建议是分开设置:连接超时短一点(比如 1-3 秒),因为 TCP 握手正常很快;读超时根据业务定,查询类可以 5 秒,写操作可以 10 秒;写超时看请求体大小,一般 5-10 秒。另外,重试次数不要太多,2-3 次就够了,而且只对幂等操作重试。

注意:超时时间不要设成一样的。如果连接超时和读超时都是 30 秒,那网络不通的时候你要等 30 秒才知道,白白浪费了 29 秒。分级设置能让故障发现更快。

4.3 长连接与短连接的取舍

短连接每次请求都要三次握手、四次挥手,开销大但简单。长连接省掉了握手开销,但服务端要维护连接状态,而且连接可能被中间设备(防火墙、NAT)悄悄断掉。

HTTP/1.1 默认是长连接,通过Connection: keep-alive保持。但服务端通常会设一个空闲超时,比如 60 秒没请求就关掉。客户端这边也要注意,如果连接池里的连接太久没用,可能已经被服务端关了,这时候发请求会报错。解决办法是设置一个比服务端空闲超时更短的客户端超时,定期清理空闲连接。

我遇到过一个问题:服务端设了 60 秒空闲超时,客户端连接池设了 90 秒,结果客户端复用一个已经关闭的连接,报Connection reset by peer。后来把客户端超时改成 50 秒,问题就没了。

5. 抓包排查与常见故障处理

5.1 用 curl 和浏览器开发者工具快速定位

排查 HTTP 问题,第一步永远是看请求和响应。curl -v能打印出完整的请求头、响应头和 body,加上-w还能输出耗时分解。比如curl -w "@curl-format.txt" -o /dev/null -s https://example.com,可以看 DNS 解析、TCP 连接、TLS 握手、首字节、总耗时各占多少。

浏览器开发者工具的 Network 面板更直观,能看到每个请求的瀑布图、请求头、响应头、body。我经常用它来确认前端发的请求到底长什么样,因为很多时候问题出在前端多带了某个头,或者 body 格式不对。

5.2 常见问题速查表

现象可能原因排查方向
502 Bad Gateway上游服务挂了或返回异常检查上游健康状态、日志
504 Gateway Timeout上游处理超时检查上游耗时、超时配置
连接被重置连接被中间设备断开检查 keep-alive 超时、防火墙
请求体丢失Content-Length 不对检查代理是否修改了 body
缓存不生效Cache-Control 配置错误检查响应头、CDN 配置
跨域失败CORS 头缺失检查 Access-Control-* 头
重复提交非幂等接口被重试加请求 ID 去重

5.3 一个真实的排查案例

有一次线上反馈某个接口偶尔返回 400,但前端说参数没问题。我抓包发现,请求头里的Content-Type是application/json; charset=utf-8,但服务端框架只认application/json,多了个 charset 就解析失败。这个问题在测试环境没出现,因为测试环境的框架版本不同,对 charset 的容忍度更高。

这个案例告诉我两件事:一是环境差异会导致问题只在线上出现,二是 HTTP 头的解析比想象中严格。后来我在网关层加了一个统一处理,把Content-Type规范化,问题就再没出现过。

实操心得:在网关或反向代理层做请求头的规范化,比在每个服务里处理要省事得多。比如统一去掉 charset、统一大小写、统一补全缺失的头。

6. 缓存策略与安全相关的那些事

6.1 强缓存与协商缓存的配合

强缓存用Cache-Control: max-age和Expires,在有效期内浏览器直接用本地副本,不发请求。协商缓存用ETag和Last-Modified,每次发请求但服务端可能返回 304,省掉 body。

实际使用中,静态资源用强缓存加 hash 指纹,API 响应用协商缓存或no-store。对于频繁变化的数据,比如用户余额,绝对不能缓存,要设Cache-Control: no-store。对于不常变的配置数据,可以设一个短的 max-age,比如 60 秒,平衡实时性和性能。

6.2 HTTPS 不是万能药,但不用万万不能

HTTPS 解决了传输层的加密和身份验证,但应用层的安全问题它管不了。比如 SQL 注入、XSS、CSRF,这些跟 HTTPS 没关系。但 HTTPS 是基础,没有它,Cookie 可以被中间人窃取,请求内容可以被篡改。

配置 HTTPS 有几个关键点:用 TLS 1.2 以上,禁用弱加密套件,开启 HSTS 强制浏览器用 HTTPS,证书要定期更新。我见过因为证书过期导致整个站点不可用的案例,所以证书到期提醒一定要设。

6.3 安全相关的响应头

X-Content-Type-Options: nosniff防止浏览器猜测内容类型,X-Frame-Options: DENY防止页面被嵌入 iframe,Content-Security-Policy限制资源加载来源,Strict-Transport-Security强制 HTTPS。这些头加上去不费事,但能挡掉一批常见的攻击。

注意:CSP 配置不当会导致页面样式或脚本加载失败,建议先用Content-Security-Policy-Report-Only模式观察一段时间,确认没有误报再正式启用。

7. 我个人在实际操作中的体会

HTTP 这东西,入门容易精通难。表面上看就是发请求收响应,但每一个字段、每一个状态码、每一个超时设置背后都有它的设计意图和适用场景。我这些年最大的体会是:不要凭感觉配置,要看数据、看日志、看抓包。很多问题在代码层面看不出来,一抓包就一目了然。

另外,HTTP 的很多问题不是 HTTP 本身的问题,而是使用方式的问题。比如连接池设太大、超时不分级、状态码乱用,这些都是人为的。把基础的东西用对,比追求新版本、新特性要重要得多。HTTP/3 再好,如果你的 HTTP/1.1 都没用明白,升级也只是换个地方踩坑。

最后分享一个小技巧:在服务里加一个中间件,记录每个请求的方法、路径、状态码、耗时、请求大小、响应大小。这些数据积累起来,能帮你发现很多潜在问题,比如某个接口响应越来越大、某个状态码出现频率变高。日志不用多花哨,关键是持续记录和定期回顾。

相关新闻

大模型涌现能力:统计现象还是因果机制?一套分析框架

大模型涌现能力:统计现象还是因果机制?一套分析框架

这两年,只要聊大模型,就绕不开“涌现能力”这个词。我最早听到它时,第一反应是:这不就是把复杂性系统里的“涌现”概念搬到机器学习里了吗?后来看得越多越发现,这个词已经被用得非常泛,甚至互相…

2026/10/21 6:06:26 阅读更多 →
用OpenCV实现答题卡自动判卷:图像处理全流程解析

用OpenCV实现答题卡自动判卷:图像处理全流程解析

考试收尾的时候,最耗人的往往不是算总分,而是对着几摞答题卡一道道圈对错。尤其选择题多的科目,眼睛稍微一飘,就可能把A看成B,把C记成D。我试过用 Python OpenCV 做了套答题卡自动判卷工具,学生交上来的扫…

2026/10/21 6:05:26 阅读更多 →
知识工作插件化:解决信息碎片化与笔记检索难题的工作流方案

知识工作插件化:解决信息碎片化与笔记检索难题的工作流方案

做知识管理这些年,我最大的体会是:真正让人效率倍增的,往往不是某个大而全的软件,而是一群小而精的插件。今天想分享的这套 knowledge-work-plugins,就是围绕“知识工作”这个场景整理出来的插件思路与实操方案。它不是…

2026/10/21 6:05:26 阅读更多 →

最新新闻

Linux僵尸、孤儿与守护进程:原理、区别与实战实现

Linux僵尸、孤儿与守护进程:原理、区别与实战实现

1. 项目概述&#xff1a;从“进程尸体”到“后台幽灵”的真实世界映射在 Linux 系统里&#xff0c;你执行ps aux | grep defunct时看到的<defunct>标记&#xff0c;不是报错&#xff0c;而是一具“进程尸体”——它已经死亡&#xff0c;却还没被收尸&#xff1b;你用nohu…

2026/10/21 6:57:46 阅读更多 →
Docker部署Redis的四大核心问题:网络、持久化、配置、集群宣告

Docker部署Redis的四大核心问题:网络、持久化、配置、集群宣告

1. 为什么非得用 Docker 部署 Redis&#xff1f;——从三台裸机故障说起去年在某高校实验室做模拟项目X的后端支撑时&#xff0c;我负责缓存层搭建。最初按老办法&#xff1a;三台物理服务器&#xff0c;每台手动编译安装 Redis 6.2&#xff0c;配好 bind、port、requirepass、…

2026/10/21 6:57:46 阅读更多 →
Docker精讲:从Flask项目构建生产级容器镜像

Docker精讲:从Flask项目构建生产级容器镜像

1. 为什么“快速入门”反而最容易卡在第一步&#xff1f; 很多人点开“Docker 快速入门”教程&#xff0c;前五分钟信心满满——装完 Docker Desktop、跑通 docker run hello-world &#xff0c;就以为自己已经“会了”。结果第二天想把本地写好的 Python 脚本打包成镜像&am…

2026/10/21 6:57:46 阅读更多 →
机器学习到强化学习:四大AI方向实战导航

机器学习到强化学习:四大AI方向实战导航

如果你已经跟着这个系列走到第三篇&#xff0c;大概率不是刚装上 Python 的新手了。前面两篇我们聊了环境搭建、NumPy/Pandas 手感、Scikit-learn 的套路&#xff0c;也跑通了最简单的回归和分类。这些属于"会走"的阶段。这篇导航想做的是另一件事&#xff1a;把机器…

2026/10/21 6:57:46 阅读更多 →
技能提升的底层逻辑:从硬技能到元能力的系统方法

技能提升的底层逻辑:从硬技能到元能力的系统方法

1. 从“skills”这个标题说起&#xff1a;一个被严重低估的元能力“skills”这个词&#xff0c;单独拎出来看&#xff0c;信息量几乎为零。它不像“如何用Python做数据清洗”那样具体&#xff0c;也不像“三小时搭建个人博客”那样有明确交付物。但恰恰是这种模糊性&#xff0c…

2026/10/21 6:57:46 阅读更多 →
降AI率工具怎么选?八类平台实测对比与避坑指南

降AI率工具怎么选?八类平台实测对比与避坑指南

这个选题最近被问得太多了。我自己的后台留言里&#xff0c;隔三差五就有人发来一张检测报告截图&#xff0c;问为什么文章明明是自己写的还是被标成“疑似AIGC生成”&#xff0c;或者干脆是——交作业前一天才开始慌&#xff0c;到处找降AI率的工具。说实话&#xff0c;这事在…

2026/10/21 6:56:45 阅读更多 →

日新闻

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

这段时间硬件圈和AI圈都在传一个事&#xff1a;AMD锐龙AI Max PRO 400系列把最高192GB统一内存搬进了PC形态的产品里&#xff0c;按宣传口径&#xff0c;本地跑320B参数的大家伙不再是纸面概念。我第一反应倒不是“内存真大”&#xff0c;而是这话背后的含金量——它把过去只能…

2026/10/21 0:00:11 阅读更多 →
AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

1. 从“封号重开”这个行为说起&#xff1a;用户到底在追求什么“封号重开”这四个字&#xff0c;放在任何产品语境里都带着一股决绝的味道。一个账号从零开始积累使用记录、调教偏好、沉淀工作流&#xff0c;背后是大量的时间和精力成本。正常情况下&#xff0c;没有人愿意主动…

2026/10/21 0:00:11 阅读更多 →
ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

1. 为什么在ROS里硬刚YOLOv3不是“炫技”&#xff0c;而是工程落地的必经之路我第一次在某高校机器人实验室看到那台搭载Jetson TX2的移动底盘原地打转、反复识别错垃圾桶时&#xff0c;就意识到&#xff1a;单纯跑通darknet_ros demo和真正让YOLOv3在ROS里“干活”&#xff0c…

2026/10/21 0:00:11 阅读更多 →

周新闻

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

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

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

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

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

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

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

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

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

2026/10/21 4:27:17 阅读更多 →

月新闻

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