其实深入解析TCP/IP协议握手与挥手过程:原理详解及衍生问题探讨的问题并不复杂,但是又很多的朋友都不太了解,因此呢,今天小编就来为大家分享深入解析TCP/IP协议握手与挥手过程:原理详解及衍生问题探讨的一些知识,希望可以帮助到大家,下面我们一起来看看这个问题的分析吧!
下面是TCP/IP(Transmission Control Protoco/Internet Protocol)协议头的格式,这是理解其他内容的基础。对关键字段进行了一些解释。
Source Port和Destination Port :分别占用16位,分别代表源端口号和目的端口号;用于区分主机中的不同进程,IP地址用于区分不同的主机。源端口号和目的端口号与IP头中的源IP地址和目的IP地址结合可以唯一确定一个TCP连接;
序列号: TCP 连接中传输的字节流中的每个字节都按顺序编号,用于标识TCP 发送端发送到TCP 接收端的数据字节流。它代表该报文段中的数字1。数据流中一个数据字节的序号;主要用于解决网络上报乱序的问题;
确认号:期望收到对方下一条消息的第一个数据字节的序列号。因此,确认序号应该是最后一次成功接收到的数据字节序号加1。不过,确认序号字段只有在标志位中的ACK标志位(下面介绍)为1时才有效。主要用来解决问题不丢失数据包;
Offset: 表示TCP消息的数据距TCP消息段开头的距离,给出了报头中32位字的数量。需要该值是因为可选字段的长度是可变的。该字段占用4位(最多可以表示15个32位字,即4*15=60字节的报头长度),因此TCP报头最多有60字节。但没有可选字段,正常长度为20字节;
TCP Flags: TCP 标头中有6 个标志位。可以将多个同时设置为1。它们主要用于控制TCP状态机,其次是URG、ACK、PSH、RST、FIN。各个标志位的含义如下:
SYN(Synchronize Sequence Numbers)——同步序列号——同步标签
该段是同步序列号并建立连接的请求。序列号字段包含发送者的初始序列号。
该标志仅在通过三向握手建立TCP 连接时有效。它提示TCP连接的服务器检查序列号,该序列号是TCP连接的发起者(通常是客户端)的初始序列号。这里,TCP序列号可以看作是一个32位的计数器,范围从0到4、294、967、295。通过TCP连接交换的数据的每个字节都有序列号。 TCP 报头中的序列号字段包含TCP 报文段中第一个字节的序列号。
用于在连接建立时同步序列号。当SYN=1且ACK=0时,表明这是一个连接请求消息。如果对方同意建立连接,则应在响应消息中设置SYN=1和ACK=1。因此,将SYN设置为1意味着这是一个连接请求或连接接受消息。
ACK(Acknowledgement Number)-确认号-确认标志
该段携带确认并且确认号字段的值有效并且包含接收器期望的下一个序列号。
大多数情况下都会设置此标志。 TCP头中的确认号栏包含的确认号(w+1,图-1)是下一个期望的序列号,也提示远程系统已成功接收到所有数据。
TCP协议规定只有当ACK=1时才有效。它还规定连接建立后发送的所有消息的ACK必须为1。
网上有很多误解。例如,ACK可以与SYN、FIN等一起使用。例如,SYN和ACK可以同时为1。它代表连接建立后的响应。如果只有一个SYN,则代表Just make the 连接。几次TCP握手都是通过这样的ACK来表达的。其实:ACKSYN就是标志位,
FIN(完成)——结束标记
发送者想要关闭连接
用于释放连接。
当FIN=1时,表示发送方本报文段的数据已经发送完毕,需要释放连接。
URG (The Urgent Pointer)-紧急标志
段是紧急的,紧急指针字段携带有效信息。
当URG=1时,表示紧急指针字段有效。告诉系统该段有紧急数据
PSH(推)- 推标志
该段中的数据到达后应立即推送到应用层。
当PSH为1时,一般只出现在DATA内容不为0的数据包中。也就是说PSH=1表示传递的是真正的TCP数据包内容。
RST(复位)——复位标志
出现一些问题,发送者想要中止连接。
当RST=1时,表明TCP连接发生严重错误,必须释放连接并重新建立。
窗口(Advertized-Window)——窗口大小:滑动窗口,用于流量控制。占用2个字节,是指通知接收方在发送这条消息时需要接受多少空间。
CWR(拥塞窗口减少)
由支持ECN 的发送方在减少其拥塞窗口时设置(由于重传超时、快速重传或响应ECN 通知)。
ECN(显式拥塞通知)
在三向握手期间,它表明发送方能够执行显式拥塞通知。通常这意味着在正常传输过程中收到了设置了IP 拥塞标志的数据包。有关详细信息,请参阅RFC 3168。
TCP连接的建立和连接关闭都是通过请求-响应的方式完成的。我们看下图,应该就能基本明白TCP的握手和挥手过程了。
正在上传.取消
三方握手
三次握手的目的是为了防止无效的连接请求报文段突然被发送到服务器,从而导致错误。推荐阅读《TCP的三次握手与四次挥手(详解+动图》
当然,如果双方同时打开,则可能是四次握手。
推荐阅读这里《面试题·TCP 为什么要三次握手,四次挥手?》
TCP连接的初始化序列号可以固定吗?
单个TCP数据包每次打包1448字节的数据进行传输(以太网最大数据帧为1518字节,以太网帧头14字节,帧尾CRC校验4字节(总共18字节)),剩下的承载上层协议的区域,即Data字段,最大大小只有1500字节。我们称这个值为MTU(最大传输单元))。
那么要一次发送大量的数据,就必须分成多个数据包。例如,对于10MB的文件,需要发送7100多个包。
发送时,TCP协议对每个数据包进行编号(序列号,简称SEQ),以便接收方能够按顺序恢复。如果发生丢包,您还可以知道丢失的是哪个数据包。
第一个数据包的编号是一个随机数——初始化序列号(缩写为ISN:Initial Sequence Number)
为了便于理解,我们这里将其称为1号包。假设这个数据包的负载长度是100字节,那么可以推断下一个数据包的编号应该是101。这意味着每个数据包可以获得两个数字:它自己的编号和下一个数据包的编号。因此,接收者知道应该按照什么顺序将其恢复到原始文件。
如果初始化序号可以固定的话,我们看看会出现什么问题?
假设ISN固定为1,Client和Server建立TCP连接后,Client连续向Server发送10个报文。这10 个数据包以某种方式被链路上的路由器缓存(路由器将缓存或丢弃它们而不发出警告)。任何数据包),恰好此时Client挂掉了,然后Client用相同的端口号重新连接Server,Client连续向Server发送了几个数据包。假设此时Client的序列号变为5。然后,路由器之前缓存的10个数据包全部路由到服务器。服务器给客户端回复了一个确认号10。此时客户端已经完全不适了。到底是怎么回事?我的序列号才到5,为什么给我的确认号是10?整个事情一团糟。
在RFC793 中,建议将ISN 绑定到假时钟。这个时钟会每4微秒给ISN加1,直到超过2^32并再次从0开始。这将需要4 小时才能导致ISN 回绕。问题,这几乎保证了每个新连接的ISN不会与旧连接的ISN冲突。这种增量的ISN 使得攻击者很容易猜测TCP 连接的ISN。当前的大多数实现都是基于基线值的随机实现。
注:这些内容引用自《从 TCP 三次握手说起:浅析TCP协议中的疑难杂症》,建议查看。
正在上传.取消
初始化连接的SYN 超时问题
Client向Server发送SYN包后挂断。 Server向Client返回的SYN-ACK尚未收到Client的ACK确认。此时连接尚未建立,不能认为失败。这就需要Server有一个超时时间来断开连接,否则这个连接将一直在Server的SYN连接队列中占据一个位置。大量的此类连接会耗尽服务器的SYN连接队列,导致正常连接无法处理。
目前,Linux 默认会重发SYN-ACK 数据包5 次。重试间隔从1s开始。下一次重试间隔是前一次重试间隔的两倍。 5次重试间隔为1s、2s。4s,8s,16s,一共31s。第五次之后,你必须等待32秒。你就会知道第五次也超时了。因此,TCP 断开连接之前总共需要1s + 2s + 4s + 8s + 16s + 32s=63s。这个连接。由于SYN超时需要63秒,这给了攻击者攻击服务器的机会。攻击者在短时间内向服务器发送大量SYN数据包(俗称SYN洪水攻击),以耗尽服务器的SYN队列。为了处理SYN过多的问题,Linux提供了几个TCP参数:tcp_syncookies、tcp_synack_retries、tcp_max_syn_backlog、tcp_abort_on_overflow来调整响应。
什么是SYN攻击(SYN Flood)
SYN攻击是指攻击客户端在短时间内伪造大量不存在的IP地址,不断向服务器发送SYN报文,服务器回复确认报文并等待客户端的确认。由于源地址不存在,服务器需要不断重发,直到超时。这些伪造的SYN数据包会长期占用未连接队列,正常的SYN请求被丢弃,导致目标系统运行缓慢。严重时可能会导致网络拥塞甚至系统瘫痪。
SYN攻击是典型的DoS(拒绝服务)/DDoS(:分布式拒绝服务)攻击。
如何检测SYN攻击?
检测SYN攻击非常方便。当你看到服务器上出现大量的半连接状态,特别是源IP地址是随机的时,基本可以断定这是一次SYN攻击。在Linux/Unix上,可以使用系统自带的netstats命令来检测SYN攻击。
如何防御SYN攻击?
除非重新设计TCP协议,否则无法完全阻止SYN攻击。我们所做的就是尽可能减轻SYN攻击的危害。常见的防御SYN攻击的方法有:
缩短超时(SYN Timeout)时间
增加最大半连接数
过滤网关保护
SYN cookies技术
如果连接建立了但是客户端突然失败怎么办?
TCP还有一个保持活动定时器。显然,如果客户端出现故障,服务器不可能永远等待,浪费资源。服务器每次收到客户端的请求时都会重置此计时器。时间一般设置为2小时。如果两个小时内没有收到客户端的任何数据,服务器将发送一个检测段,此后每隔75秒发送一次。每分钟发送一次。如果连续发送10个探测报文后仍然没有响应,服务器就会认为客户端出现故障,然后关闭连接。
四次握手四次握手
现在我们来分析一下各种TCP状态的含义(摘录自《TCP、UDP 的区别,三次握手、四次挥手》)
TCP Peer两端同时断开
从上面的“TCP协议状态机”图可以看出
TCP的Peer端在收到对端的FIN报文之前发送了FIN报文,则Peer的状态变为FIN_WAIT1
如果对等体收到来自处于FIN_WAIT1 状态的对等体的FIN 数据包的ACK 数据包,则对等体状态更改为FIN_WAIT2。
对端在FIN_WAIT2状态下收到对端peer发来的FIN数据包。确认收到对端peer发来的所有数据包后,向对端peer回复ACK,然后进入TIME_WAIT状态。
但是,如果处于FIN_WAIT1状态的peer首先收到对端peer发来的FIN报文,那么在确认收到对端peer发来的所有数据包后,会向对端peer回复一个ACK,然后进入正在关闭状态。如果Peer在CLOSEING状态下收到了自己FIN包的ACK包,就会进入TIME WAIT状态。然后
TCP Peer两端同时发起FIN包断开连接,因此两端的Peer可能会出现完全相同的状态转变FIN_WAIT1——CLOSEING——-TIME_WAIT,最终Client和Server会同时进入TIME_WAIT状态。
TCP的TIME_WAIT状态
要解释TIME_WAIT的问题,需要回答以下问题:
对端哪一端会进入TIME_WAIT?为什么?
相信大家都知道,TCP主动关闭连接的一方最终会进入TIME_WAIT。
那么如何界定主动成交方呢?
是否主动关闭由FIN包的顺序决定。也就是说,如果你在收到对端peer发来的FIN数据包之前就发送了FIN数据包,那么你就是主动关闭连接的一方。对于TCP Peer同时断开描述的情况,Peer双方都在主动关闭,双方都会进入TIME_WAIT。为什么主动关闭的一方会执行TIME_WAIT,而被动关闭的一方却不允许进入TIME_WAIT呢?
我们看一下TCP的四次挥手可以简单地分为以下三个过程:
流程1、主动关闭方发送FIN;
流程2、被动关闭方收到主动关闭方的FIN后,发送FIN的ACK,被动关闭方发送FIN;
流程3、主动关闭方收到被动关闭方的FIN后,发送FIN的ACK。被动关闭方等待自己的FIN的ACK。问题出在流程三上。根据TCP协议规范,ACK不是ACKed。
如果主动关闭方没有进入TIME_WAIT,则主动关闭方发送ACK后离开: 如果最后发送的ACK在路由过程中丢失,未能到达被动关闭方,则被动关闭方收不到其FIN此时。 ACK无法关闭连接,那么被动关闭方会超时并重新发送FIN数据包,但此时没有对端会回复FIN给ACK,被动关闭方无法正常关闭连接,所以主动关闭方一方需要输入TIME_WAIT 才能为被动关闭方FIN 重新发送丢失的ACK。
为什么TIME_WAIT状态下关闭连接需要2MSL?
为了保证A发送的最后一个确认消息段能够到达B。这个确认消息段可能会丢失。如果B收不到这个确认报文段,就会重传第三次“挥手”发送的FIN+ACK报文,A会在2MSL时间内收到这个重传。 A每次收到这个重传的报文段,就会重新启动2MSL定时器。这样就保证了A和B都能正常关闭连接。
为了防止下次连接中出现过期的段。 A经过2MSL后,可以保证本次连接中传输的报文段将从网络中消失。这将确保旧连接生成的消息段不会出现在后续连接中。
TIME_WAIT状态用来解决或避免什么问题?
TIME_WAIT主要用来解决以下问题:
上面解释了为什么主动关闭方需要进入TIME_WAIT状态:主动关闭方需要进入TIME_WAIT,这样才能重新发送被动关闭方FIN丢失的ACK。如果主动关闭方没有进入TIME_WAIT,那么当主动关闭方丢失被动关闭方FIN数据包的ACK时,被动关闭方会因为没有收到自己FIN的ACK而重传FIN数据包。该FIN 数据包不会到达主动关闭方。稍后,由于主动关闭方不再存在该连接,因此主动关闭方此时无法识别该FIN数据包。协议栈会认为对方疯了。连接尚未建立。你能给我一个FIN数据包吗?于是回复一个RST包给被动关闭方,被动关闭方就会收到一个错误(我们比较常见的是:connect reset by peer,顺便说一句,Broken pipeline,当收到RST包的时候,也到这个时候)连接写入数据时,我将收到“损坏的管道”错误)。通常应该关闭的连接给了我一个错误,这是很难接受的。
防止断开的连接1中链路中残留的FIN数据包终止新的连接2(重用连接1的所有5个元素(源IP、目的IP、TCP、源端口、目的端口)),这个概率是比较大的低,因为它涉及匹配问题。晚到的FIN报文段的序列号必须落在与2连接的一方的预期序列号范围内。虽然概率很低,但由于初始序列号都是随机的,所以这种情况确实会发生。生成的,序列号是32位并且会环绕。
防止链路上已关闭连接的残留数据包(丢失重复包或游走重复包)干扰正常数据包,导致数据流异常。这个问题和2)类似
TIME_WAIT会带来什么问题?
TIME_WAIT带来的问题是,一个连接进入TIME_WAIT状态后,需要等待2*MSL(一般为1到4分钟)才能断开连接并释放该连接占用的资源,这会导致以下问题
作为服务器,短时间内关闭大量的Client连接,会导致服务器上出现大量TIME_WAIT连接,占用大量元组,严重消耗服务器资源。
作为客户端,短时间内大量的短连接会消耗Client机器上的大量端口。毕竟端口只有65535个。如果端口耗尽,以后将无法发起新的连接。
(由于上述两个问题,当客户端需要连接本机上的服务时,首选UNIX域套接字而不是TCP)
TIME_WAIT很麻烦。很多问题都是由TIME_WAIT引起的,但是TIME_WAIT并不是多余的,不能简单地去掉TIME_WAIT。那么如何解决或缓解TIME_WAIT问题呢? TIME_WAIT可以快速回收再利用,缓解TIME_WAIT的问题。
有什么技巧可以清除TIME_WAIT吗?
修改tcp_max_tw_buckets:tcp_max_tw_buckets控制并发TIME_WAIT的数量。默认值为180000。如果超过默认值,内核将清除过多的TIME_WAIT连接,然后在日志中打印警告。官网文档说这个选项只是为了防止一些简单的DoS攻击。请勿人为降低。
使用RST包从外部清除TIME_WAIT连接:根据TCP规范,任何发送到非监听端口的数据包,都是一个关闭的连接,并且该连接处于任何异步状态(LISTEN、SYS-SENT、SYN- RECEIVED)并且如果收到的数据包的ACK在窗口之外,或者安全层不匹配,则必须以RST响应接收(并且如果收到序列号在滑动窗口之外的数据包,则该数据包必须是丢弃且必须回复ACK 数据包),内核在收到RST 时将产生错误并终止连接。我们可以使用RST数据包来终止处于TIME_WAIT状态的连接。其实这就是所谓的RST攻击。为了描述方便:假设Client和Server有一个连接Connect1,Server主动关闭连接并进入TIME_WAIT状态。我们来描述一下如何从外部使处于TIME_WAIT状态的Server的连接Connect1提前终止。要实现这种RST攻击,首先我们需要知道Connect1中Client的端口port1(一般这个端口是随机的,很难猜测,这也是RST攻击的难点)。使用IP_TRANSPARENT套接字选项,它可以绑定非本地地址,因此可以从任何机器绑定Client地址和端口port1,然后向Server发起连接。服务器在窗口外接收到数据包并用ACK 进行响应。该ACK 数据包将被路由到客户端。此时,99%的可能性是Client已经释放了连接connect1。此时Client收到ACK数据包,会发送RST数据包。服务器收到RST包后释放连接connect1并提前终止TIME_WAIT状态。提前终止TIME_WAIT状态可能会带来(问题2)中提到的三个危害。有关具体危险,请参阅RFC1337。 RFC1337建议不要使用RST提前结束TIME_WAIT状态。
TCP延迟确认机制
TCP对于何时发送ACK有以下规定:
当发送响应数据时,ACK 将与数据一起发送。
如果没有响应数据,ACK中会有一段延迟等待是否有响应数据一起发送,但这个延迟一般在40ms~500ms之间,通常在40ms左右。如果40ms内有数据发送,那么ACK会和数据一起发送,需要注意这个延迟。这个延迟并不是指从接收到数据到发送ACK的时间延迟,而是内核会启动一个定时器,每隔200ms检查一次,比如定时服务器在0ms开始,200ms到期,180ms数据到达。如果200ms没有响应数据,仍然会发送ACK,延迟了20ms。
这有两个目的。
这样做的目的是可以组合ACK,这意味着如果连续接收到两个TCP数据包,则不一定需要ACK两次。只需回复最终的ACK即可,这样可以减少网络流量。
如果接收方有数据要发送,则发送数据的TCP 数据包中将包含ACK 信息。这样做可以避免在单个TCP 数据包中发送大量ACK,从而减少网络流量。
如果在等待发送ACK时第二个数据到达,则必须立即发送ACK!
根据TCP协议,确认机制是累积的。也就是说,确认号X的确认表明X之前但不包括X的所有数据都已被接收。确认号(ACK)本身是一个没有数据的段,因此大量的确认号会消耗大量的带宽。虽然在大多数情况下,ACK仍然可以与数据一起传输,但是如果没有捎带传输,则只能返回单个ACK。如果此类网段过多,网络利用率将会降低。为了缓解这个问题,RFC提出了延迟ACK,即ACK在收到数据后并不立即回复,而是延迟一段可接受的时间。延迟一段时间的目的是看能否将接收方想要发送给发送方的数据一起发送回来,因为TCP协议头总是包含确认号。如果可以的话,将数据一起发送回来,这样可以提高网络利用率。改进了。即使延迟ACK没有携带数据,如果依次收到两个数据包,那么只确认第二个数据包,这也可以节省一个ACK的消耗。由于TCP协议并不对ACK进行ACK,因此RFC建议最多等待2个数据包的累积确认,这样可以及时通知对端peer这里的接收状态。 Linux实现中,有延迟ACK(Delay Ack)和快速ACK,两种ACK根据当前数据包收发情况进行切换:接收到数据包时,需要发送ACK,快速ACK ;否则,延迟ACK(也在无法使用快速确认的情况下)。
一般情况下,ACK不会对网络性能产生太大影响。延迟交流电
K能减少发送的分段从而节省了带宽,而快速ACK能及时通知发送方丢包,避免滑动窗口停等,提升吞吐率。 关于ACK分段,有个细节需要说明一下: ACK的确认号,是确认按序收到的最后一个字节序,对于乱序到来的TCP分段,接收端会回复相同的ACK分段,只确认按序到达的最后一个TCP分段。TCP连接的延迟确认时间一般初始化为最小值40ms,随后根据连接的重传超时时间(RTO)、上次收到数据包与本次接收数据包的时间间隔等参数进行不断调整。 推荐查看《TCP-IP详解:Delay ACK》 TCP的重传机制以及重传的超时计算 前面说过,每一个数据包都带有下一个数据包的编号。如果下一个数据包没有收到,那么 ACK 的编号就不会发生变化 如果发送方发现收到三个连续的重复 ACK,或者超时了还没有收到任何 ACK,就会确认丢包,从而再次发送这个包。 TCP的重传超时计算 TCP交互过程中,如果发送的包一直没收到ACK确认,是要一直等下去吗? 显然不能一直等(如果发送的包在路由过程中丢失了,对端都没收到又如何给你发送确认呢?),这样协议将不可用,既然不能一直等下去,那么该等多久呢?等太长时间的话,数据包都丢了很久了才重发,没有效率,性能差;等太短时间的话,可能ACK还在路上快到了,这时候却重传了,造成浪费,同时过多的重传会造成网络拥塞,进一步加剧数据的丢失。也是,我们不能去猜测一个重传超时时间,应该是通过一个算法去计算,并且这个超时时间应该是随着网络的状况在变化的。为了使我们的重传机制更高效,如果我们能够比较准确知道在当前网络状况下,一个数据包从发出去到回来的时间RTT(Round Trip Time),那么根据这个RTT(我们就可以方便设置RTO(Retransmission TimeOut)了。 如何计算设置这个RTO? 设长了,重发就慢,丢了老半天才重发,没有效率,性能差; 设短了,会导致可能并没有丢就重发。于是重发的就快,会增加网络拥塞,导致更多的超时,更多的超时导致更多的重发。 RFC793中定义了一个经典算法——加权移动平均(Exponential weighted moving average),算法如下: 首先采样计算RTT(Round Trip Time)值——也就是一个数据包从发出去到回来的时间 然后计算平滑的RTT,称为SRTT(Smoothed Round Trip Time),SRTT = ( ALPHA * SRTT ) + ((1-ALPHA) * RTT)——其中的 α 取值在0.8 到 0.9之间 RTO = min[UpBOUND,max[LowBOUND,(BETA*SRTT)]]——BETA(延迟方差因子(BETA is a delay variance factor (e.g., 1.3 to 2.0)) 针对上面算法问题,有众多大神改进,难以长篇累牍,推荐阅读《TCP 的那些事儿》、《TCP中RTT的测量和RTO的计算》 TCP的重传机制 通过上面我们可以知道,TCP的重传是由超时触发的,这会引发一个重传选择问题,假设TCP发送端连续发了1、2、3、4、5、6、7、8、9、10共10包,其中4、6、8这3个包全丢失了,由于TCP的ACK是确认最后连续收到序号,这样发送端只能收到3号包的ACK,这样在TIME_OUT的时候,发送端就面临下面两个重传选择: 仅重传4号包 优点:按需重传,能够最大程度节省带宽。 缺点:重传会比较慢,因为重传4号包后,需要等下一个超时才会重传6号包 重传3号后面所有的包,也就是重传4~10号包 优点:重传较快,数据能够较快交付给接收端。 缺点:重传了很多不必要重传的包,浪费带宽,在出现丢包的时候,一般是网络拥塞,大量的重传又可能进一步加剧拥塞。 上面的问题是由于单纯以时间驱动来进行重传的,都必须等待一个超时时间,不能快速对当前网络状况做出响应,如果加入以数据驱动呢? TCP引入了一种叫Fast Retransmit(快速重传 )的算法,就是在连续收到3次相同确认号的ACK,那么就进行重传。这个算法基于这么一个假设,连续收到3个相同的ACK,那么说明当前的网络状况变好了,可以重传丢失的包了。 快速重传解决了timeout的问题,但是没解决重传一个还是重传多个的问题。出现难以决定是否重传多个包问题的根源在于,发送端不知道那些非连续序号的包已经到达接收端了,但是接收端是知道的,如果接收端告诉一下发送端不就可以解决这个问题吗?于是,RFC2018提出了 SACK(Selective Acknowledgment)——选择确认机制,SACK是TCP的扩展选项 一个SACK的例子如下图,红框说明:接收端收到了0-5500,8000-8500,7000-7500,6000-6500的数据了,这样发送端就可以选择重传丢失的5500-6000,6500-7000,7500-8000的包。 SACK依靠接收端的接收情况反馈,解决了重传风暴问题,这样够了吗?接收端能不能反馈更多的信息呢?显然是可以的,于是,RFC2883对对SACK进行了扩展,提出了D-SACK,也就是利用第一块SACK数据中描述重复接收的不连续数据块的序列号参数,其他SACK数据则描述其他正常接收到的不连续数据。这样发送方利用第一块SACK,可以发现数据段被网络复制、错误重传、ACK丢失引起的重传、重传超时等异常的网络状况,使得发送端能更好调整自己的重传策略。 D-SACK,有几个优点: 发送端可以判断出,是发包丢失了,还是接收端的ACK丢失了。(发送方,重传了一个包,发现并没有D-SACK那个包,那么就是发送的数据包丢了;否则就是接收端的ACK丢了,或者是发送的包延迟到达了) 发送端可以判断自己的RTO是不是有点小了,导致过早重传(如果收到比较多的D-SACK就该怀疑是RTO小了)。 发送端可以判断自己的数据包是不是被复制了。(如果明明没有重传该数据包,但是收到该数据包的D-SACK) 发送端可以判断目前网络上是不是出现了有些包被delay了,也就是出现先发的包却后到了。 TCP的流量控制 ACK携带两个信息。 期待要收到下一个数据包的编号 接收方的接收窗口的剩余容量 TCP的标准窗口最大为2^16-1=65535个字节 TCP的选项字段中还包含了一个TCP窗口扩大因子,option-kind为3,option-length为3个字节,option-data取值范围0-14 窗口扩大因子用来扩大TCP窗口,可把原来16bit的窗口,扩大为31bit。这个窗口是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。也就是: 发送端是根据接收端通知的窗口大小来调整自己的发送速率的,以达到端到端的流量控制——Sliding Window(滑动窗口)。 TCP的窗口机制 TCP协议里窗口机制有2种:一种是固定的窗口大小;一种是滑动的窗口。 这个窗口大小就是我们一次传输几个数据。对所有数据帧按顺序赋予编号,发送方在发送过程中始终保持着一个发送窗口,只有落在发送窗口内的帧才允许被发送;同时接收方也维持着一个接收窗口,只有落在接收窗口内的帧才允许接收。这样通过调整发送方窗口和接收方窗口的大小可以实现流量控制。 下面一张图来分析一下固定窗口大小有什么问题 假设窗口的大小是1,也是就每次只能发送一个数据只有接受方对这个数据进行确认了以后才能发送第2个数据。我们可以看到发送方每发送一个数据接受方就要给发送方一个ACK对这个数据进行确认。只有接受到了这个确认数据以后发送方才能传输下个数据。 这样我们考虑一下如果说窗口过小,那么当传输比较大的数据的时候需要不停的对数据进行确认,这个时候就会造成很大的延迟。如果说窗口的大小定义的过大。我们假设发送方一次发送100个数据。但是接收方只能处理50个数据。这样每次都会只对这50个数据进行确认。发送方下一次还是发送100个数据,但是接受方还是只能处理50个数据。这样就避免了不必要的数据来拥塞我们的链路。所以我们就引入了滑动窗口机制,窗口的大小并不是固定的而是根据我们之间的链路的带宽的大小,这个时候链路是否拥护塞。接受方是否能处理这么多数据了。 我们看看滑动窗口是如何工作的 首先是第一次发送数据这个时候的窗口大小是根据链路带宽的大小来决定的。我们假设这个时候窗口的大小是3。这个时候接受方收到数据以后会对数据进行确认告诉发送方我下次希望手到的是数据是多少。这里我们看到接收方发送的ACK=3(这是发送方发送序列2的回答确认,下一次接收方期望接收到的是3序列信号)。这个时候发送方收到这个数据以后就知道我第一次发送的3个数据对方只收到了2个。就知道第3个数据对方没有收到。下次在发送的时候就从第3个数据开始发。这个时候窗口大小就变成了2 。 这个时候发送方发送2个数据。 网络异常取消重新上传 看到接收方发送的ACK是5就表示他下一次希望收到的数据是5,发送方就知道我刚才发送的2个数据对方收了这个时候开始发送第5个数据。 这就是滑动窗口的工作机制,当链路变好了或者变差了这个窗口还会发生变话,并不是第一次协商好了以后就永远不变了。 TCP滑动窗口剖析 滑动窗口协议的基本原理就是在任意时刻,发送方都维持了一个连续的允许发送的帧的序号,称为发送窗口;同时,接收方也维持了一个连续的允许接收的帧的序号,称为接收窗口。发送窗口和接收窗口的序号的上下界不一定要一样,甚至大小也可以不同。不同的滑动窗口协议窗口大小一般不同。 窗口有3种动作:展开(右边向右),合拢(左边向右),收缩(右边向左)这三种动作受接收端的控制。 合拢:表示已经收到相应字节的确认了 展开:表示允许缓存发送更多的字节 收缩(非常不希望出现的,某些实现是禁止的):表示本来可以发送的,现在不能发送;但是如果收缩的是那些已经发出的,就会有问题;为了避免,收端会等待到缓存中有更多缓存空间时才进行通信。 滑动窗口机制 比特滑动窗口协议 当发送窗口和接收窗口的大小固定为1时,滑动窗口协议退化为停等协议(stop-and-wait)。该协议规定发送方每发送一帧后就要停下来,等待接收方已正确接收的确认(acknowledgement)返回后才能继续发送下一帧。由于接收方需要判断接收到的帧是新发的帧还是重新发送的帧,因此发送方要为每一个帧加一个序号。由于停等协议规定只有一帧完全发送成功后才能发送新的帧,因而只用一比特来编号就够了。其发送方和接收方运行的流程图如图所示。 后退n协议 由于停等协议要为每一个帧进行确认后才继续发送下一帧,大大降低了信道利用率,因此又提出了后退n协议。后退n协议中,发送方在发完一个数据帧后,不停下来等待应答帧,而是连续发送若干个数据帧,即使在连续发送过程中收到了接收方发来的应答帧,也可以继续发送。且发送方在每发送完一个数据帧时都要设置超时定时器。只要在所设置的超时时间内仍收到确认帧,就要重发相应的数据帧。如:当发送方发送了N个帧后,若发现该N帧的前一个帧在计时器超时后仍未返回其确认信息,则该帧被判为出错或丢失,此时发送方就不得不重新发送出错帧及其后的N帧。 从这里不难看出,后退n协议一方面因连续发送数据帧而提高了效率,但另一方面,在重传时又必须把原来已正确传送过的数据帧进行重传(仅因这些数据帧之前有一个数据帧出了错),这种做法又使传送效率降低。由此可见,若传输信道的传输质量很差因而误码率较大时,连续测协议不一定优于停止等待协议。此协议中的发送窗口的大小为k,接收窗口仍是1。 选择重传协议 在后退n协议中,接收方若发现错误帧就不再接收后续的帧,即使是正确到达的帧,这显然是一种浪费。另一种效率更高的策略是当接收方发现某帧出错后,其后继续送来的正确的帧虽然不能立即递交给接收方的高层,但接收方仍可收下来,存放在一个缓冲区中,同时要求发送方重新传送出错的那一帧。一旦收到重新传来的帧后,就可以原已存于缓冲区中的其余帧一并按正确的顺序递交高层。这种方法称为选择重发(SELECTICE REPEAT),其工作过程如图所示。显然,选择重发减少了浪费,但要求接收方有足够大的缓冲区空间。 流量控制 所谓流量控制,主要是接收方传递信息给发送方,使其不要发送数据太快,是一种端到端的控制。主要的方式就是返回的ACK中会包含自己的接收窗口的大小,并且利用大小来控制发送方的数据发送。 上图中,我们可以看到: 接收端LastByteRead指向了TCP缓冲区中读到的位置,NextByteExpected指向的地方是收到的连续包的最后一个位置,LastByteRcved指向的是收到的包的最后一个位置,我们可以看到中间有些数据还没有到达,所以有数据空白区。 发送端的LastByteAcked指向了被接收端Ack过的位置(表示成功发送确认),LastByteSent表示发出去了,但还没有收到成功确认的Ack,LastByteWritten指向的是上层应用正在写的地方。 于是: 接收端在给发送端回ACK中会汇报自己的AdvertisedWindow = MaxRcvBuffer – LastByteRcvd – 1; 而发送方会根据这个窗口来控制发送数据的大小,以保证接收方可以处理。 下面我们来看一下发送方的滑动窗口示意图: 发送端是怎么做到比较方便知道自己哪些包可以发,哪些包不能发呢? 一个简明的方案就是按照接收方的窗口通告,发送方维护一个一样大小的发送窗口就可以了。在窗口内的可以发,窗口外的不可以发,窗口在发送序列上不断后移,这就是TCP中的滑动窗口。如下图所示,对于TCP发送端其发送缓存内的数据都可以分为4类 [1]-已经发送并得到接收端ACK的; [2]-已经发送但还未收到接收端ACK的; [3]-未发送但允许发送的(接收方还有空间); [4]-未发送且不允许发送(接收方没空间了)。 其中,[2]和[3]两部分合起来称之为发送窗口。 下面两图演示的窗口的滑动情况,收到36的ACK后,窗口向后滑动5个byte。 如果接收端通知一个零窗口给发送端,这个时候发送端还能不能发送数据呢?如果不发数据,那一直等接收端口通知一个非0窗口吗,如果接收端一直不通知呢? 下图,展示了一个发送端是怎么受接收端控制的。 由上图我们知道,当接收端通知一个zero窗口的时候,发送端的发送窗口也变成了0,也就是发送端不能发数据了。如果发送端一直等待,直到接收端通知一个非零窗口在发数据的话,这似乎太受限于接收端,如果接收端一直不通知新的窗口呢?显然发送端不能干等,起码有一个主动探测的机制。为解决0窗口的问题,TCP使用了ZWP(Zero Window Probe)。 Zero Window 发送端在窗口变成0后,会发ZWP的包给接收方,来探测目前接收端的窗口大小,让接收方来ack他的Window尺寸。一般这个值会设置成3次,每次大约30-60秒(不同的实现可能会不一样)。如果3次过后还是0的话,有的TCP实现就会发RST掉这个连接。 注意:只要有等待的地方都可能出现DDoS攻击。攻击者可以在和Server建立好连接后,就向Server通告一个0窗口,然后Server端就只能等待进行ZWP,于是攻击者会并发大量的这样的请求,把Server端的资源耗尽。 Silly Window Syndrome 如果接收端处理能力很慢,这样接收端的窗口很快被填满,然后接收处理完几个字节,腾出几个字节的窗口后,通知发送端,这个时候发送端马上就发送几个字节给接收端吗?发送的话会不会太浪费了,就像一艘万吨油轮只装上几斤的油就开去目的地一样。我们的TCP+IP头有40个字节,为了几个字节,要达上这么大的开销,这太不经济了。 对于发送端产生数据的能力很弱也一样,如果发送端慢吞吞产生几个字节的数据要发送,这个时候该不该立即发送呢?还是累积多点在发送? 本质就是一个避免发送大量小包的问题。造成这个问题原因有二: 接收端一直在通知一个小的窗口; 在接收端解决这个问题,David D Clark’s 方案,如果收到的数据导致window size小于某个值,就ACK一个0窗口,这就阻止发送端在发数据过来。等到接收端处理了一些数据后windows size 大于等于了MSS,或者buffer有一半为空,就可以通告一个非0窗口。 发送端本身问题,一直在发送小包。这个问题,TCP中有个术语叫Silly Window Syndrome(糊涂窗口综合症)。解决这个问题的思路有两: 接收端不通知小窗口, 发送端积累一下数据在发送。 是在发送端解决这个问题,有个著名的Nagle’s algorithm。Nagle 算法的规则 如果包长度达到 MSS ,则允许发送; 如果该包含有 FIN ,则允许发送; 设置了 TCP_NODELAY 选项,则允许发送; 设置 TCP_CORK 选项时,若所有发出去的小数据包(包长度小于 MSS )均被确认,则允许发送; 上述条件都未满足,但发生了超时(一般为 200ms ),则立即发送。 规则[4]指出TCP连接上最多只能有一个未被确认的小数据包。从规则[4]可以看出Nagle算法并不禁止发送小的数据包(超时时间内),而是避免发送大量小的数据包。由于Nagle算法是依赖ACK的,如果ACK很快的话,也会出现一直发小包的情况,造成网络利用率低。TCP_CORK选项则是禁止发送小的数据包(超时时间内),设置该选项后,TCP会尽力把小数据包拼接成一个大的数据包(一个 MTU)再发送出去,当然也不会一直等,发生了超时(一般为 200ms ),也立即发送。Nagle 算法和CP_CORK 选项提高了网络的利用率,但是增加是延时。从规则[3]可以看出,设置TCP_NODELAY 选项,就是完全禁用Nagle 算法了。 这里要说一个小插曲,Nagle算法和延迟确认(Delayed Acknoledgement)一起,当出现( write-write-read)的时候会引发一个40ms的延时问题,这个问题在HTTP svr中体现的比较明显。场景如下: 客户端在请求下载HTTP svr中的一个小文件,一般情况下,HTTP svr都是先发送HTTP响应头部,然后在发送HTTP响应BODY(特别是比较多的实现在发送文件的实施采用的是sendfile系统调用,这就出现write-write-read模式了)。当发送头部的时候,由于头部较小,于是形成一个小的TCP包发送到客户端,这个时候开始发送body,由于body也较小,这样还是形成一个小的TCP数据包,根据Nagle算法,HTTP svr已经发送一个小的数据包了,在收到第一个小包的ACK后或等待200ms超时后才能在发小包,HTTP svr不能发送这个body小TCP包; 客户端收到http响应头后,由于这是一个小的TCP包,于是客户端开启延迟确认,客户端在等待Svr的第二个包来在一起确认或等待一个超时(一般是40ms)在发送ACK包;这样就出现了你等我、然而我也在等你的死锁状态,于是出现最多的情况是客户端等待一个40ms的超时,然后发送ACK给HTTP svr,HTTP svr收到ACK包后在发送body部分。大家在测HTTP svr的时候就要留意这个问题了。 推荐阅读《TCP/IP之TCP协议:流量控制(滑动窗口协议)》 TCP的拥塞控制 由于TCP看不到网络的状况,那么拥塞控制是必须的并且需要采用试探性的方式来控制拥塞,于是拥塞控制要完成两个任务:[1]公平性;[2]拥塞过后的恢复。 重介绍一下Reno算法(RFC5681),其包含4个部分: [1]慢热启动算法 – Slow Start [2]拥塞避免算法 – Congestion Avoidance; [3]快速重传 - Fast Retransimit; [4]快速恢复算法 – Fast Recovery。 慢热启动算法 – Slow Start 我们怎么知道,对方线路的理想速率是多少呢?答案就是慢慢试。 开始的时候,发送得较慢,然后根据丢包的情况,调整速率:如果不丢包,就加快发送速度;如果丢包,就降低发送速度。慢启动的算法如下(cwnd全称Congestion Window): 连接建好的开始先初始化cwnd = N,表明可以传N个MSS大小的数据。 每当收到一个ACK,++cwnd; 呈线性上升 每当过了一个RTT,cwnd = cwnd*2; 呈指数让升 还有一个慢启动门限ssthresh(slow start threshold),是一个上限,当cwnd >= ssthresh时,就会进入"拥塞避免算法 - Congestion Avoidance" 根据RFC5681,如果MSS >2190 bytes,则N = 2;如果MSS< 1095 bytes,则N = 4;如果2190 bytes >= MSS >= 1095 bytes,则N = 3;一篇Google的论文《An Argument for Increasing TCP’s Initial Congestion Window》建议把cwnd 初始化成了 10个MSS。Linux 3.0后采用了这篇论文的建议(Linux 内核里面设定了(常量TCP_INIT_CWND),刚开始通信的时候,发送方一次性发送10个数据包,即"发送窗口"的大小为10。然后停下来,等待接收方的确认,再继续发送) 拥塞避免算法 – Congestion Avoidance 慢启动的时候说过,cwnd是指数快速增长的,但是增长是有个门限ssthresh(一般来说大多数的实现ssthresh的值是65535字节)的,到达门限后进入拥塞避免阶段。在进入拥塞避免阶段后,cwnd值变化算法如下: 每收到一个ACK,调整cwnd 为 (cwnd + 1/cwnd) * MSS个字节 每经过一个RTT的时长,cwnd增加1个MSS大小。 TCP是看不到网络的整体状况的,那么TCP认为网络拥塞的主要依据是它重传了报文段。前面我们说过TCP的重传分两种情况: 出现RTO超时,重传数据包。这种情况下,TCP就认为出现拥塞的可能性就很大,于是它反应非常"强烈" 调整门限ssthresh的值为当前cwnd值的1/2。 reset自己的cwnd值为1 然后重新进入慢启动过程。 在RTO超时前,收到3个duplicate ACK进行重传数据包。这种情况下,收到3个冗余ACK后说明确实有中间的分段丢失,然而后面的分段确实到达了接收端,因为这样才会发送冗余ACK,这一般是路由器故障或者轻度拥塞或者其它不太严重的原因引起的,因此此时拥塞窗口缩小的幅度就不能太大,此时进入快速重传。 快速重传 - Fast Retransimit 做的事情有: 调整门限ssthresh的值为当前cwnd值的1/2。 将cwnd值设置为新的ssthresh的值 重新进入拥塞避免阶段。 在快速重传的时候,一般网络只是轻微拥堵,在进入拥塞避免后,cwnd恢复的比较慢。针对这个,“快速恢复”算法被添加进来,当收到3个冗余ACK时,TCP最后的[3]步骤进入的不是拥塞避免阶段,而是快速恢复阶段。 快速恢复算法 – Fast Recovery : 快速恢复的思想是“数据包守恒”原则,即带宽不变的情况下,在网络同一时刻能容纳数据包数量是恒定的。当“老”数据包离开了网络后,就能向网络中发送一个“新”的数据包。既然已经收到了3个冗余ACK,说明有三个数据分段已经到达了接收端,既然三个分段已经离开了网络,那么就是说可以在发送3个分段了。于是只要发送方收到一个冗余的ACK,于是cwnd加1个MSS。快速恢复步骤如下(在进入快速恢复前,cwnd 和 sshthresh已被更新为:sshthresh = cwnd /2,cwnd = sshthresh): 把cwnd设置为ssthresh的值加3,重传Duplicated ACKs指定的数据包 如果再收到 duplicated Acks,那么cwnd = cwnd +1 如果收到新的ACK,而非duplicated Ack,那么将cwnd重新设置为【3】中1)的sshthresh的值。然后进入拥塞避免状态。 细心的同学可能会发现快速恢复有个比较明显的缺陷就是:它依赖于3个冗余ACK,并假定很多情况下,3个冗余的ACK只代表丢失一个包。但是3个冗余ACK也很有可能是丢失了很多个包,快速恢复只是重传了一个包,然后其他丢失的包就只能等待到RTO超时了。超时会导致ssthresh减半,并且退出了Fast Recovery阶段,多个超时会导致TCP传输速率呈级数下降。出现这个问题的主要原因是过早退出了Fast Recovery阶段。为解决这个问题,提出了New Reno算法,该算法是在没有SACK的支持下改进Fast Recovery算法(SACK改变TCP的确认机制,把乱序等信息会全部告诉对方,SACK本身携带的信息就可以使得发送方有足够的信息来知道需要重传哪些包,而不需要重传哪些包),具体改进如下: 发送端收到3个冗余ACK后,重传冗余ACK指示可能丢失的那个包segment1,如果segment1的ACK通告接收端已经收到发送端的全部已经发出的数据的话,那么就是只丢失一个包,如果没有,那么就是有多个包丢失了。 发送端根据segment1的ACK判断出有多个包丢失,那么发送端继续重传窗口内未被ACK的第一个包,直到sliding window内发出去的包全被ACK了,才真正退出Fast Recovery阶段。 我们可以看到,拥塞控制在拥塞避免阶段,cwnd是加性增加的,在判断出现拥塞的时候采取的是指数递减。为什么要这样做呢?这是出于公平性的原则,拥塞窗口的增加受惠的只是自己,而拥塞窗口减少受益的是大家。这种指数递减的方式实现了公平性,一旦出现丢包,那么立即减半退避,可以给其他新建的连接腾出足够的带宽空间,从而保证整个的公平性。 TCP发展到现在,拥塞控制方面的算法很多,请查看《wiki-具体实现算法》,《斐讯面试记录—TCP滑动窗口及拥塞控制》 总的来说TCP是一个有连接的、可靠的、带流量控制和拥塞控制的端到端的协议。TCP的发送端能发多少数据,由发送端的发送窗口决定(当然发送窗口又被接收端的接收窗口、发送端的拥塞窗口限制)的,那么一个TCP连接的传输稳定状态应该体现在发送端的发送窗口的稳定状态上,这样的话,TCP的发送窗口有哪些稳定状态呢? 其实《不为人知的网络编程:浅析TCP协议中的疑难杂症》讲的非常细,而且一遍文章根本总结不了(我也只是搬运工而已,因为所知的太少,都不像笔记了 基础科普类:https://hit-alibaba.github.io/interview/basic/network/HTTP.html 推荐文章: 《跟着动画学习 TCP 三次握手和四次挥手》 《滑动窗口控制流量的原理》 《TCP 协议简介》(阮一峰) 推荐书本:如果你还想了解更多这方面的信息,记得收藏关注本站。
【深入解析TCP/IP协议握手与挥手过程:原理详解及衍生问题探讨】相关文章:
2.米颠拜石
3.王羲之临池学书
8.郑板桥轶事十则
用户评论
感觉这篇博文很详细啊,把tcp/ip协议讲解得通俗易懂。
有16位网友表示赞同!
终于有机会好好学一下TCP/IP的三步握手和四步挥手了!
有12位网友表示赞同!
想要理解网络通信的原理,就应该从TCP/IP开始学习吧!
有16位网友表示赞同!
深谈什么衍生问题?我感觉我的网络常出现问题就没时间深入研究这类问题。
有9位网友表示赞同!
看完文章之后,感觉自己对IP协议有了更清晰的认识了。
有19位网友表示赞同!
这个长文解剖IP太棒了,受益匪浅!
有13位网友表示赞同!
我要好好的看看这篇博文的网络安全部分啊,最近遇到了一些问题。
有15位网友表示赞同!
文章讲解得很透彻,很容易理解。感谢作者的分享!
有10位网友表示赞同!
好久没见过这么详细的文章了,真的很好用。
有14位网友表示赞同!
学习TCP/IP协议很有用,以后在实际开发中可以更深入的了解网络技术呢!学习笔记马上就做完了。
有13位网友表示赞同!
感觉这个主题真是太棒了!我可以参考一下这篇文章来提升我的程序设计水平。
有7位网友表示赞同!
之前对网络通信不太懂,看了这篇博文后,感觉自己进步了不少。
有15位网友表示赞同!
这个长文很适合像我这样初学者学习TCP/IP协议的。
有6位网友表示赞同!
作者分析得很细致,每一个概念都解释得很到位,真棒!
有19位网友表示赞同!
分享这种干货贴太赞了,让我更有动力学习网络技术了。
有9位网友表示赞同!
感谢作者的劳动成果,文章内容很实用。
有17位网友表示赞同!
准备去看这篇文章来巩固一下我的网络编程基础知识。
有7位网友表示赞同!
这个长文解剖IP真是个宝藏啊!我一定会仔细阅读每一部分内容。
有6位网友表示赞同!
对于想要了解网络底层原理的人来说,这篇博文绝对是一份很好的学习资料。
有16位网友表示赞同!