因特网的网络层只提供无连接、不可靠的尽力服务。它可以将分组从一个主机通过因特网传送到另一台主机,可能出现比特错、丢失、重复和错序到达的情形。
传输层建立在网络层之上,为进程之间的数据传输提供服务。传输层可以通过不可靠的因特网在两个进程之间建立一条可靠的逻辑链路,提供字节流传输服务。
因特网的传输层有两个协议UDP和TCP:
下图即为两个端点之间TCP通信的简单示意图:
源主机的TCP进程从上层收集应用进程的数据,并在满足一定条件时发送出去,TCP发送的数据称为分段(Segment)。
TCP头部数据格式如下:
各个字段的信息说明如下:
网络报乱序
的问题;累计确认
,即只有当确认字节之前的所有数据都到达之后才能发送确认,这样就可以用一个数字概括接收到的所有数据。滑动窗口
,用来进行流量控制。指定从被确认的字节算起可以发送多少个字节,窗口大小字段为0是合法的,说明已经接收到了 确认号-1 个字节,但是接收端没有来得及取走数据。TCP协议提供可靠的连接服务,无论哪一方向另一方发送数据之前,都必须先在双方之间建立一条连接。在TCP/IP协议中,连接是通过三次握手进行初始化的,三次握手的过程如下:
前两次握手,客户端进入连接状态,后两次握手,服务器进入连接状态。所以,三次握手之后,一个全双工的连接就建立起来了,之后,客户端和服务器端就可以开始传送数据。
为什么需要三次握手建立连接?
简单来说,为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。
考虑下面一种情况:client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达server。server收到此失效的连接请求报文段后,误认为是client发出的一个新的连接请求。于是就向client发出确认报文段,同意建立连接。
假设不采用“三次握手”,那么只要server发出确认,新的连接就建立了。由于现在client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。但server却以为新的连接已经建立,并一直等待client发来数据。这样,server的很多资源就白白浪费掉了。
采用“三次握手”的办法可以防止上述现象发生。例如刚才那种情况,client不会向server的确认发出确认。server由于收不到确认,就知道client并没有要求建立连接。”
客户端和服务器数据传送完毕后,需要断开TCP连接,断开连接的时候需要进行四次握手。
四次握手的过程如下:
当然,在fin包之前发送出去的数据,如果没有收到对应的ack确认报文,发起端依然会重发这些数据
),但此时发起段还可以接受数据;如果要正确的理解四次分手的原理,还需要了解四次分手过程中的状态变化。
FIN_WAIT_1
: FIN_WAIT_1状态实际上是当SOCKET在ESTABLISHED状态时,它想主动关闭连接,向对方发送了FIN报文,此时该SOCKET即进入到FIN_WAIT_1状态。而当对方回应ACK报文后,则进入到FIN_WAIT_2状态。(主动方)FIN_WAIT_2
:FIN_WAIT_2状态下的SOCKET,表示半连接,也即主动方要求断开连接,得到了被动方的确认,但被动方还有数据要发送,因此主动方还得继续接收。(主动方)TIME_WAIT
: 表示收到了对方的FIN报文,并发送出了ACK报文,就等2MSL后即可回到CLOSED状态了。如果FINWAIT1状态下,收到了对方同时带FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。(主动方)CLOSE_WAIT
:在CLOSE_WAIT状态下,被动方还有数据需要传送。(被动方)LAST_ACK
: 被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,也即可以进入到CLOSED可用状态了。(被动方)CLOSED
: 表示连接中断。为什么要四次握手断开连接?
TCP是全双工模式,这就意味着,当主机1发出FIN报文段时,只是表示主机1已经没有数据要发送了,主机1告诉主机2,它的数据已经全部发送完毕了;但是,这个时候主机1还是可以接受来自主机2的数据;当主机2返回ACK报文段时,表示它已经知道主机1没有数据发送了,但是主机2还是可以发送数据到主机1的;当主机2也发送了FIN报文段时,这个时候就表示主机2也没有数据要发送了,就会告诉主机1,我也没有数据要发送了,之后彼此就会中断这次TCP连接。
TIME_WAIT 状态存在的理由:
可靠地实现TCP全双工连接的终止。
在进行关闭连接四次握手协议时,最后的ACK是由主动关闭端发出的,如果这个最终的ACK丢失,被动关闭方将重发最终的FIN,主动关闭端只有在维护状态信息的情况下才可以重新发送最终的那个ACK。如果不维护这个状态信息,主动关闭端将会响应一个RST,对端会将此响应标记为错误,所以不能进行正常的关闭。
允许老的重复分节在网络中消逝。
假设TCP协议中不存在TIME_WAIT状态的限制,再假设当前有一条TCP连接:(local_ip, local_port, remote_ip,remote_port),因某些原因,先关闭,接着很快以相同的四元组建立一条新连接。TCP协议栈是无法区分前后两条TCP连接的不同的,在它看来,这根本就是同一条连接,中间先释放再建立的过程对其来说是“感知”不到的。这样就可能发生这样的情况:前一条TCP连接由local peer发送的数据到达remote peer后,会被该remot peer的TCP传输层当做当前TCP连接的正常数据接收并向上传递至应用层(而事实上,在我们假设的场景下,这些旧数据到达remote peer前,旧连接已断开且一条由相同四元组构成的新TCP连接已建立,因此,这些旧数据是不应该被向上传递至应用层的),从而引起数据错乱进而导致各种无法预知的诡异现象。
local peer主动调用close后,此时的TCP连接进入TIME_WAIT状态,处于该状态下的TCP连接不能立即以同样的四元组建立新连接,即发起active close的那方占用的local port在TIME_WAIT期间不能再被重新分配。由于TIME_WAIT状态持续时间为2MSL,这样保证了旧TCP连接双工链路中的旧数据包均因过期(超过MSL)而消失,此后,就可以用相同的四元组建立一条新连接而不会发生前后两次连接数据错乱的情况。
参考 再叙TIME_WAIT
TCP要保证所有的数据包都可以到达,所以,必需要有重传机制。
注意,接收端给发送端的Ack确认只会确认最后一个连续的包,比如,发送端发了1,2,3,4,5一共五份数据,接收端收到了1,2,于是回ack 3,然后收到了4(注意此时3没收到),此时的TCP会怎么办?我们要知道,SeqNum和Ack是以字节数为单位,所以ack的时候,不能跳着确认,只能确认最大的连续收到的包,不然,发送端就以为之前的都收到了。
超时重传机制
每次发送数据包时,发送的数据报都有seq号,接收端收到数据后,会回复ack进行确认,表示某一seq号数据已经收到。发送方在发送了某个seq包后,等待一段时间,如果没有收到对应的ack回复,就会认为报文丢失,会重传这个数据包。
针对上面的情况,接收端不回ack,死等3,当发送方发现收不到3的ack超时后,会重传3。一旦接收方收到3后,会ack 回 4——意味着3和4都收到了。但是,这种方式会有比较严重的问题,那就是因为要死等3,所以会导致4和5即便已经收到了,而发送方也完全不知道发生了什么事,因为没有收到Ack,所以,发送方可能会悲观地认为也丢了,所以有可能也会导致4和5的重传。
快速重传机制
接收数据一方发现有数据包丢掉了。就会发送ack报文告诉发送端重传丢失的报文。如果发送端连续收到标号相同的ack包,则会触发客户端的快速重传。比较超时重传和快速重传,可以发现超时重传是发送端在傻等超时,然后触发重传;而快速重传则是接收端主动告诉发送端数据没收到,然后触发发送端重传。
比如:如果发送方发出了1,2,3,4,5份数据,第一份先到送了,于是就ack回2,结果2因为某些原因没收到,3到达了,于是还是ack回2,后面的4和5都到了,但是还是ack回2,因为2还是没有收到,于是发送端收到了三个ack=2的确认,知道了2还没有到,于是就马上重转2。然后,接收端收到了2,此时因为3,4,5都收到了,于是ack回6。
TCP头里有一个字段叫Window,又叫Advertised-Window,这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。滑动窗可以是提高TCP传输效率的一种机制。要注意滑动窗口只关注发送端和接收端自身的状况,而没有考虑整个网络的通信情况
。
为了说明滑动窗口,我们需要先看一下TCP缓冲区的一些数据结构:
上图中,我们可以看到:
于是:
下面我们来看一下发送方的滑动窗口示意图:
上图中分成了四个部分,分别是:(其中那个黑模型就是滑动窗口)
下面是个滑动后的示意图(收到36的ack,并发出了46-51的字节):
下面我们来看一个接收端控制发送端的图示:
上图可以看到一个处理缓慢的Server(接收端)是怎么把Client(发送端)的TCP Sliding Window给降成0的。如果Window变成0了,发送端就不发数据了,可以想像成“Window Closed”。(有两种意外情形,第一紧急数据仍可以发送,比如用户杀掉远程机器上运行的某一个进程。第二,发送段可以发送一个用来进行窗口探测的段,下面详细介绍)
Window size 变为0之后,为了防止服务器发来的窗口更新数据包丢失后发生死锁。TCP使用了窗口探测
(Zero Window Probe)技术,缩写为ZWP,也就是说发送端在窗口变成0后会发送一个1字节的段给接收方,以便强制接收端重新宣告下一个期望的字节和窗口大小。一般会尝试发送3次,如果3次过后还是0的话,有的TCP实现就会发送RST把连接断开。
此外,发送端不一定接到应用程序传递来的数据就马上把数据传送出去,同样,接收端也不一定必须尽可能快的发送确认段。特别是遇到下面这两种极端情况:
考虑下面的场景:
Nagle 算法
避免发送端发送多个小数据包,减轻发送端给网络的负载。(Nagle 算法:数据每次以很少量方式进入到发送端时,发送端只发送第一次到达的数据字节,然后将后面到达的缓存起来,直到发送出去的那个数据包被确认,然后将所有缓冲的字节放在一个 TCP 段中发送出去。并且继续开始缓冲,直到下一个端被确认。)不适用的场景:互动游戏,需要快速的短数据包流。延迟确认
的优化方法可以避免接收端发送只有一个字节的窗口更新端。Clark解决方案:禁止接收端发送只有1个字节的窗口更新端,强制必须等一段时间,直到有了一定数量的可用空间之后再通知给对方。TCP通过滑动窗口来做流量控制,但是这还不够,因为滑动窗口仅依赖于连接的发送端和接收端,其并不知道网络中间发生了什么。TCP的设计者觉得,一个伟大而牛逼的协议仅仅做到流量控制并不够,因为流量控制只是网络模型4层以上的事,TCP的还应该更聪明地知道整个网络上的事。
考虑一下这样的场景:某一时刻网络上的延时突然增加,那么,TCP对这个事做出的应对只有重传数据,但是,重传会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,于是,这个情况就会进入恶性循环被不断地放大。试想一下,如果一个网络内有成千上万的TCP连接都这么行事,那么马上就会形成“网络风暴”
,TCP这个协议就会拖垮整个网络。
所以,TCP不能忽略网络上发生的事情,而无脑地一个劲地重发数据,对网络造成更大的伤害。对此TCP的设计理念是:TCP不是一个自私的协议,当拥塞发生的时候,要做自我牺牲。就像交通阻塞一样,每个车都应该把路让出来,而不要再去抢路了。
拥塞控制主要是四个算法(相应的论文):1)慢启动,2)拥塞避免,3)拥塞发生,4)快速恢复。 这四个算法不是一天都搞出来的,它们的发展经历了很多时间,到今天都还在优化中。
慢启动的意思是,刚刚加入网络的连接,一点一点地提速。慢启动的算法如下(cwnd全称Congestion Window):
所以,我们可以看到,如果网速很快的话,ACK也会返回得快,RTT
(Round Trip Time,也就是一个数据包从发出去到回来的时间)也会短,那么,这个慢启动就一点也不慢。下图说明了这个过程。
ssthresh(slow start threshold)是一个上限,当cwnd >= ssthresh时,就会进入“拥塞避免算法”。一般来说ssthresh的值是65535,单位是字节,当cwnd达到这个值时后,算法如下:
收到一个ACK时,cwnd = cwnd + 1/cwnd,这样当每过一个RTT时,cwnd = cwnd + 1
这样就可以避免增长过快导致网络拥塞,慢慢的增加调整到网络的最佳值。很明显,是一个线性上升的算法。
前面我们说过,当丢包的时候,会有两种情况:
1)等到RTO超时,重传数据包。TCP认为这种情况太糟糕,反应也很强烈。
2)快速重传,也就是在收到3个duplicate ACK时就开启重传,而不用等到RTO超时。
快速恢复算法是认为,还有3个Duplicated Acks说明网络也不那么糟糕,所以没有必要像RTO超时那么强烈。注意,正如前面所说,进入Fast Recovery之前,cwnd 和 sshthresh已被更新:
cwnd = cwnd /2
ssthresh = cwnd
然后,真正的Fast Recovery算法如下:
上面这个算法也有问题,那就是——它依赖于3个重复的Acks。注意,3个重复的Acks并不代表只丢了一个数据包,很有可能是丢了好多包。但这个算法只会重传一个,而剩下的那些包只能等到RTO超时,于是,进入了恶梦模式——超时一个窗口就减半一下,多个超时会超成TCP的传输速度呈级数下降,而且也不会触发Fast Recovery算法了。1995年,提出了 TCP New Reno 算法,避免这个问题。
图解TCP-IP协议
简析TCP的三次握手与四次分手
TCP 的那些事儿(上)
TCP 的那些事儿(下)
TCP keepalive overview
Detection of Half-Open (Dropped) Connections