来源丨经授权转自 假装懂编程(ID:suntalkrobot) 作者丨康师傅 hi,大家好,我是康师傅,今天和大家聊聊网络协议那些常见的知识点,为什么要聊这个知识呢?主要是因为自己快忘完了,同时这不今年快要结束了,可能很多同学都在开始准备明年的面试了,那么我想不管你是前端、后端还是客户端,网络协议这块的复习应该是少不了的。 网络协议离不开我们常说的http、tcp这些,在网络分层模型中 http 属于应用层协议,tcp属于传输层协议,其实应用层协议还有像smtp、ftp等协议,传输层还有udp协议,当然我们今天重点说下http和tcp相关的知识,http离不开tcp,因此我们先说说tcp、ip相关的知识。 TCP、IP某一天,你和你的同事正在用微信聊天,不知道你是否思考过,你们的电脑上装了很多软件,比如有网易云、QQ...等等,为什么你通过微信发的消息会正确的发送到对方的微信上,而不是发送到其他应用软件上?同时再说夸大一点,为什么你发送的消息会发送你同事的电脑上,而不是隔壁老王的电脑上,这么问题看起来有点傻,其实这些离不开我们今天要说的TCP、IP协议。首先IP大家肯定都能明白,每个电脑都有一个IP,这也是为什么我们的信息可以精准的发给我们的同事而不是隔壁老王,因为我们知道同事电脑的IP,这就是IP层干的的事。 当通过IP找到了你同事的电脑后,还要找到你同事正在运行的微信软件,电脑上软件这么多,而且大家的IP都是一样的,这可怎么办?答案是端口,这也就是TCP层干的事,在数据经过TCP层的时候,会加上目标端口也就是我们微信进程占用的端口,然后数据包到达你同事的电脑上时,在TCP层会拆包,拆包后会发现目标端口号,然后把数据丢给我们的微信进程(电脑视角:端口号是10086,哦,这个数据丢给微信进程处理吧)。 其实TCP层不仅仅会加上目标的端口号,还会加上发送者的端口号,IP层不仅仅会加上目标IP,还会加上发送者的IP,发现没,这就是我们常说的socket四元组:发送端IP+发送端端口+接收端IP+接收端端口。一个socket四元组就可以确定一个连接。 我们常说TCP协议是一种基于字节流、面向连接的、可靠的传输层通信协议,这里我们需要思考定义中的三个抽象描述:
基于字节流的我们先说第一个问题,TCP 协议是基于字节流传输的,这是什么意思呢?举个例子,其实当我们往 socket 中写入1000个字节的时候,会分很多情况的,这时1000个字节会被 copy 到内核缓冲区的,但是1000个字节具体是怎么通过网卡发出去是不确定的,有可能一次性发出去,也可能分成2次,分别是300、700,也有可能是500、500,但是不管怎么分,每个字节都有自己的序号。 造成这么多情况的原因是因为受到路径最大传输单元 MTU、发送窗口大小、拥塞窗口大小等因素的影响(这些概念后面会讲,just follow me),在这里我们也说下可靠性,因为数据包已经在 TCP 层分段了,等于一块数据被打散了,这些打散的数据包被接收的顺序可能不一样,但是内核在收到乱序的数据包后,并不会直接丢给上层应用(http等),需要按照数据包的顺序组装好,这个组装依赖的就是序列号,那基于字节流方式传输的数据包,如何确定这个数据包的序列号呢?其实这个序列号就是这个包的第一个字节的序号。 三次握手再说第二个问题:面向连接。对没错,我们还是要说说老掉牙的问题:三次握手、四次挥手。三次握手、四次挥手其实也是一种可靠性的表现。因为需要可靠,所以在建立连接的时候需要先确认双方是否都ok,也就是三次握手。我们先看看三次握手干了什么?同时我们看看为什么三次就行了,两次或者十次行不行? 看到上图中的一堆玩意比如syn、seq、ack、isn等等,先不要害怕,我们一一解释下,然后你就会明白了,上面我们也说到了,因为需要可靠,不能一上来就直接发送数据,万一对方不在线,那数据岂不是丢失了,因此握手的目的就是先确认两边的状态都ok,那如何区分这次通信是握手而不是正常的发送数据呢?这就是SYN包的作用,SYN相当于一个双方通信中附带的一个标志,当数据包中有它的时候,说明这次通信的目的是握手。 三次握手发SYN包之后,还有一个重要的事情:交换彼此的初始序列号seq,这是因为基于字节流的TCP其实每个字节的数据都有序号,在握手确认彼此的初始序列号之后,接下来所有的字节数据都是基于初始序列号向后累加的,初始序列号的生成方法就是ISN函数,它大概会随机生成一个数字,需要注意的是它的值并不是从0开始的。当一端发送了自己的初始序列号之后,并且收到了对端的ack就说明此次交互通畅,其中ack的值就是自己发过去的序列号加1。 ok,搞懂了几个名词的概念和意义之后我们再来看看三次握手的过程。
我们总结下,由于TCP是可靠的传输层通信协议,握手的目的主要是确认双方都有收发包的能力,从上文的描述来看三次刚刚好,如果少了,首先某一端的收发包能力就无法得到确认,比如最后一次如果发动端不发送最后的ack,那么接收端就不知道它是不是收到了数据包。当然超过3次肯定也是没问题的,但是没必要,因为3次已经可以知道双方的状况了。 不知道你发现没有,建立连接的过程双方都消耗了一个序列号,这里可不可以不消耗一个序列号呢?答案不可以,必须要消耗一个,关于这一点你先记住:不占用序列号的段是不需要确认的,比如ack,凡是消耗序列号的 TCP 报文段,一定需要对端确认。如果这个段没有收到确认,会一直重传直到达到指定的次数为止,像SYN 包就是需要确认的报文段。 四次挥手看完了三次握手,我们再来看看四次挥手的过程,四次挥手的过程双方会处于某种状态,这是需要注意的,这也是面试考察点。依然一样我们来看看为什么需要四次挥手,以及每次挥手的过程干了什么? 为了方便描述,这里定义下主动断开方叫「A」,被动断开方叫做「B」。
能不能三次挥手? 看流程四次挥手绝对没问题,那问题来了,三次行不行?其实某些情况下三次也是可以的,比如被动断开方没有要处理的数据也就不存在DATA那一部分,那其实ACK和FIN一起发过去问题也是没问题的,如果存在DATA,非要把ACK+DATA+FIN合并在一起发过去会发生什么呢?首先处理DATA需要时间,那么为了等DATA处理完再发ACK,可能会导致主动断开方因为迟迟没收到ack,而重发FIN包。 为啥最后一步主动断开方需要处于TIME_WAIT状态,这个状态代表什么? TIME_WAIT是主动关闭方最后进入的一种状态,TIME_WAIT是2MSL的,MSL是报文最大的生命周期,正常来说一个数据包如果在网络中超过MSL之后还没被对端收到就会被丢弃,那为什么主动断开方需要2MSL呢?
因此2MSL = 去向 ACK 消息最大存活时间(MSL) + 来向 FIN 消息的最大存活时间(MSL)。 为什么FIN包也需要消耗一个序列号? 上图并没有说到序列号的事,其实 FIN 包和 SYN 包一样的,也是需要消耗序列号的,如果要问为什么?只是回答“因为 FIN 包需要对端确认,而需要确认的报文段都是消耗序列号的”难免有些牵强,我们来看个图你就知道了。
因此无论是 SYN 包还是 FIN 包,为了和正常的数据区分,都需要消耗一个序列号。 可靠我是大哥我来分段-MTU和MSS netstat -i 注意这只是本机的MTU,真实的网络中,你的数据从电脑网卡出去之后,可能要经过一系列的路由器、交换机等物理硬件,其中每个物理硬件都有自己的MTU,那在这漫长的网络路径中,起关键作用的MTU是哪个?答案是最小的那个,最小的那个就叫做路径MTU,这就像木桶效应,桶的容量是由最短的那一块板决定的,当你的数据包大于MTU时,会被拆成一个一个合适的网络包发出去。IP层发现链路层的数据包有大小限制,因此IP层就说:'既然链路层有大小限制,发再大的数据包过去也是会被拆解的,还不如我自己做,在把数据发给链路老弟之前,我直接按照它的要求把数据分好段,就不麻烦它了。' IP层干了数据分段的事情之后,TCP层不高兴了,'弄啥嘞,弄啥嘞,我在他们的上层,数据竟然还要 IP 层小弟分段,我颜面何存!',于是 TCP 层为了避免数据被发送方分片,会主动把数据分割成小段再交给IP 层,TCP 能分的最大段我们称之为 MSS (Max Segment Size),这个 MSS 的值是多少呢?其实它的值是这个:
其中 IP 头和 TCP 头各占 20 个字节,以 MTU=1500来说,那么 MSS = 1500-20-20=1460。就这样 TCP 层主动的把数据分好,从而得到了 IP 层和链路层的一致好评。IP 层:'大哥靠谱'。链路层:'大哥的大哥靠谱'。 我只能吃这么多-滑动窗口 这是站在发送端的角度来看数据包的状态的,其中的滑动窗口部分可以看作是发送端的滑动窗口,对于已发送已确认的部分,算是过去时了,它只会使滑动窗口向右移,真正影响滑动窗口大小的是「已发送未确认」和「未发送可发送」部分,剩下的「不能发送」是因为接收端没有足够的空间了。我们再来站在接收端角度看看滑动窗口是什么样的。 可以发现窗口的大小其实是一样的,唯一的区别是对于接收端来说要么已接收,要么未接收,不能接收的话说明没有足够的空间了。那发送端怎么知道当前接收端剩余空间的大小?其实接收端在ACK的时候会带上自己窗口的大小,这样发送端就知道了接收端窗口的大小。以上图为例,当接收端拿到了32-35的数据后,就会ACK=36告诉发送端,同时接收端的滑动窗口会向后移动4位,发送端收到ACK=36后,就知道36之前的数据接收端都收到了,因此会把发送端的窗口也向后移动4位。 滑动窗口很棒,可以在能力范围内处理数据,但是有个问题呀:如果发送端能力极强,发的很快,接收端能力极弱,处理的很慢,这会导致什么问题?某一刻滑动窗口为0了,这时候接收端就会告诉发送端:'你奶奶的,消停会吧,没空间了'。发送端收到了通知之后:'原来是个弱鸡,休息会吧,等它下次ack通知我吧',正常来说,接收端在处理完数据之后可以告诉发送端可以继续发数据了,然而意外出现了,由于接收端所在的主机的主人正在听网易云音乐、玩着2k,同时还尼玛欣赏着b站舞蹈区up主娥罗多姿的舞蹈,导致网卡压力很大,最后一个ack丢失了,这样发送端就不知道接收端其实已经处理了一部分数据,这可怎么办,如果一直丢失,岂不是要一直傻等,得主动出击呀,于是搞了个「零窗口探测定时器」,这个定时器的功能相信大家也知道了,就是当接收方的接收窗口为0时,每隔一段时间,发送方会主动发送探测包,通过迫使对端响应来得知其接收窗口的状态,不得不说零窗口探测够稳。 悠着点慢慢来-拥塞控制
即使是加1个 MSS,随着时间的推移也可能无限大,但是为什么现实中没出现问题?我想其中之一就是上面的说到的真正的发送窗口的大小是两者中的最小的那个,毕竟接收窗口不可能无限大。其二就是随着网络包的越来越大,会发生网络拥堵,这时候 ssthresh 会降级, 也就是 ssthresh = cwnd / 2,然后 cwnd 会被设置为1个报文段,重头重新开始缓慢启动和拥塞避免,关于第二点,我借鉴网上一个例子:'假设 TCP 的 ssthresh 的初始值为 8。当拥塞窗口上升到 12 时网络发生了超时,于是TCP 开始使用慢开始和拥塞避免。试分别求出第 1 次到第 15 次传输的各拥塞窗口大小。' 一开始cwnd是1,然后不停的翻倍,直至到达 ssthresh,也就是8,这时开始每次加1个 MSS,当到12的时候,发生超时,也就是 ssthresh 会变成6,然后 cwnd 重新从1开始,也是不停的翻倍,当到4,准备翻倍到8的时候,发现sthresh=6,因此会变成6,然后开始每次加一个 MSS。因此第1次和第15次分别是1和9。 牛逼的算法让我不由的手舞足蹈,ちょっと待って(等一下),怎么判断网络拥堵超时了?这个其实很好判断,当超过一定时间之后,发送端没收到ack,可能就是网络超时了,正常来说,这时候,发送端会使用退避策略来重新发送,每次重传的间隔大概是几百毫秒,这几百毫秒毫秒对人类来说还挺快的,但是对计算机来说其实挺慢的,那有没有什么更快的方法?我们先来看个例子:假设现在要发送4个数据包分别是[1,100],[101,200],[201,300],[301,400],正常来说发完第一个数据包之后,会回复ACK=101,没毛病,但是在发第二个数据包的时候,网络超时了,丢包了,当发送端继续发送第三个、第四包的时候,并不会回复 ACK=301,401,而是会继续回复 ACK=101,这里请再记住 ACK 代表这个序列号之前的数据都已收到。正如上文说到的,正常来说,此时要等几百毫秒才会意识到丢包,重发,而如果想要更快点,比如收到三次重复的ACK说明就是丢包了,这样是不是快很多,这就是「快速重传(SACK)」,但是只是单纯的告诉101之前的数据收到了(第一个数据包)有点低效,万一第三个也丢了怎么办,因此SACK做了进一步的优化:在通知ACK的同时也告诉比如第三个包也丢了、第四个数据包我收到了,这样发送端就知道了此刻除了第二个数据包丢失了,第三个包也丢失了,重传第二个、第三个即可。 |
|