【javaEE】TCP协议总结

前言:本文将简单介绍TCP协议的基本特征以及TCP是如何做到可靠传输的。

一、为什么需要TCP?

  • 原因:互联网底层传输不可靠,数据丢包,乱序,重复问题频繁出现
  • TCP的定义:TCP是一种有连接,可靠传输,面向字节流,全双工的传输层通信协议
  • 核心价值:在不可靠的IP层协议之上,建立起可靠的通信链路

二、TCP 协议段格式

下面是一个TCP协议段的简图 :

字段名称 位数 (bits) 说明
源端口 (Source Port) 16 发送方的应用程序端口号。
目的端口 (Destination Port) 16 接收方的应用程序端口号。
序号 (Sequence Number) 32 本报文段所发送数据的 第一个字节的序列号。用于解决网络包乱序问题。
确认号 (Acknowledgment Number) 32 期望收到对方下一个报文段的第一个数据字节的序号。只有 ACK 标志位为 1 时才有效。
首部长度 (Data Offset) 4 指出 TCP 报文段的数据起始处距离报文段起始处有多远。实际上就是 TCP 首部的长度
保留位 (Reserved) 6 保留为今后使用,目前置为 0。
标志位 (Flags) 6 包含 6 个非常重要的标志位(见下文)。
窗口大小 (Window Size) 16 用于流量控制,告知对方:从本报文段确认号开始,接收方目前允许对方发送的数据量。
检验和 (Checksum) 16 检验范围包括首部和数据两部分,用于错误检测。
紧急指针 (Urgent Pointer) 16 配合 URG 标志位使用,指出紧急数据的末尾在报文段中的位置。
选项 (Options) 可变 最常见的选项是最大报文段长度(MSS)、窗口扩大因子、时间戳等。

核心标志位

  • URG (Urgent): 紧急指针有效标志。告诉系统此报文段中有紧急数据,应尽快传送。
  • ACK (Acknowledgment): 确认序号有效。TCP 规定,在连接建立后所有传送的报文段都必须把 ACK 置 1。
  • PSH (Push): 推送标志。接收方应该尽快将报文段交付给应用层,而不是等到缓存填满。
  • RST (Reset): 重置连接标志。用于复位由于主机崩溃或其他原因而出现的错误连接。
  • SYN (Synchronize) : 同步序号,用于 建立连接(在三次握手的前两次中使用)。
  • FIN (Finish) : 终止标志。用于 释放连接,表示发送方数据已发送完毕。

三、TCP的四大核心特征

有连接

  • 机制 :UDP在正式传输业务数据报(载荷数据)之前,会传输一个只包含TCP首部20字节的数据报,进行"三次握手"来与接受方建立连接。
  • 目的 :校验通信双方是否具备正常的接受数据的能力,同时还起到一个投石问路 的效果,可以验证通信链路是否畅通。好比火车发车前都需要先发一程空车用于检验路况是否畅通。
  • 状态维护 :连接状态会占用服务器资源(如缓存、定时器等),直到通过"四次挥手 "释放连接

可靠传输

TCP和UDP的最大区别显现于此,TCP通过一系列复杂的机制来保证数据在传输的过程中不会出现丢包,重传,乱序等问题。

  • 序列号和确认应答(ACK) :序列号使得TCP能够得知下一个需要传输的数据的起始序号;如果数据没发生出去或者ACK未返回,TCP还会进行超时重传操作来保证可靠传输
  • 校验和:校验和帮助校验数据在传输的过程中是否出现bit位错误,ACK值为1时代表数据无误,否则传输有误
  • 流量控制:流量控制是指在数据传输的过程中,接收方需要根据缓冲区剩余大小,动态调整发送方数据传输速率,防止缓冲区溢出导致丢包,这一机制还需要配合滑动窗口来实现,下面会具体介绍。
  • 拥塞控制:拥塞控制是通过维护拥塞窗口大小,以防止过多数据注入到数据链路或者路由器导致网络拥塞的情况发生。

面向字节流

