1. 再谈 UDP
报文长度:也是 2 个字节, 0 - 65535,也就是 64 kb。这表示一个 UDP 数据包一次最多只能传输 64 kb 的数据
校验和:验证数据是否在传输过程中发生修改。数据在传输过程中可能受到信号干扰,发生 “比特翻转”。若 UDP 计算的两次校验和不一致,则直接丢弃该数据包。
2. 再谈 TCP
报头长度:4 bit,表示 0-15,单位为 4 字节(32 bit),则最大表示 60 字节。TCP 报头中除 “可选的” 外,均为固定长度,共 20 字节。则 “可选的” 最多 40 字节。
保留:预留出的空闲位置,以备后续拓展。
序号和确认序号:序号和确认序号组成 TCP 的序列号系统,用于保证 TCP 传输数据的顺序正确。
2.1 TCP 传输管理之确认应答
TCP 的确认应答机制保证数据在传输层的顺序正确性,而无需应用层手动验证顺序。
发送方为每个字节数据分配的唯一序列号,用于标记数据在数据流中的位置。例如:发送一段 1000 字节的数据,首字节序列号为 100,则后续字节序列号为 101~1099。发送方按序发送数据,序号中填写起始序列号。
确认序号(Ack)表示接收方 “期望接收的下一个字节的序列号”,隐含前序数据已正确接收。例如:接收方收到序列号 100~1099 的数据后,会返回 Ack = 1100,表示 “已正确接收前 1000 字节,期待下一个字节从 1100 开始”。发送方只有收到 ACK (TCP 报头标志位中的 ACK 位为 1)后,才确认数据已成功接收,否则超时重传。
接收方收到数据后,会根据序列号将数据放入接收缓冲区。若数据有序(序列号连续),则按序交付上层应用;若数据失序(序列号不连续),则缓存该数据,等待缺失的中间数据到达后再重组交付。
例如,发送方依次发送 Seq = 100、200、300 的数据段,若接收方仅收到 Seq = 100 和 300,则接收方会缓存 Seq = 300 的数据(因失序暂时无法交付上层),仅返回 Ack = 200,表示 “期待从 200 开始的字节”,隐含 Seq = 100 之前的数据已确认,Seq = 200 及之后未确认。
2.2 TCP 传输管理之超时重传
保证 TCP 可靠传输 的关键机制是 “确认应答” 和 “超时重传”,而不是 “三次握手”。这些机制都体现了协议分层 “专业层做专业事” 的原则:传输层处理分包细节,应用层专注业务逻辑。(若使用 UDP 开发则需要应用层手动处理分包和重发。不过现在的 UDP 已经被 QUIC 封装,HTTP/3 + QUIC 已经实现了更高级的可靠传输机制)
2.3 TCP 连接管理之三次握手
注:
TCP 对于最后 ACK 丢失的处理。
客户端发送 ACK 后即进入 ESTABLISHED 状态。正常情况下,ESTABLISHED 状态的客户端不应该收到 SYN=1 的包,因为 SYN 标志仅用于连接建立阶段。如果收到 SYN=1 的包,客户端会判断这是服务器对丢失 ACK 的重传,则忽略 SYN 标志并重发 ACK。
关于三次握手的底层细节。
服务器创建监听描述符 listen_fd,进入 LISTEN 状态。
监听描述符是服务器专用于接收客户端连接请求的套接字描述符,服务器通过调用 listen() 将普通套接字转换为监听状态(Java 中无需显式调用 listen(),因为 ServerSocket 已经封装了这一步操作)。
监听描述符不参与数据传输,仅维护一个未完成连接队列(SYN Queue)和一个已完成连接队列(Accept Queue)。多个客户端连接请求由同一个监听描述符处理。
客户端发送 SYN 数据包。IP 报头包括源 IP(客户端 IP)和目的 IP(服务器 IP)。TCP 报头包括源端口(客户端随机高位端口)和目的端口(服务器主动指定的端口)。载荷主要包括随机生成的初始序列号 Client_ISN、
自己能接收的数据缓冲区大小(窗口大小)、期望接收的单个数据段最大字节数(MSS)。
服务器内核解析出源 IP、源端口,与自身 IP 和端口组合成五元组,用于标识该连接(底层协议的基础设施)。并通过监听描述符接收载荷,将连接放入 SYN Queue。
服务器通过监听描述符响应 SYN + ACK 数据包。主要包括确认序号(数值为 Client_ISN + 1)、随机生成的初始序列 Server_ISN、窗口大小、MSS 等。
客户端收到 SYN + ACK 后,发送 ACK 数据包,确认序号为 Server_ISN + 1。客户端状态变为 ESTABLISHED,服务器在收到 ACK 后,连接转移至 Accept Queue,状态也变为 ESTABLISHED,连接正式建立。
服务器调用 accept(),该方法会从 Accept Queue 中取连接,并为每个连接生成连接描述符(conn_fd)。后续数据传输通过 conn_fd 完成,监听描述符继续监听新连接请求。
每个 TCP 连接对应一个套接字结构体(Struct Sock),包含发送缓冲区、接收缓冲区、连接状态等信息。
2.4 TCP 连接管理之四次挥手
TCP 四次挥手在 Java 代码中的核心触发点是 socket.close()
方法,该方法会触发 FIN 包的发送。而 ACK 包的发送和接收由 Java 底层的 TCP 协议栈自动处理,无需手动干预。
由于主动断开连接的一方既有可能是客户端,也可能是服务器(虽然实际上大部分是客户端),此处用主动和被动来区分通信双方,而不用客户端和服务器。
注:
被动方的 ACK 和 FIN 不能合并表示。
因为它们的发送时机不一定相同。被动方收到主动方的 FIN 后,Java 底层的 TCP 协议栈会自动发送 ACK,而 FIN 要等到被动方在应用层代码调用 close 方法才会发送。
2.5 TCP 效率机制之滑动窗口与快速重传
TCP 保障传输的可靠性需要牺牲效率,滑动窗口是 TCP 为了提升效率而产生的机制。不过,滑动窗口只是在一定程度上提高了效率,比起 UDP 这种协议,TCP 依然效率比较低。
在滑动窗口机制下,数据的发送方不再是接收一条 ACK 才发送下一条消息,而是连续发送一定量的消息后再等待(能够连续发送的最大消息量取决于双方协商的窗口大小)。
发送前几条消息时,无需等待任何 ACK,直接发送。相当于将分开等待多个 ACK 的时间合并成一个。发送方收到 ACK 后,滑动窗口向后移动。网络中可能存在后发先至的情况,假设发送方先收到接收方对第二条数据的 ACK,那么滑动窗口直接向后移动两格即可。因为这条 ACK 的意思是,之前的数据都已确认,发送方就没必要再维护前面的数据了,对第一条数据的 ACK 也没意义了。
为了维护这个滑动窗口,内核会开辟发送缓冲区和接收缓冲区来记录数据的应答和使用情况。所以这也是空间换时间的体现。
滑动窗口提升效率的地方就在于,它合并了一部分等待 ACK 和发送数据的时间。假设窗口大小为 4000 个字节,发送方收到 ack = 1000 时,就可以发送 seq = 4000 的数据了,发送完这条数据,它大概率也能收到 ack = 2000 了。
在滑动窗口过程中丢包,若丢的是 ACK,其实没有关系,因为后面的 ACK 会覆盖丢失 ACK 的含义。若丢的是发送方的数据,假设 seq = 3000 丢失,那么接收方一定会一直索要从 3000 开始的数据,其他序号的数据会被存入接收方的发送缓冲区中。无论接收方收到序号多么靠后的数据,它都不会索要后面的数据,因为前面尚有缺失的数据。当发送方连续收到三个相同的 ACK,就会立即重发这条数据,这种机制叫做快速重传。
2.6 TCP 安全机制之流量控制
TCP 可以根据接收端的处理能力来决定发送端的发送速度,这个机制被称为流量控制。接收方将自己接收缓冲区的剩余大小填入 TCP 首部中的 “窗口大小” 字段,通过 ACK 告知发送方。
发送方会根据这个数字,动态调整发送速度。数字越大,表示网络吞吐率越高。
如果接收方缓冲区已满,则会将窗口置为 0,此时发送方不再发送数据。但发送方仍需定期发送一个窗口探测包,这个报文只是为了触发 ACK,使接收方将窗口大小告知发送方。
TCP 首部中还包含窗口扩大因子字段,通过这个字段来扩展窗口大小,使其不再拘泥于 65535 字节。
2.7 TCP 安全机制之拥塞控制
流量控制根据接收方的处理能力进行限制,拥塞控制根据传输设备的转发能力进行限制。在两台设备建立 TCP 连接之初,如果一方在不清楚当前网络状态的情况下,贸然发送大量数据,可能引发许多问题。
因此 TCP 采用 “慢启动” 模式,先发送少量数据探测当前的网络拥堵状态,再决定传输速度。
发送方会维护一个变量,这个变量称为拥塞窗口。发送开始时,拥塞窗口大小为 1,每收到一个应答,拥塞窗口加 1。取拥塞窗口和滑动窗口中较小的值作为实际发送的窗口。这里的拥塞窗口实际上描述的是最大报文段长度的倍数,因此初始时拥塞窗口会呈指数增长。
当增长到一个慢启动的阈值时,改为线性增长。
2.8 TCP 效率机制之延迟应答
接收方如果在收到数据后立刻回复 ACK,可能会返回一个较小的窗口值。这是因为接收方刚刚接收完数据,缓存区被占用。因此这个窗口值并不能真实反映接收方的处理能力。有可能接收方处理速度很快,但仍立即返回了一个较小的窗口,此时接收方远没有达到自己的极限。
如果接收方稍作等待,再进行应答,那么此时的窗口值就可以比较正确地反映其真实处理能力了。
延迟应答的模式分为,隔几个包应答一次,隔一段时间应答一次。因为 ACK 的特点是,后一个可以覆盖前一个的内容,并且滑动窗口的机制也使得 TCP 不会一直等待某个未归的 ACK,因此少几个 ACK 没什么影响。
但也不能延迟太长时间,不能引起发送方重发数据。
2.9 TCP 效率机制之捎带应答
捎带应答是指在同一个 TCP 包中既发送数据又发送确认应答的一种机制。
捎带应答是在延迟应答的基础上实现的。ACK 报文实际上就是在 TCP 报头中设置几个字段,比如标志位中的 ACK 位设为 1,设置合适的窗口大小和确认序号等,并不携带具体数据。为了提高网络利用率,可以将其与响应数据合并应答,但前提是必须有延迟应答这一机制提供基础。
3. 经典问题
如何基于 UDP 实现更为可靠的传输?
在应用层实现 TCP 的可靠性机制。比如,引入序列号,采用确认机制,超时重传,数据分片与重组的控制,流量控制,拥塞控制等。目前已经有一些成熟方案,如 QUIC。具体实现需根据实际业务场景灵活调整机制的严格程度,最终目的是在效率和可靠性之间取得平衡。
UDP 大小受限,如果想基于 UDP 协议传输超过 64 kb 的数据,应该怎样做?
在应用层实现数据分片和重组机制。不仅要考虑 UDP 的分片,也要考虑到当前链路层的 MTU。为了实现分片和重组,每个分片需维护额外信息,这包括:会话标识用于区分不同传输任务,总片数用于告知接收方重组时机,序号,确认序号,等等。
为什么要三次握手?
其一,确保通信路径畅通,保证连接合法性。三次握手中,通信双方均需确认彼此的响应能力,避免资源浪费。
其二,同步参数,确保兼容。告知对端自己随机生成的初始序列号,而不是每次连接都使用同一个初始序列号,避免新旧连接的数据包混淆。协商适合双方网络路径的数据包大小,避免分片。告知对方接收缓冲区容量,实现流量控制。
解释 SYN 洪水攻击。TCP 怎样解决这个问题?
SYN Flood 是指,攻击者向服务器发送大量虚假 SYN 包(使用随机源 IP),服务器回复 SYN+ACK 后,永远不能收到返回的 ACK(因为源 IP 是伪造的)。此时服务器积累大量半开连接,表现为服务器中大量进程处于 SYN_RCVD 这个中间状态。
但服务器仍会为这些半开连接分配资源,比如会将连接维护在未完成连接队列(SYN Queue)中。如果 SYN Queue 被恶意连接占满,那么合法请求将无法进入。
通常使用 SYN Cookies 技术来抵御 SYN Flood 攻击。在正常的 TCP 三次握手时,服务器收到 SYN 包会先分配半连接队列资源,再返回 SYN+ACK 包。而 SYN Cookies 技术下,服务器收到 SYN 包后不创建半连接状态,而是根据 SYN 包中的源地址、源端口、目的地址、目的端口等通过加密计算出一个 Cookie,并将其作为 SYN+ACK 包的初始序列号发送出去。
此时正常请求的客户端会响应 ACK,该 ACK 的确认号为 Cookie + 1。服务器收到该 ACK 后,首先根据 IP 报头,TCP 报头等信息重新计算一次 Cookie,若与 ACK 的确认号 - 1 一致,则直接建立全连接。
该技术的优势在于,其仅通过协议栈优化而在软件上实现防御,无需硬件辅助。而缺点在于,服务器不再维护半开连接也就意味着牺牲了一部分 TCP 的参数协商功能,如 MSS 被设为默认值,可能降低传输效率。因此该技术通常在半连接队列即将满时自动启用,以避免正常场景下的性能损耗。
解释 TIME_WAIT。TIME_WAIT 为何持续 2MSL?
MSL 全称 Max Segment Life,即 TCP 报文最大生存时间。TIME_WAIT 持续存在 2MSL 可以保证两个传输方向上都没有尚未被接收或迟到的报文段(第一个 MSL 可以保证最后的 ACK 过期,第二个 MSL 可以保证可能重发的 FIN 过期)。这可以规避两个问题。
其一,避免服务器由于立即重启并复用相同端口而收到来自上一个进程的迟到的数据。
其二,让主动关闭方有足够时间等待被动关闭方可能重发的 FIN,并再次发送 ACK。
服务器大量出现 CLOSE_WAIT 是何原因?
处于 CLOSE_WAIT 的进程在等待上层协议调用 close 方法。如果该状态持续时间很久,意味着应用层代码无法执行到 close()。这代码逻辑的 BUG,需要尽快排除。许多连接无法正常关闭,久而久之连接描述符被占满,会导致资源泄露。
如何解决 TCP 粘包问题?
这个问题在传输层无法解决,因为这是 TCP 面向字节流的特性导致的问题。UDP 不会出现这样的问题,因为我们的发送和读取都是以一个 UDP 数据包为单位的。
此时需要应用层协议明确包与包之间的界限。可以约定包的结束标记,也可以在每个包开头预留字段表示包的长度。这两种方式在 HTTP 协议中均有体现。
TCP 如何应对异常情况?
1. 进程崩溃:进程崩溃和主动退出没有本质区别,都会正常释放文件描述符表,发送 FIN。TCP 的实现位于操作系统内核,而非用户态进程,进程只是 TCP 连接的使用者。因此,即使进程终止,内核中的 TCP 协议栈仍能独立完成四次挥手。
2. 正常关机:正常关机前,系统都会先 kill 掉所有的进程,所以和正常关闭没什么区别。但理论上来说,四次挥手仍可能因时间不足而未完成。不过此时通信双方都已进入挥手阶段,如果某个结束报文迟迟没有响应,那么超时后该设备会单方面释放连接,并正常释放连接资源。由于双方只是未正常完成挥手的流程,因此对数据影响不是很大。
3. 接收方断电或断网:这属于异常关机。发送方久久收不到任何 ACK,触发重传,重传依旧无果,此时发送方会发送一个 RST 报文。RST 报文用于强制终止一个异常的 TCP 连接,跳过四次挥手,立即释放连接资源。
4. 发送方断电或断网:接收方久久收不到任何数据,每间隔一定时间,接收方会发送 “心跳包” 用以尝试触发 ACK。多次发送无果,接收方发送 RST 报文强制关闭连接。TCP 协议的心跳包周期较长,真实场景下通常在业务层重新实现毫秒级的心跳包。