✨个人主页:bit me👇
✨当前专栏:Java EE初阶👇
🍃一. 确认应答
TCP 诞生的初衷就是可靠传输
可靠传输是 TCP 最核心的部分,TCP 内部的很多机制,都是在保证可靠传输。(可靠传输是发了之后我知道对方收没收到,而不是 100% 能收到(可能没收到 --> 网线断了))
可靠性如何保证的呢?确认应答 !!
此处说的 “可靠性” 和 “安全性” 没有任何关系,千万不能说 TCP 这个可靠的通信协议,说成 “安全的” 通信协议。安全性:指的是,数据被截获之后,不容易被理解内部的意思或者被篡改,通过加密来做到的。
确认应答,要针对数据进行编号,然后才能明确,应答报文是在应答哪个数据,应对了网络传输的 “后发先至”。
TCP 就引入了 “序号”
32 位序号给发送的每个数据,都进行编号了,32 位确认序号表示:如果当前报文是一个普通的报文,确认序号不生效;如果当前报文是应答报文,确认序号就表示应答的是哪个普通报文。
报文的字节:
由于 TCP 是面向字节流的,编号的时候,也不是说按照 “条” 的方式编的,而是按照字节来编。
这一口气发了 1000 个字节的数据(一个 TCP 的数据报,长度是 1000,序号是 1),TCP 报头中的序号是 1 ,整个报文长度是 1000,就相当于把序号 1 - 1000 的这些字节都发过来了。
应答报文中的确认序号,就是 1001。应答报文,可以视为只有 TCP 报头,没有载荷,在这个报头里,确认序号字段,填写了 1001 ,意思就是 < 1001 的数据,B 已经收到了,接下来 A 就要从 1001 开始往后发送!!!
此处有人会问,TCP 不是传输的字节流吗?怎么传输的数据报?
例如在我们的 UDP 中:
B 这边调用一个 recv 方法:
- 第一次调用读取的肯定是 1111
- 第二次调用读取的肯定是 2222
- 第三次调用读取的肯定是 3333
每次调用 recv 方法就是从接收缓冲区里取走一个数据,UDP 的接收缓冲区,相当于一个链表,里面有三个节点,每次读,都以一个节点为单位
这就是面向数据报
在 TCP 中:
用 InputStream.read(buffer)
- byte[1] buffer 读出来的就是 1
- byte[3] buffer 读出来的就是 111
- byte[5] buffer 读出来的就是 11112
- byte[7] buffer 读出来的就是 1111222
TCP 的接收缓冲区,更像是一个数组,若干个 TCP 数据报的载荷会一直追加到这个数组里,融为一体。
这就是面向字节流
面向字节流和面向数据报,主要是影响代码咋写(影响着应用层!!!),而在传输层这里,仍然是一个一个报文来传输数据的。(传输了报文,不意味着 “面向数据报”,两个不同的概念)
如何区分,一个报文是普通报文还是应答报文呢?
在 TCP 报头里有六个非常重要的 bit 位,其中第二位 ACK 就表示是否是应答报文
- ACK 为 0 表示不是应答报文!!
- ACK 为 1 表示是应答报文!!!
这六个 bit 位的各个含义:
- URG:紧急指针是否有效
- ACK:确认号是否有效
- PSH:提示接收端应用程序立刻从TCP缓冲区把数据读走
- RST:对方要求重新建立连接;我们把携带RST标识的称为复位报文段
- SYN:请求建立连接;我们把携带SYN标识的称为同步报文段
- FIN:通知对方,本端要关闭了,我们称携带FIN标识的为结束报文段
在确认应答的情况下,如果收到了 ACK 就好办,如果没收到呢?还需要通过其他途径来处理!
🍂二. 超时重传
ACK 没有收到的时候,不应立即放弃,需要重新再发一遍。
网络的环境是非常复杂的,尤其是有时候网络会拥堵,拥堵就可能导致丢包
丢包是 “无差别” 的,任何一个数据报,都有可能会丢包。发送的普通的报文是可能丢的;发送的 ACK 也是可能丢的。
业务数据丢了:
ACK 丢了:
业务数据已经到了主机 B 了,反馈的 ACK 没有回过去,发送方等待了一会儿之后,就触发了重传。对于发送方来说,无法区分是业务数据丢了还是 ACK 丢了,因此发送方能做的就是达到一定时间之后,就重传。
这个情况下,主机 B 收到了两份 1 - 1000 这个数据!此时,如果在应用层调用 read ,读出来的是 1 份数据好,还是读出两份一样的数据好?
当然是一份数据好,毕竟两份数据不是咱想要的。
那么 TCP 协议需要能够识别出那些包是重复的包,并且把重复的丢弃掉。这时候我们可以利用前面提到的序列号,就可以很容易做到去重的效果。B 知道自己都收到了哪些数据,当发现又收到了一份之前的数据,就会自动丢弃,保证应用层读到的数据是不重复的。
但是在数据转发过程中,可能会发生很多种可能:
- 本来是 1 - 1000 -> 1 - 500
- 本来是 1 - 1000 -> 1 - 2000
- 本来是 1 - 1000 -> 1 - 1000(内容变了)
数据发送出去之后,就会同时发送数据内容 + 校验和,数据接收方会按照同样的规则,再算一次校验和,并且和收到的校验和作比较。在任意传输过程中,某个中间节点,发现校验和不对,都会主动触发丢包。
内容变了 / 长度变了 校验和也就变了,就可以通过校验和知道这里的变化,发现数据有问题就丢弃!
超时重传机制下,发一个数据,丢包了,重传数据,是否还会丢包呢?当然会!比如我们概率来算,丢包几率为 10% ,连续两次丢包概率就是 10% * 10% = 1%。
丢包操作,还有一个超时时间,超时时间具体是多少,在操作系统内核是可以配置的。
- 第一次丢包,超时时间是 T1
- 第二次丢包,超时时间是 T2
T2 > T1 这里等待的时间间隔随着时间的推移越来越大,连续两次没发过去,意味着当前单次丢包的概率已经相当大了,很可能是网络上遇到了非常严重的事故,短期内恢复不了,发送的再频繁也没用。超时重传也不会无限制的重传下去,尝试几次之后,仍然无法传送过去,此时就会放弃重传,然后尝试断开重连,如果重连还没连上去,就彻底放弃了。
超时的时间如何确定?
- 最理想的情况下,找到一个最小的时间,保证 “确认应答一定能在这个时间内返回”。
- 但是这个时间的长短,随着网络环境的不同,是有差异的。
- 如果超时时间设的太长,会影响整体的重传效率;
- 如果超时时间设的太短,有可能会频繁发送重复的包;
TCP为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间。
- Linux中(BSD Unix和Windows也是如此),超时以500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍。
- 如果重发一次之后,仍然得不到应答,等待 2*500ms 后再进行重传。
- 如果仍然得不到应答,等待 4*500ms 进行重传。依次类推,以指数形式递增。
- 累计到一定的重传次数,TCP认为网络或者对端主机出现异常,强制关闭连接。
以上两种机制可以认为是保证 TCP 可靠性的,最核心的机制
🍁三. 连接管理
- 网络知识中,最高频的考点,没有之一!!!
- 谈到操作系统,最高频的考题是进程和线程的区别
- 谈到网络,最高频的考点是 TCP 的三次握手四次挥手
- 谈到数据结构,最高频的考点是哈希表
在正常情况下,TCP要经过三次握手建立连接,四次挥手断开连接
主动的那一方是客户端,三次握手,一定是客户端先发起的
中间有两步,不过都是从服务器到客户端,所以合并为一次。经历了这四次交互,就完成了建立连接的过程。两对操作,客户端和服务器,相互给对方发送了一个 SYN ,再互相给对方发送了一个 ACK ,中间的两次合并为一次,所以称为 “三次握手”。
为啥要和并为一次传输?每次报文传输,都要经过一系列的封装分用!分成两个包来发就比一个包代价大很多。
由我们的标志位可知,第五个标志位 SYN 是同步报文,第二个标志位 ACK 是确认报文,所以合并的那个报文第二个和第五个报文都是 1 。
- 为啥要三次握手?(意义 / 初心)
- 三次握手,认为是一种保证可靠性的机制!这个东西相当于 “投石问路” ,在正式通信之前,先确认好通信链路是否通畅!如果通信链路不通畅,后续大概率要丢包!
- 让通信双方协商一些重要的参数(比如一次连接中不一定是从序号 1 开始的。也例如结婚)
2. 为啥三次握手要握三次呢?四次行不行?两次行不行?
四次可以,但是没必要,因为效率低;两次铁定不行,三次握手也是在验证通信双方的发送能力和接收能力是否正常!
四次挥手:断开连接的过程。(客户端和服务器都可以主动!)
和三次握手不同,四次挥手看起来也是双方各自给对方发送 FIN ,各自给对方发送 ACK,这里是四次挥手,中间两次不一定能合并!!
B 返回 ACK 是操作系统内核行为,操作系统内核收到 FIN 之后,就会立即返回 ACK ,而接下来 B 的 FIN 是用户代码的行为,用户在代码中调用 socket.close 方法,才会触发 FIN 。
因此,B 发送 FIN 和 发送 ACK 之间会有不可忽略的时间间隔!正因为有了时间间隔,就不能合并!(但是 TCP 中还有 延时应答和捎带应答 会可能造成合并,后文详解)
图中有三部分信息:
- 三次握手四次挥手中间数据传输流程
- 三次握手四次挥手过程中 TCP 状态转换
- 每个环节涉及到的 socket.api
TCP 的状态比多线程还多很多:(无需都背下来)
- LISTEN 服务器的状态
服务器已经启动完毕,已经绑定端口成功!(手机开机完毕,信号良好,随时可以给我打电话)
- ESTABLISHED
连接建立好了之后,可以进行后续通信,打电话号码拨通,对方已经接听了,接下来就可以随时说话了
- CLOSE_WAIT
被动接受 FIN 的一方,进入 CLOSE_WAIT ,这个状态就是自己收到了 FIN 也返回了 ACK ,在自己发送 FIN 之前处在的状态。调用 close 方法,就是在发送 FIN,CLOSE_WAIT 意思就是 wait close ,等待代码中调用 close 方法,发送 FIN。(如果在服务器发现大量 CLOSE_WAIT 说明是代码 bug !!哪里忘记了 close socket
- TIME_WAIT
主动发起 FIN 的一方,会进入 TIME_WAIT 。主动的这一方,收到对方的 FIN,并且返回 ACK 之后,就会进入这个状态,而不是直接进入 CLOSED 状态,在 TIME_WAIT 状态下停留一定的时间,才会进入 CLOSED 彻底释放连接。(TIME_WAIT 的意义,就是防止最后一个 ACK 丢失,万一最后一个 ACK 丢失了,在 TIME_WAIT 状态下,还可以重传,而如果连接释放了,就没得重传了!!)
如:A 和 B 之间传输,到最后的一次相互传输中,在 A 这里如果是 CLOSED ,最后 A 给 B 返回数据的时候万一丢失了包,A 连接释放了,此时 B 会触发超时重传,B 重传的 FIN 就会没人来处理。如果在 A 这里是 TIME_WAIT ,等待一段时间没见到 B 重传的话,A 就可以放心的释放连接了(如果 B 需要重传早就重传了)
TIME_WAIT 等待的时间是 2 MSL(操作系统中的一个配置参数)MSL 表示两个主机之间,数据从一边到另一边所花费的最大时间(一般来说 60s / 30s / 120s…)(为啥是 2 MSL ?等待的时间要包含 ACK 从 A -> B 重传的 FIN 从 B -> A)
🌿四. 滑动窗口
- 提高传输效率的机制
刚才我们讨论了确认应答策略,对每一个发送的数据段,都要给一个ACK确认应答。收到ACK后再发送下一个数据段。这样做有一个比较大的缺点,就是性能较差。尤其是数据往返的时间较长的时候。
可靠性和效率是冲突的,保证可靠性肯定会影响到效率的!
TCP 在可靠性的前提下,尽可能的提高效率
提高效率的机制,本质就是把等待 ACK 的时间重叠起来,减少了等待时间,就相当于提高了效率
原图:每次传输都需要等待 ACK,收到 ACK 再发下一条数据
改动后:不再是一次发送一条,等待一条了,而是一次发送一批,等待一批 ACK 。
窗口大小:在不等待的前提下,最多一次发送 N 条数据。(N 就是窗口大小)
这里的 N 越大,则同时批发量的数据就越多,传输效率就越高!但是 N 也不是越大越好。传输效率 = 发送效率 & 接收效率。
这一批的数据会不会乱序?
网络上任意传输的数据,都可能会出现后发先至的情况!TCP 就会在接收缓冲区里"整队"(TCP 数据上是带有序号的)接收方就可以根据序号,来对数据进行重新排序。
其中灰色的一块一块的区域都是一个 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),效率还是要差一些。与其说它是提高效率,不如说它是在补救低效率。
🌻五. 流量控制
- 本质就是对滑动窗口的制约
滑动窗口,窗口大小越大,发送速率就越快!流量控制,就是针对发送速率进行制约!
整体的传输速率 = 发送速率 & 接收速率
如果发送速率 > 接收速率,这个时候继续提高发送速率,就不能够提高整体的效率了,反而会因为接收方丢包,触发更多的重传,反而还降低了速率。
要做的是,让发送速率和接收速率相当(步调一致)
发送速率:发送数据的时候窗口大小,用于衡量发送速率
接收速率如何衡量?
图中圈出来的部分操作的快慢就是衡量接收速率快慢的(和应用程序代码相关)
举例:
流量控制,就是通过接收缓冲区剩余空间大小来作为下一次发送时候的窗口大小
接收方如何把接收缓冲区剩余空间大小告知发送方呢?
可以在 ACK 这个报文中带上这个信息
当前是 ACK 报文的时候会生效,这个窗口大小,就表示了接收缓冲区的剩余空间大小,根据这个大小,就可以进一步的影响到发送速率了
16位表示的最大数值 64 KB 是否意味着窗口大小最大就是 64 KB 呢?
不是!我们可以有选项,也可以没有,可以有一个,也可以有多个,这里有一个特殊的字段,窗口扩大因子~,窗口扩大因子可以是2,可以是4,可以是任何数,相乘即可,如果没有窗口扩大因子,默认是1。
需要注意的是在右边接收缓冲区满了的情况下,窗口大小就会被填成 0 ,此时发送方就会暂停发送数据!但是这里的暂停不会一直暂停,停了一会儿之后,会尝试发送一个 “探测包”。
🍀六. 拥塞控制
流量控制,站在接收方的角度,来控制发送速率。但是整体的传输,其实不光有发送方和接收方,还有中间一系列用来转发的设备!
控制 A 发的快慢,不仅考虑 B 的接收能力,也要考虑中间设备的转发能力!
衡量 B 的处理能力,是使用了 B 的接收缓冲区的剩余空间
想要衡量中间的设备,咋办?
- 中间的设备都有几个?
- 中间的设备各个参数是啥?
- 两次传输,经历的中间设备是否相同?
…
对于拥塞控制,采取的办法是做实验。通过实验的方式,找到一个合适的窗口大小!
- 刚开始按照小的窗口来发送
- 如果不丢包,说明网络中间环境比较畅通,就可以逐渐放大发送窗口的大小
- 放大到一定程度,速率已经比较快,网络上就容易出现拥堵,进一步出现丢包!当发送方发现丢包之后,就减小发送的窗口~
反复在 2-3 之间循环!这个过程就达到了一个 “动态平衡”
- 发送速率不慢,接近了能承载的极限
- 同时还可以尽量减少丢包
- 还能够适应网络环境的动态变化
流量控制 和 拥塞控制 都是通过控制窗口大小,来制约发送方的发送速率的,在保证可靠性的前提下,尽量提高一下发送的速度;都能影响发送方滑动窗口大小!最终的滑动窗口大小,就取决于流量控制的窗口和拥塞控制的窗口的 " 较小值 " 。
- 如果是拥塞控制的窗口大,流量控制的窗口小,中间的节点转发能力强,接收端的代码,处理的慢。
- 如果是拥塞控制的窗口小,流量控制的窗口大,中间的节点转发能力弱,接收端的代码,处理的快。
其中拥塞控制的窗口大小是发送方自己做实验做出来的,流量控制的窗口大小是接收方通过接收缓冲区剩余空间大小,通过 ACK 报文的报头,返回给发送方的。最终发送方下一次发送窗口的大小,就是通过这两个值的较小值来确定的。
上述测试是定性测试,如果是定量测试呢?(TCP 实现的时候,也是有明确的策略的,拥塞控制,窗口大小变化策略)
初始时候,拥塞窗口,从一个很小的数字开始,指数增长~(慢开始),刚开始的时候网络环境是否拥堵我们不知道!先拿一个小的速率发送,是稳健的做法!如果窗口大小到达阈值之后,就不再指数增长了,变成了线性增长。当线性增长达到一定程度之后,此时就可能丢包,这个时候直接把窗口大小回归到一个特别小的窗口,重复上述的指数增长 / 线性增长的过程,同时,会把刚才线性增长的阈值进行调整。
上述策略是一种经典策略,现实中还有一些更优的改进方法
💐七. 延迟应答
- 让流量控制别限制的太狠
也是一个用来提高效率的机制,延时应答则是让窗口能大一些!在流量控制中,通过 ACK 告知对方,窗口大小(接收缓冲区的空余空间)是多少合适
在这个等待的时间中,应用程序不停的在消费接收缓冲区(如果立即返回 ACK ,可能缓冲区剩余空间是 5 KB,稍等一会儿(例如500ms),在这个时间里,应用程序就可能取走了很多数据,缓冲区的空余空间可能 100 KB了)
这种发送方式是滑动窗口来发送的,发送方是在批量发送数据,所以不会对发送方等待时间造成很大影响,整体影响不大
在接收缓冲区少了一个 1001 应答报文,在延时应答的机制下,ACK 不一定要和发送的数据报一一对应,少点也可以,毕竟 2001 涵盖了 1001
🌱八. 捎带应答
在延迟应答的基础上,我们发现,很多情况下,客户端服务器在应用层也是 “一发一收” 的。意味着客户端给服务器说了 “How are you”,服务器也会给客户端回一个 “Fine, thank you”;那么这个时候ACK就可以搭顺风车,和服务器回应的 “Fine,thank you” 一起回给客户端
正常情况下,ACK 是收到请求之后,内核立即返回的;响应数据,则是应用程序代码发送的。所以他们是处于不同的时机发生的,不同的时机,就不能把上个 ACK 和下个响应报文合并。但是上面的延时应答,延时一会儿,就可能和返回响应的数据,时间上就重合了。
响应在收到请求之后,多长时间之内返回?不确定!可能快(几个 ms),可能慢(几百 ms)。快的时候本来数据报文也要发送,就和 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 ,就认为对方挂了。
“心跳包” => 1. 周期性的 2. 判定对方是否存活的
“心跳包” 是非常重要的机制,TCP 里面有,应用程序有的时候也会实现心跳~
- 网线断开
和主机掉电相同
🎄总结
- 前三个是保证可靠性机制
- 下一个是提高效率的机制
- 下两个是保证可靠性机制
- 下两个是提高效率的机制
- 下两个是其他方面的问题
典型问题:如何使用 UDP 实现可靠传输?
在应用层代码里面,参考 TCP 策略来实现~(TCP 咋做 咱就咋做)
拓展:TCP 和 UDP 对比?文章来源:https://www.toymoban.com/news/detail-442139.html
什么时候使用 UDP 什么时候使用 TCP?文章来源地址https://www.toymoban.com/news/detail-442139.html
- 如果需要关注可靠性传输,优先考虑 TCP
- 如果传输的单个数据报比较大(UDP 报文上限是 64kb)优先考虑 TCP
- 使用 UDP,对于可靠性传输要求不高,但是对于性能要求很高(同一个机房内部的主机之间通信,网络环境简单,宽带充裕,并且又希望主机间通信能够足够快)
- 如果是需要进行 “广播” ,优先考虑 UDP(一个发送方,N 个接收方)(TCP 广播就需要在应用层打开多个连接的方式来实现…)
到了这里,关于【网络原理】TCP原理的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!