请求数据包从发送到接收,都经历什么()

千磨万击还坚劲,任尔东西南北风。这篇文章主要讲述请求数据包从发送到接收,都经历什么?相关的知识,希望能为你提供帮助。
之前讲了「从输入 URL 再到浏览器成功看到界面」中的域名是如何变成 IP 地址的,了解了 DNS 相关的东西。这篇文章就聊聊发生在 DNS 解析之后的操作——建立连接。也就是我们常说的三次握手。
三次握手不就是:

  1. 服务器开始为 CLOSE 状态,然后监听某个端口,此时服务器会进入 LISTEN 状态
  2. 客户端最初也是 CLOSE 状态,客户端会向服务器发送一个带 SYN 标志位的数据包,主动发起连接。此时客户端会变成 SYN-SENT 状态
  3. 服务器接收到客户端的数据包之后,通过标志位判断出了客户端想要建立连接。然后返回一个 SYNACK ,此时服务器的状态变为了 SYN-RCVD
  4. 客户端收到了服务器的 ACK 之后,会回一个 ACK 给服务器,回完这个 ACK 之后,服务器的状态就变为了 ESTABLISH
  5. 服务器收到了客户端回复的 ACK 之后,服务器的状态也变成了 ESTABLISH
这篇文章不会涉及到上面提到的什么各种状态的变化,包内的标志位是什么,而是会更加关注于底层的东西,也就是上面那些发来发去的数据包是如何发送出去的。
其实不仅仅是建立连接时的三次握手,像浏览器中调用的很多 HTTP 接口,都会和服务器进行通信。
这还用问?不就是发个 HTTP 请求就过去了吗?
当然,这个答案可能是很多不了解网络的人可能会说出的答案。
其实更具体、更准确的说法是通过协议栈和网卡发送出去的。
其中,协议栈负责对数据进行打包,打包完成之后就由网卡将数据转换成电信号,通过光纤发送出去了。
请求数据包从发送到接收,都经历什么()

文章图片

网卡自不必说,用来和其他的计算机进行通讯的硬件,我们常说的 MAC(Medium Access Control) 地址,其实就是网卡的编号,从其被生产出来的那一刻就被确定的一个唯一编号。MAC 地址长为 48 个比特,也就是 6 个字节,用十六进制进行表示。
当我们知道了和我们通信的 IP 地址之后,就可以委托操作系统中的协议栈将来来自应用程序的数据,打包成数据包然后发送出去。那协议栈,具体是啥呢?协议栈其实是一系列网络协议的总和,例如:
  • TCP
  • UDP
  • IP
不同的应用程序在进行数据传输的时候,可能会选择不同的协议。例如我们使用的浏览器就是使用的 TCP 协议,而像之前讲过的 DNS 解析就用的 UDP 协议。
就拿我们向服务器发送一个 HTTP 请求作为例子,我们知道 HTTP 请求中有:
  • 请求行
  • 请求头
  • 请求体
HTTP 是属于应用层的协议,而应用层还有很多其他的协议,每个协议所涉及到的数据也都不同,协议栈要怎么去兼容不同协议之间的数据呢?
答案是不做兼容。对于协议栈来说,所有的数据都只不过是一堆二进制序列。
请求数据包从发送到接收,都经历什么()

文章图片

我都这么问了,那显然不是了...
其实协议栈在收到数据之后并不会马上就会就发送出去,而是会先写入位于内存的 Buffer 中。那为啥不直接发出呢?
协议栈之所以不立即发出去,其实也是同样的道理。其实这背后无非基础两种考虑:
  1. 数据的长度
  2. 等待的时间
应用层的程序发送过来的数据可能长度都不太一样,有的可能一个字节一个字节的发, 有的可能一次性就传入所有的数据。
如果收到数据就发送出去,会导致在网络中传输着很多小包,而这会降低网络传输的效率。
请求数据包从发送到接收,都经历什么()

文章图片

所以,协议栈在收到数据之后会等待一段时间,等数据达到一定量之后,再执行发送操作。
请求数据包从发送到接收,都经历什么()

文章图片

但是,协议栈又不能等的太久是吧?等太久了你让正在电脑面前操作的用户情何以堪,这种发送延迟会让用户体验刷刷的往下掉。
但是吧,想做到对这两者的平衡却不是一件简单的事。数据包太短,降低网络传输效率,等待太长时间,又会造成发送延迟。所以协议栈索性就把控制权交给了应用程序。
应用程序可以自己控制到底采取哪种措施,例如我们常用的浏览器,因为和用户实时的在进行交互,用户对整个页面的响应速度也相当敏感,所以一般都会采用直接发送数据的方式,即使其数据并没有达到「一定的量」
的确,上面都只说一定的量、一定的量,那这个量到底是多少?
要了解这个我们需要知道两个参数,分别是:
  1. MTU(Maximum Transmission Unit)最大传输单元
  2. MSS(Maximum Segment Size)最大分段大小
