Linux TCP 可靠性与性能优化详解:从确认应答到拥塞控制
TCP 可靠性与性能优化涉及确认应答、超时重传、滑动窗口、流量控制及拥塞控制等核心机制。确认应答确保数据接收,超时重传处理丢包,滑动窗口提升吞吐。流量控制防止接收方过载,拥塞控制避免网络拥堵。此外还包括延迟应答、捎带应答优化,以及面向字节流带来的粘包问题解决方案和异常情况处理如保活定时器。理解这些机制有助于构建高效稳定的网络通信。

TCP 可靠性与性能优化涉及确认应答、超时重传、滑动窗口、流量控制及拥塞控制等核心机制。确认应答确保数据接收,超时重传处理丢包,滑动窗口提升吞吐。流量控制防止接收方过载,拥塞控制避免网络拥堵。此外还包括延迟应答、捎带应答优化,以及面向字节流带来的粘包问题解决方案和异常情况处理如保活定时器。理解这些机制有助于构建高效稳定的网络通信。

确认应答(ACK):接收方告诉发送方"我已经收到了你的数据"。
类比:
唐僧讲经:唐僧说一句,悟空答一句"师父,我听懂了" 唐僧继续说下一句
TCP 的确认应答:
发送方发送数据 接收方收到数据后,发送 ACK 发送方收到 ACK 后,继续发送下一批数据
序列号(Sequence Number):
确认号(Acknowledgment Number):
例子:
发送方发送:序列号 1000,数据 100 字节(字节序号 1000-1099)
接收方收到后,发送 ACK:确认号 1100
含义:"我已收到 1000-1099,下一个要接收的是 1100"
发送方 接收方 ||| 发送:seq=1000, data=100 字节 ||----------------------------->|||
收到 1000-1099 ||| 收到 ACK | 发送:ack=1100|<-----------------------------||||
发送:seq=1100, data=100 字节 ||----------------------------->|||
收到 1100-1199 ||| 收到 ACK | 发送:ack=1200|<-----------------------------|||
问题:一发一收的方式性能太低。
分析:
发送数据 → 等待 ACK → 收到 ACK → 发送下一批数据
如果网络延迟高(比如 100ms),每次都要等待 100ms
吞吐量 = 数据量 / (数据传输时间 + 等待 ACK 时间)
例子:
数据量:1000 字节
数据传输时间:1ms
等待 ACK 时间:100ms
吞吐量 = 1000 / (1 + 100) = 9.9 字节/ms ≈ 10KB/s
太慢了!
解决方法:滑动窗口(后面详细讲)。
问题:数据或 ACK 可能在网络中丢失。
场景 1:数据丢失
发送方发送数据 数据在网络中丢失了 接收方收不到数据,不会发送 ACK 发送方一直等待 ACK
场景 2:ACK 丢失
发送方发送数据 接收方收到数据,发送 ACK ACK 在网络中丢失了 发送方收不到 ACK,以为数据丢失了
解决方法:超时重传。
核心思想:如果一段时间内没收到 ACK,就重发数据。
流程:
1. 发送数据
2. 启动定时器
3. 等待 ACK
* 如果在超时时间内收到 ACK:停止定时器,发送下一批数据
* 如果超时:重发数据,重新启动定时器
场景 1:数据丢失,超时重传
发送方 接收方 ||| 发送:seq=1000||------------X 数据丢失 |||| 等待 ACK... || 超时! |||| 重传:seq=1000||----------------------------->|||
收到 1000 ||| 收到 ACK | 发送:ack=1100|<-----------------------------|||
场景 2:ACK 丢失,接收方去重
发送方 接收方 ||| 发送:seq=1000||----------------------------->|||
收到 1000 ||| 等待 ACK... | 发送:ack=1100| X<-----| ACK 丢失 | 超时! |||| 重传:seq=1000||----------------------------->|||
收到重复的 1000(去重) ||| 收到 ACK | 发送:ack=1100|<-----------------------------|||
问题:超时时间设多长合适?
太长的问题:
超时时间=10 秒
数据丢失了,要等 10 秒才能重传
吞吐量严重下降
太短的问题:
超时时间=10ms
网络延迟=100ms
每次都会超时重传,导致大量重复数据
浪费带宽
理想的超时时间:
超时时间 = RTT(往返时间) + 一些余量
RTT(Round-Trip Time):数据发送到接收并收到 ACK 的时间。
TCP 的超时时间计算:
RTO (Retransmission Timeout) = SRTT + 4 × RTTVAR
SRTT (Smoothed RTT):平滑的 RTT
RTTVAR (RTT Variance):RTT 的方差
动态调整:
每次收到 ACK 时,测量 RTT
更新 SRTT 和 RTTVAR
重新计算 RTO
Linux 的实现:
超时时间以 500ms 为单位
如果重传一次后仍未收到 ACK,等待 2×500ms 后再重传
如果再重传仍未收到 ACK,等待 4×500ms 后再重传
依次类推:500ms → 1000ms → 2000ms → 4000ms → ...
指数退避(Exponential Backoff)
问题:一直重传下去吗?
答案:不是,有次数限制。
Linux 的限制:
累计重传次数达到一定限制后,认为网络或对端异常
强制关闭连接
查看重传次数限制:
cat /proc/sys/net/ipv4/tcp_retries2 # 通常是 15 次
问题回顾:一发一收的性能太低。
解决方法:一次发送多个数据段,不用等待 ACK。
滑动窗口:
允许发送方在收到 ACK 之前,连续发送多个数据段
窗口大小:无需等待 ACK 就能发送的最大数据量
类比:
一发一收 = 单车道(一次只能过一辆车)
滑动窗口 = 多车道(可以同时过多辆车)
窗口大小:假设窗口大小为 4000 字节(4 个段,每个段 1000 字节)。
发送过程:
1. 一次性发送 4 个段:seq=1000, 2000, 3000, 4000
2. 收到第一个 ACK(ack=2000)
3. 窗口向右滑动,可以发送 seq=5000
4. 收到第二个 ACK(ack=3000)
5. 窗口继续滑动,可以发送 seq=6000...
图示:
发送缓冲区: ┌────┬────┬────┬────┬────┬────┬────┬────┐
│1000│2000│3000│4000│5000│6000│7000│8000│
└────┴────┴────┴────┴────┴────┴────┴────┘
└─────────────┘ 已发送未确认(窗口)
收到 ack=2000 后,窗口滑动:
┌────┬────┬────┬────┬────┬────┬────┬────┐
│1000│2000│3000│4000│5000│6000│7000│8000│
└────┴────┴────┴────┴────┴────┴────┴────┘
└─────────────┘ 已发送未确认(窗口)
发送方 接收方 ||| 发送:seq=1000||----------------------------->||
发送:seq=2000(不等 ACK) ||----------------------------->||
发送:seq=3000||----------------------------->||
发送:seq=4000||----------------------------->|||
收到 1000 || 发送:ack=2000| 收到 ack=2000|<----||
窗口滑动,发送 seq=5000||----------------------------->|||
收到 2000 || 发送:ack=3000| 收到 ack=3000|<----||
窗口滑动,发送 seq=6000||----------------------------->|||
性能提升:
无滑动窗口:
每次发送 1000 字节,等待 100ms
吞吐量 = 1000 / 100ms = 10KB/s
有滑动窗口(窗口大小 4000 字节):
一次发送 4000 字节,等待 100ms
吞吐量 = 4000 / 100ms = 40KB/s
性能提升 4 倍!
核心公式:
吞吐量 ≈ 窗口大小 / RTT
结论:
窗口越大,网络吞吐量越高
问题:如果 ACK 丢失了,如何重传?
答案:使用发送缓冲区。
发送缓冲区的作用:
1. 保存已发送但未收到 ACK 的数据
2. 如果超时,可以从缓冲区重传
3. 收到 ACK 后,删除对应的数据
发送缓冲区的状态:
┌──────────────┬──────────────┬──────────────┐
│ 已确认 │ 已发送未确认 │ 未发送 │
│ (可删除) │ (等待 ACK) │ (待发送) │
└──────────────┴──────────────┴──────────────┘
└──────────────┘ 滑动窗口
场景:
发送:seq=1000, 2000, 3000, 4000
接收方收到所有数据
ACK:ack=2000 丢失
ACK:ack=3000 到达
ACK:ack=4000 到达
ACK:ack=5000 到达
处理:
收到 ack=3000,说明 1000 和 2000 都已收到
收到 ack=5000,说明 1000-4000 都已收到
部分 ACK 丢失不要紧,后续 ACK 可以确认
图示:
发送方 接收方 |||seq=1000||----------------------------->| 收到 |seq=2000|ack=2000|----------------------------->|<----X 丢失 |seq=3000| 收到 |----------------------------->|ack=3000||<----|| 收到 ack=3000|| (说明 1000 和 2000 都已收到) |||
场景:
发送:seq=1000, 2000, 3000, 4000
seq=2000 丢失
接收方收到:1000, 3000, 4000
接收方的行为:
收到 1000:发送 ack=2000(期望下一个是 2000)
收到 3000:发送 ack=2000(还是期望 2000)
收到 4000:发送 ack=2000(仍然期望 2000)
发送方的行为:
连续收到 3 个 ack=2000
判断:seq=2000 肯定丢失了
立即重传 seq=2000(不等超时)
这就是"快速重传"(Fast Retransmit)。
发送方 接收方 |||seq=1000||----------------------------->| 收到 1000 ||ack=2000|seq=2000|<----||-------X 丢失 ||seq=3000||----------------------------->| 收到 3000(乱序) ||ack=2000(重复 ACK) |seq=4000|<----||----------------------------->| 收到 4000(乱序) ||ack=2000(重复 ACK) |seq=5000|<----||----------------------------->| 收到 5000(乱序) ||ack=2000(重复 ACK) | 收到 3 个 ack=2000|<----|| 快速重传 seq=2000||----------------------------->| 收到 2000ack=6000|<----|| (说明 2000-5000 都已收到) |||
对比超时重传:
超时重传:等待 RTO(可能是几秒)
快速重传:连续 3 个重复 ACK 立即重传(几十毫秒)
性能提升:
减少等待时间
提高吞吐量
问题:接收方收到乱序的数据怎么办?
答案:使用接收缓冲区。
接收缓冲区的作用:
1. 保存已接收但乱序的数据
2. 等待缺失的数据到达
3. 按顺序交付给应用层
例子:
接收顺序:1000, 3000, 4000, 2000
缓冲区:先存 3000 和 4000,等待 2000
收到 2000 后,按顺序交付:1000, 2000, 3000, 4000
问题:接收方的处理速度有限。
场景:
发送方:每秒发送 100MB 数据
接收方:每秒只能处理 10MB 数据
接收缓冲区:只有 1MB
结果:接收缓冲区很快被打满,后续数据被丢弃
后果:
大量丢包
大量重传
性能严重下降
核心思想:接收方告诉发送方"我的缓冲区还有多少空间"。
实现方式:
接收方在 ACK 中告诉发送方:窗口大小(剩余缓冲区大小)
发送方根据窗口大小调整发送速度
TCP 首部的窗口字段:
16 位窗口大小字段
最大值:65535 字节
场景:
接收缓冲区大小:10000 字节
已接收但尚未被应用层处理的数据:6000 字节
剩余可用缓冲区:4000 字节
接收方发送 ACK:
ACK 标志 =1
确认号 =...
窗口大小 =4000 (告诉发送方:我还能再接收 4000 字节)
发送方的行为:
收到窗口大小=4000
最多再发送 4000 字节
等待接收方处理数据,窗口变大
发送方 接收方 ||| 发送 4000 字节 ||----------------------------->| 接收缓冲区:4000/10000 || ack, window=6000|<-----------------------------||||
发送 6000 字节 ||----------------------------->| 接收缓冲区:10000/10000(满了) || ack, window=0|<-----------------------------||||
收到 window=0,停止发送 |||
应用层读取数据... || 接收缓冲区:5000/10000 || ack, window=5000|<-----------------------------||||
收到 window=5000,继续发送 |||
问题:如果接收方缓冲区满了(window=0),发送方停止发送。但接收方的窗口更新 ACK 丢失了怎么办?
场景:
接收方:window=0
发送方:停止发送
接收方处理了数据:window=5000,发送 ACK
ACK 丢失
发送方:不知道窗口变大了,一直等待
死锁!
解决方法:窗口探测(Window Probe)。
窗口探测:
发送方在收到 window=0 后,定期发送 1 字节的探测数据
接收方收到探测数据,回复当前的窗口大小
发送方获得最新的窗口大小
问题:窗口大小字段只有 16 位
高带宽:1Gbps = 125MB/s
高延迟:RTT=100ms
理想窗口大小 = 125MB/s × 0.1s = 12.5MB
远超 65535 字节!
解决方法:窗口扩大因子(Window Scale)。
窗口扩大因子:
在 TCP 选项中协商一个扩大因子(0-14)
实际窗口大小 = 窗口字段值 << 扩大因子
例如:窗口字段=65535,扩大因子=7
实际窗口 = 65535<<7=8388480 字节 ≈ 8MB
问题:网络拥堵时,盲目发很拥堵(路由器缓冲区快满了) 发送方继续高速发送数据 路由器缓冲区满了,丢弃数据包 发送方重传,继续发送大量数据 网络更加拥堵 恶性循环!
后果:
网络拥塞崩溃(Congestion Collapse)
所有连接的吞吐量都下降
目标:
1. 尽可能快地发送数据(提高吞吐量)
2. 避免造成网络拥堵(维持稳定性)
平衡:
发送太慢:浪费带宽
发送太快:造成拥堵
拥塞窗口(Congestion Window, cwnd):
发送方估算的网络能承受的数据量
根据网络状况动态调整
实际发送窗口:
实际窗口 = min(拥塞窗口,接收方窗口)
拥塞窗口:避免网络拥堵
接收方窗口:避免接收方过载
取两者的最小值
核心思想:从小窗口开始,逐渐增大。
初始值:
拥塞窗口 cwnd = 1 个 MSS(Maximum Segment Size,最大报文段大小)
通常 MSS = 1460 字节
增长规则:
每收到一个 ACK,cwnd += 1 个 MSS
例子:
初始:cwnd = 1
发送 1 个段,收到 1 个 ACK:cwnd = 2
发送 2 个段,收到 2 个 ACK:cwnd = 4
发送 4 个段,收到 4 个 ACK:cwnd = 8
发送 8 个段,收到 8 个 ACK:cwnd = 16...
增长速度:
指数增长:1 → 2 → 4 → 8 → 16 → 32 → 64 → ...
问题:一直指数增长会导致网络拥堵。
解决方法:引入慢启动阈值(Slow Start Threshold, ssthresh)。
规则:
cwnd < ssthresh:慢启动,指数增长
cwnd >= ssthresh:拥塞避免,线性增长
初始值:
ssthresh = 接收方窗口大小(或一个很大的值)
核心思想:到达阈值后,缓慢增长。
增长规则:
每收到一个完整窗口的 ACK,cwnd += 1 个 MSS
例子:
cwnd = 8(8 个段)
发送 8 个段,收到 8 个 ACK:cwnd = 9
发送 9 个段,收到 9 个 ACK:cwnd = 10...
增长速度:
线性增长:每个 RTT 增加 1 个 MSS
判断:
超时未收到 ACK
说明网络严重拥堵(可能大量丢包)
处理:
1. ssthresh = cwnd / 2(阈值减半)
2. cwnd = 1(重新慢启动)
3. 重传数据
判断:
收到 3 个重复 ACK
说明网络轻微拥堵(部分丢包)
处理(快速恢复):
1. ssthresh = cwnd / 2(阈值减半)
2. cwnd = ssthresh(不是 1,而是阈值)
3. 重传数据
4. 进入拥塞避免阶段(线性增长)
由上面知识我们可以知道,图中所显示的网络拥塞一定是发生了超时重传,cwnd=1

