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最精妙的设计之一。它解决了两个问题:流量控制(防止发送方太快)和可靠传输(丢包重传)。

你想想看,如果发送方一股脑把数据全扔出去,接收方来不及处理怎么办?滑动窗口就是让接收方告诉发送方:「我还能收多少,你看着发」。

我画了一张图,帮你理解滑动窗口的工作机制:

TCP滑动窗口工作机制 发送方缓冲区: 已发送并确认 已发送未确认 未发送(窗口内) 未发送(窗口外) ← 滑动窗口(大小由接收方通告) → 接收方缓冲区: 已接收并确认 已接收未确认 空闲缓冲区(可接收更多数据) 关键机制: 1. 接收方通过TCP头部中的「窗口大小」字段告知发送方自己的剩余缓冲区 2. 发送方根据窗口大小调整发送速率,避免接收方缓冲区溢出

滑动窗口的大小是动态变化的。接收方处理得快,窗口就变大;处理得慢,窗口就缩小。这就是流量控制的本质。

实战经验:我调优过一个文件传输服务,发现吞吐量上不去。抓包一看,接收方窗口经常缩到很小。原因是接收方的应用层读取数据太慢。后来把读取缓冲区从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信任开发者。你选哪个,取决于你的业务需要什么。