当浏览器这类应用需要与服务端传输信息的时候会经历“三次握手”建立连接,经过这一阶段后就可以互相发送信息了。对于建立连接,一般的说法是可以想象客户端和服务端之间建立了一条可以通信的管道将两者连接起来。这种说法是为了便于理解,
实际情况当然不是如此,而是发送方和接受方之间通过交换信息来改变自己当前的状态和确定下一步应该做什么,通过这样的方式来达成默契。
好比我在微信问你:你现在忙么?我当前的状态是想要与你聊天。
等你忙过了后回答:不忙了,你想要对我说什么呢,可以开始说了么?你现在的状态准备要与我聊天了。
我回答到:可以了,那我接下来要开始说了。
我们现在的状态是确定好可以开始聊天了,并且接下来会发送正式聊天的信息,你也准备好要接收信息并回复,期间的状态又会有正在编写消息,等待消息,崩溃怒删等等多种状态的转变。而这中间其实并没有管道,而是通过交换信息来知道自己应该处于什么状态下一步应该做什么。
那来了解下真实的情况是什么样子?
首先服务端是监听并等待通信的状态,客户端知道需要通信的服务端地址后会向其发送一个数据包来请求建立连接,数据包里包含了要交换的信息,其中一个叫做TCP头,这是TCP附加在传送信息前的一段信息,里面包含了"三次握手"的信息。先来看下有什么内容:
注意其中的SYN和ACK,在第一次发送时会把SYN的值写为1,理解为要建立连接,ACK为0。服务端返回给客户端的消息则将SYN的值也写为1表示接受建立连接,ACK为1理解为确认刚才的消息收到。
而客户端收到消息后发现SYN为1则知道连接建立成功,并再次向发送服务端消息,其中ACK写为1,因为服务端也需要确认刚才发送的消息已被收到。
浏览器等程序发送的数据会先经过处理,被拆分为多个小包,来看下是怎么拆分的:
其中MTU为最大传输单元,表示一个包的最大容量,MSS则是去除头部之后,真正可发送的数据容量。
拆分过程中,每一块数据从头算起的位置会被写在交换信息即TCP头部的“序号”中,这样接收数据的一方就可以判断是都有遗漏,比如初始传输中数据大小为100,初始序号为1,第二次传输过来的序号应该为101,如果是201,则中间有遗漏。如果确认没有遗漏,接受方则将下次对方应该传过来的序号值写在返回内容的ACK中(比如第三次应该是201),返回给之前的发送方,并将交换信息中的ACK字段写为1,这是为了让对方知道这个包是用来告知ACK号的,需要检查ACK号。
在实际的传输过程中序号的初始值并不会是1,而是一个随机数防止被预测和被利用。看一下ACK号和序号传输过程:
然而传送数据是双向的,服务端也会向客户端发送数据,客户端计算出初始序号然后与数据一起发给服务端,服务端收到后计算ACK号再返回去。而服务端也需要计算出一个自己的初始序号,与数据一起发送给客户端,然后客户端得出ACK返回给服务端。
这样两边都告诉了对方自己的初始序号并开始传送数据,过程如图:
发送的数据都会先保留在缓存区,当一定时间内没有收到对方ACK号,就会重发数据。但是如果因为网络拥挤等造成ACK的返回延迟,这时重发这些包后又收到了之前延迟的ACK,就浪费了。
因此问题来了,如何设置等待ACK的时间呢?这时会动态调整等待的时间,根据每次ACK返回时间来调整,ACK返回变慢就延长等待时间。等待ACK号这段时间仍然可以利用起来,可以在等待时间内继续发送数据。接受方将收到的数据放在缓存区,然后从其中取出数据组装还原。
那么问题又来了,如果发送的数据太多超出接收方的处理速度会导致填满缓存区,所以要发送多少数据才算合适?
这时前面提到的交换信息即TCP头部中的“窗口”字段便发挥作用了,当接收方处理完数据将缓存区空间释放出来后,可以通过“窗口”告诉对方下次发送多少数据。
这时问题又又来了,到底什么时候更新这个“窗口”值才合适呢?
可以设想,如果缓存区一有空间就可以告诉对方发送数据,但同时考虑到ACK号是收到数据确认无误就后发送,两者完全分开可能会造成发送过多数据包,因此可以将两者结合,在需要发送ACK号时,再延迟一段时间,延迟过程中如果有“窗口”更新,就合并到一起发送。如果是连续多个发送ACK号,那我们可以直接取最新的ACK号和“窗口”更新一起发送,因为ACK号是根据已收到数据量来告知对方下一次发送的序号值,只需要取最新的值,前面的可以忽略,这样便提高了效率。
从上面的内容可以看出,在传输过程中,双方是通过不停的交换信息来确定自身的动作,其中也有很多状态的改变,比如建立连接过程中服务端一开始处于监听的LISTEN状态,收到对方的ACK后转为SYN_RECD状态等等。
文中仍有许多可以继续深挖的地方,比如如果初始序号值为1的话是如何被利用的,除了TCP头部中的信息还有其它什么需要用的信息,发送的数据长度是不是一定要满足MSS才发送等等。
关键词: 网络