TCP协议(全面)

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

TCP协议

TCP的全称是Transmission Control Protocol即传输控制协议TCP工作在传输层上

其职责是实现主机间进程到进程的通信其次还需要保证可靠性不是安全性换言之不能保证安全性

什么是可靠性(重点在前3条)

  1. TCP会尽自己所能尽量将数据发送给对方但并不能保证100%可以发送给对方
  2. TCP会在数据发送不到对方的情况下会给应用层一个错误提示告知用户发送失败
  3. TCP可以保证接收方(应用层)严格按照发送时的数据顺序接收
  4. TCP保证数据不会出现无意间的损坏(UDP 也做到这点)
  5. TCP尽可能地维护网络质量

TCP的机制

  1. TCP通过确认机制 ( acknowledge ) 保证了信息的成功发送

我们通过时序图(时间从上往下流失)来大致描述一下

image-20220720103052040

发送方发送了数据如果对方收到了确认代表对方收到了数据如果没有收到确认可以合理推测对方没有收到数据

如果发送方同时发送了很多数据如何知道对方确认收到的是哪一份

对数据进行编号同时确认也带上编号这样对方收到数据的时候就能知道收到的是哪一份数据

如果没有收到对方的确认

如果没有收到确认就重新发送信息一般超过一定时间没有收到确认才进行重发因此也称为超时重发机制

数据编号和确认编号

发送的数据编号被称为序列号(Sequence Number - SN)

确认的数据编号被称为确认序列号(Ackonwledge Sequence Number - ASN)

编号的规则每个字节都要占用一个号发送时的起始编号称为初始序列号(Initial SN - ISN)ISN不一定为0而是一个随机值不过编号是连续的因此可以记为0(相对值)

SN在发送TCP Segment 的 header中如何体现TCP 发送/接收的完整数据一般称为segment(段)TCP segment = header + payload

当一次性发送一些数据的时候SN只需要填写本次发送的数据中的第一个字节的数据即可因为编号是连续的且segment会携带payload长度

image-20220720104234975

TCP协议正是通过序列号保证了传输顺序

参考博客https://blog.csdn.net/wyq_tc25/article/details/51504642

TCP由于要进行发送也要进行确认所以实际上TCP Segment有两种不同的角色

  • send segment
  • acknowledge segment

TCP 设计的时候一个segment可以身兼两种不同的角色

无论什么时候一个segment都视为send segment的角色而当某个标志位被置位时segment具备了acknowledge segment的角色

img

在TCP Header 中通过ACK的标志位处理ack角色一个ack只占据一个bit位的数据也就是说ack的值为0或1为1的时候就是被置位了表示ASN字段有意义如果为0则无论ASN为多少其字段都无意义

TCP规定在连接建立后所有传送的报文段都必须把ACK置1

ASN 的填写规则

填写要接收的下一个字节的数据(本次收到的数据的最后一个字节的下一个)

  1. 超时重传机制

如果没有收到对方发送的应答可能的情况有

  1. 对方没有收到发送的数据所以没有应答没收到的原因可能很多比如数据还在路上但是还未到达或者数据已经在路上丢失了永远不可能到达对方
  2. 对方收到发送的数据也应答了但我们没有收到没收到的应答的原因比如应答还在路上但是还未到达或应答已经丢失了

数据或者应答还在路上的情况可以通过一定的超时机制解决该问题

如果数据在发送的过程中就丢包了重发肯定没有问题

如果数据发送到对方了但是应答在路上丢失了这个时候的重发可能会导致对方收到重复的数据

不过也没关系通过序列号就能判断这个数据是不是重复的TCP会保存SN如果收到的信息的SN已经存在了那就直接丢弃如果不存在说明是新的数据那就保存并发送应答

超时时间怎么计算比较合理

一般来说设置一个稍大于Round Trip Time(RTT)的时间即可RTT就是数据发送加上收到应答这个过程的时间

但是TCP没有办法知道RTT所以现实中无法做到特别理想

早期的时候就直接给一个相对比较大的时间保证比地球最远的两个主机之间的RTT都稍大一点这就一定能保证超时时间大于RTT

