4、传输层基石:TCP与UDP
传输层,说白了就是网络通信的「调度中心」。
我经常跟团队里的新人说:你写应用层代码时,感觉不到传输层的存在,但一旦出问题,十有八九就是这里埋的坑。今天咱们就把TCP和UDP这两个老伙计彻底聊透。
4.1 端口号:进程的「门牌号」
一台服务器上跑着几十个服务,数据包来了,怎么知道该交给哪个进程?
答案就是端口号。IP地址定位到机器,端口号定位到进程。两者合在一起,就是套接字(Socket)。
| 端口范围 | 分类 | 常见例子 |
|---|---|---|
| 0 - 1023 | 系统端口(Well-known) | HTTP(80)、HTTPS(443)、SSH(22) |
| 1024 - 49151 | 注册端口 | MySQL(3306)、Redis(6379) |
| 49152 - 65535 | 动态/私有端口 | 客户端临时分配 |
避坑指南:我曾经在生产环境遇到过端口冲突,一个服务怎么都起不来。查了半天,发现是另一个进程占用了同一个端口。所以,写代码时一定要做端口可用性检查,别想当然。
4.2 TCP:可靠的「快递员」
TCP的核心就两个字:可靠。它保证数据不丢、不乱、不重复。
4.2.1 三次握手:建立连接
为什么是三次?不是两次,也不是四次?
我个人的理解是:三次握手是为了「确认双方的收发能力都正常」。
- 第一次握手(SYN):客户端说「我要连你」,此时服务端知道客户端能发。
- 第二次握手(SYN+ACK):服务端说「收到,我也要连你」,此时客户端知道服务端能收能发。
- 第三次握手(ACK):客户端说「好的,开始传数据」,此时服务端知道客户端能收。
关键点:第三次握手可以携带数据。前两次不行。这是TCP协议设计上的一个优化点。
4.2.2 四次挥手:断开连接
断开比建立多一次,因为TCP是「全双工」的。每一方都要单独关闭自己的通道。
- 主动方发FIN,表示「我不发了」。
- 被动方回ACK,表示「知道了,但我可能还有数据要发」。
- 被动方发FIN,表示「我也不发了」。
- 主动方回ACK,表示「收到,再见」。
注意:主动方在收到FIN后,会进入TIME_WAIT状态,持续2MSL(约2分钟)。这个状态是为了防止最后一个ACK丢失。我见过不少线上问题,就是因为TIME_WAIT太多,导致端口耗尽。
4.2.3 滑动窗口:流量控制
发送方不能一股脑把数据全扔出去,得看接收方的「消化能力」。这个能力,就是窗口大小。
滑动窗口的核心逻辑:
- 接收方告诉发送方「我的窗口还剩多少」。
- 发送方根据窗口大小,决定发多少数据。
- 窗口会动态调整,防止接收方被撑爆。
嗯,这里要注意:窗口大小是0的时候,发送方会停止发送,但会定期发一个「窗口探测」包,看看接收方缓过来了没有。
4.2.4 拥塞控制:网络保护
流量控制是保护接收方,拥塞控制是保护整个网络。
TCP的拥塞控制有四个阶段:
- 慢启动:刚开始发得慢,指数增长,直到遇到丢包。
- 拥塞避免:达到阈值后,线性增长,慢慢试探。
- 快重传:收到三个重复ACK,立即重传,不等超时。
- 快恢复:重传后,降低发送速度,但不回到慢启动。
个人经验:我在做高频交易系统时,发现默认的TCP拥塞控制算法(Cubic)对低延迟场景不友好。后来换成了BBR算法,延迟降低了30%以上。所以,别迷信默认配置,要根据场景调优。
4.3 UDP:简单粗暴的「飞毛腿」
UDP不建立连接,不保证可靠,不控制拥塞。它只做一件事:把数据扔出去,不管死活。
那它有什么用?
- 实时性要求高:视频通话、在线游戏,丢一帧没关系,但延迟高了就卡死。
- 广播/多播:UDP天然支持,TCP做不到。
- 轻量级场景:DNS查询、DHCP,一个请求一个响应,没必要搞三次握手。
核心对比:
- TCP:可靠、有序、有连接、有拥塞控制。适合文件传输、网页浏览。
- UDP:不可靠、无序、无连接、无拥塞控制。适合实时通信、流媒体。
4.4 Python实现:TCP回射服务器
理论讲完了,咱们动手写个东西。回射服务器(Echo Server)是网络编程里的「Hello World」。客户端发什么,服务器就原样返回什么。
我个人习惯用Python的socket模块,简洁明了。
# tcp_echo_server.py
import socket
def start_server(host='0.0.0.0', port=8888):
# 创建TCP套接字
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 允许地址重用,避免「Address already in use」
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 绑定地址和端口
server_socket.bind((host, port))
# 开始监听,最大等待队列为5
server_socket.listen(5)
print(f'[服务器已启动] 监听 {host}:{port}')
try:
while True:
# 接受客户端连接
client_socket, client_addr = server_socket.accept()
print(f'[新连接] 来自 {client_addr}')
# 处理客户端数据
with client_socket:
while True:
data = client_socket.recv(1024)
if not data:
break # 客户端关闭连接
print(f'[收到] {data.decode()}')
# 原样返回
client_socket.sendall(data)
print(f'[回射] {data.decode()}')
except KeyboardInterrupt:
print('\n[服务器关闭]')
finally:
server_socket.close()
if __name__ == '__main__':
start_server()
# tcp_echo_client.py
import socket
def start_client(host='127.0.0.1', port=8888):
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect((host, port))
print(f'[已连接] {host}:{port}')
try:
while True:
msg = input('请输入消息(输入exit退出): ')
if msg.lower() == 'exit':
break
client_socket.sendall(msg.encode())
echo_data = client_socket.recv(1024)
print(f'[服务器回射] {echo_data.decode()}')
finally:
client_socket.close()
print('[客户端关闭]')
if __name__ == '__main__':
start_client()
运行方式:先开终端运行服务器,再开另一个终端运行客户端。输入什么,就返回什么。你想想看,这个简单的模型背后,其实已经跑完了三次握手、滑动窗口、四次挥手这一整套流程。
4.5 本章知识体系
下面这张图,把传输层的核心脉络串起来了。我建议你多看几遍,把每个模块之间的关联搞清楚。
这张图把传输层的核心脉络串起来了。TCP负责可靠,UDP负责快速,端口号负责寻址。你写代码时,心里要有这张图。
最后提醒一句:别以为TCP就万无一失。我曾经在弱网环境下测试,TCP重传导致延迟飙升,反而UDP+应用层重试的方案更稳定。选型没有银弹,只有最适合场景的方案。
公众号:蓝海资料掘金营,微信deep3321