面向字节流使得TCP的数据间没有严格的边界划分,在数据的发送,传输,接收时都可以像水流一样随意读取。

  • 发送时:应用程序调用write写入数据时,没有严格的数据大小规范,你可以一次写1个字节,也可以一次写入大量的字节。就像往水桶中倒水一样,可以一次倒一小杯,也可以一次倒一大瓶。
  • 传输时 :数据在通信的过程中,依据拥塞控制来对传输的数据大小进行划分,大的数据报会进行拆分 为合适大小的数据段,小的也会合并为大的数据报。
  • 接收时 :接收方从缓冲区read读取数据时无需按照发送时的数据报大小来严格规范读取的字节数,可以任意读取。好比从木桶中取水,你可以一次取一小勺,也可以一次取一大盆,这和你事前怎么向水桶中倒水的没有关系。

全双工

全双工使得通信的双方在同一时间,既可以发送数据,也可以接收数据。如此一来,发送和接收这两个操作就可以并发完成,无需等待另一个操作结束,实现了双通道传输(既可以发送,也可以接收),提高了传输效率。类似于一条公路划分为左右车道。

四、基础可靠性机制

确认应答(ACK)

确认应答是TCP可靠性传输保障的基石,TCP数据报整体可以划分为报头+载荷两个结构,其中报头中的序号中记载着TCP数据报的载荷末尾数据的序号(无载荷时则以报头为结尾),确认应答则与报首的确认序号相关。

  • 机制:当接收到收到数据时,会返回一个ACK给接收方
  • 含义:向接收方表示已经收到数据
  • 捎带确认:如果返回ACK独立发送的话,又会占用网络资源,同时发送方还得等待对端ACK返回,为了优化这一过程。返回的ACK会与接收方的应答数据合并在一起,也就是接收方返回数据的时候顺带把ACK给带着,省点开销。(确认应答ACK坐挂票顺路返回)

超时重传

当发送方发送数据时,在数据传输的过程中,数据可能会在传输到接收方的过程中丢失,也可能是接收方返回的ACK丢失。无论哪种情况发送,这都是可靠性缺失的表现。为了解决这一问题,TCP引入了超时重传。

  • 机制 :当发送方发送一个数据报时,TCP会启动一个重传计时器,计时器开始计时(时间开始流逝)。
  • 发送成功:当接收方收到了数据并且成功把ACK返回给发送方时,计时停止。
  • 发送失败:一旦超出了这个最大时间限制,就会触发超时重传,发送方会再一次发送该数据报。

以超时时间做为依据来判断数据是否丢包,带来的一大问题就是超时时间如何设置?

  • 时间设置太短:发送的数据可能还在传输半途,没到达接收方就被认定为丢包导致重传
  • 时间设置太长:数据丢包了,由于重传计时器的超绝反射弧,导致过了好一会儿才能反应过来:"哦,数据丢了,我要重传",导致传输效率低下。

超时重传时间设置

为了设立一个合理的超时重传时间,TCP会动态监测每一次数据报传输所消耗的往返时间,当有一次传输触发了超时重传,就会把超时重传时间进行放大。

比如当初始的时间设为1s,触发了超时,就会把时间调整为2s,如果再次触发超时,就调整为4s,(1s -> 2s -> 4s -> 8s...)反复类推

快速重传

超时重传虽然解决了数据超时传输带来的传输问题,但同时也隐藏一个问题?当数据丢包了,往往要等到超时重传时间到达了才能让发送方触发重传机制,这样就会带来一定的网络卡顿。有无可以在数据丢包的情况下,不用在等待那些无意义的重传时间呢,这就能即使重传,提高效率。有的,快速重传的出现优化了这一问题

当发送方连续发送了1,2,3,4,5多个数据包时,假设2号数据报丢失:

五、连接管理

一般情况下,TCP要经过三次握手建立连接,四次挥手断开连接,这是保证传输可靠性基础的两大阶段

建立连接

在TCP正式传输业务数据包之前会进行三次握手,尝试与服务端建立连接

目的

  • 投石问路,判断当前的网络传输链路是否畅通
  • 判断彼此之间的接收与发送能力
  • 进行参数协商:确认TCP报头序号的起始序号

