4、传输层协议详解:TCP三次握手与四次挥手、UDP协议特点、端口与套接字
传输层,说白了就是网络通信的「调度中心」。它负责把数据从源端送到目的端,至于怎么送、送得稳不稳,就看选 TCP 还是 UDP 了。我做了这么多年网络抓包和协议分析,可以负责任地告诉你:搞懂传输层,你才算真正入门了网络数据采集。
4.1 TCP 协议:可靠的「快递员」
TCP 全称是传输控制协议。它的核心就两个字:可靠。你发出去的数据,它保证能按顺序、不丢失、不重复地到达对端。代价是什么?慢,而且占用资源多。
TCP 的核心特性:
- 面向连接:通信前必须先建立连接,通信完必须释放连接。
- 全双工通信:双方可以同时收发数据。
- 可靠传输:通过确认、重传、序号、校验和等机制保证数据完整。
- 流量控制:防止发送方太快,把接收方「撑死」。
- 拥塞控制:网络堵了,就主动降速,别给网络添乱。
4.1.1 三次握手:建立连接的门票
TCP 建立连接的过程,就是著名的「三次握手」。为什么是三次,不是两次或四次?我当年面试时也被问过这个问题。咱们一步步拆解。
- 第一次握手(SYN):客户端发送一个 SYN 报文,告诉服务器「我想跟你建立连接,我的初始序号是 x」。
- 第二次握手(SYN + ACK):服务器收到后,回复一个 SYN+ACK 报文,意思是「收到你的请求,我同意。我的初始序号是 y,你的序号 x 我确认了」。
- 第三次握手(ACK):客户端再发一个 ACK 报文,确认服务器的序号 y。至此,连接建立完成。
我个人的经验: 抓包分析时,三次握手是最容易识别的特征。如果你看到 SYN、SYN+ACK、ACK 三个报文连续出现,基本可以断定这是 TCP 连接的开始。我曾经排查过一个连接超时问题,发现第三次握手丢了,客户端一直重传 SYN,服务器却以为连接已经建立——典型的「半连接」状态。
为什么会这样?因为三次握手能防止「历史连接」干扰。如果只有两次握手,服务器收到一个延迟的旧 SYN 报文,就会误以为客户端要建立新连接,白白浪费资源。三次握手让客户端有机会在第三步拒绝这个历史连接。
4.1.2 四次挥手:优雅地告别
断开连接比建立连接多一次握手,一共四次。为什么?因为 TCP 是全双工的,每一方向都要单独关闭。
- 第一次挥手(FIN):客户端发送 FIN 报文,告诉服务器「我没数据要发了」。
- 第二次挥手(ACK):服务器回复 ACK,表示「知道了,但我还有数据要发,你先等着」。
- 第三次挥手(FIN):服务器数据发完了,发送 FIN 报文,告诉客户端「我也没数据了」。
- 第四次挥手(ACK):客户端回复 ACK,然后进入 TIME_WAIT 状态,等待 2MSL 后彻底关闭。
我曾经踩过的坑: 有一次做高并发服务器压测,发现大量连接处于 TIME_WAIT 状态,导致端口被占满,新连接无法建立。原因就是客户端主动关闭连接后,TIME_WAIT 持续了 2MSL(约 2 分钟)。解决方案是调整内核参数,或者让服务器主动关闭连接。嗯,这里要注意:TIME_WAIT 不是 bug,它是为了保证最后一个 ACK 能到达对端,防止旧连接的数据干扰新连接。
4.2 UDP 协议:简单粗暴的「飞毛腿」
UDP 全称是用户数据报协议。它跟 TCP 完全相反:不可靠、无连接、尽最大努力交付。你发出去的数据,它不保证能到,也不保证顺序。但好处是快,开销小。
| 特性 | TCP | UDP |
|---|---|---|
| 连接方式 | 面向连接 | 无连接 |
| 可靠性 | 可靠,有确认重传 | 不可靠,尽最大努力 |
| 传输效率 | 低,有拥塞控制 | 高,无拥塞控制 |
| 报文边界 | 字节流,无边界 | 数据报,有边界 |
| 典型应用 | HTTP、FTP、SMTP | DNS、DHCP、视频直播 |
你想想看,视频通话时如果丢了一帧画面,你是愿意等它重传导致画面卡顿,还是直接跳过这一帧继续播放?显然是后者。这就是 UDP 的用武之地。
我个人的建议: 做网络数据采集时,如果采集的是实时流媒体或游戏数据,优先考虑 UDP。但如果你要采集网页内容或文件传输,必须用 TCP。选错了协议,数据要么不完整,要么延迟高到没法用。
4.3 端口与套接字:通信的「门牌号」
IP 地址定位到主机,端口号定位到进程。一个端口号是 16 位的,范围 0~65535。其中 0~1023 是知名端口,比如 HTTP 用 80,HTTPS 用 443,DNS 用 53。
套接字(Socket)是什么?说白了就是 IP 地址 + 端口号 的组合。一个 TCP 连接由四元组唯一标识:(源 IP,源端口,目的 IP,目的端口)。
端口分类:
- 知名端口(0~1023):系统服务使用,需要管理员权限绑定。
- 注册端口(1024~49151):用户进程使用,IANA 登记。
- 动态/私有端口(49152~65535):客户端临时使用,由系统自动分配。
我记得有一次排查一个 Web 服务无法启动的问题,发现是端口 80 被另一个进程占用了。用 netstat -ano 一看,果然有个残留的 nginx 进程没杀掉。所以做网络编程时,端口冲突是个很常见的坑。
4.3.1 套接字编程的简单示例
下面是一个最简单的 TCP 客户端代码,用 Python 写的。你一看就明白套接字是怎么用的。
import socket
# 创建套接字
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 连接服务器(IP + 端口)
server_address = ('192.168.1.100', 8080)
sock.connect(server_address)
# 发送数据
sock.sendall(b'Hello, Server!')
# 接收响应
data = sock.recv(1024)
print('Received:', data.decode())
# 关闭连接
sock.close()
这段代码做了四件事:创建套接字、连接、收发数据、关闭。你想想看,三次握手就发生在 connect() 这一步,四次挥手发生在 close() 这一步。底层细节都被操作系统封装好了,但作为网络工程师,你必须知道背后发生了什么。
注意: UDP 套接字不需要 connect(),直接用 sendto() 和 recvfrom() 收发数据。如果你用 UDP 写了一个客户端,却调用了 connect(),那它实际上只是绑定了一个对端地址,并没有建立真正的连接——UDP 的 connect() 是「伪连接」。
4.4 本章小结
传输层是网络协议栈的「腰部」,承上启下。TCP 用三次握手和四次挥手保证了可靠性,UDP 用无连接换取了速度。端口和套接字则是通信的「门牌号」,没有它们,数据就找不到对应的应用程序。
我个人习惯在抓包时先看传输层:如果是 TCP,重点关注握手和挥手是否完整;如果是 UDP,关注报文长度和校验和。这些细节,往往就是问题所在。