TCP协议及特性详解

TCP

TCP 协议是一个有连接, 可靠传输, 面向字节流, 全双工的传输层通信协议

相比 UDP , TCP 最大的差异和优势无疑是可靠传输, 而为了保证可靠传输, TCP 协议中有两个核心的机制, 确认应答超时重传

确认应答

发送方在发送完数据之后, 为了确认接收方是否收到了数据, 接收方会返回一个应答报文, 表示自己已经收到了数据。那怎么判断这是个普通的数据报还是一个应答报文呢 ? 这就涉及到了 TCP 报头结构, 如下, TCP 报头中有 6 位比特位, 其中第二个比特位 ACK 就用来表示这是不是应答报文, 如果 ACK 为 1 说明这是个应答报文 (也称为 ACK 报文, 后面统称应答报文为ACK报文), 相反为 0 则不是。

在这里插入图片描述

如果发送方一次性发送了多组数据, 怎么判断 ACK 接收了哪组数据呢

在这里插入图片描述

如下, TCP 报头中有个32位序号, TCP 会给数据报中数据的每一个字节进行编号, 而由于一个 TCP 数据报只能记录一个序号, 所以这个序号就是字节流的起始序号, 而确认序号就是接收方期望从发送方接收到的下一个字节的序号, 例如, 发送方发送了一个序号为1, 长度为 1000 的数据报, 也就是发送了序号为 1 - 1000 的数据, 接收方期望下次接收到的字节序号为1001, 所以此时确认序号就是 1001, 确认序号就是表明当前序号之前的数据都收到了, 而发送方下次发送的数据也应该从1001开始。需要注意, 确认序号只有当 ACK 为1 的时候有效, 也就是应答报文。

在这里插入图片描述

网络中数据传输中又是充满不确定性的, 晚发的数据先到达的情况也经常出现, 那么如何保证数据的有序性呢 ? 实际上接收方还会根据序号对TCP数据报进行排序。

超时重传

由于网络环境复杂, 数据传输过程中难免遇到意外, 每一次数据传输都是由可能丢失的, 如果数据发送后丢失了(丢包)怎么办?

每次数据传输如果成功, 都应收到 ACK , 如果发送方发送完数据, 等待一段时间后还没有收到 ACK , 就会触发超时重传机制, 重新发送一次数据, 连续触发超时重传都会让等待时间增加。超过一定次数后还没收到 ACK 后, 就会断开连接, 尝试重新连接, 如果还连不上, 说明现在网络情况糟糕, 于是放弃传输数据。

具体来说会有两种情况: (1) 发送方发送的数据丢了 (2) 发送方发送的数据到了, 但是接收方返回的 ACK 丢了。
如果发送的数据丢了, 重新发一次即可, 但是如果发送的数据到了, ACK 丢了, 这种情况会超时重传一次, 不过这样会收到两份一样的数据, 接收方就会根据数据报序号去重, 保证没有两个一样的数据报

那么如何保证数据的正确性呢?
这和 UDP 协议中的校验和是一样的道理, TCP 数据报的每一次转发, 发送前都会计算校验和, 并将这个结果一起发送出去, 到达某台设备后都会重新计算校验和, 如果不相同, 那就直接丢包来保证数据不能是错误的。而丢包带来的结果就是发送方不会收到 ACK 而超时重传, 利用时间换取数据的准确。

连接建立与断开

为了保证可靠性传输, TCP 在真正传输数据之前应该建立连接, 数据传输完成后断开连接, 建立连接也就是"三次挥手", 断开连接即"四次挥手"

三次挥手

  1. 首先客户端(A)会发送给服务器(B)一个连接请求(SYN, 是一个同步报文段, 表示建立连接)
  2. B 接收到SYN, 返回ACK (复习一下, ACK 就是应答数据报)
  3. B 也给 A 发送建立连接的请求 (SYN)
  4. A 返回 ACK
    在这里插入图片描述

虽然有四个步骤, 但是 2 和 3 步骤是可以合并的, B 在收到 A 的 SYN 后, 就会在操作系统内核一起将 ACK 和 SYN 一并发送出去。具体点, 在代码中, new Socket(IP, port) , 就会触发三次握手建立连接。同时 SYN 也是这 6 个比特位中的一个。

在这里插入图片描述

