TCP/IP协议到底在讲什么?

TCP、IP

某一天,你和你的同事正在聊天,不知道你是否思考过,你们的电脑上装了很多软件…等等,为什么你通过聊天软件发的消息会正确的发送到对方聊天软件,而不是发送到其他应用软件上/p>

同时再说夸大一点,为什么你发送的消息会发送你同事的电脑上,而不是隔壁老王的电脑上,这么问题看起来有点傻,其实这些离不开我们今天要说的TCP、IP协议。

首先IP大家肯定都能明白,每个电脑都有一个IP,这也是为什么我们的信息可以精准的发给我们的同事而不是隔壁老王,因为我们知道同事电脑的IP,这就是IP层干的的事。

3f9fd564ac7d43ad98add5039160b23e.png
其实TCP层不仅仅会加上目标的端口号,还会加上发送者的端口号,IP层不仅仅会加上目标IP,还会加上发送者的IP,发现没,这就是我们常说的socket四元组:发送端IP+发送端端口+接收端IP+接收端端口。一个socket四元组就可以确定一个连接。 

我们常说TCP协议是一种基于字节流、面向连接的、可靠的传输层通信协议,这里我们需要思考定义中的三个抽象描述:

基于字节流是什么意思br> 什么叫做面向连接的br> 如何算可靠br> 基于字节流的

我们先说第一个问题,TCP 协议是基于字节流传输的,这是什么意思呢个例子,其实当我们往 socket 中写入1000个字节的时候,会分很多情况的,这时1000个字节会被 copy 到内核缓冲区的,但是1000个字节具体是怎么通过网卡发出去是不确定的,有可能一次性发出去,也可能分成2次,分别是300、700,也有可能是500、500,但是不管怎么分,每个字节都有自己的序号。

61542219dcfa4134a48bb42a04607b4b.png
三次握手 

再说第二个问题:面向连接。对没错,我们还是要说说老掉牙的问题:三次握手、四次挥手。三次握手、四次挥手其实也是一种可靠性的表现。因为需要可靠,所以在建立连接的时候需要先确认双方是否都ok,也就是三次握手。我们先看看三次握手干了什么时我们看看为什么三次就行了,两次或者十次行不行/p>

efa7978fd266446e898c545730b19e02.png
为了方便描述,这里定义下主动断开方叫「A」,被动断开方叫做「B」。 

第一次:A突然想要关闭连接,不想玩了,于是它会发送一个FIN包,这个FIN包和上面的SYN包是对立的,说明此刻A想要断开连接,同时FIN包也是需要对端确认的,所以FIN包是需要消耗一个序列号的,发送完FIN包后,A处于FIN_WAIT1状态。

第二次:B收到对端的FIN包之后,心想:“这小子是不想玩耍了呀,不想玩就算了”,于是B会对FIN包做出个回应也就是ack,意思是:“对面的,我知道了”,同时自己处于CLOSE_WAIT状态,A收到对端的ack之后同时自己处于FIN_WAIT2状态。

第三次:B在发送ack之后,如果还有未处理完的数据,需要接着把未发送完的数据发给对端,当数据发完之后,其实也就是和A没啥关系了,于是B也会发个FIN包,意思是“对面的,数据都发完了,你就断开吧”,此时B会处于LAST_ACK状态。

第四次:还是一样的,FIN包需要确认,因此A再收到FIN包后,会立马回复一个ACK,那此时对端就会断开连接处于CLOSED状态,同时A处于TIME_WAIT状态,也就是2MSL之后自动断开。

能不能三次挥手/p>

看流程四次挥手绝对没问题,那问题来了,三次行不行实某些情况下三次也是可以的,比如被动断开方没有要处理的数据也就不存在DATA那一部分,那其实ACK和FIN一起发过去问题也是没问题的,如果存在DATA,非要把ACK+DATA+FIN合并在一起发过去会发生什么呢/p>

