大家好,这里是编程Cookbook。本文详细介绍计算机网络中的TCP/UDP协议相关的内容,包括单不限于基础概念、连接的建立与断开、TCP可靠传输的实现等。
TCP/UDP 基础概念
关注公众号「编程Cookbook」,获取更多编程学习/面试资料!
什么是 TCP 连接?
TCP(Transmission Control Protocol,传输控制协议)是面向连接的、可靠的、基于流的传输层协议。TCP 报文段如下所示:
TCP 报文段的首部通常为 20 字节(无选项时),最大可扩展至 60 字节。具体结构如下:
字段 | 长度(字节) | 说明 |
---|---|---|
源端口(Source Port) | 2 | 发送方的端口号(如 54321 ) |
目的端口(Destination Port) | 2 | 接收方的端口号(如 80 表示 HTTP) |
序列号(Sequence Number) | 4 | 本报文段数据的第一个字节的编号(用于数据排序) |
确认号(Acknowledgment Number) | 4 | 期望收到的下一个字节的序号(用于确认接收) |
数据偏移(Data Offset) | 4 bits | TCP 首部长度(以 4 字节为单位,最小 5 → 20 字节) |
保留(Reserved) | 6 bits | 未使用,必须置 0 |
控制标志(Flags) | 6 bits | 用于连接控制(如 SYN , ACK , FIN ) |
窗口大小(Window Size) | 2 | 接收方的可用缓冲区大小(流量控制) |
校验和(Checksum) | 2 | 校验首部 + 数据的完整性(含伪首部,类似 UDP) |
紧急指针(Urgent Pointer) | 2 | 仅当 URG=1 时有效,指向紧急数据的末尾 |
选项(Options) | 可变(0-40字节) | 可选字段(如 MSS、窗口缩放因子、时间戳等) |
填充(Padding) | 可变 | 确保选项字段对齐 4 字节边界 |
数据(Data) | 可变 | 上层应用数据(如 HTTP 请求、文件内容等) |
TCP 连接的特点:
面向连接
:通信前,必须通过“三次握手”建立连接,确保双方准备好数据传输。可靠传输
:TCP 采用 确认机制(ACK)、超时重传、流量控制、拥塞控制,确保数据不丢失、不重复、按序到达。基于流
:TCP 以字节流(Byte Stream)形式传输数据,没有明确的消息边界,需要应用层自行处理分包和粘包。- 全双工:双方可以同时发送和接收数据。
- 有序传输:TCP 通过 序列号(Sequence Number) 确保数据按正确顺序到达。
TCP 连接建立过程(简要):
- 三次握手:保证双方通信能力,并初始化必要的参数(如序列号)。
- 数据传输:通过滑动窗口、超时重传、ACK 确保可靠传输。
- 四次挥手:确保数据完整性后关闭连接。
关注公众号「编程Cookbook」,获取更多编程学习/面试资料!
什么是 UDP 连接?
UDP(User Datagram Protocol,用户数据报协议)是无连接的、不可靠的、基于报文的传输层协议。 其数据报格式包含 首部(Header) 和 数据部分(Data)。此外,在计算校验和时还会用到 伪首部(Pseudo Header)。UDP 数据报如下所示:
UDP 数据报由 8字节首部 + 数据部分 组成,具体结构如下:
伪首部 仅用于 校验和计算,不会真正传输。它的作用是确保数据报被正确路由到目标 IP 和端口,防止 IP 欺骗或错误转发:
UDP 的特点:
- 无连接:发送数据前不建立连接,接收方随时可以处理数据。
- 不可靠:不保证数据到达,不保证数据顺序,也不提供重传机制。
- 基于报文:数据以独立的 UDP 报文(Datagram) 发送,每个报文是完整的,没有流的概念。
- 低开销:UDP 头部仅 8 字节,较 TCP(20 字节)开销小,适用于低延迟场景。
- 适合实时传输:UDP 适用于 音视频、在线游戏、DNS 查询 等场景,即使部分数据丢失,也不会影响整体体验。
UDP 传输过程:
- 发送端直接将数据封装成 UDP 数据报,通过 IP 层发送给目标主机。
- 接收端从 UDP 缓冲区获取数据报,并交给应用层处理(但可能丢包、乱序)。
TCP 和 UDP 对比
对比项 | TCP(传输控制协议) | UDP(用户数据报协议) |
---|---|---|
是否连接 | 面向连接 ,需建立连接(3 次握手) | 无连接 ,直接发送数据 |
可靠性 | 可靠传输 ,保证数据不丢失、不重复、按序到达 | 不可靠传输 ,可能丢包、乱序、重复 |
数据边界 | 面向字节流 ,无明确的消息边界,可能粘包 | 面向数据报 ,有独立的数据包边界 |
传输效率 | 开销较大,需要维护连接和状态 | 轻量级,无需连接管理,低延迟 |
流量控制 | 通过滑动窗口调整传输速率 | 无流量控制,可能导致接收方过载 |
拥塞控制 | 通过 AIMD 算法防止网络拥塞 | 无拥塞控制,可能造成网络拥塞 |
应用场景 | 需要高可靠性,如 HTTP、FTP、数据库 | 需要低延迟,如视频流、DNS、VoIP |
关注公众号「编程Cookbook」,获取更多编程学习/面试资料!
TCP 是用来解决什么问题的?
TCP 主要用于解决以下问题:
- 可靠传输:确保数据能够按序、完整地传输,避免数据丢失、重复或乱序。
- 流量控制:通过滑动窗口机制,控制发送方的发送速率,避免接收方缓冲区溢出。
- 拥塞控制:通过拥塞窗口和算法(如慢启动、拥塞避免),避免网络拥塞。
- 连接管理:通过三次握手和四次挥手,确保连接的建立和释放是可靠的。
典型应用场景:
- 文件传输(如 FTP)
- 网页浏览(如 HTTP/HTTPS)
- 电子邮件(如 SMTP)
- 数据库访问
UDP 是用来解决什么问题的?
UDP 主要用于解决以下问题:
- 低延迟传输:无需建立连接,直接发送数据,适合对实时性要求高的场景。
- 简单高效:头部开销小,传输效率高,适合轻量级通信。
- 广播和多播:支持一对多通信,适合广播和多播场景。
典型应用场景:
- 实时音视频传输(如 VoIP、视频会议)
- 在线游戏
- DNS 查询
- 广播和多播应用
TCP 和 UDP 分别对应的常见应用层协议有哪些?
协议类型 | 常见应用层协议 |
---|---|
TCP | HTTP/HTTPS(网页浏览)、FTP(文件传输)、SMTP(电子邮件)、SSH(远程登录)、Telnet |
UDP | DNS(域名解析)、DHCP(动态主机配置)、SNMP(网络管理)、TFTP(简单文件传输) |
为什么要 TCP,IP 层实现控制不行吗?
首先需要明白TCP,UDP 和 IP 协议之前的区别:
- TCP 适合可靠传输,提供流量控制、拥塞控制,保证数据有序。
- UDP 适合实时传输,低延迟、高吞吐,但可能丢包、乱序。
- IP 层只是尽力传输,TCP 是在其基础上提供可靠性。
IP 层(互联网协议)只负责无连接、尽力而为(Best-effort)的数据传输,但它本身存在以下问题:
- 不可靠:IP 数据报可能丢失、乱序、重复,应用层需要额外处理。
- 无流量控制:IP 层不会限制发送速率,可能导致接收方过载。
- 无拥塞控制:IP 层不会检测网络拥塞,可能导致全网性能下降。
TCP 之所以存在,是为了弥补 IP 层的不足,提供可靠、稳定的传输。
如果没有 TCP,应用层需要自己处理丢包、重传、乱序等复杂问题,大大增加了开发难度。
关注公众号「编程Cookbook」,获取更多编程学习/面试资料!
TCP 连接建立与断开
TCP 三次握手(Three-Way Handshake)
TCP 是面向连接的协议,在数据传输前,通信双方需要通过 三次握手(Three-Way Handshake) 建立连接,确保双方都具备发送和接收数据的能力。
三次握手的过程
假设 客户端(Client) 要与 服务器(Server) 建立 TCP 连接,三次握手的步骤如下:
- SYN = 1,seq = x。
- SYN = 1,ACK = 1,seq = y,ack = x + 1。
- ACK = 1,seq = x + 1,ack = y + 1。
1️⃣ 第一次握手(Client → Server,发送 SYN)
- 客户端发送一个 SYN(同步) 报文,请求建立连接,并携带一个 初始序列号 ISN(Initial Sequence Number)。
SYN=1, seq=x
- 此时,客户端进入 SYN-SENT 状态。
2️⃣ 第二次握手(Server → Client,发送 SYN-ACK)
- 服务器收到 SYN 请求后,回应一个 SYN-ACK 报文,表示同意连接,并指定自己的初始序列号 ISN(y)。
SYN=1, ACK=1, seq=y, ack=x+1
- 此时,服务器进入 SYN-RECEIVED 状态。
3️⃣ 第三次握手(Client → Server,发送 ACK)
- 客户端收到 SYN-ACK 后,回复一个 ACK 报文,确认连接建立,并表明自己可以发送数据了。
ACK=1, seq=x+1, ack=y+1
- 客户端进入 ESTABLISHED(已建立连接) 状态。
- 服务器收到 ACK 后,也进入 ESTABLISHED 状态,连接正式建立。
📌 完成三次握手后,双方可以正式开始数据传输。
TCP 四次挥手(Four-Way Handshake)
TCP 连接关闭时,需要四次挥手(Four-Way Handshake) 来保证数据完全传输,并确保双方都同意断开连接。
关注公众号「编程Cookbook」,获取更多编程学习/面试资料!
四次挥手的过程
假设 客户端(Client) 先发起关闭连接的请求,四次挥手的步骤如下:
- FIN = 1,seq = u。
- ACK = 1,seq = v,ack = u + 1。
- FIN = 1,ACK = 1,seq = w,ack = u + 1。
- ACK = 1,seq = u + 1,ack = w + 1。
1️⃣ 第一次挥手(Client → Server,发送 FIN)
- 客户端发送 FIN(Finish) 报文,表示不再发送数据,但仍可以接收数据。
FIN=1, seq=u
- 客户端进入 FIN-WAIT-1 状态。
2️⃣ 第二次挥手(Server → Client,发送 ACK)
- 服务器收到 FIN 后,发送一个 ACK 确认。
ACK=1, seq=v, ack=u+1
- 服务器进入 CLOSE-WAIT 状态,客户端进入 FIN-WAIT-2 状态。
- 服务器仍然可能需要处理未完成的任务,因此连接暂时不会关闭。
3️⃣ 第三次挥手(Server → Client,发送 FIN)
- 服务器处理完数据后,向客户端发送 FIN 报文,表示自己也不再发送数据。
FIN=1, seq=w, ack=u+1
- 服务器进入 LAST-ACK 状态。
4️⃣ 第四次挥手(Client → Server,发送 ACK)
- 客户端收到 FIN 后,回复一个 ACK 报文,表示确认断开。
ACK=1, seq=u+1, ack=w+1
- 客户端进入 TIME-WAIT 状态,等待 2MSL(最大报文生存时间) 后,才真正关闭。
- 服务器收到 ACK 后,立即进入 CLOSED(关闭) 状态,连接完全关闭。
📌 为什么服务器的 ACK 和 FIN 不能合并?
- 服务器在接收到 FIN 后,可能仍然有未发送的数据,所以需要 先发送 ACK,处理完数据后,再发送 FIN。
TCP 初始序列号 ISN
ISN 的定义
- ISN(Initial Sequence Number) 是 TCP 连接建立时,每个通信方选定的起始序列号,用于数据传输中的字节编号。
如在 TCP 三次握手 过程中:
- 客户端 发送 SYN 请求,并携带自己的 ISN(x)。
- 服务器 回复 SYN-ACK,并携带自己的 ISN(y)。
ISN 的作用
- 解决 TCP 可靠传输中的数据编号问题:TCP 以字节为单位 进行数据传输,每个字节都需要一个序列号。ISN 作为初始编号,确保每个 TCP 段都有唯一的序列号,方便数据接收端按序重组数据。
- 防止 TCP 连接中的数据包混淆:TCP 连接断开后,可能仍有旧的 TCP 报文在网络中滞留。TCP 采用动态 ISN 生成,让每次连接的 ISN 随机变化,防止旧连接数据包干扰新连接。
ISN 的取值
- TCP 初始序列号 ISN(Initial Sequence Number) 并不是固定的,而是动态生成的。
ISN 的生成方式
- 传统方法:每次连接 ISN 可能固定为 0,但这样容易被攻击者预测,造成数据包劫持。
- 现代方法:ISN 通常使用时间戳加随机数,防止连接劫持:
ISN = 当前时间戳 + 随机增量
- 现代操作系统(如 Linux、Windows)采用基于时间的 ISN 生成算法,让 ISN 随着时间增加,确保安全性。
TCP 三次握手时,发送 SYN 之后就宕机了会怎么样?
假设客户端发送 SYN 之后,宕机了:
- 服务器收到了 SYN,并回复 SYN-ACK,但客户端已经宕机,无法回复 ACK。
- 服务器会一直等待 ACK,但由于没有响应,会重传 SYN-ACK 若干次(通常是 3-6 次)。
- 最后,服务器会超时,进入 CLOSED 状态,释放资源。
注意:
- 每次 SYN-ACK 重传的间隔不是固定的,而是 指数退避(Exponential Backoff) 的策略(1s → 2s → 4s → 8s → 16s → 32s),每次重传的间隔会逐渐增加。
📌 影响:
- 服务器的资源(如连接队列)会被占用,可能导致 SYN Flood 攻击。
SYN Flood 攻击
SYN Flood(SYN 泛洪攻击) 是一种 DoS(拒绝服务攻击),利用 TCP 三次握手的机制,导致服务器资源耗尽。
攻击原理
- 攻击者伪造大量 SYN 请求,但不发送 ACK,导致服务器一直等待(SYN-RECEIVED 状态),耗尽服务器的资源(如半连接队列),无法处理新的请求,造成拒绝服务(DoS)。
防御措施
1. 增加半连接队列大小
- 原理:通过增大服务器的半连接队列(SYN 队列)容量,可以暂时缓解大量 SYN 报文导致的队列溢出问题。
- 实现:
- 调整操作系统的 TCP 参数,例如
net.ipv4.tcp_max_syn_backlog
(Linux 系统)。 - 增大服务器的内存资源,以支持更大的队列。
- 调整操作系统的 TCP 参数,例如
关注公众号「编程Cookbook」,获取更多编程学习/面试资料!
2. 减少 SYN+ACK 重试次数
- 原理:当服务器发送 SYN+ACK 后未收到客户端的 ACK 时,会进行多次重试。减少重试次数可以更快地释放半连接资源。
- 实现:
- 调整操作系统的 TCP 参数,例如
net.ipv4.tcp_synack_retries
(默认是5,Linux 系统)。
- 调整操作系统的 TCP 参数,例如
3. 启用 SYN Cookie 机制
- 原理:SYN Cookie 是一种防御 SYN Flood 攻击的技术。服务器在收到 SYN 报文后,不立即分配资源,而是通过加密算法生成一个 Cookie 值作为初始序列号。只有收到合法的 ACK 报文后,服务器才会分配资源。
- 实现:
- 在 Linux 系统中,启用 SYN Cookie:
sysctl -w net.ipv4.tcp_syncookies=1
。 - 思想是:服务器不再维护 SYN 半连接队列,而是基于客户端的 SYN 报文计算出一个“Cookie”(即特定格式的 ISN,初始序列号),并在 SYN+ACK 报文中返回给客户端。客户端在第三次握手(ACK 报文)中返回这个 Cookie,服务器通过计算验证它的正确性,再正式建立连接。
- 在 Linux 系统中,启用 SYN Cookie:
- 优点:
- 无需维护半连接队列,节省资源。
- 有效抵御伪造源 IP 的 SYN Flood 攻击。
半连接状态分配的典型资源有哪些?
- 内存资源:服务器需在内存中开辟空间记录半连接相关信息,如客户端 IP 地址、端口号、连接请求时间、序列号等。
- 连接队列资源:服务器设置了专门的半连接队列(SYN 队列)。
三次握手过程中可以携带数据吗?
简短回答:理论上可以,但通常不携带,实际应用中只有第三次握手可能携带数据。
三次握手过程分析
在 TCP 三次握手的过程中,只有第三次握手才能发送数据:
-
普通 TCP:
客户端 -> 服务器: SYN 服务器 -> 客户端: SYN+ACK 客户端 -> 服务器: ACK 客户端 -> 服务器: 数据包
- 需要 额外一次 RTT 之后才能发送数据。
-
第三次握手携带数据:
客户端 -> 服务器: SYN 服务器 -> 客户端: SYN+ACK 客户端 -> 服务器: ACK + 数据包
- 直接在第三次握手的
ACK
报文中携带数据,减少一次 RTT。
- 直接在第三次握手的
为什么第一次、第二次握手不能携带数据?
- TCP 规定
SYN
报文 不能携带数据,因为此时连接还未建立,无法确认对方是否能正确接收数据。 - 服务器收到
SYN
后,需要分配资源,并在SYN + ACK
里回复自己的初始序列号,因此 不能提前接收数据。
为什么第三次握手可以携带数据?
- 连接已经建立(服务器收到
ACK
之后,连接状态变为ESTABLISHED
)。 - 服务器已经具备接收能力,理论上可以直接处理数据。
- 减少一次 RTT(往返时延),提高传输效率。
特殊情况:TCP Fast Open(TFO)
TCP Fast Open(TFO) 是 TCP 的一个优化,它允许 在第一步 SYN 报文中携带数据,但前提是:
- 客户端和服务器都支持 TFO。
- 客户端之前已经和服务器通信过,并缓存了 TFO Cookie。
- 服务器只有在确认 TFO Cookie 合法后,才会处理数据。
- SYN 携带的数据在服务器接收到 SYN+ACK 之后才能被处理。
TFO 的工作流程
-
第一次握手(SYN):
- 客户端发送 SYN 报文,并在报文中携带一个特殊的 TFO Cookie(由服务器在之前的连接中生成)。
- 客户端可以在 SYN 报文中携带数据(例如 HTTP 请求)。
-
第二次握手(SYN+ACK):
- 服务器验证 TFO Cookie 的合法性。
- 如果 Cookie 有效,服务器可以在 SYN+ACK 报文中携带响应数据。
-
第三次握手(ACK):
- 客户端发送 ACK 报文,确认服务器的响应。
- 连接正式建立。
除了四次挥手,还有什么方法断开连接?
-
RST(Reset)强制断开
- 直接发送 RST 报文,立即终止连接,不等待对方确认。
- 适用于异常情况下的连接终止(如端口错误、程序崩溃)。
-
超时自动关闭
- 如果长时间没有数据传输,连接可能被 超时机制(Keep-Alive 机制) 自动关闭。可以命令设置 TCP Keepalive 参数:
sysctl -w net.ipv4.tcp_keepalive_time=600 # 600 秒后开始检测
sysctl -w net.ipv4.tcp_keepalive_intvl=75 # 每次检测间隔 75 秒
sysctl -w net.ipv4.tcp_keepalive_probes=9 # 最多检测 9 次
- 如果长时间没有数据传输,连接可能被 超时机制(Keep-Alive 机制) 自动关闭。可以命令设置 TCP Keepalive 参数:
TCP 挥手的 TIME_WAIT 状态
简短回答:
TIME_WAIT
状态的存在是为了 确保对方正确关闭连接
和 避免旧连接影响新连接
。TCP 在主动关闭连接的一方需要等待 2MSL(最大报文生存时间的两倍) 后才能彻底释放连接资源,防止未收到的 FIN 报文或延迟的旧数据干扰新连接。
1. 什么是 TIME_WAIT 状态?
- TIME_WAIT 是 TCP 连接断开过程中的一个状态,出现在主动关闭方(即先发送 FIN 报文的一方)。
- 当主动关闭方发送
FIN
报文并接收到对方的FIN + ACK
后,会进入TIME_WAIT
状态。 TIME_WAIT
状态会持续 2MSL(Maximum Segment Lifetime,最大报文生存时间的两倍),之后连接才会彻底关闭。
关注公众号「编程Cookbook」,获取更多编程学习/面试资料!
2. 为什么需要 TIME_WAIT 状态?
1. 确保对方正确关闭连接(可靠性保证)
- 在四次挥手中,主动关闭方发送最后一个
ACK
报文后,需要等待一段时间,以确保对方能够收到这个ACK
。 - 如果
ACK
丢失,对方会重传FIN
报文,主动关闭方可以重新发送ACK
。
📌 示例:如果 TIME_WAIT 过早释放
1. 客户端 -> 服务器: FIN
2. 服务器 -> 客户端: ACK
3. 服务器 -> 客户端: FIN (ACK 丢失)
4. 客户端已关闭(但服务器仍在等待 ACK)
5. 服务器重发 FIN,客户端已经不在,服务器永远等待超时
🔹 TIME_WAIT
让客户端有机会重新发送 ACK
,防止这种情况发生!
2. 避免旧连接数据影响新连接(延迟数据问题)
- 在网络中,可能存在延迟的报文(即旧连接的报文)。
- 如果立即关闭连接,这些延迟报文可能会被误认为是新连接的报文,导致数据混乱。
TIME_WAIT
状态确保旧连接的报文在网络中完全消失,避免干扰新连接。
📌 示例:如果 TIME_WAIT 过早释放
1. 旧连接(A)正在传输数据,但关闭过早,没有进入 TIME_WAIT。
2. 旧连接(A)上的某个数据包在网络中延迟到达。
3. 服务器开启新连接(B),端口号与旧连接相同。
4. 旧数据包误入新连接(B),导致数据混乱!
🔹 TIME_WAIT
确保旧连接的残余数据在 2MSL 期间自然消亡,避免影响新连接!
3. TIME_WAIT 的持续时间—— 2MSL
- MSL(Maximum Segment Lifetime):报文在网络中的最大生存时间,通常为 30 秒到 2 分钟。
- 2MSL:确保报文在网络中完全消失的时间。
在 Linux 系统中,TIME_WAIT
的默认持续时间是 60 秒(即 2MSL=60 秒)。
4. TIME_WAIT 导致的问题及其解决方案
在高并发服务器(如 Nginx 代理、大量短连接应用)中,TIME_WAIT
可能导致 大量端口占用,系统资源耗尽。
在高并发场景下,可以通过以下方式优化 TIME_WAIT
状态:
(1)启用 TCP 重用(SO_REUSEADDR 和 SO_REUSEPORT)
- 允许重用处于
TIME_WAIT
状态的端口。 - 在 Linux 系统中,可以通过以下代码启用:
int opt = 1; setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); setsockopt(sockfd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
(2)调整 TCP 参数
- 减少
TIME_WAIT
状态的持续时间或者TIME_WAIT
连接的最大数量,超出后直接关闭最早的连接。sysctl -w net.ipv4.tcp_tw_reuse=1 # 允许重用 TIME_WAIT 状态的连接 sysctl -w net.ipv4.tcp_tw_recycle=1 # 快速回收 TIME_WAIT 状态的连接(不推荐) sysctl -w net.ipv4.tcp_fin_timeout=10 # 减少 FIN_WAIT_2 状态的超时时间
(3)使用长连接
- 减少短连接的频繁创建和关闭,从而减少
TIME_WAIT
状态的数量。