第三章:网络传输优化——TCP vs UDP的选择,Nagle算法与TCP_NODELAY,连接复用与长连接
聊到实时数据流的网络传输,我脑子里第一个蹦出来的问题就是:到底用TCP还是UDP?这个问题看似基础,但我在项目中见过太多人踩坑。选错了协议,后面再怎么优化都是白搭。
3.1 TCP vs UDP:实时场景下的抉择
先说说我的个人习惯。做实时数据管道时,我90%的场景会用TCP。为什么?因为实时数据最怕丢。你想想看,一条交易数据丢了,对账对不上,那麻烦就大了。
但UDP也不是一无是处。我记得有一次做游戏服务器,要求毫秒级的帧同步。TCP的拥塞控制和重传机制反而成了累赘——网络一抖,延迟飙升到几百毫秒。换成UDP后,丢几个包无所谓,延迟稳稳控制在10ms以内。
核心判断标准:
- 数据完整性 > 延迟 → 选TCP(金融交易、订单系统)
- 延迟敏感 > 完整性 → 选UDP(音视频、游戏帧同步)
- 两者都要 → 考虑QUIC或自建可靠UDP
这里有个避坑指南:千万别在TCP上做应用层心跳检测。我曾经见过一个团队,每100ms发一个心跳包,结果TCP的确认机制和心跳包互相干扰,延迟反而更高了。TCP本身就有保活机制,别画蛇添足。
| 特性 | TCP | UDP |
|---|---|---|
| 可靠性 | 有确认重传 | 无确认,尽力而为 |
| 延迟 | 较高(握手+重传) | 低(无握手) |
| 适用场景 | 数据完整性要求高 | 实时性要求极高 |
3.2 Nagle算法与TCP_NODELAY:小数据包的噩梦
说到TCP的延迟,就不得不提Nagle算法。这玩意儿本意是好的——把小包攒起来再发,减少网络拥塞。但在实时场景下,它就是个坑。
我举个例子。你的应用每10ms产生一个100字节的数据包。Nagle算法一看:嗯,包太小,先攒着。等攒到MSS(最大报文段长度,通常是1460字节)再发。结果呢?你的数据延迟从10ms变成了150ms。实时系统直接废了。
注意:Nagle算法默认是开启的。如果你的应用发送的是小数据包(< 1460字节),一定要显式关闭它。
解决方案很简单:设置TCP_NODELAY选项。代码示例如下:
// Java示例
Socket socket = new Socket();
socket.setTcpNoDelay(true); // 禁用Nagle算法
// C/C++示例
int flag = 1;
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (char *)&flag, sizeof(flag));
// Go示例
conn, _ := net.Dial("tcp", "server:8080")
tcpConn := conn.(*net.TCPConn)
tcpConn.SetNoDelay(true)
我曾经在一个实时风控系统里排查延迟问题。数据从采集到处理,平均延迟200ms,怎么都降不下来。最后抓包一看,Nagle算法在作祟。关掉之后,延迟直接降到20ms。嗯,有时候问题就这么简单。
小技巧:如果你既要批量发送又要低延迟,可以考虑用TCP_CORK(Linux)或TCP_NOPUSH(BSD)手动控制数据发送时机。但说实话,大部分场景下直接关掉Nagle就够了。
3.3 连接复用与长连接:减少握手开销
TCP的三次握手是延迟的另一个来源。每次建立连接,至少多花一个RTT(往返时间)。如果你的应用频繁创建和销毁连接,那延迟就全耗在握手上了。
我的做法是:能复用就复用,能长连就别短连。
具体来说,有几种模式:
- HTTP/1.1的Keep-Alive:复用同一个TCP连接发送多个请求。但有个问题——队头阻塞。一个请求慢了,后面的都得等。
- HTTP/2的多路复用:在同一个连接上并行发送多个流。解决了队头阻塞,但TCP层的队头阻塞依然存在。
- 连接池:维护一组长连接,按需取用。我习惯用
commons-pool2(Java)或connection-pool(Go)来实现。
// 连接池示例(伪代码)
ConnectionPool pool = new ConnectionPool("server:8080", 10); // 最大10个连接
// 从池中获取连接
Connection conn = pool.borrow();
try {
conn.send(data);
} finally {
pool.return(conn); // 归还连接,不是关闭
}
这里有个坑:长连接需要心跳保活。但心跳间隔不能太短,否则浪费带宽;也不能太长,否则连接断了都不知道。我一般设30秒,配合TCP的keepalive(默认2小时,可以调小)。
最佳实践:
- 连接池大小 = 核数 × 2(经验值,具体要压测)
- 心跳间隔 = 30秒(可调)
- 空闲超时 = 60秒(超过就关闭,释放资源)
- 重试机制:失败后等待1秒、2秒、4秒...指数退避
3.4 本章知识体系
说了这么多,我画了一张图帮你理清思路。这张图展示了网络传输优化的三个核心维度,以及它们之间的关联。
说白了,网络传输优化就是在可靠性和低延迟之间找平衡。没有银弹,只有根据场景做取舍。TCP还是UDP?关不关Nagle?用长连接还是连接池?这些问题没有标准答案,但有了上面的思路,你至少知道该从哪里入手。
嗯,这一章就到这里。下一章我们聊聊序列化与压缩——数据包的大小,往往比你想的更影响延迟。