4. 传输层协议:TCP与UDP深度解析
传输层是整个网络协议栈里最「有人情味」的一层。为什么这么说?因为IP层只管把数据包送到对方主机,但至于怎么送、送得对不对、对方收没收到——它一概不管。传输层就是来解决这些问题的。
我个人习惯把传输层比作快递公司。IP层是货车,只管在路上跑;传输层才是那个给你打电话、让你签收、还提供保价服务的客服。今天咱们就好好聊聊TCP和UDP这两位「快递员」。
4.1 TCP协议:可靠的「挂号信」
TCP,全称传输控制协议。说白了,它就是网络世界里的「挂号信」——保证你发的每一封信都能送到,而且顺序不乱。
我在项目中遇到过不少刚入行的同事,总觉得TCP太复杂,不如UDP简单粗暴。但等你真正处理过丢包、乱序、重传这些问题后,你就会明白TCP的设计有多精妙。
4.1.1 三次握手:建立连接的艺术
TCP建立连接前,双方要先「握手」三次。为什么是三次?不是两次,也不是四次?
我们来看一个典型的握手过程:
客户端 服务端
| |
| SYN (seq=x) |
|-------------------------->| // 第一次握手:客户端说「我要连你」
| |
| SYN+ACK (seq=y, ack=x+1) |
|<--------------------------| // 第二次握手:服务端说「收到,我也要连你」
| |
| ACK (seq=x+1, ack=y+1) |
|-------------------------->| // 第三次握手:客户端说「好,开始传数据」
| |
嗯,这里要注意一个关键点:第三次握手是可以携带数据的。前两次不行。我刚开始调优Web服务器时,就利用这个特性做过优化——把HTTP请求的头部塞在第三次握手里,能省掉一个RTT(往返时间)。
核心要点:三次握手的本质是「双方都要确认对方的收发能力正常」。第一次证明客户端能发;第二次证明服务端能收能发;第三次证明客户端能收。少一次,总有一方不确定对方的状态。
避坑指南:我曾经遇到过一个问题——客户端频繁建立短连接,导致服务端出现大量半连接(SYN_RECV状态)。后来发现是客户端发完SYN后就不管了,服务端一直在等ACK。解决方案是调整tcp_synack_retries参数,减少重试次数。
4.1.2 四次挥手:优雅的告别
断开连接比建立连接多一次挥手。为什么?因为TCP连接是全双工的——双方可以同时收发数据。断开时,每一方都要单独关闭自己的发送通道。
客户端 服务端
| |
| FIN (seq=u) |
|-------------------------->| // 第一次挥手:客户端说「我不发了」
| |
| ACK (ack=u+1) |
|<--------------------------| // 第二次挥手:服务端说「知道了」
| |
| FIN (seq=v) |
|<--------------------------| // 第三次挥手:服务端说「我也不发了」
| |
| ACK (ack=v+1) |
|-------------------------->| // 第四次挥手:客户端说「好,再见」
| |
这里有个常见的坑:TIME_WAIT状态。主动关闭连接的一方(通常是客户端)在发送最后一次ACK后,会进入TIME_WAIT状态,持续2MSL(最大报文段生存时间,通常约2分钟)。
注意:我曾经在压测时发现服务器端口不够用了。查了半天,原来是客户端大量TIME_WAIT堆积。解决方案有两个:一是开启tcp_tw_reuse(复用TIME_WAIT连接),二是调整应用层逻辑,让服务端主动关闭连接。
4.1.3 滑动窗口:流量控制的智慧
滑动窗口是TCP最精妙的设计之一。它解决了两个问题:流量控制(防止发送方太快)和可靠传输(丢包重传)。
你想想看,如果发送方一股脑把数据全扔出去,接收方来不及处理怎么办?滑动窗口就是让接收方告诉发送方:「我还能收多少,你看着发」。
我画了一张图,帮你理解滑动窗口的工作机制:
滑动窗口的大小是动态变化的。接收方处理得快,窗口就变大;处理得慢,窗口就缩小。这就是流量控制的本质。
实战经验:我调优过一个文件传输服务,发现吞吐量上不去。抓包一看,接收方窗口经常缩到很小。原因是接收方的应用层读取数据太慢。后来把读取缓冲区从4KB改成64KB,吞吐量直接翻了三倍。
4.2 UDP协议:高效的「明信片」
UDP和TCP完全是两个极端。它不建立连接,不保证可靠,不控制流量。说白了,就是「我把数据扔出去,收不收得到随缘」。
但别小看UDP。它在某些场景下比TCP强得多:
| 特性 | TCP | UDP |
|---|---|---|
| 连接状态 | 面向连接 | 无连接 |
| 可靠性 | 可靠(确认+重传) | 不可靠(尽力而为) |
| 有序性 | 保证顺序 | 不保证顺序 |
| 头部开销 | 20-60字节 | 8字节 |
| 传输效率 | 较低(有确认机制) | 高(无确认机制) |
| 典型应用 | HTTP、FTP、SMTP | DNS、DHCP、视频流 |
我举个例子你就明白了。视频通话如果用TCP,一旦丢包就要重传,画面就会卡住等重传。但用UDP,丢几个包最多画面花一下,马上就能恢复。用户宁愿看到一帧花屏,也不愿意等两秒黑屏。
选择原则:对可靠性要求高、数据完整性优先的场景用TCP;对实时性要求高、可以容忍少量丢包的场景用UDP。
4.3 端口与Socket编程
端口是什么?说白了,就是进程的「门牌号」。IP地址找到主机,端口找到主机上的具体进程。
端口号范围是0-65535,分为三类:
- 知名端口(0-1023):系统保留,需要root权限才能使用。比如HTTP用80,HTTPS用443。
- 注册端口(1024-49151):用户进程可以使用,但需要向IANA注册。比如MySQL用3306。
- 动态/私有端口(49152-65535):客户端临时使用,系统自动分配。
Socket编程,就是让应用程序能通过端口收发数据。我写一个最简单的TCP服务端示例:
# Python TCP服务端示例
import socket
# 创建Socket对象
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定IP和端口
server_socket.bind(('0.0.0.0', 8888))
# 开始监听,最大等待队列为5
server_socket.listen(5)
print('服务端已启动,等待客户端连接...')
while True:
# 接受客户端连接(阻塞)
client_socket, client_addr = server_socket.accept()
print(f'客户端 {client_addr} 已连接')
# 接收数据
data = client_socket.recv(1024)
print(f'收到数据: {data.decode()}')
# 发送响应
client_socket.send(b'Hello, Client!')
# 关闭连接
client_socket.close()
对应的UDP版本更简单,因为不需要建立连接:
# Python UDP服务端示例
import socket
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_socket.bind(('0.0.0.0', 8888))
while True:
data, client_addr = udp_socket.recvfrom(1024)
print(f'收到来自 {client_addr} 的数据: {data.decode()}')
udp_socket.sendto(b'Hello, UDP Client!', client_addr)
踩坑记录:我曾经写过一个UDP服务,客户端发来的数据偶尔会「丢失」。查了半天,发现是UDP接收缓冲区太小,数据被内核丢弃了。解决方案是用setsockopt把SO_RCVBUF调大。记住,UDP不保证可靠,应用层必须自己处理丢包和重传。
4.4 本章小结
TCP和UDP,一个像挂号信,一个像明信片。没有谁好谁坏,只有谁更适合你的场景。
TCP的核心机制——三次握手、四次挥手、滑动窗口——都是为了解决一个根本问题:在不靠谱的IP层之上,构建一个靠谱的传输通道。而UDP选择信任应用层,把控制权交给你自己。
做网络编程这么多年,我最大的体会是:理解传输层,就是理解「信任」与「效率」的权衡。TCP信任协议栈,UDP信任开发者。你选哪个,取决于你的业务需要什么。