传输层有两大协议:TCP和UDP。主要掌握TCP,这也是面试的高频考点。
TCP和UDP的区别如下:
UDP:用户数据报协议。它是无连接
的,所以减少了建立连接和释放连接(三次握手、四次挥手)
的开销。UDP是尽最大能力进行交付,它不会保证可靠性,换句话说:它会一直传输,但是它并不保证这个数据能否成功传输到目的设备。
所以UDP的首部字段比较简单,如下:
源端口号、目的端口号,这都没什么好说的。指的是这个数据来自(去往)系统的某个端口。
UDP长度:指的是这个数据段的长度,也就是首部 + 数据部分。单位字节。
UDP校验和:为了保证这个数据段到达目的地后,数据是完好的。计算内容是:伪首部+首部+数据。
什么是TCP协议?
TCP是面向连接的、可靠的、基于字节流的传输层通信协议。
序列号:在建立连接时,由计算机生成的随机数作为初始值,通过SYN包传递给接收端,每当传递一次数据,序列号就会累加本次数据的长度(数据字节数)。在建立连接后,表示这一次传给接收端的TCP数据部分的第一个字节的编号。目的是为了解决网络包乱序问题
。
确认应答号:指下一次期望收到的数据的序列号,发送端收到这个确认应答号之后,就可以认为这个序号之前的所有数据都已经接收到了。目的是为了解决不丢包问题
。
控制位:有6个。但是主要掌握ACK、RST、SYN、FIN。
ACK=1时,确认应答号这个字段才有效
。TCP规定除了最初建立连接时的SYN包之外,该位必须设置为1。希望建立连接
,并在其序列号字段设置初始值。希望断开连接
,即后续不会再有数据发送。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换FIN位为1的TCP段。窗口:占2个字节。这个字段有流量控制功能,用于告诉对方下一次允许一次性发送的数据大小。
首部长度:占4位。范围在 0x0101 ~ 0x1111。在此基础之上 * 4,就是真正的首部长度。所以真正的首部长度是20字节 ~ 60字节。
校验和:和UDP的校验和是一样的。都是通过 伪首部+首部+数据部分 进行计算的。伪首部占12字节,仅在计算校验和时起到作用,也不会传递到网络层。
TCP实现可靠传输的方式之一,是通过序列号与确认应答。在TCP中,当发送端的数据达到接收端后,接收端会返回一个确认应答消息,表示已经收到数据了。
当然整个网络错综复杂,难免有时会出现丢包的情况,所以TCP中有一个重传机制,它会自动重传丢失的数据。
还有一种超时重传的可能性,如下图:
滑动窗口
产生窗口的原因?
在如上图所示,TCP是每发送一个数据,都要进行一次确认应答。当上一个数据包收到之后,接收端返回确认应答之后,发送端才会发送下一个数据。
很显然,这样传递数据,效率太低了。假设数据包往返的时间越长,通信的效率就越低。
为了解决这个问题,所以才引入了窗口的概念。即使在往返时间比较长的情况下,也不会减低网络通信的效率。
窗口的大小指的是 无需等待确认应答,而可以继续发送数据的最大值。
窗口的实现就是在操作系统上开辟的一块缓存空间,发送方主机在等到确认应答之前,必须在缓冲区中保留已经发送的数据。后续如果接收到相应的确认应答,才会将刚发送的数据从缓冲区清除。
TCP首部中有一个字段:窗口,也就是窗口大小。
这个字段是接收端告诉发送端自己还有多少缓冲区空间可以用来接收数据。于是发送端就可以根据这个接收端的处理能力(窗口)来发送数据,而不会导致接收端处理不过来的问题。
发送方的滑动窗口
如上图,在发送完1、2、3这几个数据段之后,接收方会返回确认应答,并且还会返回窗口的大小。
此时发送方看到接收方已经收到了前面的数据,它的窗口就会往后面滑动。这就是滑动窗口的意思。
切记,窗口大小的单位是字节。上图只是举例子的形式,表示窗口的大小。比如 窗口大小=4000,表示接收方可以一次性接收4000字节的数据,而4000字节,到底可以分为几个数据段,我们后面再说。
整个滑动窗口+自动重传的流程如下:
重传机制-SACK
SACK(Selective Acknowledgement)即 选择性确认,作用是 使TCP只重传丢失的包,而不是重传后续所有的包
。
举个例子:
如上图,确认应答回复的是 期望下一次数据包发送2,也就是说 2之前的数据包都已经接收到了。2、3、4数据包有可能都丢失了。那么发送端就要重新发送2、3、4数据包。
很明显,3、4数据包已经接收到了,再重传过来也没啥作用。所以只需让发送端重传2数据包即可。
那么如何通知发送方只重传2数据包呢? 就是SACK起作用了。
SACK是存储在TCP首部的选项字段中。如上图所示,
下图来自“小林coding”。
发送方不能无脑的一直发送数据,还要考虑到接收方的情况,要考虑接收方的缓冲区是否还能存储。
如果接收方的缓冲区已经满了,发送方还在继续发送数据,此时当数据来到接收方时,并不会被存储到缓冲区,而是直接丢掉,然后就会触发重传机制,浪费带宽资源。
为了避免这种现象发生,TCP提供了一种机制可以让发送方根据接收方的实际接收能力来控制发送的数据量,这就是常听说的流量控制。通过确认报文中的窗口字段来控制发送方的发送速率,发送方的发送窗口大小不能超过接收方给出的窗口大小。当发送方收到接收窗口的大小为0时,发送方就会停止发送数据。
如上图,当接收方的缓冲区满了之后,就会向发送方返回一个窗口大小=0的消息,发送方看到接收方的缓冲区满了后,就会暂时停止发送数据。
当接收方的缓冲区有剩余空间时,就会发送一个ACK报文给发送方,告诉发送方我又有缓冲空间啦。但是存在一个问题,假设这个给发送方的ACK报文丢了怎么办?会出现什么问题?如下图:
当新的窗口大小的这个包丢了之后,二者就会陷入死锁状态,在哪里一直等待。
如何解决死锁的问题呢?
为了解决这个问题,TCP为每个连接都设有一个持续定时器,只要TCP连接一方收到对方的0窗口通知时,就会启动持续定时器。
如果持续定时器的时间到点了,就会发送一个窗口探测报文,而对方在接收到这个报文后,就会返回自己当前的窗口大小。
下图来自“小林coding”。
当然,如果窗口探测之后,窗口大小还是0,那就会重新启动持续计时器。
窗口探测一般为3次,每次大约30秒 ~ 60秒(不同的实现可能不一样)。如果3次过后,接收窗口大小还是0,有的TCP实现就会发送RST报文来断开连接。
如果接收方太忙了,根本来不及处理缓冲区的数据,导致缓存区的数据囤积的越来越多,使缓存区的空间越来越小,最后使其占满(即窗口大小=0)。
如果在窗口大小=0后,接收方处理了几个字节的数据,使窗口大小稍微变大一点点。而发送方看见接收方还有一点点缓冲区空间,就发送一点数据过来,可能这个数据才几个字节的长度。
存在的问题就是:TCP+IP的首部就有40字节
,而数据部分才几个字节,这买卖属实是不划算,要用这么多的开销,获得最低的效益。就好比路边拉乘客的私家车,明明可以乘坐8个人,结果每次只乘坐1个人就开车走了,这1个人的车费还不如一去一来的油费呢。
所以为了解决这个问题,从两个方向出发:
从接收方着手,让接收方不通告小窗口
,当窗口大小 < min(MSS, 缓存空间/2)时,就会向发送方返回 窗口大小=0
的报文。只有当接收方的窗口大小 >= MSS 或者 >= 缓存空间/2时,才把窗口打开,让发送方发送数据过来。
从发送方着手,发送方通常的策略是 使用Nagle算法,该算法的思路是延时处理,它满足以下两个条件的其中一个时,就可以发送数据:
只要上述两个条件都没满足,发送方就会一直囤积数据,直到满足上的发送条件即可。
另外, Nagle 算法默认是打开的,如果对于⼀些需要⼩数据包交互的场景的程序,⽐如, telnet 或 ssh 这样的交互性⽐较强的程序,则需要关闭 Nagle 算法。
可能你会不理解,前面不是已经有了流量控制吗?怎么还有拥塞控制呢?
因为二者所解决的问题是不一样的。前面说了流量控制是防止接收端的缓冲区空间不够用的情况;而拥塞控制解决的是网络带宽不够用的情况。
一般来说,计算机网络都处在一个共享的环境。因此也有可能会因为其他主机之间的通信使得网络拥堵。在网络出现拥堵时,如果继续发送大量的数据包,可能会导致很多的数据包出现延迟、丢包等状况,进而可能就会触发TCP超时重传机制,但一旦又开始重传,就又导致网络的负担增加,可能就会导致更多的数据包延迟、丢失,进入恶性循环。
所以TCP协议就会自己调整,当网络拥堵的时候,就尽量少发送点数据包。于是就有了拥塞控制,目的就是为了避免发送方的数据填满整个网络
。上文为了控制发送方的发送数据量,增加了发送窗口swnd
和接收窗口rwnd
,现在又增加一个拥塞窗口cwnd
;拥塞窗口是发送方维护的一个状态变量,它可以根据网络拥塞程度来变化。
所以 发送窗口swnd = min(接收窗口rwnd, 拥塞窗口cwnd)
。
拥塞窗口的变化规则就是 只要网络中没有出现拥塞,窗口就会增大,反之就会减小。
那么如何判断网络是否出现拥塞?
只要发送方没有在规定时间内接收到 ACK确认应答报文,换句话说就是触发了超时重传,就会认为网络出现了拥塞。
拥塞控制有四个算法
在TCP刚建立连接后,首先是慢启动阶段,字面意思就是刚开始发送数据时,是一个比较缓慢的过程。
慢启动算法记住一个规则:每收到一个ACK确认应答报文,拥塞窗口cwnd的大小就+1。
就如下图所示,来自《无名之辈》桥段,告诉我们起步要慢。
慢启动趋势图如下:来自“小林coding”。
如图,慢启动阶段的拥塞窗口cwnd是指数级增长
。
当然,这个增长是有一个上限的,上限称为慢启动门限 ssthresh
(slow start threshold)状态变量。一般情况下,ssthresh的大小是65535字节。
当拥塞窗口cwnd < ssthresh时,就是处于慢启动阶段;当拥塞窗口 >= ssthresh时,就是处于拥塞避免阶段。
当进入拥塞避免阶段,拥塞窗口cwnd的增长就会稍微变得缓慢一点。增长规则是每收到一个ACK确认应答报文,cwnd 就会增加1/cwnd。
举个例子:假设现在发送方一次性发出去 8个数据段,理应来说将会收到8个ACK确认应答报文(但只会收到一个ACK确认应答报文,就是上文提交到的 自动重传+滑动窗口部分提及的),所以cwnd = cwnd + 8 * 1/8 = cwnd + 1。
下图所示,假设慢启动门限 ssthresh = 8。
所以拥塞避免阶段,就是从慢启动阶段的指数级增长变为了线性增长,增长速度变慢了。就这样一直增长下去,就会进去拥塞发送阶段。
来到拥塞发生阶段,就说明已经有数据包丢失或者数据包延迟抵达。就会触发重传机制,这里的重传机制有两种:
当发生超时重传,就会使用拥塞发生算法。ssthresh和cwnd都会发生变化。
ssthresh = cwnd / 2
下图来自“小林coding”。
如上图,cwnd就会重新从1开始,直接从拥塞避免阶段 -> 慢启动阶段
。这种大起大落的情况,反应太过激烈,会造成网络卡顿。所以还有第2种拥塞发生情况,即快重传。
快重传
当接收方发现丢了其中某一个包,就会发送三次这丢失的这个包的ACK,于是发送端就会快速重传这个丢失的包,就不必等待超时重传。
这种情况的拥塞发送,ssthresh和cwnd的变化如下:
cwnd = cwnd / 2
ssthresh = cwnd
当使用快重传的拥塞发送,就会进入下一个阶段:快速恢复。
快速重传和快速恢复一般都是同时搭配使用的,快速恢复算法认为,还能收到3个重复的ACK报文时,至少说明此时的网络还不算太拥堵。在上文说过,进入快速恢复时,慢开始门限ssthresh
和拥塞窗口cwnd
已经发生变化了。
cwnd = cwnd / 2
ssthresh = cwnd
快速恢复的具体流程如下:
cwnd = ssthresh + 3
.(+3的原因是已经确认收到那3个重复的ACK报文)cwnd = 第一步的慢启动门限ssthresh
,原因是该ACK确认了新的数据,即该恢复过程已经结束,可以回到恢复之前的状态,也就是说可以再次进入拥塞避免状态。好处就是,直接就进入了拥塞避免状态,不再是从慢启动状态开始。
TCP协议是面向连接的,所以在传输数据之前,首先需要建立连接。建立连接过程如下:
过程:
SYN标志=1
。然后TCP首部字段的序号是系统随机生成的。这样的一个SYN报文就称为建立连接请求。注意:这3次握手中,前2次握手是不包含任何应用数据的,这前2次握手只是单纯为了建立连接,且前2次握手的TCP首部的长度一般在32字节,这里面包含了缓存区窗口大小、MSS(Maximum Segment Size)、是否支持SACK、Window scale(窗口缩放系数)等等。但是在第3次握手时,是可以携带应用数据的,这也是面试常问的话题。
Window scale(窗口缩放系数):在TCP首部字段中,有窗口大小字段(16位),这个字段里面的数据并不是真实的缓存区窗口的大小,真实的缓冲区窗口大小 = 窗口大小字段值 * Window scale。
为什么是3次握手?为什么不是2次或者4次握手?
从3个方面说:
情况一:
3次握手的主要原因就是为防止旧的重复连接初始化造成混乱。就如上图,第1次SYN报文超时了,导致客户端发送了第2次SYN报文。此时应该以第2次SYN报文来建立连接。
所以这第3次握手,有两种情况:如果返回的第2次握手是历史连接,就发送RST进行中止连接;如果返回的第2次握手不是历史连接,就返回ACK报文建立连接。
情况二:
TCP 协议的通信双⽅, 都必须维护⼀个序列号, 序列号是可靠传输的⼀个关键因素,它的作⽤:
当客户端发送携带初始序列号的 SYN 报⽂的时候,需要服务端回⼀个 ACK 应答报⽂,表示客户端的 SYN 报⽂已被服务端成功接收,那当服务端发送初始序列号给客户端的时候,依然也要得到客户端的应答回应, 这样⼀来⼀回,才能确保双⽅的初始序列号能被可靠的同步 。
而两次握手,只能保证其中一方的初始序列号被接受,并不能保证另一方的初始序列号也能被接受。
情况三:
如果只有2次握手,假设网络有点拥堵,导致客户端发送了多个SYN报文来建立连接,此时服务器就会返回ACK表示同意建立连接。且返回ACK后就直接进入ESTABLISHED状态;后续再次接到客户端发送的重复的SYN报文时,也是会进行建立连接的,此时就建立了多个冗余的TCP连接,造成资源浪费。
即两次握⼿会造成消息滞留情况下,服务器重复接受⽆⽤的连接请求 SYN 报⽂,⽽造成重复分配资源。
综上所诉,通过3次握手能防止历史连接的建立、减少双方不必要的资源开销、能帮助双方同步初始序列号。
双方都可以主动断开连接,断开连接后主机中的资源将会被释放。
过程:
此时只是客户端想断开连接,服务器还是可以向客户端发送数据。
此时只是客户端不能发送数据,但还是能接收来自服务器的数据
。注意:所谓的4次挥手,指的就是双方都会发送一个FIN、一个ACK。主动提起关闭连接的一方才有TIME-WAIT状态。
为什么挥手需要4次?不可以是3次吗?
答:假设客户端提起关闭连接,在前2次挥手(FIN+ACK)之后,只能说是客户端不再向服务器发送任何应用数据;但在服务器发送第2次挥手时,此时的服务器可能还有一些应用数据需要返回给客户端,所以导致这里的第2次挥手(ACK)和第3次挥手(FIN)不能合并在一起。所以一般情况下,第2次和第3次挥手是分开的,就成了4次挥手。
是有可能变成3次挥手的,只要在服务器发送第2次挥手时,服务器后续不会再发送任何应用数据,就有可能将第2次和第3次挥手合并在一起。
为什么TIME-WAIT等待的时间是2MSL?
答:MSL(Maximum Segment Lifetime)报文最大生存时间
。它指的是任何报文在网络中存在的最长时间,超过这个时间的报文就会被丢弃。前面的文章提到过网络层的TTL(存活时间),这二者是不一样的。一个是在网络层,一个是在传输层。且MSL的单位是时间,TTL的单位是路由器数量。所以MSL应该要大于等于TTL消耗为0的时间,以确保报文已被自然消亡。
2倍的MSL的原因是:网络中的数据包进行传递时,来自发送方的数据,被接收方收到后会返回一个ACK,这样一去一来就是2倍的MSL。
设置这个等待时间的原因:在客户端发送最后一个ACK报文后,客户端是需要保证这个ACK能够到达服务器。如果在一定的时间内,服务器没有收到最后一个ACK报文,它会重发FIN报文来断开连接,而客户端在发送ACK报文后,需要做的就是等。在2MSL时间内,没有收到来自服务器的FIN报文,就说明发送的ACK报文已经到达了。也就是要保证被动关闭的一方能够正确地关闭连接。
在Linux系统中的2MSL默认是60秒,即一个MSL是30秒。也就是说Linux系统停留在TIME-WAIT的时间是固定的60秒。
如果想更改这个值,那就需要改动Linux内核里面的参数,并重新编译Linux内核。
如果被动关闭的一方还没有收到最后一个ACK,但主动关闭的一方已经进入CLOSE状态,会发送什么?
答:这种情况下,被动关闭一方会发送多次FIN报文(第3次挥手),在进行多次尝试后,还没有结果,就会发送RST报文,强制断开连接。
如果已经建⽴了连接,但是客户端突然出现故障了怎么办?(长连接、短连接)
答:TCP连接中,有长连接和短连接的概念。所谓的短连接就是每当发送数据时,才会建立连接,数据发送完之后就会立刻断开连接;长连接则相反。
在长连接中,可能很长一段时间,双方都没有进行交互数据,此时并不知道这个TCP连接是否还存活(即有用)。TCP就有一个机制:保活机制。
它定义了一个时间段,在这个时间段了,双方没有进行连接相关的活动,TCP保活机制就会每隔一段时间就发送一个探测报文(俗称心跳包),当另一方收到这个探测报文就会作出回应,表示这个TCP连接还是存活的。如果连续几个探测报文都没有响应,则认为这个TCP连接已经消亡,系统内核会将错误信息传递给上层应用程序。
在Linux内核中都有相应的参数来设置保活时间、探测报文次数、探测报文时间间隔:
7200的单位是秒,即表示保活时间是2个小时。在2个小时之内,没有进行任何连接相关的活动,就会触发保活机制。
75的单位是秒,指的是每个75秒发送一个探测报文。
9指的是一共会进行9次探测报文。
既然 IP 层会分⽚,为什么 TCP 层还需要 MSS 呢?
在前面的数据链路层提到过MTU的概念,也在网络层说过IP分片的问题。
MSS是在TCP建立连接的时候,双方都会告诉对方自己的MSS是多大,一般都是1200~1400字节左右。
可能会有人疑惑,既然网络层也有分片功能,为啥传输层现在又有分片功能?
答:二者是有一定的目的的。在使用UDP协议时,它在传输层就不会进行分段,在网络层时,当数据部分太大时,才会进行分片。分片的过程:假设现在A数据包有4000字节,它在网络层分片,可能就是会分为3~4片左右;而分出来的片中,只有第一片有TCP首部,其余的片并没有TCP首部,而是只加了IP首部。当数据传递到对方的网络层时,才会合并数据,并传递到上一层传输层。
这看起来是没有什么问题。但是 如果这个UDP协议是自己更改为具有可靠传输功能的,那么当其中某一片数据丢包时,此时发送端就会超时重传,而重传数据就只能重传分片以前的整个数据包,此时就出现了传输的数据重复,浪费了带宽资源。
所以,才有了传输层就分段的功能,作用是在传输层分段,这样分出来的每一个段都是有TCP首部的,分出来的每一段的长度是小于MTU的,所以在网络层不会再分片。此时如果丢了其中一段,那么在重传时,就只传丢失的这一段即可。
好啦,本期更新就到此结束啦。
以上内容部分参考“小林coding”和“小码哥教育”。