另一种就是实时计算RTT这个有点智能目前还在实验阶段暂时不需要了解

超时时间是会改变的一开始会设置的比较短出现ack丢失的情况后会适当把超时时间延长

比如在Linux中超时以500ms为一个单位进行控制每次判定 超时重发的超时时间都是500ms的整数倍如果重发一次之后仍然得不到应答等待 2 * 500ms 后再进行重传如果仍然得不到应答等待 4 * 500ms 进行重传

重发不会一直进行否则如果物理层出现了问题无论重发多少次都是没有用的

所以一般就是尝试几次(不同的OS实现不同但一般也能配置)重发发现仍然收不到ack则停止发送

然后就会通知应用层告知发送者发送失败在Java中write()就会以异常的形式提示

最后放弃之前会最后尝试联系下对方会发送一种叫做reset segment

缓冲区

TCP是有发送缓冲区的用于暂时保存已发送未应答的数据为什么要进行保存因为TCP为了保证数据确切地被对方接收到需要对方发送的ASN如果对方没有应答就需要重发如果不对数据进行保存就没有办法重发了所以发送缓冲区主要也是为了保证可靠性而存在的

TCP有接收缓冲区这与UDP协议一直因为接收到的信息不一定马上就能被应用层取走使用

应用层使用TCP进行数据发送如果本次发送成功只能意味着数据放在本机TCP的发送缓冲区中

ISN值的设置

为什么ISN不设置成0而是采用随机值这主要是站在安全角度考虑

如果ISN设计成0很容易有恶意的用户推算出来合法的SN的值这样伪造TCP SN的成本很低使用随机值能一定程度避免安全隐患

连接

作为一台主机上的TCP需要

  1. 内部针对每一条TCP的通信链路(信道)维护一组数据至少包括ISN、当前SN、ASN、发送缓冲区、接收缓冲区、五元组信息等等
  2. TCP为了可靠性以及保证数据的交换在正式的数据通信之前需要和对方(TCP)进行一定的同步(synchronize)操作本机把一些初始的重要信息发送给对方若收到了对方的应答就能知道对方一定存在

根据这两点TCP就有了连接和连接管理的概念(一条连接的一生 = 一开始创建 + 正式使用 + 销毁)

连接(Connection)是一个人为抽象的概念是看不见摸不着的

主动连接方和被动连接方的连接就代表一条TCP信道但实际在TCP两层的内部仅仅是一些数据而已

用Java的视角就是用一个Conection对象维护

连接建立称为握手(handshacke)销毁连接称为挥手

三次握手

双方相互同步自己的基本信息

TCP的主动连接方一般是客户端这个角色承担TCP的被动连接方一般是服务器这个角色承担但实际上离开了应用层很少提客户端和服务器的概念因此建议还是使用主动连接方和被动连接方

TCP的所有segment都要确认应答同步信息segment也不例外因此主动连接方发送SYN然后被动连接方回应ACK然后被动接收方也发送SYN主动接收方也回应ACK逻辑上至少要有4层少任意一个就会存在漏洞

被动连接方的ACK和SYN几乎是同时的且TCP也支持一个segment同时起到SYN和ACK的作用所以这两个可以合并

最终就变成了主动连接方发送SYN被动接收方发送并回应SYN + ACK然后主动接收方收到后再次回应ACK这就是三次握手

SYN标志位在header中也只占据一个bit位当其值为1的时候具有SYN功能如果为0则不具备SYN功能

关于三次握手是否能够携带payload问题

  1. 第一次SYN不能携带数据
  2. 第二次SYN + ACK携带数据因为前两次不能确认连接一定是成功的如果在这个阶段携带数据会提升发送成本但有可能失败所以协议设计时禁止了携带数据
  3. 第三次ACK可以携带数据但不强制

image-20220720135914533

交换信息双方的SN是独立不同的

  • 第一次发送SYN[SYN] (这个方括号表示置位) len = 0 SN = a ASN = 0(这里还没置位什么值都无意义)
  • 第二次发送SYN + ACK[SYN, ACK] len = 0 SN = b ASN = a + 1
  • 第三次发送ACK[ACK] len > 0 SN = a + 1 ASN = b + 1