这三次握手除了建立连接之外, 也在判断通信双方的发送能力和接受能力是否正常, 通信链路是否正常, 除此之外, 也在调整通信中重要的参数, 例如序号: 序号要从哪里开始

四次挥手

  1. A 向 B 发送 FIN (FIN 也是TCP报头中 6 个比特位中的一个, 表示关闭连接的请求)
  2. B 接收到了 FIN, 返回 ACK
  3. B 向 A 发送 FIN
  4. A 返回 ACK
    在这里插入图片描述

需要注意的是 : 接收方接收到 FIN 后, 操作系统内核就会立刻返回 ACK, 而 FIN 是代码中的行为, 具体点就是 Socket.close(); 当代码执行到上述语句后, 才会发送 FIN

四种常见状态

  1. LISTEN
    表示服务器的状态——服务器已经准备好了, 具体到代码中就是 new ServerSocket(port)

  2. ESTABLISHED
    表示客户端和服务器连接建立完成, 可以随时准备通信

  3. CLOSE_WAIT
    最先收到 FIN 的一方会进入这个状态, 收到 FIN , 并且返回 ACK后, 进入CLOSE_WAIT状态, 并且等待 Socket.close() 的调用, 执行到这个语句之后就会发送 FIN
    在这里插入图片描述

  4. TIME_WAIT
    如下, 在最后一次收到 FIN 并返回 ACK 之后, 进入TIME_WAIT状态, 然后会在这里等待一段时间后进入 CLOSED 状态, 彻底结束本次通信。而等待的时间是 2 MSL, MSL —— 数据从一个主机传输到另一个主机所要花费的最大时间。这样做的好处是如果最后一个 ACK 没有正常到达, 有时间超时重传, 确保 B 也能正常关闭
    在这里插入图片描述

效率提升机制

滑动窗口

TCP 为了保证可靠性, 速度肯定会受此影响, 而希望能够弥补为了可靠性牺牲的速度, 引入了滑动窗口

相比发送一条数据, 收到ACK后发送下一条, 滑动窗口可以一次性发送 N 条数据报(N 可以视为窗口的大小), 收到 M 条 ACK 的应答后, 窗口向右移动 M 个位置, 并继续发送窗口中没有发送的数据。这样就可以做到将多个 ACK 的等待时间重叠在一起, 降低等待总时间, 提升发送效率

如下, 假设发送方发送的数据和序号如下, 窗口内的数据报如果收到ACK就表明完成了传输, 窗口就可以移动继续传输其他数据报

在这里插入图片描述连续发送多条数据后, 数据最终都会在接收缓冲区中按照数据报的序号进行排序, 保证有序性

那如果这期间发生了某些意外怎么办 ? 比如 (1) 数据报正常到达, ACK 丢失 (2) 数据报丢失

(1) 数据报到达, ACK 丢失

如下图, 假设发送方发送的数据和序号如下, 虽然2001-3000, 和3001-4000的数据发到了, 但是确认序号为3001和4001的ACK丢失了, 这时候会重新发送2001-3000和3001-4000的数据吗? 不会 ! 因为发送方收到了确认序号为5001的应答报文, 而应答报文表示, 这个确认序号之前的数据都接收到了, 即2001-4000都收到了
在这里插入图片描述
(2) 数据报丢失

例如, 2001-3000的数据报丢失

在这里插入图片描述接收方返回2001的ACK后, 接收方下一个接收的序号就应该从2001开始, 但是2001-3000的包丢了, 后面发送的数据的序号都不是2001开始的, 接收方就会连续多次发送 2001 的ACK报文, 发送方收到了三个2001的ACK后就会重传2001-3000的数据报, 如下图
在这里插入图片描述
重传完2001-3000的数据后, 这时候返回的不应该是3001吗? 也不是 ! 因为除了2001-3001那块丢失的数据, 其他数据都是收到了的, 现在发送方补发丢失的数据后, 接收方下一个应该收到的数据报的序号就应该从8001开始, 于是发送了确认序号为8001的ACK, 表明8001前面的数据都已经收到

流量控制

理论上来说, 滑动窗口越大, 发送速度就越快, 但是这也需要兼顾接收方的接收速度, 如果发送速度太快, 接收速度又跟不上, 反而容易导致频繁丢包, 增加超时重传的次数, 反而降低了速度, 所以理想的情况应该是你发过来的, 我刚好都能处理, 于是有了"流量控制"这一机制, 目的就是限制滑动窗口的大小, 尽量让发送速率和接收速率对等