过程

  1. 第一次握手: 客户端发送 SYN报文(序号为 xxx),进入 SYN_SENT 状态。

    • 意图: "喂,你能听到吗?我想和你建立连接。"
  2. 第二次握手: 服务端收到后,回复 SYN + ACK 报文(确认号为 x+1x+1x+1,序号为 yyy),进入 SYN_RCVD 状态。

    • 意图: "听到了,我也能听到你。你能听到我吗?"
  3. 第三次握手: 客户端收到后,回复 ACK 报文(确认号为 y+1y+1y+1),进入 ESTABLISHED 状态。服务端收到此包后也进入该状态。

    • 意图: "没问题,我也能听到你!那我们开始聊天吧。"

为什么不是两次?

为了防止"已失效的连接请求报文"突然又传到了服务端。如果只有两次握手,服务端回复后连接就建立了,但这可能只是一个在网络中绕路太久的旧请求,这会浪费服务端的资源。

亦或是客户端返回服务端的响应之后,倘若缺失第三次握手,则无法判断正式发送信息前服务端的接收能力是否正常,对于其能力的验证只是在第一次握手时确认,但是经过第二次握手的间隔时间之后,对于服务端接收能力仍需确认。

断开连接

目的

  • 断开连接,保证数据传输的完整性,不丢包
  • 释放TCP建立连接所占用的系统资源
  • 防止该连接结束产生的旧报文干扰后续连接

过程

由于 TCP 是全双工的(双方都可以同时发数据),所以每个方向的连接都需要单独关闭。

  1. 第一次挥手: 客户端发起的 FIN 报文。客户端进入 FIN_WAIT_1 状态。

    • 意图: "我的话写完了,我要挂了。"
  2. 第二次挥手: 服务端收到后回复 ACK。此时服务端进入 CLOSE_WAIT 状态,客户端收到后进入 FIN_WAIT_2

    • 意图: "我知道了,但我这边可能还有点数据没传完,你等我一下。"
  3. 第三次挥手: 服务端处理完数据后,发送 FIN 报文。服务端进入 LAST_ACK 状态。

    • 意图: "好了,我也写完了,挂吧。"
  4. 第四次挥手: 客户端回复 ACK,进入 TIME_WAIT 状态。经过 2MSL (最大报文生存时间)后,连接彻底关闭。服务端收到 ACK 后直接进入 CLOSED 状态。

    • 意图: "行,拜拜。"(客户端等一会儿确保对方收到了再走)。

TIME_WAIT 状态的作用

客户端在第四次挥手后为什么要等 2MSL?

  • 保证可靠关闭: 如果最后的 ACK 丢了,服务端会重发第三次挥手的 FIN。客户端必须活着才能重发 ACK。
  • 防止旧包干扰: 确保这个连接中产生的所有报文都在网络中消失,以免影响下一个相同端口的新连接。

1.为什么通常需要"四次"?

默认情况下,四次挥手是因为 TCP 的全双工 性质和被动关闭方(服务端)可能还有数据要发

  1. 客户端发 FIN

  2. 服务端先回 ACK(表示:收到了,但我可能还没忙完)。

  3. (忙碌中) 服务端继续把剩下的一点数据发完。

  4. 服务端发 FIN(表示:我也忙完了,可以挂了)。


2. 什么时候可以变成"三次"?

如果满足以下条件,第二次和第三次挥手就可以合并:

  • 条件一: 服务端在收到客户端的 FIN 后,正好没有额外的数据需要发送给客户端了。

  • 条件二: 服务端开启了 延时应答(Delayed ACK) 机制。

在这种情况下:

  1. 客户端发送 FIN

  2. 服务端收到后,由于没有数据要发,它不急着回 ACK,而是等一小会儿(通常是 40ms~200ms)。

  3. 就在这等待期间,服务端的应用层也调用了 close() 发出了 FIN

  4. 于是,服务端就把 ACK(对客户端 FIN 的确认)自己发的 FIN 合并成一个包发出去。

  5. 客户端回复最后一个 ACK