请求连接则置同步位SYN=1确认连接就置确认位ACK=1TCP规定SYN报文段不能携带数据但要消耗一个序号ACK报文段可以携带数据但如果不携带数据则不消耗序号

image-20220720112227576

这里的首部header长度表示TCP头部有多少个32bit位也就是多少个4字节上图中是8所以总共32字节然后整个segment也是32个字节说明payload的长度为0

三次握手的状态转移过程

状态是指连接当前处于何种情况引入状态就是因为通过状态管理者(TCP)能了解每个连接当前处于何种情况

观察下面的状态转移图三次握手阶段主要关注红色部分

虚线表示被动连接方实线表示主动连接方线的起点是起始状态线的终点是转移后的状态

线上的文字有两层含义第一个是因为什么原因导致的状态转移第二个是状态转移期间需要做的动作

image-20220720141628667
  • CLOSE表示初始状态
  • LISTEN: 表示服务器端的某个SOCKET处于监听状态可以接受连接了

ServerSocket serverSocket = new ServerSocket(8888); 代码中创建服务器套接字就开始监听了即close到listen

  • SYN_SENT当客户端主动发送SYN之后就会进入该状态然后等待对方回复SYN + ACK收到对方的SYN和ACK之后回复ACK该状态结束进入ESTABLISHED状态
  • SYN_RCVD: 当接收到了客户端发送的SYN报文时进入该状态然后给客户端发送SYN + ACK当再次收到对方的ACK的时候它会进入到ESTABLISHED状态
  • ESTABLISHED表示连接已经建立了

需要注意的是这些状态并不代表某一时刻的状态而是一个时期或者说一个过程这一过程的任意时刻都是该状态

再次结合下图理解

image-20220720143204805

这个过程无法被应用层看到换言之应用层无法看到三次握手的中间过程

Socket socket = serverSocket.accept();执行该语句前是listen状态执行完之后就是establish状态了

为什么要有握手阶段(同步阶段)

为了可靠性确保对方在线并且需要同步给对方一些基本信息

三次握手过程中的状态转移

  1. 为什么要有状态
  2. 学会阅读状态转移图
  3. 状态变化和外部事件的关系

四次挥手

挥手的标志位FIN

挥手过程中主机的角色

  • 主动挥手方主动断开连接的一方
  • 被动挥手方被动断开连接的一方
  • 同时关闭双方均属于主动挥手方

要注意主动挥手方并不一定就是主动连接方两者没有直接关系是相互独立的

四次挥手的变化

image-20220723091807839

梳理一下四次挥手的整体流程

客户作为主动挥手方发送断开连接的请求状态从ESTABLISHED转换到FIN_WAIT1状态

服务器作为被动挥手方接收到FIN后状态从ESTABLISHED转换到CLOSE_WAIT(该状态将在下面做详细解释这里先略过)然后发送应答ACK标志位置位

然后主动挥手方在接收到对方发送的ACK之后进入FIN_WAIT2状态等待被动挥手方发送FIN

被动挥手方过了一段时间之后(这一段时间都在做什么详见下文CLOSE_WAIT状态分析)发送FIN给主动挥手方然后进入LAST_ACK状态等待对方的ACK应答

主动挥手方接收到了FIN之后进入TIME_WAIT状态(关于为什么这里要等待一段时间下文会与CLOSE_WAIT一起分析)然后发送ACK应答

被动挥手方收到应答之后就进入CLOSED状态主动挥手方在经过一段时间等待之后也进入CLOSED状态至此连接断开

这里我们看到和三次握手不同的是FIN和ACK并没有合并那是因为TCP协议允许一方挥手而另一方不挥手的情况

当然想合并也不是不行来看一下状态转移图

红色表示三次挥手蓝色表示同时关闭 没有任何标注的是标准的四次挥手

image-20220723093721320

三次挥手时序图

image-20220723155137063

同时挥手时序图