在网络通信中, 发送方的数据会通过网卡发送到B的接收缓冲区中, 接收缓冲区在操作系统内核中, 可以视为一个阻塞队列, 而接收方的应用程序则是会不断从接收缓冲区中处理数据。

在这里插入图片描述

流量控制会根据接收缓冲区中的剩余容量来判断计算滑动窗口的大小——我能装多少, 你就发多少。这个窗口大小信息会通过 ACK 的方式告知发送方窗口大小, 如下图, 存储在 TCP 报文中的16位窗口大小字段中, 并且只有 ACK 为1的时候, 这个字段才有效。总结: 这个字段用于描述接收缓冲区大小的,发送方再根据这个数据计算滑动窗口的大小

在这里插入图片描述
但是如果接收缓冲区满了怎么办 ? 这时候窗口为0, 也就是发送方暂时发不了数据了, 只能等待缓冲区位置空出来, 这期间会向接收方发送探测报文, 这个数据报的作用就是让接收方返回带有滑动窗口大小数据的 ACK 数据报, 以重新调整窗口大小

拥塞控制

好了, 有了滑动窗口和流量控制, 发送方和接收方满意了, 但是中间商(中间设备)不满意了, 如果发送速度对中间设备来说太快也不行, 他们忙不过来还是照样得丢包, 最终速度还是不满意, 那只能通过另一个机制来限制发送速度, 来同时兼顾中间设备和接收方的接收速度, 这就是拥塞控制。

拥塞控制有点不一样, 拥塞控制是通过做实验的方式来得出滑动窗口的大小值的, 我们以这个网图为例

  1. 首先先以一个很小的窗口值启动(cwnd是几, 当前窗口就能发送几个数据报), 比如1
  2. 然后窗口数量以指数形增长, 到达ssthresh(阈值)后, 就改变为线性增长, 慢慢增加
  3. 增加到一定数量(如图中24)的时候发现出现了丢包情况, 并判断这情况下网络环境拥塞
  4. 更新阈值为刚刚出现拥塞状态时的一半, 窗口数量重新定为1
  5. 重复上述过程

这样就可以尽可能地得到窗口值最大, 丢包率又低的窗口数。像图中红线左侧就是一个循环

在这里插入图片描述
而我们说到流量控制和拥塞控制都要用来控制滑动窗口大小, 具体点, 就是Math.min(流量控制窗口大小, 拥塞控制窗口大小) , 这也好理解, 要尽量避免超过中间设备和接收方的处理能力, 兼顾两者的处理速度。

延时应答

设想一下, 在流量控制中, 在某时刻, 接收方会发送含窗口大小信息的ACK, 那么如果晚一段时间后再发ACK会怎么样 ? 会发现这段时间内, 接收方的应用程序会持续消耗接收缓冲区中的数据, 也就是说, 晚一点发意味着接收缓冲区的剩余空间更大, 进一步, 返回给发送方的ACK中窗口大小字段会更大, 增加发送速率。
在这里插入图片描述
简单来说, 就是 ACK 会延迟发送, 让缓冲区剩余空间更大, 让滑动窗口调整到更合适的大小, 即延迟应答

捎带应答

捎带应答是实现在延时应答的基础上的, 如果接收方的ACK延时应答了, 然后刚好又遇到了一个要发送的数据, 这时候就顺带把这两个一起发送出去, 例如"三次握手"中四个步骤合并成三个步骤

在这里插入图片描述

粘包问题

由于 TCP 是面向字节流的, 它发送的数据报之间, 并不会主动去调整数据报之间的边界, 所以在接收方接收数据的时候, 也无法辨别从哪到哪是一个数据报中的数据

如下, 三个数据报发过去之后, 接收方无法识别数据的边界, 对接收方来说, 就是所有数据都粘在一起了
在这里插入图片描述为了解决这个问题, 我们通常有两种方法 (1) 利用分隔符作为数据报的边界 (2) 规定数据的长度

这里我们以 (1) , 为例, 我们规定每一份数据报中的数据最后都要有 \n 符号作为分隔符, 而接收方遇到 \n 符后, 就知道当前数据报到此结束, 就能够将不同的数据报区分开来了如下

在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

答辣喇叭

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值