切,老掉牙的TCP知识

某一天,你和你的同事正在用微信聊天,不知道你是否思考过,你们的电脑上装了很多软件,比如有网易云、QQ…等等,为什么你通过微信发的消息会正确的发送到对方的微信上,而不是发送到其他应用软件上?同时再说夸大一点,为什么你发送的消息会发送你同事的电脑上,而不是隔壁老王的电脑上,这么问题看起来有点傻,其实这些离不开我们今天要说的TCP、IP协议。首先IP大家肯定都能明白,每个电脑都有一个IP,这也是为什么我们的信息可以精准的发给我们的同事而不是隔壁老王,因为我们知道同事电脑的IP,这就是IP层干的的事。

当通过IP找到了你同事的电脑后,还要找到你同事正在运行的微信软件,电脑上软件这么多,而且大家的IP都是一样的,这可怎么办?答案是端口,这也就是TCP层干的事,在数据经过TCP层的时候,会加上目标端口也就是我们微信进程占用的端口,然后数据包到达你同事的电脑上时,在TCP层会拆包,拆包后会发现目标端口号,然后把数据丢给我们的微信进程(电脑视角:端口号是10086,哦,这个数据丢给微信进程处理吧)。

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

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

  1. 基于字节流是什么意思?

  2. 什么叫做面向连接的?

  3. 如何算可靠?

基于字节流的

我们先说第一个问题,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,搞懂了几个名词的概念和意义之后我们再来看看三次握手的过程。

  1. 首先发送端c和接收端s一开始谁也没联系谁,大家没啥交集,不存在交易,大门紧闭,也就是假想的_close_状态。

  2. 第一次:发送端c发送SYN包过去,同时带上初始序列号ISN©,然后发送端c处于_SYN-SENT_状态,这时说明_发送端c具有发包的能力_。

  3. 第二次:接收端s收到发送端c发过来的SYN包之后,就知道了此次要进行的是握手认证,于是它也发送一个SYN包并且带上自己的初始序列号ISN(s),同时回复发送端c一个ack,ack的值就是ISN©+1,其实这个ISN©+1的意思就是说“发送端老弟,你的初始序号我收到了,下次通信的话,数据包直接从ISN©+1开始”,此时接收端处于「SYN-RCVD」状态,并且第二次握手也说明了_接收端具有收包和发包的能力_。

  4. 第三次:发送端c收到ack之后,得回一下接收端s,这样接收端s才知道_发送端c也具有收包的能力_,这时发送端会回一个ack=ISN(s)+1,这个的意思和上面的差不多:“接收端老哥,你的初始序列号我也收到了,下次通信的话,你的数据包序号就从ISN(s)+1开始吧”,至此三次握手完毕,双方的状态都是_ESTABLISHED_。

我们总结下,由于TCP是可靠的传输层通信协议,握手的目的主要是确认双方都有_收发包的能力_,从上文的描述来看三次刚刚好,如果少了,首先某一端的收发包能力就无法得到确认,比如最后一次如果发动端不发送最后的ack,那么接收端就不知道它是不是收到了数据包。当然超过3次肯定也是没问题的,但是没必要,因为3次已经可以知道双方的状况了。

不知道你发现没有,建立连接的过程双方都消耗了一个序列号,这里可不可以不消耗一个序列号呢?答案不可以,必须要消耗一个,关于这一点你先记住:不占用序列号的段是不需要确认的,比如ack,凡是消耗序列号的 TCP 报文段,一定需要对端确认。如果这个段没有收到确认,会一直重传直到达到指定的次数为止,像SYN 包就是需要确认的报文段。

四次挥手

看完了三次握手,我们再来看看四次挥手的过程,四次挥手的过程双方会处于某种状态,这是需要注意的,这也是面试考察点。依然一样我们来看看为什么需要四次挥手,以及每次挥手的过程干了什么?

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

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

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

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

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

能不能三次挥手

看流程四次挥手绝对没问题,那问题来了,三次行不行?其实某些情况下三次也是可以的,比如被动断开方没有要处理的数据也就不存在DATA那一部分,那其实ACK和FIN一起发过去问题也是没问题的,如果存在DATA,非要把ACK+DATA+FIN合并在一起发过去会发生什么呢?首先处理DATA需要时间,那么为了等DATA处理完再发ACK,可能会导致主动断开方因为迟迟没收到ack,而重发FIN包。

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

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

  1. 一个MSL主要是保证最后一次 ACK 能到达对端,如果 1MSL 后,ACK 还没到达对端会怎么办?

  2. 如果主动断开方的 ACK 没到达对端,这时候会触发被断开方重传 FIN,那么另一个 MSL 就是保证重传的 FIN 包也能到达对端。

因此_2MSL = 去向 ACK 消息最大存活时间(MSL) + 来向 FIN 消息的最大存活时间(MSL)_ 。

为什么FIN包也需要消耗一个序列号?

上图并没有说到序列号的事,其实 FIN 包和 SYN 包一样的,也是需要消耗序列号的,如果要问为什么?只是回答“因为 FIN 包需要对端确认,而需要确认的报文段都是消耗序列号的”难免有些牵强,我们来看个图你就知道了。

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

总结

虽然我个人也经常自嘲,十年之后要去成为外卖专员,但实际上依靠自身的努力,是能够减少三十五岁之后的焦虑的,毕竟好的架构师并不多。

架构师,是我们大部分技术人的职业目标,一名好的架构师来源于机遇(公司)、个人努力(吃得苦、肯钻研)、天分(真的热爱)的三者协作的结果,实践+机遇+努力才能助你成为优秀的架构师。

如果你也想成为一名好的架构师,那或许这份Java成长笔记你需要阅读阅读,希望能够对你的职业发展有所帮助。

image

业目标,一名好的架构师来源于机遇(公司)、个人努力(吃得苦、肯钻研)、天分(真的热爱)的三者协作的结果,实践+机遇+努力才能助你成为优秀的架构师。

如果你也想成为一名好的架构师,那或许这份Java成长笔记你需要阅读阅读,希望能够对你的职业发展有所帮助。

[外链图片转存中…(img-h0PslZHr-1720088579135)]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值