image-20220723155506548

关于CLOSE_WAIT和TIME_WAIT状态

  • CLOSE_WAIT发生在被动方出现在单方面挥手的情况下主动方发送了FIN之后被动挥手方做出ACK应答作出应答之后进入该状态这种状态的含义其实是表示在等待关闭为什么要特意等待主要是为了看你现在是否还有数据需要发送给对方的如果没有了才发送FIN报文
  • TIME_WAIT在该状态下我们去思考一个问题为什么主动挥手方在最后一次挥手之后即最后一次发送ACK给被动挥手方之后还要等待一段时间事实上主动挥手方在该状态下的主要目的是为了保证对方收到了ACK如果主动挥手方发送的ACK没有准时到达对方对方会因为超时等待机制再次发送FIN报文处于TIME_WAIT状态下的主动挥手方收到后会再次发送ACK假设没有TIME_WAIT状态主动挥手方发送了ACK就直接关闭之后完全有一种可能就是对方没有收到该ACK最终导致连接无法断开因此这个阶段的存在是非常有必要的

思考如果服务器上出现了大量的CLOSE_WAIT状态的TCP连接请问这种现象是否合理并说明理由

答案是不确定单纯从现象上看无法断定是否合理

因为如果程序设计的时候会出现较长时间的单方面关闭的情况时出现大量的CLOSE_WAIT是合理现象

但如果程序没有这么设计那么就是不合理可能的原因是被动挥手方忘记调用socket.close()所致

为什么会有TIME_WAIT是否有存在的必要参考上面对TIME_WAIT的介绍

先说结论TIME_WAIT的存在是为了确保被动挥手方已经关闭并且有存在的必要

在进入TIME_WAIT状态前主动挥手方发送了ACK应答如果该报文没有被对方接收对方会再次发送FIN报文为了确保连接能够正常断开就不能直接释放连接需要在该状态等待确认对方是否因为没收到ACK而再次发送FIN的情况如果没有才可以正常关闭

针对TIME_WAIT状态我们可以设想一个场景来说明其必要性

路人甲拥有一个手机号码123123有一天甲注销了这个手机号运营商回收该手机号如果运营商回收之后直接放开申请然后路人乙就把这个号码申请了

结果路人甲的朋友丙想给甲打电话于是拨通了手机号码123123路人乙接到电话之后发现丙不是来找自己的两个都很奇怪你是谁啊

同样的数据的传输中TCP靠五元组来区分连接五元组作为一条连接的主键(PK)如果主动挥手方不经过TIME_WAIT直接关闭连接之后五元组又立刻被分配出去了如果这个时候收到了发给五元组的segment(可能是网络传输较慢的数据)那这个数据到底是给谁传的很显然我们已经不能区分了

为什么TIME_WAIT的时间是2MSL

首先说一下什么是MSL(Maximum Segment Live)一个Segment能在网络上活着的最大时间MSL是个理论值实际中很多OS取的是经验值一般是一分钟所以默认情况下TIME_WAIT持续的时间是2分钟但这个值可以被修改

2 * MSL时间过去之后Segment的一个来回肯定是够了

如果在2MSL中没有收到segment则说明

  • 认为对方收到了ack(即使对方没有收到由于我们也没收到fin说明网络出现了问题 )
  • 网络上肯定没有发送给甲的segment了之后五元组收到的segment一定是给新的连接的

思考服务器上发现了大量的TIME_WAIT状态的TCP连接是否合理并说明理由

理论上来说确实是合理的从标准上来说没有任何问题代码正常地关闭了连接

但从实践的角度来看是不合理的因为维护连接是有成本的最主要的硬件成本是内存

客户端和服务器之间的压力是不同的客户端身上背负的连接比较少几百条服务器身上背负的连接很多几十万 - 几百万

所以如果让服务器背负这个TIME_WAIT连接的成本相对压力较大所以一般建议让客户端来背负这个成本

因此一般做网络编程设计的时候不建议服务器去主动关闭连接某些特殊情况下该主动还是要主动

