目 录
🍃一. 确认应答
TCP 诞生的初衷就是可靠传输
可靠传输是 TCP 最核心的部分,TCP 内部的很多机制,都是在保证可靠传输。(可靠传输是发了之后我知道对方收没收到,而不是 100% 能收到(可能没收到 --> 网线断了))
确认应答,要针对数据进行编号,然后才能明确,应答报文是在应答哪个数据,应对了网络传输的 “后发先至”。
TCP 就引入了 “序号”
报文的字节:
应答报文中的确认序号,就是 1001。应答报文,可以视为只有 TCP 报头,没有载荷,在这个报头里,确认序号字段,填写了 1001 ,意思就是 < 1001 的数据,B 已经收到了,接下来 A 就要从 1001 开始往后发送!!!
如何区分,一个报文是普通报文还是应答报文呢?
在 TCP 报头里有六个非常重要的 bit 位,其中第二位 ACK 就表示是否是应答报文
- ACK 为 0 表示不是应答报文!!
- ACK 为 1 表示是应答报文!!!
在确认应答的情况下,如果收到了 ACK 就好办,如果没收到呢?还需要通过其他途径来处理!
🍂二. 超时重传
ACK 没有收到的时候,不应立即放弃,需要重新再发一遍。
网络的环境是非常复杂的,尤其是有时候网络会拥堵,拥堵就可能导致丢包
丢包是 “无差别” 的,任何一个数据报,都有可能会丢包。发送的普通的报文是可能丢的;发送的 ACK 也是可能丢的。
业务数据丢了:
ACK 丢了:
业务数据已经到了主机 B 了,反馈的 ACK 没有回过去,发送方等待了一会儿之后,就触发了重传。对于发送方来说,无法区分是业务数据丢了还是 ACK 丢了,因此发送方能做的就是达到一定时间之后,就重传。
那么 TCP 协议需要能够识别出那些包是重复的包,并且把重复的丢弃掉。这时候我们可以利用前面提到的序列号,就可以很容易做到去重的效果。B 知道自己都收到了哪些数据,当发现又收到了一份之前的数据,就会自动丢弃,保证应用层读到的数据是不重复的。
但是在数据转发过程中,可能会发生很多种可能:
- 本来是 1 - 1000 -> 1 - 500
- 本来是 1 - 1000 -> 1 - 2000
- 本来是 1 - 1000 -> 1 - 1000(内容变了)
数据发送出去之后,就会同时发送数据内容 + 校验和,数据接收方会按照同样的规则,再算一次校验和,并且和收到的校验和作比较。在任意传输过程中,某个中间节点,发现校验和不对,都会主动触发丢包。
超时重传机制下,发一个数据,丢包了,重传数据,是否还会丢包呢?当然会!比如我们概率来算,丢包几率为 10% ,连续两次丢包概率就是 10% * 10% = 1%。
丢包操作,还有一个超时时间,超时时间具体是多少,在操作系统内核是可以配置的。
超时的时间如何确定?
TCP为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间。
以上两种机制可以认为是保证 TCP 可靠性的,最核心的机制
🍁三. 连接管理
在正常情况下,TCP要经过三次握手建立连接,四次挥手断开连接
主动的那一方是客户端,三次握手,一定是客户端先发起的
中间有两步,不过都是从服务器到客户端,所以合并为一次。经历了这四次交互,就完成了建立连接的过程。两对操作,客户端和服务器,相互给对方发送了一个 SYN ,再互相给对方发送了一个 ACK ,中间的两次合并为一次,所以称为 “三次握手”。
由我们的标志位可知,第五个标志位 SYN 是同步报文,第二个标志位 ACK 是确认报文,所以合并的那个报文第二个和第五个报文都是 1 。
四次挥手:断开连接的过程。(客户端和服务器都可以主动!)
和三次握手不同,四次挥手看起来也是双方各自给对方发送 FIN ,各自给对方发送 ACK,这里是四次挥手,中间两次不一定能合并!!
因此,B 发送 FIN 和 发送 ACK 之间会有不可忽略的时间间隔!正因为有了时间间隔,就不能合并!(但是 TCP 中还有 延时应答和捎带应答 会可能造成合并,后文详解)
🌿四. 滑动窗口
- 提高传输效率的机制
刚才我们讨论了确认应答策略,对每一个发送的数据段,都要给一个ACK确认应答。收到ACK后再发送下一个数据段。这样做有一个比较大的缺点,就是性能较差。尤其是数据往返的时间较长的时候。
TCP 在可靠性的前提下,尽可能的提高效率
原图:每次传输都需要等待 ACK,收到 ACK 再发下一条数据
改动后:不再是一次发送一条,等待一条了,而是一次发送一批,等待一批 ACK 。
窗口大小:在不等待的前提下,最多一次发送 N 条数据。(N 就是窗口大小)
其中灰色的一块一块的区域都是一个 TCP 数据报。其中白色的区域就是批量发送的。如图一中 发送了 1001-2000 2001-3000 3001-4000 4001-5000 在针对这四个数据报,等待 ACK ,当 2001 ACK 回到 A 的时候,此时 1001-2000 这个数据就已经被对方收到了,就可以继续发送 5001-6000 这个数据了。
每次收到一个 ACK ,这里的窗口,都会对应的往后移动(继续发后续的数据了)
如果出现了丢包,如何进行重传?这里分两种情况讨论。
-
情况一:数据包已经抵达,ACK被丢了。(丢包是个小概率事件,肯定是不会全丢的)
如果 1001 丢了,2001 到了,此时对于 A 来说,就知道 1-1000 这个数据也是到了的,最后一个会覆盖前一个! -
情况二:数据包就直接丢了
数据包丢了,肯定要重传!!!啥时候触发重传,怎样告知发送方要重传?
如上主机 A 发了半天之后,发现好几个连续的 1001,就明白了 1001 可能丢失了,接下来 A 就会重传 1001 这个数据了!此处的原则是哪条丢了就重传哪条,已经传输过的数据就不用再重传了,不必重复传输!快速重传(不是重传的有多快,而是没有多余的冗余动作)
滑动窗口能提高效率,指的是相比于没有滑动窗口,普通的确认应答。但是如果和无可靠性的传输相比(UDP),效率还是要差一些。与其说它是提高效率,不如说它是在补救低效率。
🌻五. 流量控制
- 本质就是对滑动窗口的制约
滑动窗口,窗口大小越大,发送速率就越快!流量控制,就是针对发送速率进行制约!
整体的传输速率 = 发送速率 & 接收速率
要做的是,让发送速率和接收速率相当(步调一致)
接收速率如何衡量?
图中圈出来的部分操作的快慢就是衡量接收速率快慢的(和应用程序代码相关)
举例:
流量控制,就是通过接收缓冲区剩余空间大小来作为下一次发送时候的窗口大小
接收方如何把接收缓冲区剩余空间大小告知发送方呢?
🍀六. 拥塞控制
流量控制,站在接收方的角度,来控制发送速率。但是整体的传输,其实不光有发送方和接收方,还有中间一系列用来转发的设备!
对于拥塞控制,采取的办法是做实验。通过实验的方式,找到一个合适的窗口大小!
- 刚开始按照小的窗口来发送
- 如果不丢包,说明网络中间环境比较畅通,就可以逐渐放大发送窗口的大小
- 放大到一定程度,速率已经比较快,网络上就容易出现拥堵,进一步出现丢包!当发送方发现丢包之后,就减小发送的窗口~
反复在 2-3 之间循环!这个过程就达到了一个 “动态平衡”
- 发送速率不慢,接近了能承载的极限
- 同时还可以尽量减少丢包
- 还能够适应网络环境的动态变化
上述测试是定性测试,如果是定量测试呢?(TCP 实现的时候,也是有明确的策略的,拥塞控制,窗口大小变化策略)
初始时候,拥塞窗口,从一个很小的数字开始,指数增长~(慢开始),刚开始的时候网络环境是否拥堵我们不知道!先拿一个小的速率发送,是稳健的做法!如果窗口大小到达阈值之后,就不再指数增长了,变成了线性增长。当线性增长达到一定程度之后,此时就可能丢包,这个时候直接把窗口大小回归到一个特别小的窗口,重复上述的指数增长 / 线性增长的过程,同时,会把刚才线性增长的阈值进行调整。
💐七. 延迟应答
- 让流量控制别限制的太狠
也是一个用来提高效率的机制,延时应答则是让窗口能大一些!在流量控制中,通过 ACK 告知对方,窗口大小(接收缓冲区的空余空间)是多少合适
在这个等待的时间中,应用程序不停的在消费接收缓冲区(如果立即返回 ACK ,可能缓冲区剩余空间是 5 KB,稍等一会儿(例如500ms),在这个时间里,应用程序就可能取走了很多数据,缓冲区的空余空间可能 100 KB了)
在接收缓冲区少了一个 1001 应答报文,在延时应答的机制下,ACK 不一定要和发送的数据报一一对应,少点也可以,毕竟 2001 涵盖了 1001
🌱八. 捎带应答
在延迟应答的基础上,我们发现,很多情况下,客户端服务器在应用层也是 “一发一收” 的。意味着客户端给服务器说了 “How are you”,服务器也会给客户端回一个 “Fine, thank you”;那么这个时候ACK就可以搭顺风车,和服务器回应的 “Fine,thank you” 一起回给客户端
正常情况下,ACK 是收到请求之后,内核立即返回的;响应数据,则是应用程序代码发送的。所以他们是处于不同的时机发生的,不同的时机,就不能把上个 ACK 和下个响应报文合并。但是上面的延时应答,延时一会儿,就可能和返回响应的数据,时间上就重合了。
所以在上面的延时应答的条件下,在四次挥手中,中间的 ACK 是可能和下面的 FIN 一起发的,四次挥手就可能变成三次!
🌾九. 面向字节流
面向字节流,指的是读写载荷数据的时候,是按照 “字节流” 的方式来读取的。TCP 数据报,本身仍然是一个一个 “数据报” 这样的方式来传输的。(应用程序这里是感知不到从哪里到哪里是一个数据报的)
此时,应用程序,在读取数据的时候,就可以很灵活的进行了,可以一次读取 M 个字节,分 N 次读。
面向字节流的最核心问题:粘包问题!
- 如果一个 TCP 连接,里面只传了一个应用层数据报,这个时候不会粘包(短连接)
- 如果一个 TCP 连接,里面传输多个应用层数据报,这个时候就容易区分不清,从哪到哪是一个完整的应用层数据!
只要是面向字节流的传输,都有粘包问题(文件读写)
粘包问题解决方案:(在应用程序代码中,明确包之间的边界)
- 使用分隔符
- 约定长度
通过上述方式,就可以明确从哪里到哪里,是一个完整的应用层数据报!
粘包问题,根本原因,是因为 TCP 面向字节流,但是直接影响应用层代码!
🌴十. 异常处理
TCP 连接出现异常的时候,如何处理?
- 主机关机(按照固定的程序关机)
按照程序关机,会先杀死所有的用户进程(也就包括咱们自己写的 tcp 程序)
杀死进程 => 释放进程 PCB => 释放文件描述符表上对应的文件资源(相当于调用 close)
这个时候就会触发 FIN ,开启四次挥手的流程!这里的异常比较好处理~
如果挥手挥完了,继续关机没事;如果挥手没挥完,就已经关机了,对端重传 FIN 若干次,没有响应,也就放弃了。
- 程序崩溃
同上,程序是正常关闭,还是异常崩溃,都会释放 PCB,都会释放文件描述符表(相当于调用 close)
也还是会正常四次挥手(虽然进程没了,但是本身 TCP 连接也是内核负责,内核仍然会继续完成后续的挥手过程)
- 主机掉电(突然拔电源)
笔记本还好,有内置电源;台式电脑就直接没了,来不及挥手。
-
接收方掉电,对方尝试发送数据,发现没有 ACK,尝试重传,重传几次,仍然没有 ACK,发送方尝试重新建立连接,如果重新建立也不成,认为是当前网络出现了严重问题,也就自然放弃了。
-
发送方掉电,接收方就在等待发送方发送数据,由于发送方掉电了,这个数据就发不过来,接收方不知道是对方没发还是对方出了问题(接收方区分不了)。如果接收方一段时间没有接收到数据,就会定期的给发送方,发送 “心跳包” ,接收方给发送方发一个特殊的报文(ping),对方返回一个特殊的报文(pong),如果这个东西有了,就认为对方是正常的状态,如果 ping 没有回应的 pong ,就认为对方挂了。
- 网线断开
和主机掉电相同
🎄总结
- 前三个是保证可靠性机制
- 下一个是提高效率的机制
- 下两个是保证可靠性机制
- 下两个是提高效率的机制
- 下两个是其他方面的问题
拓展:TCP 和 UDP 对比?
什么时候使用 UDP 什么时候使用 TCP?
- 如果需要关注可靠性传输,优先考虑 TCP
- 如果传输的单个数据报比较大(UDP 报文上限是 64kb)优先考虑 TCP
- 使用 UDP,对于可靠性传输要求不高,但是对于性能要求很高(同一个机房内部的主机之间通信,网络环境简单,宽带充裕,并且又希望主机间通信能够足够快)
- 如果是需要进行 “广播” ,优先考虑 UDP(一个发送方,N 个接收方)(TCP 广播就需要在应用层打开多个连接的方式来实现…)