类比:热恋的感觉
初期(慢启动):
感情快速升温,每天见面次数翻倍
1 次 → 2 次 → 4 次 → 8 次 → ...
稳定期(拥塞避免):
到达一定程度后,缓慢增加
每周增加 1 次见面
吵架(拥塞发生):
小吵架(快速重传):见面次数减半,但不回到初期
大吵架(超时重传):回到初期,重新开始
问题:立即应答可能窗口太小。
场景:
接收缓冲区:1MB
收到 500KB 数据
立即回复 ACK:window=500KB
但实际上:
应用层很快处理了这 500KB 数据(10ms)
如果延迟 200ms 再回复 ACK:window=1MB
延迟应答的好处:
窗口更大 → 吞吐量更高
延迟应答的规则:
1. 数量限制:每收到 N 个包,就应答一次(通常 N=2)
2. 时间限制:超过最大延迟时间,立即应答(通常 200ms)
例子:
收到第 1 个包:不立即应答
收到第 2 个包:应答(确认 1 和 2)
收到第 3 个包:不立即应答
收到第 4 个包:应答(确认 3 和 4)
场景:很多应用层协议是"一发一收"的。
例子:
客户端发送:"How are you?"
服务器收到后:
1. 需要发送 ACK(确认收到)
2. 需要发送响应:"Fine, thank you!"
捎带应答:
把 ACK 和响应数据合并发送
一个 TCP 段同时包含:
- ACK 标志=1,确认号=...
- 数据="Fine, thank you!"
图示:
客户端 服务器 ||----------------------------->|||
收到 |||
收到捎带应答 | 发送:ACK + "Fine, thank you!"|<-----------------------------|||
省略了一个单独的 ACK 报文
优势:
减少报文数量
提高效率
TCP 是面向字节流的:
应用层看到的是连续的字节流
没有明确的消息边界
对比 UDP:
UDP 是面向数据报的
应用层看到的是一个个独立的数据报
有明确的消息边界
发送缓冲区:
应用层调用 send(),数据先放入发送缓冲区
TCP 根据情况决定何时发送:
- 缓冲区满了:立即发送
- 收到 PSH 标志:立即发送
- 达到一定时机:发送
接收缓冲区:
TCP 收到数据,放入接收缓冲区
应用层调用 recv(),从缓冲区读取数据
全双工:
一个 TCP 连接同时有发送缓冲区和接收缓冲区
既可以读,也可以写
TCP 的读写可以不匹配:
例子 1:多次写,一次读
// 发送端
send("hello"); // 5 字节
send("world"); // 5 字节
send("!"); // 1 字节
// 接收端
recv(buf, 100); // 一次读取 11 字节:"helloworld!"
例子 2:一次写,多次读
// 发送端
send("hello world!", 12); // 12 字节
// 接收端
recv(buf, 5); // 读取 5 字节:"hello"
recv(buf, 7); // 读取 7 字节:" world!"
结论:
TCP 的读写次数和数量可以不匹配
站在应用层的角度,看到的是连续的字节流
粘包问题:
应用层无法区分两个数据包的边界
例子:
发送端发送两个数据包:
包 1:"hello"
包 2:"world"
接收端可能收到:
情况 1:"hello" 和 "world"(正常)
情况 2:"helloworld"(粘包)
情况 3:"hel" 和 "loworld"(拆包)
情况 4:"he""llo""wo""rld"(拆包)
为什么会粘包:
1. TCP 的发送缓冲区可能合并数据
2. TCP 的接收缓冲区可能合并数据
3. 网络层可能分片或合并数据
核心:明确两个包之间的边界。
// 每个包固定 100 字节
struct Packet {
char data[100];
};
// 发送
Packet pkt;
strcpy(pkt.data, "hello");
send(&pkt, sizeof(pkt)); // 固定 100 字节
// 接收
Packet pkt;
recv(&pkt, sizeof(pkt)); // 固定接收 100 字节
优点:简单 缺点:浪费空间(数据不够 100 字节也要占用 100 字节)
// 包头包含长度字段
struct PacketHeader {
uint32_t length; // 数据长度
};
struct Packet {
PacketHeader header;
char data[]; // 可变长度
};
// 发送
std::string msg = "hello world";
PacketHeader header;
header.length = msg.size();
send(&header, sizeof(header));
send(msg.c_str(), msg.size());
// 接收
PacketHeader header;
recv(&header, sizeof(header)); // 先接收包头
char* buf = new char[header.length];
recv(buf, header.length); // 根据长度接收数据
优点:不浪费空间 缺点:需要两次 recv(先接收包头,再接收数据)
// 使用特殊分隔符(如\n 或\r\n)
// HTTP 协议使用\r\n
// 发送
send("hello\n");
send("world\n");
// 接收
while(true) {
char c;
std::string line;
while(recv(&c, 1) > 0) {
if(c == '\n') { break; // 遇到分隔符,一个包结束 }
line += c;
}
// line 是一个完整的包
}
优点:简单直观 缺点:
// 例如 HTTP 协议
// 包头用\r\n\r\n分隔
// 包头中包含 Content-Length 字段
// 例子:GET / HTTP/1.1\r\n Host: www.example.com\r\n Content-Length:11\r\n \r\n hello world
// 接收流程:
// 1. 读取包头(直到\r\n\r\n)
// 2. 解析 Content-Length 字段
// 3. 根据 Content-Length 读取数据
答案:UDP 没有粘包问题。
原因:
UDP 是面向数据报的
每个数据报都是独立的
有明确的边界
例子:
// 发送端
sendto("hello", 5); // 数据报 1
sendto("world", 5); // 数据报 2
// 接收端
recvfrom(buf, 100); // 要么收到完整的"hello",要么收不到
recvfrom(buf, 100); // 要么收到完整的"world",要么收不到
// 不会收到"helloworld"或"hel"
结论:
UDP:
- 要么收到完整的数据报
- 要么收不到(丢失)
- 不会出现"半个"或"粘在一起"的情况
场景:进程意外终止(如 Ctrl+C、crash)。
处理:
1. 进程终止
2. 操作系统自动关闭所有文件描述符(包括 socket)
3. 发送 FIN(正常关闭流程)
结论:
和正常关闭没有什么区别
四次挥手正常进行
场景:机器重启(reboot)。
处理:
1. 操作系统关闭所有进程
2. 发送 FIN 或 RST(取决于操作系统)
3. 对端收到 FIN 或 RST,关闭连接
结论:
和进程终止类似
连接会被正常关闭
场景:机器突然掉电,或网线被拔掉。
问题:无法发送 FIN。
对端的处理:
情况 1:对端有数据要发送
1. 对端发送数据
2. 等待 ACK,超时
3. 重传数据,再次超时
4. 多次重传失败后,判断连接已断开
5. 返回错误给应用层
情况 2:对端没有数据要发送
1. 对端一直等待
2. 连接看起来仍然存在
3. 但实际上对方已经不在了
解决方法:保活定时器(Keep-Alive)
保活定时器:定期检测对方是否还在。
工作原理:
1. 如果一段时间内没有数据传输
2. 定期发送保活探测包(1 字节数据)
3. 如果收到 ACK,说明对方还在
4. 如果多次探测都没有响应,判断连接已断开
Linux 的保活参数:
# 保活时间(多久没数据后开始探测)
cat /proc/sys/net/ipv4/tcp_keepalive_time # 默认 7200 秒(2 小时)
# 保活间隔(探测包的间隔)
cat /proc/sys/net/ipv4/tcp_keepalive_intvl # 默认 75 秒
# 保活探测次数(多少次探测失败后判断断开)
cat /proc/sys/net/ipv4/tcp_keepalive_probes # 默认 9 次
总时间:
2 小时 + 75 秒 × 9 = 2 小时 11 分 15 秒
才能判断连接已断开
启用保活:
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, &opt, sizeof(opt));
应用层的心跳:
TCP 的保活时间太长(2 小时)
很多应用层协议自己实现心跳机制
例如:HTTP 长连接、QQ、游戏
通常间隔:30 秒到 5 分钟
可靠性机制:
性能优化:
核心矛盾:
可靠性 vs 性能
TCP 的选择:
在保证可靠性的前提下,尽可能提高性能
具体体现:
确认应答:保证可靠性
滑动窗口:提高性能
超时重传:保证可靠性
快速重传:提高性能
流量控制:保证可靠性
拥塞控制:提高性能并维持网络稳定

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online