总结

  • 为什么说四次挥手而不是三次挥手因为被动挥手方在收到主动方发送的FIN报文之后还有一些数据需要发送处理不能直接关闭连接所以先发送一个ACK告知主动方“报文已收到等我把数据都发送完了再给你发FIN + ACK报文”所以在被动方确认FIN报文时要分两次完成所以就说是四次挥手
  • 四次挥手的三种情况正常情况下的四次挥手三次挥手、同时挥手
  • 四次挥手的tcp header的标志位变化FIN -> ACK -> FIN + ACK -> ACK
  • 四次挥手的状态变化主动方ESTABLISHED -> FIN_WAIT1 -> FIN_WAIT2 -> TIME_WAIT -> COLSED被动方ESTABLISHED -> CLOSE_WAIT -> LAST_ACK -> CLOSED
  • 重点掌握CLOSE_WAIT和TIME_WAIT

三次握手、四次挥手中的细节都是TCP协议内部所做的事情作为应用层是看不到这些细节的对Java来说三次握手只有简单的connect() 和 accept()

异常情况

情况1在甲的任务管理器中直接把A进程kill(停止)掉请问这条连接的命运如何

虽然进程被kill掉了(即进程没有走完main方法也没有执行close()方法)但是一个进程的资源都是由OS分配的一个进程有哪些资源OS都直到所以即使进程内部没有关闭TCP连接OS也会走进程资源释放流程将TCP连接正常关闭因此这条连接看起来还是甲主动关闭正常执行了四次挥手

如果资源通过close方法释放那么OS还会不会执行资源释放流程了

  • TCP就是OS提供的机制应用层可以通过Socket使用这些有OS提供的机制比如socket.close()方法
  • 由OS提供的系统调用称为System Call Interface - SCI

情况2直接重启电脑连接的命运是怎么样的

点击重启之后执行OS的逻辑在电脑关闭之前关闭所有的进程并释放所有进程的资源

因此也是正常的四次挥手

情况3直接关机

同理只要OS的代码还能执行连接就会正常关闭

情况4直接拔掉甲的电源或者强制关机

首先要知道OS是软件(软件就是程序就是数据 + 指令就是运行在CPU上的指令以及指令要处理的数据)拔掉电源之后硬件都无法工作了作为软件的OS更不能再执行任何操作

至于该链接的命运需要分情况讨论

  • 甲主机的命运由于连接只是逻辑上的概念表现在现实中只不过是内存中的一段数据罢了如果断开电源作为硬件的内存无法工作这些数据就不存在了因此连接也就不存在了但这个连接既不属于正常关闭也不属于异常关闭就是突然消失了

  • 乙主机上的连接的命运需要分情况讨论

    1. 如果乙发生了写事件(即乙向甲主机发送数据了)由于甲主机都关机了因此乙主机无法收到应答即使超时重传之后还是收不到经过多次尝试的乙主机开始走异常关闭流程关闭该TCP连接以异常的形式通知应用层最后发送一条reset segment。从甲消失到乙最终断开连接需要一段时间而不是瞬时的
    2. 如果乙只是在单纯的读取数据那么乙根本无法得知甲到底还在不在只能说甲一直没发送数据乙也一直收不到数据那么这条连接就永远无法断开一直保持ESTABLISHED状态

针对这种情况4的解决方案

  1. TCP层面有种Keepalive机制定期地发送一些数据给对方payload长度为0segment长度不是0就可以根据对方有没有应答来判断这个机制应用不多。。。。

  2. 更常见的办法是应用层自己来做这个工作

    • 一种方法是应用在进行read的时候不要无限制地read而是带上一个超时时间(read timeout)
    • 另一种方法是定期主动给对方发送数据(相互报平安)这种数据包称为heartbeat - 心跳包

标志位中的RST表示异常

Reset Segment - RST收到这种rst segment就代表异常了立即关闭连接不用在四次挥手了然后以异常的方式通知应用层

通过命令行命令可以查看主机上的TCP由于macOS的操作有些不同因此不在这里解释

流量控制

Flow Control - 流量控制