结果: 整个过程变成了:FIN -> ACK + FIN -> ACK。一共三次。

六、传输性能与效率优化

滑动窗口

滑动窗口初次听说可能会与双指针算法中的滑动窗口产生练习,但是其实是两个东西。TCP的滑动窗口是实现效率传输,流量控制的核心机制

该窗口是一个虚拟的窗口,用于控制窗口中的数据包的批量传输

为什么需要滑动窗口?

在最初的TCP传输设计中,发送方只能等到接收方返回对应的ACK确认应答才能进行下一个数据包的发送,这在发送方来看其发送的操作就像是串行化发送的。如此一来发送方的大部分时间都可能用于等待接收方返回的ACK数据包,这无疑是一个耗时的操作。于是就引入了滑动窗口来实现批量传输数据包

  • 机制 :当假设初始的窗口大小是4,即最多能同步发送四个数据报给服务器。

这里要注意一个细节:有人可能认为窗口是一位一位的向后移动,这样不就会导致只有初次进窗口的数据可以实现批量发送,但是后续数据接收的ACK不是会接收到窗口中最左端的ACK数据包,导致只能单个出,单个进窗口吗?这样看来似乎效率也没有提升多少?

实则不然,要知道由于缓冲区存在数据重排序 的问题以及服务器处理每个任务的耗时不同。返回的ACK数据包不一定按照发送的数据。比如发送了12345,接收方先拿到了2,不急返回对应ACK2,而是先存在接收方缓冲区 。随后又收到了1,4,5数据,此时数据为1,2,4,5

最后数据3珊珊来迟,填补空位,接受方就可以直接返回ACK6的确认应答,表示12345已全部收到。窗口就会把6之前的数据全部出窗口,实现批量入窗口,批量发送数据。

流量控制

如果说引入滑动窗口批量发送数据包是为了解决数据传输太慢的问题,那流量控制就是为了解决发送方和接收方"速度不匹配 "的问题

为什么会出现速度不匹配的问题?有两个原因

1)发送方发的太快,导致缓冲区被迅速填满,后发的数据丢包

2)接收方处理数据太慢,缓冲区的数据不能即使被处理导致数据堆积

无论是哪一种情况都是发送方和接收方"速度不匹配"的表现,流量窗口就是为了协调二者的发送速度由此而生。

机制

实现流量控制的机制还需要与上文的滑动窗口作为基础

过程

接收方收到数据时,会观察缓冲区的剩余空间大小,并把其量化为一个数值rwnd (Receiver Window)于返回的ACK中一起发回给发送方。这样一来,发送方就可以得知对方的剩余缓冲区空间还有多大。

当发送方发送的速率过大时,缓冲区内剩余空间迅速变小,把对应信息返回给发送方时,发送方就会把窗口调小来降低速率

当发送的速率过大时,窗口也会调大,增加吞吐量提高速率

零窗口情况

当缓冲区被填满时,剩余缓冲区大小为0,此时会rwnd = 0(Receiver Window)这个信息返回给发送方,表示"我处理不过来了,你先别发了",于是发送方就会暂时停止发送数据。

等到缓冲区空间富余时,会返回一个窗口更新的报文给发送方"我已经有空间了,你可以发数据了"

死锁问题

此死锁非多线程的死锁问题。而是在接收方先把rwnd = 0 返回给发送方的情况下,之后打算像接收方发送更新窗口大小的数据包,但是更新窗口数据包却出现丢包现象。

这就会导致陷入:发送方没收到更新窗口的数据,一般保持窗口为0的状态,不发数据;接收方已经处理完手头上的数据,但是迟迟等不到发送方发数据,陷入进退两难的境地

  • 如何解决添加计时器,反复询问
    很简单,只需要给发送方添加一个计时器,作用是发送方在接收到窗口 = 0的信息时,计时器启动。如果接收窗口的数据报丢包了,等到了计时上限,会发送一个探测报文 (Zero Window Probe),从而使得接收方发送窗口更新的数据,打破死锁状态。
    就好比两个人正在线上聊天:
    对方说先等会儿了,我先处理手头上的一点事,你不再发消息(不发数据)
    过了一会儿,对方有空,回了一句:"好了",但是此数据却发送失败,导致你也不知道对面是否有空,对面也没等到你的消息。二者僵持住(陷入死锁)
    为了打破僵局,你等了一会儿之后主动询问:"现在有空没?",从而触发对方的回应
    :"OK了"