首先处理DATA需要时间,那么为了等DATA处理完再发ACK,可能会导致主动断开方因为迟迟没收到ack,而重发FIN包。

为啥最后一步主动断开方需要处于TIME_WAIT状态,这个状态代表什么/p>

TIME_WAIT是主动关闭方最后进入的一种状态,TIME_WAIT是2MSL的,MSL是报文最大的生命周期,正常来说一个数据包如果在网络中超过MSL之后还没被对端收到就会被丢弃,那为什么主动断开方需要2MSL呢/p>

一个MSL主要是保证最后一次 ACK 能到达对端,如果 1MSL 后,ACK 还没到达对端会怎么办br> 如果主动断开方的 ACK 没到达对端,这时候会触发被断开方重传 FIN,那么另一个 MSL 就是保证重传的 FIN 包也能到达对端。

2a90459636e84ccebf99b1bbe70194c4.png
假设现在发送端的序列号是100,而且发送了100字节的数据,按道理接下来的ack应该是200(199+1)。 

690c04eaefe740928f6bb1cf9485230a.png 

这是站在发送端的角度来看数据包的状态的,其中的滑动窗口部分可以看作是发送端的滑动窗口,对于已发送已确认的部分,算是过去时了,它只会使滑动窗口向右移,真正影响滑动窗口大小的是「已发送未确认」和「未发送可发送」部分,剩下的「不能发送」是因为接收端没有足够的空间了。 我们再来站在接收端角度看看滑动窗口是什么样的。

c0fb4fcb14944d679156a925d91f512c.png 

滑动窗口很棒,可以在能力范围内处理数据,但是有个问题呀:如果发送端能力极强,发的很快,接收端能力极弱,处理的很慢,这会导致什么问题/p>

某一刻滑动窗口为0了,这时候接收端就会告诉发送端:”你奶奶的,消停会吧,没空间了”。发送端收到了通知之后:”原来是个弱鸡,休息会吧,等它下次ack通知我吧”,正常来说,接收端在处理完数据之后可以告诉发送端可以继续发数据了,然而意外出现了,由于接收端所在的主机的主人正在听着音乐、玩着游戏,同时还欣赏着某站舞蹈区up主娥罗多姿的舞蹈,导致网卡压力很大,最后一个ack丢失了,这样发送端就不知道接收端其实已经处理了一部分数据,这可怎么办,如果一直丢失,岂不是要一直傻等,得主动出击呀,于是搞了个「零窗口探测定时器」,这个定时器的功能相信大家也知道了,就是当接收方的接收窗口为0时,每隔一段时间,发送方会主动发送探测包,通过迫使对端响应来得知其接收窗口的状态,不得不说零窗口探测够稳。

悠着点慢慢来-拥塞控制

上面我们说到滑动窗口可以合理的控制接收端能处理数据的量,注意这里说的只是量,如果网络状况很差,发送端一次性发了很多数据,并且窗口未被填满(此处的意思就是和窗口的大小没关系),这时会发生什么/p>

我想大概率是发送端疯狂的重传(因为网络差,未收到ack),那么我们反过来想一想网络状态差,我们还有必要发送大量的数据过去吗,大量的重试不是给发送端自己找麻烦,因此需要悠着点,这里的悠着点说的就是「拥塞窗口(cwnd)」,拥塞窗口指的是在收到对端 ACK 之前自己还能传输的最大 MSS 段数,那它和之前说的发送窗口有什么关系/p>

其实真正的发送窗口大小是拥塞窗口和接收端那个窗口之间的最小值。 MSS 我们知道在 MTU=1500 的情况下它的值是1460,拥塞窗口指的就是能发多少个1460,由于在连接建立之初,发送端并不知道网络状况,如果网络状态很差,一口气传过去很多数据是不明智的,缓慢启动才是正确的选择,缓慢启动可以及时止损,同时缓慢启动也不并是说一直缓慢,如果网络ok,会随着时间慢慢增长,这就是缓慢启动的目的,那怎么个增长法呢/p>