流量控制(广义)发送端会根据对方接收能力和网络承载能力动态地调节自己的发送流量

如果在对方只能接收少量的文件或者网络很堵的情况下发送大量的数据对方只能收到一部分收不到的另一部分就可能来不及接收或者根本就没收到而导致数据的丢失因此通过流量控制来提高数据的到达率来提高可靠性

广义可以分为狭义的流量控制(其实指TCP协议下专门的流量控制)和拥塞控制 - Congestion Control

  • 流量控制(狭义)根据对方的接收能力来调节发送流量
  • 拥塞控制根据网络的承载能力来调节发送流量

接下来就专门针对TCP协议下的流量控制进行介绍

流量控制

  1. 需要知道对象的接收能力最好是实时的感知到怎么做到

让对方主动告知也就是放在Segment Header中把接受能力携带发送过来

image-20220723174821859

发送segment的时候把自己的接收能力(接收窗口)填写到segment header的串口字段中发送给对方

如何做到实时

  1. 让所有发送的segment都携带接收窗口
  2. 即使一段时间内没有数据要发送也时不时发送一个ACK + WINDOW过去
  1. 拿到对方的接收能力后怎么换算成发送流量

接收窗口大致 = 接收缓冲区大小 - 已用大小(接收的数据暂时没被应用层读走)

最大发送量 = 对方的接收窗口

  1. 通过什么机制来控制发送量

通过滑动窗口机制控制发送量

前置知识梳理下发送缓冲区的逻辑部分有哪些

image-20220723181704634

应用层写入的数据有可能大于接收窗口也有可能小于接收窗口为了便于理解之后的介绍都将基于后者

其次对于应用层写入的数据TCP既可以全部发送也可以部分发送

image-20220723232819020

在TCP协议下发送的数据也可能只有部分数据收到应答也有可能全部都会应答

image-20220723233333359

那么在上图中发送并应答的数据就没必要再保留了因此我们拿来做可用空间

image-20220723233514056

整个发送缓冲区被看做逻辑上的几个部分

  1. 未使用空间
  2. 应用层已写入数据包括目前可发送的(处于滑动窗口内)、暂不可发送的
  3. 目前已发送(包括已发送未应答、已发送已应答(这部分空间已经可以利用了))

滑动窗口机制

如果发送方每次都只发送一个数据接收方每收到一个数据就做一次应答然后发送方收到应答之后再发送下一个数据这样的效率是比较低的那么我们可以一次发送多条数据这样就可以大大提高性能

比如一次性发送四个数据然后发送方收到第一个数据的ACK后滑动窗口向后移动继续发送第五个段的数据依次类推操作系统内核为了维护这个滑动窗口需要开辟发送缓冲区来记录当前还有哪些数据没有应答只有确认应答过的数据才能从缓冲区删掉

我们下图中发送方发送的数据就是建立在连续发送的基础上的

TCP滑动窗口

TCP主动发送数据情况下导致窗口滑动的原因

左侧是根据ASN进行变化右侧是根据ASN + window进行变化的

左侧数据收到应答之后就可以丢弃了滑动窗口的左侧就要右移

而右侧则根据ASN应答和window来决定

通过滑动窗口机制保证了TCP不会发送过量即不会发送对方没有能力接收的数据

如果滑动窗口发送的数据在中途丢包了会如何

  • 情况一数据包已经到达但是ACK在中途丢失了

    image-20220724210754083

    这种情况无需担心如果发送方接收到某个ACK就能推测该ACK之前所有的数据都已经到达因此前面的数据也可以通过这个ACK进行确认

  • 情况二数据报还未到达就丢失了

    image-20220724211034720

    这种情况下如果中间某个数据包丢失那么后续的数据包的ACK全是上图中1001这样的ASN因为ASN的填写规则就是要接收的下一个字节的数据0-1000之后的下一个数据就是1001由于一直没有收到1001数据包接收方收到数据就发送1001的应答就好像在告诉发送方我的下一个数据是1001

    当发送方收到3次重复的应答时就认为该数据包已经丢失了因此不会等到超时重传而是立即重发

    当接收方正确收到这个1000-2000的数据以后后续ASN恢复到要接收的下一个数据

    这种机制被称为“高速重发控制”也叫“快重传”

