温故而知新
上一章节我们了解到了http的请求链接组成以及基本发展历程,今天我们来说说数据传输的一些故事!
通过本章节,我们将明白:为什么说TCP是一个「面向连接的」、「可靠的」、「基于字节流的」传输层协议。
传输层的诞生
下三层「物理层、数据链路层、网络层」为我们提供了一种保障,只要两个站点之间的网络是通畅的,那么我就可以从一端发送数据到另一端,这三层居功至伟,是网络传输的基石。
假设在客户端和下三层之间添加一层,只负责转发客户端的请求和服务器的响应。我们称之为「传输层」。
传输层站在第四层,利用下三层所做的铺垫,随心所欲地发送数据,而不必担心找不到对方了。
此时,传输层相当于什么也没做,只是负责转发,起到一个中介的作用,但是问题来了:
问题一:数据包来自何处,又去往哪里
下三层协议只能把数据包从一个主机搬到另外一台主机,但是,到了目的地以后,数据包具体交给哪个程序(进程)呢?
所以,需要把通信的进程区分开来,于是就需要给每个进程分配一个数字编号「端口号」。
并在要发送的数据包上,增加了传输层的头部,「源IP」、「源端口号」与「目标IP」「目标端口号」。
OK,数据包就知道自己从哪里来,到哪里去了。这样就将原本主机到主机的通信,升级为了进程和进程之间的通信。
就这样,数据传输层又成了数据包的搬运工。但很快,有新的问题出现了......
问题二:丢包啦......
由于网络的不可靠,数据包可能在半路丢失,而 A 和 B 却无法察觉。
对于丢包问题,只要解决两个事就好了。
第一个,A 怎么知道包丢了?
答案:让 B 告诉 A
第二个,丢了的包怎么办?
答案:重传
于是有了新的方案,A 每发一个包,都必须收到来自 B 的确认(ACK),再发下一个,否则在一定时间内没有收到确认,就重传这个包。
这就是停止等待协议。只要按照这个协议来,虽然 A 无法保证 B 一定能收到包,但 A 能够确认 B 是否收到了包,收不到就重试,尽最大努力让这个通信过程变得可靠,于是你们现在的通信过程又有了一个新的特征,可靠交付。
问题三:停止等待引发的效率低下
停止等待虽然能解决问题,但是效率太低了,A 原本可以在发完第一个数据包之后立刻开始发第二个数据包,但由于停止等待协议,A 必须等数据包到达了 B ,且 B 的 ACK 包又回到了 A,才可以继续发第二个数据包,这效率慢得可不是一点两点。
于是你对这个过程进行了改进,采用流水线的方式,不再傻傻地等。
问题四:复杂网络/流水线引发的顺序问题
但是网路是复杂的、不可靠的。
有的时候 A 发出去的数据包,分别走了不同的路由到达 B,可能无法保证和发送数据包时一样的顺序。
首先我们要明确,如果是依靠停止等待协议,是没有乱序一说的。
在流水线中有多个数据包和ACK包在乱序流动,他们之间对应关系就乱掉了。
难道还回到停止等待协议?A 每收到一个包的确认(ACK)再发下一个包,那就根本不存在顺序问题。应该有更好的办法!
A 在发送的数据包中增加一个序号(seq),同时 B 要在 ACK 包上增加一个确认号(ack),这样不但解决了停止等待协议的效率问题,也通过这样标序号的方式解决了顺序问题。
而 B 这个确认号意味深长:比如 B 发了一个确认号为 ack = 3,它不仅仅表示 A 发送的序号为 2 的包收到了,还表示 2 之前的数据包都收到了。这种方式叫累计确认或累计应答。
注意,实际上 ack 的号是收到的最后一个数据包的序号 seq + 1,也就是告诉对方下一个应该发的序号是多少。但图中为了便于理解,ack 就表示收到的那个序号,不必纠结。
问题五:流量问题「请求过快,服务器忙不过来」
有的时候,A 发送数据包的速度太快,而 B 的接收能力不够,但 B 却没有告知 A 这个情况。
怎么解决呢?
同上述确认机制不同,确认机制是B告诉A数据包是否收到了,在这里,B并不是因为收不到数据包,而是接受的速度同A发送的速度不匹配,造成B压力过大,最终崩溃。这个时候的让B告诉A自己的能力,否则就真的得累死了。
于是 B 决定,每次发送数据包给 A 时,顺带传过来一个值,叫窗口大小(win),这个值就表示 B 的接收能力。同理,每次 A 给 B 发包时也带上自己的窗口大小,表示 A 的接收能力。
B 告诉了 A 自己的窗口大小值,A 怎么利用它去做 A 这边发包的流量控制呢?
很简单,假如 B 给 A 传过来的窗口大小 win = 5,那 A 根据这个值,把自己要发送的数据分成这么几类。
1、A会将数据包分成已发送成功和未发送两拨。当A收到B发送过来的窗口大小win = 5 的时候,A将从已发送成功的最后一个位置开始,发送B指定的窗口大小的数量的数据包,并将状态更新成已发送未确认或未发送可发送;
2、发送数据包,更新数据包状态,已发数据包更新成已发送未确认;
有点像分页。
当 A 不断发送数据包时,已发送的最后一个序号就往右移动,直到碰到了窗口的上边界,此时 A 就无法继续发包,达到了流量控制。
但是当 A 不断发包的同时,A 也会收到来自 B 的确认包,此时整个窗口会往右移动,因此上边界也往右移动,A 就能发更多的数据包了。
以上都是在窗口大小不变的情况下,而 B 在发给 A 的 ACK 包中,每一个都可以重新设置一个新的窗口大小,如果 A 收到了一个新的窗口大小值,A 会随之调整。
如果 A 收到了比原窗口值更大的窗口大小,比如 win = 6,则 A 会直接将窗口上边界向右移动 1 个单位。
如果 A 收到了比原窗口值小的窗口大小,比如 win = 4,则 A 暂时不会改变窗口大小,更不会将窗口上边界向左移动,而是等着 ACK 的到来,不断将左边界向右移动,直到窗口大小值收缩到新大小为止。
OK,终于将流量控制问题解决得差不多了,你看着上面一个个小动图,给这个窗口起了一个更生动的名字,滑动窗口。
问题六:拥塞问题
但有的时候,不是 B 的接受能力不够,而是网络不太好,造成了网络拥塞。
拥塞控制与流量控制有些像,但流量控制是受 B 的接收能力影响,而拥塞控制是受网络环境的影响。
拥塞控制的解决办法依然是通过设置一定的窗口大小,只不过,流量控制的窗口大小是 B 直接告诉 A 的,而拥塞控制的窗口大小按理说就应该是网络环境主动告诉 A。
但网络环境怎么可能主动告诉 A 呢?只能 A 单方面通过试探,不断感知网络环境的好坏,进而确定自己的拥塞窗口的大小。
假如拥塞窗口的大小为 cwnd,上一部分流量控制的滑动窗口的大小为 rwnd,那么窗口的右边界受这两个值共同的影响,需要取它俩的最小值。
窗口大小 = min(cwnd, rwnd)
含义很容易理解,当 B 的接受能力比较差时,即使网络非常通畅,A 也需要根据 B 的接收能力限制自己的发送窗口。当网络环境比较差时,即使 B 有很强的接收能力,A 也要根据网络的拥塞情况来限制自己的发送窗口。正所谓受其短板的影响嘛~
问题七:连接问题
我们先来看一个动图
有的时候,B 主机的相应进程还没有准备好或是挂掉了,A 就开始发送数据包,导致了浪费。
这个问题在于,A 在跟 B 通信之前,没有事先确认 B 是否已经准备好,就开始发了一连串的信息。就好比是老师在辛苦讲课,下面的同学在睡觉开小差,导致老师的课都白讲了......
该怎么让资源更好的利用起来呢,杜绝浪费:
三次握手建立连接
实现A和B的正常通信,主要有两点:
1、证明A到B的通信正常;
2、证明B到A的通信正常;
想要实现着两个功能,需要怎么做呢?
1、A向B发送连接请求,说明要建立连接了;(第一次握手)
2、B回复A,你的消息我收到了;(第二次握手)
3、B向A发送请求,你看看我发的消息能收到不;(第三次握手);
4、A向B发送请求,你的消息我收到了;(第四次握手)
综上,是通过四次握手实现链接的,但是由于确认消息体积很小,可以将第二步和第三步合并成一步,B发送A链接的同时将确认消息捎带上,这就成了三次握手。
A -> B [SYN] Seq=0 Win=64240 Len=0
MSS=1460 WS=256
B - >A [SYN, ACK] Seq=0 Ack=1 Win=29200 Len=0
MSS=1424 WS=512
A -> B [ACK] Seq=1 Ack=1 Win=132352 Len=0
四次挥手断开链接
B -> A [FIN, ACK] Seq=8 Ack=4096 Win=37888 Len=0
A -> B [ACK] Seq=4096 Ack=9 Win=132352 Len=0
A -> B [FIN, ACK] Seq=4096 Ack=9 Win=132352 Len=0
B -> A [ACK] Seq=4096 Ack=9 Win=37888 Len=0
同建立连接不同的是,B在请求断开连接的时候有肯能A还有没有发送的数据,所以握手的确认消息不能捎带,所以需要四次握手。
小结
TCP 在建立连接时,需要告诉对方 MSS(最大报文段大小)也就是说,如果要发送的数据很大,在 TCP 层是需要按照 MSS 来切割成一个个的TCP 报文段 的。切割的时候不管你原来的数据表示什么意思,需要在哪里断句啥的,我就把它当成一串毫无意义的字节,在我想要切割的地方咔嚓就来一刀,标上序号,只要接收方再根据这个序号拼成最终想要的完整数据就行了。在我TCP 传输这里,会把它当做一个个的字节,也就是基于字节流的含义了。
OK,到这里大家应该明白为什么说TCP是面向连接的、可靠的、基于字节流的传输层通信协议了吧。
如有问题,欢迎留言讨论!
附录
1、TCP协议头
2、上述关键名称的含义
SYN:同步序列编号(Synchronize Sequence Numbers)。是TCP/IP建立连接时使用的握手信号。在客户机和服务器之间建立正常的TCP网络连接时,客户机首先发出一个SYN消息,服务器使用SYN+ACK应答表示接收到了这个消息,最后客户机再以ACK消息响应。这样在客户机和服务器之间才能建立起可靠的TCP连接,数据才可以在客户机和服务器之间传递。
ACK:(Acknowledge character)即是确认字符,在数据通信中,接收站发给发送站的一种传输类控制字符。表示发来的数据已确认接收无误
FIN:关闭连接的信号标示(https://www.cnblogs.com/borey/p/5626124.html)。
URG:(紧急位)急指针是一个正的偏移量,和序号字段中的值相加表示紧急数据最后一个字节的序号。TCP的紧急方式是发送端向另一端发送紧急数据的一种方式。紧急指针指向包内数据段的某个字节(数据从第一字节到指针所指字节就是紧急数据,不进入接收缓冲就直接交给上层进程,余下的数据要进入接收缓冲的)
PSH:(push)表示有 DATA数据传输
RST:表示连接重置
参考文档:
1、《HTTP权威指南》
2、图解TCP