MTU 其实就代表了上面途中数据包的最大长度,一般来说是 1500 字节。而我们需要知道数据包是由以下部分组成的:
  1. 各种头部信息
  2. 真实数据
而从 MTU 中减去各种头部数据的大小,剩下的就是 MSS 了,也就是实际的数据。
请求数据包从发送到接收,都经历什么()

文章图片

知道了数据包的组成和 MTU、MSS 的概念之后,我们就可以继续接下来的步骤了。某次发送的数据,没有超过 MSS 还好,就可以直接发送出去了。
请求数据包从发送到接收,都经历什么()

文章图片

此时就需要对数据进行拆分,按照 MSS 的长度为单位进行拆分,将拆出来的数据分别装进不同的数据包中。拆分好之后,就可以发送给目标服务器了。
TCP 会确保通信的服务器能够收到数据包。传输时对每个字节都进行了编号,举个例子,假设此次传输的数据是 1 - 1000 字节,然后服务器回的 ACK 就会是 1001,这就代表没有丢包。
这些发送过的包都会暂存在 Buffer 中,如果传输的过程中出错,则可以进行重发的补偿措施。这也是为什么在数据链路层(例如网卡、路由器、集线器)等等都没有补偿机制,它们一旦检测到错误会直接将包丢弃。然后由传输层重发就好。
在服务器端,我们去和其他第三发进行交互时,是不是都会设定一个超时的时间?如果不设置超时时间那难道一直在这等下去吗?
TCP 也同理。客户端在等待服务器响应时,会有一个时间叫 ACK 等待时间,其实也是超时时间。
当网络发生拥堵时,其实你完全也可以把网络拥堵理解成路上堵车。此时,ACK 的返回就会变慢。如果返回时间长到了让客户端认为服务器没有收到,就有可能会重发。
并且有可能刚刚重发完,ACK 就到了。虽然服务器端可以通过序号来对包进行判重,不会造成错误,但是这种没有意义的重复包,在本身网络负担已经很重的情况下,你还往里怼重复的无用的数据包,这不是扯淡吗?这明显不行的。
那怎么避免上面的这个情况呢?答案很简单,稍微延长一点 ACK等待时间,这样一来就能一定程度上避免上述的问题。但是用屁股想想应该也知道,这个时间肯定不是越长越好,再长用户那又该等爆炸了。
除了网络波动会影响到 ACK 的返回时间,通信的物理距离也是一个影响的因素。说白了就是这玩意儿不可能设置一个固定的时间。所以,实际上,这个等待时间是动态调整的,这次稍微返回慢了点,那我下次就稍微延长一点等待时间。返回 ACK 的速度如果很给力,那么就会相应的减少 等待。
上面的概念也有一个大家很熟悉的名字,叫——超时重传。
我们来设想一个更加极端的情况,假设你们通信的网线被挖断了,甚至机房起火了,这个时候无论你重发多少次都没用。那 TCP 不就一直无限循环的把请求发下去了?
当然 TCP 设计时也考虑到了这种情况,其在重传几次无效之后,就会强制中断通信,并抛出错误给应用程序。
当然不是,这样极其的浪费资源,降低通信效率。发送完一个数据包之后,不用等待 ACK 的返回,会直接继续发送下一个包,这就是滑动窗口。
但是这样会有一个问题,应用程序发送包发送的过于频繁,导致服务器接收不过来了。
因为刚刚说过,应用程序发送的时候,会将发送过的数据存储在 buffer 中。而对于接收方也是一样的,接收方收到消息之后,会将数据存储在 Buffer 中,然后在 Buffer 中对收到的数据进行重组,还原成最初的应用程序发送的数据。
但是如果发送的数据太快,超过了重组的速度,缓冲区就会被填满。而缓冲区一旦被填满,后续的数据就无法再接收了,然后丢包就出现了。
那 TCP 是如何解决这个问题的呢?答案是 流量控制。为了防止传输方发送的过快直接造成丢包,继而触发上面的超时重传机制,根据接收方的接受能力,来决定发送方的传输速度,这个机制就是流量控制。
该机制作用于接受方。在TCP报文头部中会用一个16位的字段来表示窗口大小,非常重要的调优参数。这个数字越大,则说明接收方的缓冲区越大,能够接收更多的数据。接收方会在确认应答的时候,将自己的剩余窗口大小写入,随ACK一起发送给发送方。
请求数据包从发送到接收,都经历什么()

文章图片

如果发送方接收到的大小为0,那么此时就会停止发送数据。这样会有一个问题,如果下一个应答(也就是窗口大小不为0)在过程中丢了,那么发送方就会进入死锁,相互等待。所以发送方会定期的向接收方发送窗口探测的数据段。
好了,关于数据包的发送就介绍到这里。之后有机会再聊聊 TCP 的拥塞控制相关的东西。
【请求数据包从发送到接收,都经历什么()】
请求数据包从发送到接收,都经历什么()

文章图片


    推荐阅读