滑动窗口的变化和拥塞控制也有关下面就对拥塞控制进行介绍

拥塞控制

Congestion Control - 拥塞控制根据网络目前的承载能力控制发送量

我们从3个角度来理解拥塞控制

  1. 作为发送方是如何得到当前的网络承载能力的

网络的承载能力就好像现实中路面拥堵的情况这个值无法通过某角色发送一个精确值告诉你

所以拥塞窗口实际上是一个通过动态算法来实时计算出来的结果 - 本质上是一个估算值

拥塞控制的算法有很多慢启动、拥塞避免算法、拥塞状态时的算法、快速恢复算法接下去我们要介绍的算法是「慢开启快启动」即上述的拥塞避免算法这是一种比较古老的算法该算法的精确度不够高不适用于现在的网络环境但是面试要考

该算法根据丢包率作为重要的因素来推算丢包率 = 单位时间内没有收到应答的占比或者说TCP重发的次数占比

拥塞避免算法又称为慢开始快启动算法

  • 横坐标时间
  • 纵坐标当前计算出来的拥塞窗口
  • 慢开始cwnd的初始值非常小为1
  • 指数增长转变为线性增长的中间阈值ssthresh
  • 指数增长cwnd = cwnd * C(常数下图中取2)
  • 线性增长cwnd = cwnd + C(常数下图中取1)
  • 快启动指数增长速率快于线性增长

image-20220724191959859

当处于指数增长的过程中时如果cwnd大于ssthresh指数增长就变为线性增长如果一直没有丢包cwnd就会一直增长到最大值

当丢包率大于某个阈值了就相当于网络拥塞了会发生一下变化

  1. ssthresh根据当前cwnd重新计算得到一个新的ssthresh = cwnd / 2
  2. cwnd会重新变为初始值即cwnd = 1

这种算法一遇到网络拥塞就把cwnd变为初始值因为现代的网络哪怕是丢包也很久就会恢复而早期网络不好的时候一旦遇到丢包可能一段时间内都会丢包所以说该算法只适用于早期网络不适用于现在的网络

至于为什么ssthresh要除以2也好理解如果cwnd在处于一个较高层次才发生丢包说明当前网络状况还不错可以适当提高阈值让下一次开始的前期启动更快一些如果cwns处于一个较低层次就发生了丢包说明当前网络状况可能并不乐观于是适当降低阈值让前期的启动慢一些尽可能避免发生拥塞

  1. 发送最大流量(发送窗口) = f ( 拥塞窗口 , 接收窗口 ) f(拥塞窗口, 接收窗口) f(拥塞窗口,接收窗口)

发送窗口 = m i n ( 拥塞窗口 , 接收窗口 ) min(拥塞窗口, 接收窗口) min(拥塞窗口,接收窗口)

如果拥塞窗口为100接收窗口为10那么发送窗口就是10

反过来如果拥塞窗口为10接收窗口为100那么发送窗口也是10

​ 因此发送窗口并不需要担心拥塞窗口一直增长因为接收窗口可能达不到那个大小

  1. 如何进行发送流量控制 - 流量控制

依然是通过滑动窗口当拥塞窗口发生了变化(可能增长也可能发生拥塞而减少)滑动窗口就会根据拥塞窗口进行调整由于只是发送窗口的变化导致的滑动窗口的变化因此在滑动窗口的右侧改变左侧不动

至此我们能够直到滑动窗口的左边根据发送并应答移动右边则是根据发送并应答 + 发送窗口移动而发送窗口由拥塞窗口以及对方接收窗口决定

思考

由于有流量控制、拥塞控制的存在请问发送方的应用层本次写入[a, b, c, d]4个字节的数据请问发送方的TCP层能包装数据是按照[a, b, c, d]作为完整的segment的方式去发送的吗

并不一定由于滑动窗口的存在我们并不能保证 [a, b, c, d] 这四个字节的数据正好处在滑动窗口内完全哪有可能就被一刀两断了