拥塞控制

如果说流量控制是防止接收方缓冲区被撑满,那么拥塞控制就是避免路由器和交换机被堵死;

也可以说流量控制是端到端的(保护接收端),拥塞控制是全局的(保护整个网络)

拥塞控制先使用少量的数据去试探当前网络的整体状态,从而避免贸然发送大量的数据导致整个网络拥塞的情况出现,以此来确定一个合适的发送数据包的速率

  • 原理 :拥塞控制是通过维护一个拥塞窗口,来计算发送方具体可以发送多少的数据量。
  • 过程:TCP拥塞控制是需要经过四个阶段,分别是慢启动,拥塞避免,拥塞发生,快速恢复这四个阶段。

以下是一个拥塞控制的流程图:

关键控制变量

变量名称 英文原词 功能定义
cwnd Congestion Window 拥塞窗口:发送方根据网络拥塞程度维护的状态变量。
rwnd Receiver Window 接收窗口:接收方通过 TCP 报文首部告知的剩余缓存大小。
ssthresh Slow Start Threshold 慢启动阈值:决定何时从慢启动切换到拥塞避免算法。
SWND Send Window 发送窗口 :实际发送量 SWND=min⁡(cwnd,rwnd)SWND = \min(cwnd, rwnd)SWND=min(cwnd,rwnd)。

慢启动

  • 逻辑 :连接建立后,从 cwnd=1cwnd = 1cwnd=1 (或初始值 SMSS) 开始,每收到一个确认帧 (ACK),cwndcwndcwnd 增加 1。

  • 增长速率指数增长 。每经过一个往返时间 (RTT),cwndcwndcwnd 翻倍。

  • 终止条件 :当 cwnd≥ssthreshcwnd \ge ssthreshcwnd≥ssthresh 时,进入拥塞避免阶段。

拥塞避免

  • 逻辑 :每收到一个 ACK,cwndcwndcwnd 增加 1/cwnd1/cwnd1/cwnd。

  • 增长速率线性增长 。每经过一个 RTT,cwndcwndcwnd 仅增加 1。

  • 目的:使窗口增长更缓慢,探测网络带宽上限,延缓拥塞发生。

快重传

  • 逻辑 :当接收端收到失序报文段时,立即发送重复确认 (Duplicate ACK)

  • 触发条件 :发送端连续收到 3 个冗余 ACK,判定该报文丢失。

  • 动作:不必等待超时计时器(RTO)结束,立即重传丢失报文。

快恢复

  • 逻辑:配合快重传使用。

  • 计算公式

    1. ssthresh=cwnd2ssthresh = \frac{cwnd}{2}ssthresh=2cwnd

    2. cwnd=ssthresh+3cwnd = ssthresh + 3cwnd=ssthresh+3 (加上收到的 3 个冗余 ACK 占用的空间)

  • 后续:直接进入拥塞避免阶段(线性增长),而非回到慢启动。

归根结底,拥塞控制就是一个既想增加传输效率,但是又担心初始吞吐量过大导致网络拥堵情况产生的一个控制方案

七、进阶机制

延时应答

延迟应答是一种提高网络传输吞吐量的一种手段,当接受方收到数据时,不会立即返回一个ACK,而是先等待一段时间

  • 等待目的
    1.期待捎带应答 :把多个ACK数据包合并返回,减少网络资源占用
    2.提高吞吐量:等待期间给接受方缓冲区腾空间,返回一个更大的接收窗口
  • 触发条件
    1.时间限制 :等待时间通常不超过 200ms )。
    2.数量限制 :每收到 2 个 最大报文段 (MSS) 必须发送一次 ACK

捎带应答