在通信之初,只要发送端收到一个 ACK 就会把 cwnd 翻倍,比如一开始是10,收到一个 ACK 后,下次就是20,再收到一个 ACK 后,就是40…,这个做法很聪明,我们只需忍受刚启动的时的慢速,随着时间的增长 cwnd 会以指数级的增长快速赶上来。

喝茶聊天,万事大吉。不对,这指数级的增长若不控制,一会便要超过了首富的财富了,这可不得了,于是搞了个慢启动阈值(ssthresh),当 cwnd 达到 ssthresh 时,这时说明 cwnd 不小了,在翻倍的涨下去可能会危险,这时可以选择小涨,不翻倍,每次在cwnd的基础上再加个1 MSS 就行了。

当 cwnd

当 cwnd > ssthresh 时,拥塞窗口按线性增长(拥塞避免)

即使是加1个 MSS,随着时间的推移也可能无限大,但是为什么现实中没出现问题想其中之一就是上面的说到的真正的发送窗口的大小是两者中的最小的那个,毕竟接收窗口不可能无限大。

其二就是随着网络包的越来越大,会发生网络拥堵,这时候 ssthresh 会降级, 也就是 ssthresh = cwnd / 2,然后 cwnd 会被设置为1个报文段,重头重新开始缓慢启动和拥塞避免,关于第二点,我借鉴网上一个例子: “假设 TCP 的 ssthresh 的初始值为 8。当拥塞窗口上升到 12 时网络发生了超时,于是TCP 开始使用慢开始和拥塞避免。试分别求出第 1 次到第 15 次传输的各拥塞窗口大小。”

5b02e44fd8ae4a6fae1fefb97f75612e.png 

一开始cwnd是1,然后不停的翻倍,直至到达 ssthresh,也就是8,这时开始每次加1个 MSS,当到12的时候,发生超时,也就是 ssthresh 会变成6,然后 cwnd 重新从1开始,也是不停的翻倍,当到4,准备翻倍到8的时候,发现sthresh=6,因此会变成6,然后开始每次加一个 MSS。因此第1次和第15次分别是1和9。

牛逼的算法让我不由的手舞足蹈,ちょっと待って(等一下),怎么判断网络拥堵超时了个其实很好判断,当超过一定时间之后,发送端没收到ack,可能就是网络超时了,正常来说,这时候,发送端会使用退避策略来重新发送,每次重传的间隔大概是几百毫秒,这几百毫秒毫秒对人类来说还挺快的,但是对计算机来说其实挺慢的,那有没有什么更快的方法/p>

我们先来看个例子: 假设现在要发送4个数据包分别是[1,100],[101,200],[201,300],[301,400],正常来说发完第一个数据包之后,会回复ACK=101,没毛病,但是在发第二个数据包的时候,网络超时了,丢包了,当发送端继续发送第三个、第四包的时候,并不会回复 ACK=301,401,而是会继续回复 ACK=101,这里请再记住 ACK 代表这个序列号之前的数据都已收到。

正如上文说到的,正常来说,此时要等几百毫秒才会意识到丢包,重发,而如果想要更快点,比如收到三次重复的ACK说明就是丢包了,这样是不是快很多,这就是「快速重传(SACK)」,但是只是单纯的告诉101之前的数据收到了(第一个数据包)有点低效,万一第三个也丢了怎么办/p>

因此SACK做了进一步的优化:在通知ACK的同时也告诉比如第三个包也丢了、第四个数据包我收到了,这样发送端就知道了此刻除了第二个数据包丢失了,第三个包也丢失了,重传第二个、第三个即可。

 

文章知识点与官方知识档案匹配,可进一步学习相关知识网络技能树支撑应用程序的协议应用层的作用22399 人正在系统学习中

来源:蜀州凯哥

声明:本站部分文章及图片转载于互联网,内容版权归原作者所有,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2022年8月11日
下一篇 2022年8月11日

相关推荐