那么假设接收方收到数据[a, b, c, d, e, f, g, h, i, j, k]请问接收方能够分辨发送方写了几次每次是哪几个吗

很显然是不能的因此作为接收方的应用层也无法直到这些数据到底是分了几次来的属于那一部分的

这就说明面向报文的特点已经无法做到了TCP协议为了可靠性放弃了面向报文的特性称之为面向字节流

延迟应答

如果接收数据的主机立刻返回ACK应答这时候返回的窗口可能比较小

假设接收方每收到一次数据马上就应答那么对于发送方来说每次应答返回的窗口都很小远小于接收端的接收缓冲区这么小的数据又会很快被处理掉这样的传输效率是不高的

因此可以让接收方收到数据之后先等一等等到窗口比较大了在发送应答这样一次就能处理较多的数据传输效率也就比较高

当然并非一定要等到窗口足够大了才发送应答还是存在限制的

数量限制每隔N个包就应答一次

时间限制每隔最大延迟时间就应答一次

具体的数量和超时时间依操作系统不同也有差异一般N取2超时时间取200ms

捎带应答

该机制是为了减少应答次数的如果需要应答的时候正好有数据要发送就“搭顺风车”一起发送给对方

粘包问题(面向字节流)

面向字节流给应用层提出了新的挑战

应用层的协议设计必须手动设定边界常见的方法有

  1. 采用定长的信息比如一个请求一定是100个字节
  2. 先定长发送后续数据的长度再发送数据也就是携带长度的数据比如先发送4个字节的定长数据(内容是后续数据的长度)然后在发送该长度的数据
  3. 通过特殊字符来分隔比如“/r/n”, "$$$"等这个有程序员自己设定只要不与正文内容冲突即可

TCP的三个特点

  1. 可靠
  2. 有连接意味着使用TCP协议的应用在建立联系之前彼此需要先建立TCP联系在一个TCP连接中仅有两方进行彼此通信。广播和多播不能用于TCP
  3. 面向字节流为了可靠性放弃了面向报文的特性

关于TCP的标志位URG和PSH

URGUrgent(紧急)配合16位的紧急指针来使用(这套设计已经过时了现在都是通过两条信道实现)

假设通过TCP协议发送了一段数据[a, b, c, d, e, f, g]其中d这个字节的数据非常重要(称为紧急指令)

将urg置位为1然后让紧急指针指向d字节所在的偏移量 = 3让接收方优先处理这个字节优先传递给对方应用层

现在比较好的处理是使用两条信道让紧急指令单独走一条信道其他走普通信道接收方收到之后会优先处理紧急指令所在信道的数据

PSHPush(推)要求发送方和接收方的TCP尽快发送数据出去

TCP会针对数据发送做优化(一次尽量多发一点数据)

这个标志位主要是给接收方用的让发送方赶紧发送数据哪怕只有一点数据也先发过来的意思

在这个标志位现在基本失去了作用因为被“滥用”如果所有人的PSH都置位也就是每个人都说自己很急那就没办法处理了

TCP协议如何保证数据的有序性

TCP协议通过序列号保证了数据的有序发送端主机每次发送数据的时候会带上SN而对于接收端来说它需要通过SN对发送来的数据进行确认只有当接收端收到了连续的序号的数据时才会将数据上交给应用层否则它不会上交给应用层比如发送0-1000,1000-2000,2000-3000的数据中间的1000-2000丢包了那么接收方最多只把0-1000上传给应用层而不会把后面的数据上传给应用层并且由于快速重传机制会一直发送1001告诉发送方这部分数据没收到当接收方收到这个数据之后就对这些数据重新排序所以就保证了数据的有序性

TCP总结

可靠性

  • 校验和
  • 序列号按序到达
  • 确认应答
  • 超时重发
  • 连接管理
  • 流量控制
  • 拥塞控制

提高性能

  • 滑动窗口
  • 快速重传
  • 延迟应答
  • 捎带应答

其他

  • 定时器超时重传定时器保活定时器TIME_WAIT定时器等

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6