在全双工 (Full-duplex) 通信中,接收端在回复 ACK 的同时,如果正好有数据要发往发送端,就将 ACK 信息直接放入该数据报文的头部(利用 TCP 报头中的 ACK 标志位和确认序列号字段)合并返回。

  • 核心机制 :将控制信息(ACK)与数据信息(Data)合并在一个 TCP 段中发送。

  • 前提条件:接收端必须有待发送的数据,且发送时机恰好在延时应答的窗口期内。

八、数据传输的本质与坑

使用TCP字节流传输数据页带来了几个隐患

面向字节流

面向字节流使得接收到的数据无边界,read操作可能出错,这是粘包问题的本质原因

粘包问题

TCP面向字节流,接收方read操作则以字节为基本单位,并不是对方几次send 我就几次recv。比如一次读1.5个包或者一次读取多个数据报,导致粘包。一次读取0.5个包就会导致拆包。

解决方法

  • 添加分隔符,以/n 或者 其他偏僻分割符 作为字节流中每个完整业务数据的边界,当读到/n时,服务器读取到对应的字节流,防止多读。
  • 在业务数据包头部前添加一个长度字符,告示服务器向后该读取多少个字节。比如客户端两次send 操作传输hello cheems,服务器接收到的就是4hello5cheems,使得服务器每次从缓冲区read操作都能有一个明确的目的:该read几个字节?而不是盲读

九、异常情况处理

进程崩溃

这是最"温和"的异常。

  • 内核介入:操作系统内核(Kernel)会监控进程状态。当进程崩溃时,内核会负责关闭该进程打开的所有文件描述符(File Descriptors)。

  • 动作 :内核自动发送 FIN 包 给对端,触发正常的 **四次挥手断开连接。

  • 结果:对端会收到连接关闭的信号,能够优雅地释放资源。

关机断电

区分"有序关机"与"突发断电":

  • 有序关机:系统会尝试终止所有进程,回归到上述"进程崩溃"的逻辑,发送 FIN。

  • 突发断电

    • 瞬间消失:本端没有任何包发出。

    • 对端滞留 :对端(Receiver)完全不知道对方已掉线,连接处于 半开放 (Half-open) 状态。

    • 触发机制 :只能依靠 TCP Keep-alive (默认 2 小时,极慢)或应用层的 心跳检测 (Heartbeat) 来发现并强行关闭连接。

网线断开 / 网络中断

这是最复杂的物理层异常。

  • 即时状态 :两端的内核状态均显示 ESTABLISHED

  • 数据传输中 :若此时有数据发送,由于收不到 确认应答 (ACK) ,会触发 超时重传 (Retransmission) 。重传达到最大次数(由 tcp_retries2 决定)后,内核会返回错误并强制关闭连接。

  • 静默状态:若无数据往来,连接将无限期"假死",直到开启心跳检测。

以上就是关于TCP协议内容的简单介绍,如有纰漏,还请指出~

相关推荐
麦德泽特2 小时前
设计一个安全的时效性遥控协议:对抗重放攻击
服务器·网络·安全
我命由我123452 小时前
Kotlin 面向对象 - 匿名内部类、匿名内部类简化
android·java·开发语言·java-ee·kotlin·android studio·android jetpack
Web极客码2 小时前
如何让你的 WordPress 博客完全私密
运维·服务器·网络
余瑜鱼鱼鱼2 小时前
总结 IP 协议的相关特性
服务器·网络·tcp/ip
xuxg20052 小时前
4G AT命令解析框架LwAtParser V2.0设计及实现(基于uCOS II)--中级篇 第七章 TCP协议实现
网络·网络协议·tcp/ip
unirst19850072 小时前
nginx中的proxy_set_header参数详解
运维·网络·nginx
良许Linux2 小时前
嵌入式安全和加密技术
网络·安全
wsad05322 小时前
CentOS 7 Minimal 配置静态 IP 完整指南(VMware NAT 模式)
linux·tcp/ip·centos
岳来2 小时前
网络小白对容器参数endpoint 和gateway 对比
网络·docker·容器·gateway·endpoint