TCP协议(全面)

TCP协议

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

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

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

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

TCP的机制

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

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

TCP协议(全面)_第1张图片

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

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

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

如果没有收到对方的确认

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

数据编号和确认编号

发送的数据编号被称为序列号(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长度

TCP协议(全面)_第2张图片

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的角色

TCP协议(全面)_第3张图片

在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,可以携带数据,但不强制

TCP协议(全面)_第4张图片

交换信息双方的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=1(TCP规定,SYN报文段不能携带数据,但要消耗一个序号;ACK报文段可以携带数据,但如果不携带数据则不消耗序号)

TCP协议(全面)_第5张图片

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

三次握手的状态转移过程

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

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

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

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

TCP协议(全面)_第6张图片
  • 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:表示连接已经建立了

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

再次结合下图理解:

TCP协议(全面)_第7张图片

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

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

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

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

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

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

四次挥手

挥手的标志位:FIN

挥手过程中主机的角色:

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

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

四次挥手的变化:

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

客户作为主动挥手方,发送断开连接的请求,状态从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协议允许一方挥手,而另一方不挥手的情况

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

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

TCP协议(全面)_第8张图片

三次挥手时序图:

TCP协议(全面)_第9张图片

同时挥手时序图:

TCP协议(全面)_第10张图片

关于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长度为0),segment长度不是0,就可以根据对方有没有应答来判断;这个机制应用不多。。。。

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

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

标志位中的RST表示异常

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

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

流量控制

Flow Control - 流量控制

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

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

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

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

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

流量控制:

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

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

TCP协议(全面)_第11张图片

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

如何做到实时?

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

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

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

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

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

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

TCP协议(全面)_第12张图片

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

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

TCP协议(全面)_第13张图片

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

TCP协议(全面)_第14张图片

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

TCP协议(全面)_第15张图片

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

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

滑动窗口机制

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

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

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

TCP协议(全面)_第16张图片

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

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

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

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

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

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

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

    TCP协议(全面)_第17张图片

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

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

    TCP协议(全面)_第18张图片

    这种情况下,如果中间某个数据包丢失,那么后续的数据包的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)
  • 快启动:指数增长速率快于线性增长

TCP协议(全面)_第19张图片

当处于指数增长的过程中时,如果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:

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

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

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

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

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

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

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

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

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

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

TCP总结

可靠性:

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

提高性能:

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

其他:

  • 定时器(超时重传定时器,保活定时器,TIME_WAIT定时器等)

你可能感兴趣的:(JavaEE,java,java-ee,网络,tcp)