【JavaEE初阶系列】——1.5W字让你深度了解TCP中十大主要机制

 目录

🚩TCP协议段格式

🚩TCP的特性

🚩TCP原理

🎈确认应答机制(安全机制)

👩🏻‍💻应答报文ack

👩🏻‍💻引入序号和确认序号

👩🏻‍💻如果区分数据包是普通数据还是ack应答数据

🎈超时重传机制(安全机制)

👩🏻‍💻丢包的两种情况

👩🏻‍💻发送方,何时进行重传以及如何确定等待时间 

👩🏻‍💻接收缓冲区 (去重)

🎈连接管理机制(安全机制)

❗三次握手(建立连接)

👩🏻‍💻介绍syn 

👩🏻‍💻三次握手解决什么问题?四次握手两次握手行嘛? 

👩🏻‍💻如何区分是否是同步报文段

👩🏻‍💻三次握手的三大核心作用

❗四次挥手(断开连接)

👩🏻‍💻介绍FIN

👩🏻‍💻 中间两次交互能否合并

👩🏻‍💻延时应答(后面详细说明)

👩🏻‍💻TIME_WAIT

❗三次握手和四次挥手图

🎈滑动窗口(效率机制)

❗缩短确认应答的等待时间 

❗丢包

👩🏻‍💻ACK丢了

👩🏻‍💻数据包丢了

❗快速重传和超时重传的应用场景 

🎈流量控制 

❗如何量化衡量

❗TCP首部”16位窗口大小“字段

 🎈拥塞控制

🎈延时应答

🎈捎带应答

🎈面向字节流 

❗粘包问题

👩🏻‍💻如何解决粘包问题

🎈异常情况的处理

❗进程崩溃

❗主机关机 

❗主机掉电 

👩🏻‍💻心跳包

❗网线断开

🚩TCP和UDP之间的对比


TCP,即Transmission Control Protocol,传输控制协议。人如其名,要对数据的传输进行一个详细的控制。

🚩TCP协议段格式

  • 16位源端口号和16位目的端口号是和UDP是相同的。
  • 4位首部长度:TCP报头长度是不固定的(变长的),报头最短是20字节(没有选项),报头最长,是60字节(选项最多是40字节),此处是4字节(选项都是4字节一个单位的)
  • 保留(6位) UDP的长度是无法修改的,保留位就是我现在不用,但是先占个位置,后面如果有需要,再使用(留下了扩展的余地)
  • 16位检验和是和UDP一样的,都是检验是否原始数据和现在的数据相同。

  • /目的端口号:表示数据是从哪个进程来,到哪个进程去;
  • 32位序号/32位确认号:后面详细讲;
  • 4TCP报头长度:表示该TCP头部有多少个32bit(有多少个4字节);所以TCP头部最大长度是 15 * 4 = 60
  • 6位标志位:
  1. URG:紧急指针是否有效
  2. ACK:确认号是否有效
  3. PSH:提示接收端应用程序立刻从TCP缓冲区把数据读走
  4. RST:对方要求重新建立连接;我们把携带RST标识的称为复位报文段
  5. SYN:请求建立连接;我们把携带SYN标识的称为同步报文段
  6. FIN:通知对方,本端要关闭了,我们称携带FIN标识的为结束报文段
  • 16位窗口大小:后面再说
  • 16位校验和:发送端填充,CRC校验。接收端校验不通过,则认为数据有问题。此处的检验和不光包含TCP首部,也包含TCP数据部分。
  • 16位紧急指针:标识哪部分数据是紧急数据;
  • 40字节头部选项:暂时忽略;

上述的报头是有很多陌生的,所以接下来我们在介绍主要的十大机制中详细介绍这些协议段格式中的。


🚩TCP的特性

  • 有连接

  • 可靠传输 (可靠传输在代码中显示不出来)
  • 面向字节流

  • 全双工

可靠传输 ,是TCP最最核心的特性(初心) 

可靠传输,不是说,发送方把数据能够100%的传输给接收方。(这样要求太高了)。

真正的可靠传输就是

  • 发送方发出去数据之后,能够知道接收方是否接收到数据。
  • 一旦发现对方没有收到,就可以通过一系列的手段来“补救”。 

🚩TCP原理

TCP 对数据传输提供的管控机制,主要体现在两个方面: 安全和效率
这些机制和多线程的设计原则类似: 保证数据传输安全的前提下,尽可能的提高传输效率。

后面我们围绕这俩个方面展开TCP背后十大主要机制。


🎈确认应答机制(安全机制)

👩🏻‍💻应答报文ack

发送方:把数据发给接收方之后,接收方收到数据就会给发送方返回一个应答报文(acknowledge ,ack)

发送方,如果收到这个应答报文了,就知道自己的数据是否发送成功了。

就类似于和别人聊天,我问zyf吃饭了吗?zyf回答说我吃完饭了,zyf回答我就是应答报文。

但是这种情况有些理想化,如果出现这种情况就有问题了。

其实zyf的意思是他同意和我吃麻辣烫,但是不愿意跟我在一起,

但是现在的情况就是,他拒绝和我吃麻辣烫,但是同意了和我在一起。

后发的信息,先回答了。

这俩种情况,就说明了,实际上在网络传输数据可能会出现“后发先至”这种情况。

一个数据包在进行传输的过程中,走的路径可能是非常复杂的,不同的数据包,可能走不同的路线。就比如结婚结亲,如果亲家和娘家相隔很远,那么在路途中我们就会走着不同的路线,等到快到你家的时候,我们约定好一个地点集合,然后重新排好队。


👩🏻‍💻32位序号和确认序号

TCP在此处要完成俩个工作:

  • 确保应答报文和发出去的数据,能对上号,不要出现歧义
  • 确保在出现后发先至的现象时,能够让应用程序这边仍然按正确的顺序来理解数据

所以我们就开始介绍TCP协议段格式中的32位序号和32位确定序号。

这样的话,我就可以很清楚明白的说,zyf同意了和我吃麻辣烫,但是拒绝了和我在一起。

序号就是一个整数,大小关系,就描述了数据的先后顺序。

上面的图不够严谨,更准确地说,序号不是按照“一条俩条”方式来进行编号的,而是按照字节来编号的。 因为TCP是面向字节流的。

假设最开始传输的第一个字节,序号是1.(最开始传输的第一个字节有时候并不是1,看具体情况)
每一个ACK都带有对应的确认序列号,意思是告诉发送者,我已经收到了哪些数据;下一次你从哪里开始发。
  • 一个TCP数据包里一共有1000个字节的载荷数据。
  • 其中第一个字节的序号是1,就在TCP报头的序号字段中,写“1”。由于一共是1000个字节,此时最后一个字节的序号自然就是1000了,但是1000这样的数据并没有在TCP报头中记录。
  • TCP报头中记录的序号,是这一次传输的载荷数据中第一个字节的序号,剩下其他字节的序号,都需要依次的推出~在应答报文中,就会在确认序号字段中填写1001
  • 因为收到的数据是1-1000,所以 1001之前的数据,都被B收到了,或者可以理解成,B接下来向A索要1001开始的数据。 

通过特殊的ack数据包,里面携带的“确认序号”告诉发送方,哪些数据已经被确认收到了,此时发送方,就心中有数了,就知道了自己刚发的数据是到了还是没到——》可靠传输。

TCP的初心:是为了实现可靠传输,达成可靠传输的最核心的机制——》确认应答

后面都是根据应答来展开的,没有应答你怎么传输信息呢?


👩🏻‍💻如果区分数据包是普通数据还是ack应答数据

如何区分一个数据包是普通的数据,还是ack应答数据呢?

我们看到ACK那里

  • 如果那一位是 1,表示当前数据包是一个应答报文,此时该数据包中“确认序号字段”就能够生效。
  • 如果那一位是0,表示当前数据包是一个普通报文,此时数据包中的“确认序号字段”是不生效。

确认应答,是TCP最核心的机制,支持了TCP的可靠传输。

发送方发了多条信息,那么接收方应答的时候,可能会错峰应答,就是后发先至的情况。所以要想按照原先的顺序来理解数据,ack数据包里面携带的“确认序号”告诉来发送方,TCP是面向字节流的,TCP数据包中载荷了1000个字节数据,在接收方中如果应答报文是1001,那么心里有数了,就知道刚发的1000字节的数据是到了的,否则接收方怎么会收到1001的数据呢?此时就实现了可靠传输。


🎈超时重传机制(安全机制)

确认应答,描述的是一个比较理想的情况,如果网络传输过程中,出现丢包了,咋办呢?

发送方,势必就无法收到ACK了,使用超时重传机制,针对确认应答,进行补充。


👩🏻‍💻丢包的两种情况

由于丢包是一个“随机”的事件,因此在上述tcp传输过程中,丢包就存在两种情况。

1.传输的数据丢了

主机A发送数据给B之后,可能因为网络拥堵等原因,数据无法到达主机B;
如果主机A在一个特定时间间隔内没有收到B发来的确认应答,就会进行重发;
但是,主机 A 未收到 B 发来的确认应答,也可能是因为 ACK 丢失了
2.返回的ack丢了
因此主机 B 会收到很多重复数据。那么 TCP 协议需要能够识别出那些包是重复的包,并且把重复的丢弃掉。
这时候我们可以利用前面提到的序列号,就可以很容易做到去重的效果。
无论出现上述哪种情况,发送方都会进行“重新传输”,第一次是丢了,重传一下试试,很大概率就可能传过去了。
假设,丢包概率是10%(比较大的数字了),在吃鸡/LOL,出现10%的丢包,这局游戏,你就玩不了了。
俩次传输都丢包的概率,是多少,10%*10%=>1%
重传操作,大幅度的提升了数据能够被传过去的概率。重传就是一个很好的丢包下的补救措施。

👩🏻‍💻发送方,何时进行重传以及如何确定等待时间 

发送方,何时进行重传? 

    发送方,发出去数据之后,会等待一段时间,如果这个时间之内,ack来了,此时就自然视为数据到达,如果达到这个时间之后,数据还没到,就会出发重传机制。

怎么确定等待时间呢?

  •  1.初始的等待时间,是可配置的,不同的系统上都不一定一样。也可以通过修改一些内核参数来引起这里的时间变化
  • 2.等待的时间,也会动态变化,每多经历一次超时,等待时间就会变长。

A——>B 发了一条数据,第一次,A等待ACK的时间,假设是50ms,此时如果达到50ms,还没有ack,A就重传。当A重传的数据,还是没有收到ack,第二次等待的时间就会比第一次的长,拉长不是无限拉长,重传若干次时,时间拉长到一定程度,认为数据再怎么重传也没用了,就放弃tcp连接(准确的说是会出发tcp的重置连接操作)重置连接错误后面会出现。


👩🏻‍💻接收缓冲区 (去重)

站在B的角度上,收到了两条一样的数据,收到重复数据,是否会给程序员带来一些bug?

比如发的是一条数据,收的时候收到两条数据(inputStream.read读出来的是两条一样的数据)

其实TCP已经非常贴心的帮我们把这个问题解决了。TCP会有一个“接收缓冲区”就是一个内存空间,会保存当前已经收到的数据,以及数据的序号。

接收方如果发现,当前发送方发来的数据,是已经在接收缓冲区中存在的(收到的是重复数据),接收方就会直接把这个后来的数据给丢弃掉,确保应用程序进行read的时候,读到的是只有一条数据。

接收缓冲区,不仅仅是能进行去重,还能重新排序,确保发送的顺序,和应用程序读取的顺序是一致的。(接亲,到了娘家村口,进行整队)


🎈连接管理机制(安全机制)

建立连接+断开连接。

面试中,最经典的问题:三次握手和四次挥手~~


❗三次握手(建立连接)

握手就相当于打招呼,打招呼的内容,没有实际意义,也比较简短,只是为了唤起对方的注意。tcp这里的握手,也是类似,也就是给对方传输一个简短的,没有业务数据的数据包,通过这个数据包,来唤起对方的注意,从而触发后续的操作。(这就是建立连接)

握手这个操作,不是TCP独有的,甚至不是网络通信独有的,计算机很多操作,都会涉及“握手”。比如:手机充电,不同的充电头,插上之后,手机这边显示的充电方式不一样,普通充电/快速充电。一插的过程,就要先握手。U盘等等。把一个支持3.0的U盘,慢慢插入到3.0的USB口里,有一定概率,握手成USB2.0的通信速度~~~(USB接口设计的bug)

TCP的三次握手,TCP在建立连接的过程中,需要通信双方一共“打三次招呼"才能完成连接建立的。

A想和B建立连接,A就会主动发起握手操作,实际开发中,主动发起的一方,就是所谓”客户端被动接收的一方,就是“服务器”。

上述之后,握手完成,此时A和B记录了对方的信息(也就是逻辑上的连接)

 建立连接的过程,其实是,通信双方都要给对方发起syn,也都要给对方反馈ack,一共4次握手,但是中间俩次,恰好合并成一次了。


👩🏻‍💻介绍syn 

介绍syn

同步报文段,就也是一个特殊的TCP数据包(没有载荷的,不携带业务数据的)

就相当于我在淘宝上,买个显卡,卖家给我先发个包裹,我收到包裹一打开,里面啥都没用,先和你握手。(就是先发个包裹看是否在途中运输过程中会出现问题),就是对一个内容进行试探。防止发生意外。


👩🏻‍💻三次握手解决什么问题?四次握手两次握手行嘛? 

三次握手是要解决什么问题?通过四次握手,是否可行?通过两次握手,是否可行呢?

四次握手可以,但是没有必要,俩个数据合并成一个数据,效率更高。

TCP初心,是为了实现“可靠传输”

进行确认应答和超时传输有个大前提,当前的网络环境是基本可用的,通畅的。

如果当前网络已经存在重大故障,此时,可靠传输,无从谈起。 


👩🏻‍💻如何区分是否是同步报文段

TCP协议段格式中SYN

  • 如果这一位是1,这个报文是一个同步报文段,
  • 如果这一位是0,则不是。

👩🏻‍💻三次握手的三大核心作用

投石问路,确认当前网络是否畅通的  syn

地铁系统,地铁晚上末班车结束之后,就休息了,第二天一早,6点左右,才开始发车,每天早上在正式载客之前,都会先有一趟空车,先跑一趟~~在这晚上休息的过程中,地铁铁路上是否会出现异常情况,假设,某一段铁路,塌方了,地铁开不过去了~~,所以三次握手syn是没有什么业务的,就是来确认当前网络是否畅通。

要让发送方和接收方都能确认自己的发送能力和接收能力均正常

比如我和zyf开黑,我们在不同的地方,我们只能用qq语音连麦,经常会出现 耳机/麦克风 不好使的情况。

  • 1.初始情况下,我们双方都不知道自己的耳机和麦克风的情况
  • 2.zyf听见“喵”,zyf就知道自己的耳机是ok的以及我的麦克风是ok的,   但是不知道自己的麦克风是否ok以及我的耳机是否ok
  • 3.我听到“喵喵”,我就知道了,我的耳机是ok的,zyf的麦克风是ok的, 进一步想,我收到喵喵,前提是zyf收到了喵,说明我的麦克风是ok的以及zyf的耳机是ok的。这样就证明了我的耳机和麦克风是ok的以及zyf的耳机和麦克风是ok的,设备都没有问题了。此时再发了,喵喵喵,就是把我掌握的情报,同步给zyf。
  • 4.zyf听到“喵喵喵”,前提是我收到了“喵喵”,zyf也知道了,设备都是好的。

让通信双方,在握手的过程中, 针对一些重要的参数,进行协商 

握手这里协商的信息,其实是由有好几种的,但是大家至少要知道,tcp通信过程种的序号从几开始,就是双方协商出来的。每次连接建立的时候,都会协商出一个比较大的,和上次不一样的值。

这种设定方式,是避免“前朝的剑,斩本朝的宫”。

有的时候,网络如果不太好,客户端和服务器之间可能会连接断开,再重新建立连接。

重连的时候,就可能在新的连接好了之后,旧连接的数据珊珊来迟~这种迟到的数据,应该是丢弃掉的,不应该让这个上个朝代的数据影响到本朝的业务逻辑。

如何区分数据是否来自上个朝代?就可以通过上述序号的规定来实现

如果发现收到的数据序号和当前正常数据的序号差异非常大,就可以判定为是上个朝代的数据就可以直接丢弃掉了



❗四次挥手(断开连接)

建立连接,一般都是客户端主动发起的,断开连接,客户端和服务器都可以主动发起。(就相当于,俩个人开始交往,基本上大多是男生主动追女生,但是分手的时候,可能男生主动提的也可能是女生开始提的)


👩🏻‍💻介绍FIN

FIN就叫做“结束报文段”


👩🏻‍💻 中间两次交互能否合并

和三次握手不同,此处的四次握手,能否把中间的两次交互合二为一?不一定!

不能合并的原因,ACK和第二个FIN的触发时机是不同的

ACK是内核响应的,B收到FIN就会立即返回ACK,第二个FIN是应用程序的代码触发B, 这边调用了close方法,才会触发FIN。

从服务器收到FIN(同时返回ACK),再次执行到close,发起FIN,这中间经历了多少时间,和多少行代码,是不确定的。FIN就会在socket对象close的时候被发起,可能是手动调用close,也有可能结束进程。

当服务器发出了FIN后,客户端立即返回ACK,这时候连接就断开了,就相当于A和B都把对端的信息删除了,(就相当于给对方拉黑了)


  • 前面的三次握手,ACK和第二个syn都是内核触发的,同一个时机,可以合并。
  • 这里的四次挥手,ACK是内核触发的,第二个FIN是应用程序执行close触发的,时机不相同,不能合并。 

(有没有一种可能,如果代码这里没有手动的close/或者执行不到,是不是第二个FIN就一直发不出去呢)——这是有可能的。

  • 如果是正常的四次挥手,那么就相当于一个成语”好聚好散“,正常的流程断开连接。
  • 如果是不正常的挥手(没有挥手四次),异常的流程断开连接(也是存在的)

就像离婚一样,正常的离婚,是双方都认可的,不正常的离婚,是一方认可,一方不认可,只能去起诉离婚。


👩🏻‍💻延时应答(后面详细说明)

TCP中还有一个机制,延时应答,能够拖延ACK的回应时间,一旦ACK滞后了,就会有机会和下一个FIN合并在一起了。


👩🏻‍💻TIME_WAIT

哪一方,主动断开连接,哪一方就会进入TIME_WAIT.

TIME_WAIT状态主要存在的意义:就是为了防止最后一个ACK丢失,留下后手。

如果最后一个ACK丢了,站在B的角度,B就会触发超时重传,重新把刚才的FIN给重新传一遍。

如果没有TIME_WAIT状态,就意味着A这个时候就已经真的释放连接了(closed),此时重传的FIN也就没人处理,没人能返回ACK了,B永远都收不到ACK了。

A这边使用TIME_WAIT状态进行等待,等待的这个时间,就是为了处理后续B重传的FIN,此时有重传的FIN来了,就可以继续返回ACK了,B这边的重传FIN才有意义。

TIME_WAIT等待多久呢?

假设网络上两个节点通信消耗的最大时间为MSL

此时TIME_WAIT的时间是2MSL。

2MSL是上限了,绝大部分的数据包不会达到这个时间的,就会比这个时间短很多。


❗三次握手和四次挥手图

我们要清楚三次握手和四次挥手的过程。 


🎈滑动窗口(效率机制)

知道效率没有那么高,所以需要用滑动窗口来弥补。——亡羊补牢

确认应答机制,超时重传机制以及连接管理机制这三个机制都是来保证TCP的可靠性的。

TCP的可靠传输,是会影响传输的效率的(多出一些等待ack的时间,单位时间内能传输的数据就少了)。

滑动窗口就让可靠传输对性能的影响,更少一点。

TCP只要引入了可靠性,传输效率是不可能超过没有可靠性的UDP的

TCP这里”效率机制“都是为了让影响更小,缩短和UDP的差距。


❗缩短确认应答的等待时间 

缩短确认应答的等待时间。

刚才我们讨论了确认应答策略,对每一个发送的数据段,都要给一个 ACK 确认应答。收到 ACK 后再发送下一个数据段。这样做有一个比较大的缺点,就是性能较差。尤其是数据往返的时间较长的时候。
既然这样一发一收的方式性能较低,那么我们一次发送多条数据,就可以大大的提高性能(其实是将多个段的等待时间重叠在一起了)。
  • 窗口大小指的是无需等待确认应答而可以继续发送数据的最大值。上图的窗口大小就是4000 个字节(四个段)。
  • 发送前四个段的时候,不需要等待任何ACK,直接发送;
  • 收到第一个ACK后,滑动窗口向后移动,继续发送第五个段的数据;依次类推;
  • 操作系统内核为了维护这个滑动窗口,需要开辟 发送缓冲区 来记录当前还有哪些数据没有应答;只有确认应答过的数据,才能从缓冲区删掉;
  • 窗口越大,则网络的吞吐率就越高;
批量传输数据,不等ack回来,直接再发下一个数据,批量传输,也不是”无限的“传输,批量传输也是存在一定的上限的,达到上限之后,再统一等待ack,不等待的情况下,批量最多发多少数据,这个数据量,称为”窗口大小“。

当前A——>B是批量的发了四份数据,此时B也要给A回应四组ACK,此时A已经达到了窗口大小,再收到ACK之前,不能继续往下发了。需要等待有ACK回来了之后,才能继续往下发。

回来一个ACK就继续发一个,窗口越大,等待的ack越多,此时传输效率也就越高。


❗丢包

TCP的初心是”可靠传输“,上述滑动窗口中,确认应答是可以正常工作的,但是,如果出现丢包了咋办?

这里的重传,和之前的超时重传,是有变化的。


👩🏻‍💻ACK丢了

情况一:数据包已经抵达,ACK被丢了

这种情况,不需要任何重传。

确认序号,表示含义是:当前序号之前的数据,已经确认收到了,下一个你应该从确认序号这里,继续发送。

我们看到上面的,虽然1001的ack丢了,但是2001的ack到了,说明2001之前的数据都已经确认传输成功了,涵盖了1001的情况。

此时3001的ack丢了,但是5001的ack到了,说明5001之前的数据都已经确认传输成功了,涵盖了5000之前的情况。


👩🏻‍💻数据包丢了

 由于前面的1001-2000这个数据没了,此时返回的ack仍然是索要1001,无论当前传输的数据具体是几,都在索要1001这个数据,此时主机A看啊都B这边连续的几个ack都是在索要1001,A就知道了,1001这个数据就是丢了,就重传了1001.1001-2000重传之后,顺序到达B索要的7001了。举个例子:就像有些同学问问题,发了好多消息,老师看到第一条消息,回复一个你截个图,看到第二条消息,又回复一个你截个图,看到第三条笑死,又回复一个你截个图,然后这个同学就知道了老师反复索要截图,然后这个同学就截了个图给老师,老师收到数据之后,就可以根据截图,结合同学的文字描述,大概率知道问题是什么了。

主机A需要知道哪个数据丢了、主机B也就得告诉A 是哪个数据丢了。

上述的重传过程中,并没有额外的冗余操作,哪个数据丢了,就重传哪个,没丢的数据则不需要重传,整个过程中都是比较快速的(快速重传)——滑动窗口下,超时重传的变种。


有人会疑惑,为什么数据包丢了,ack一直返回索要当前丢了的数据包,当收到该数据包的时候,我们就不需要知道之前是否接收到呢?直接就ack最近收到的数据包的后一位,你看上面6001-7000数据包发送给B,B此时也索要到了2001的数据了,这时候直接下一个是7001.

举个例子:人生的一般顺序

  • 大学毕业
  • 参加工作
  • 结婚
  • 生娃

有人问你大学毕业了嘛?然后回答说我已经有孩子了,那么问的人心里就领会到了,我肯定大学毕业了,也肯定参加工作赚钱了,也肯定结婚了。所以这就是。一旦索要到了1001的数据,那么就会覆盖前面7000 个数据。


 如果ack全都丢了呢?

平时丢包率达到10%,就已经非常非常高了

直接丢包率100%,此时相当于网线都断了,无从谈起”可靠传输“了/


❗快速重传和超时重传的应用场景 

  • 如果通信双方,传输数据的量比较小,也不频繁,就仍然是普通的确认应答和普通的超时重传。
  • 如果通信双方,传输数据的量比较大,也比较频繁,就会进入滑动窗口模式,按照快速重传的方式进行处理。

通过 滑动窗口的方式传输数据,效率是会提升的。窗口越大,传输效率就越大(一份时间,等待的ack更多了,总的等待时间更少了)

滑动窗口设置的越大,越好吗?

如果传输的速度太快,就可能会使接收方,处理不过来了,此时,接收方也会出现丢包,发送方还得重传,TCP前提使可靠性,可靠性的基础上,再提升传输效率。

所以引入 了流量控制,就是站在接收方的角度,反向制约发送方的传输速率。


🎈流量控制 

如果传输的速度过快,那么接收方就处理不过来了。

站在接收方的角度,反向制约发送方的传输速率。发送方发送的速率,不应该超过接收方的处理能力。

数据到达B的系统内核中,tcp socket对象上带有接收缓冲区,A->B发的数据,就会先到达B的接收缓冲区。B这边还有应用程序,就会调用read这样的方法,把数据从接收缓冲区中读出来,进一步的进行处理。(一旦数据被read了,就可以从接收缓冲区删除了)

生产者消费者模型.生产者A,消费者B的应用程序,交易场所是B的接收缓冲区(相当于一个阻塞队列),消费速度,就是所谓的”处理能力"(取决于B的应用程序代码是怎么写的)。


❗如何量化衡量

如何量化衡量呢?

直接通过接收方,缓冲区的剩余空间大小,作为衡量处理能力的指标

  • 剩余空间越大,意味着消费速度越快,处理能力就越强
  • 剩余空间越小,意味着消费速度越慢,处理能力就越弱 

接收方每次收到数据之后,都会把接收缓冲区剩余空间大小通过ack返回给发送方,发送方就会按照这个数据来调整下一轮的发送速度。进而解决了上述说的问题。

就像一桶水,一方是A对B发送的数据,一方是B的应用程序read的数据。

我们可以根据一桶水剩余的多少,来进行看自己发送多少数据(加多少水)。也可以通过水位来知道B 的read数据的速度了。


❗TCP首部”16位窗口大小“字段

  • 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 "窗口大小" 字段,通过ACK端通知发送端;
  • 窗口大小字段越大,说明网络的吞吐量越高;
  • 接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值通知给发送端;
  • 发送端接受到这个窗口之后,就会减慢自己的发送速度;
  • 如果接收端缓冲区满了,就会将窗口置为0;这时发送方不再发送数据,但是需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端。
接收缓冲区,总的空间是4000,收到1000数据之后,还剩3000,于是就把3000放到应答报文中,告诉发送方了。依据收到的3000,确定了这一轮发送的窗口大小。假设这个过程中,接收方的应用程序还没来得及处理任何数据呢?此时收到一个数据,接收缓冲区的剩余空间就缩小一分。反馈0,意味着告诉A我接受方这边已经满了,你暂时先别发数据 了,此时A确实就要暂时发送了,暂停多久呢?——虽然不传输业务数据了,然后会 周期性的发送一个”窗口探测包“,并不携带具体的业务数据,探测包就只是为了触发ack,为了查询当前接收方这边的接收缓冲区剩余空间。

 接收缓冲区剩余空间大小(这里是否是最大只能表示64kb?其实不是的),TCP报头中,选项部分里有一项是叫做”窗口扩展因子“,通过扩展因子,就可以让窗口大小表示一个更大的值。


 🎈拥塞控制

流量控制,是考虑接收方的处理能力,不仅仅是接收方,还有你整个通信的路径。

拥塞控制就是考虑/衡量 通信过程中中间节点的情况

这中间的转发过程中,任何一个节点,处理能力达到了上限,都可能会对发送方产生影响,都可能会影响到可靠传输。

关键问题:接收方的处理能力,很方便进行量化。

由于中间节点,结构更复杂,更难以直接的进行量化,因此就可以使用”实验“的方式,来找到个合适的值。

让A先按照比较低的速度先发送数据(小的窗口),如果数据传输过程中非常顺利,没有丢包,再尝试使用更大的窗口,更高的速度进行发送(一点一点的变化)。随着窗口大小不停的增大,达到一定程度,可能中间节点就会出现问题了,此时这个节点就可能会出现丢包。发送方发现丢包,就把窗口大小调整小,此时如果发现还是继续丢包,继续缩小, 如果不丢包了,就继续尝试变大。

再这个过程中,发送方不停的调整窗口大小,逐渐达成”动态平衡“。

这种做法,就相当于把中间节点,都视为”整体“,通过实验的方式,来找到中间节点的瓶颈在哪里。


 此处引入一个概念程为拥塞窗口

  • 发送开始的时候,定义拥塞窗口大小为1;
  • 每次收到一个ACK应答,拥塞窗口加1;
  • 每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小做比较,取较小的值作为
  • 实际发送的窗口;
像上面这样的拥塞窗口增长速度,是指数级别的。 " 慢启动 " 只是指初使时慢,但是增长速度非常快。
  • 为了不增长的那么快,因此不能使拥塞窗口单纯的加倍。
  • 此处引入一个叫做慢启动的阈值
  • 当拥塞窗口超过这个阈值的时候,不再按照指数方式增长,而是按照线性方式增长

  • 拥塞窗口:拥塞控制下,发送方应该按照多块的速度(多大的窗口大小)来进行传输 
  • 传输轮次:当前TCP是第几次发数据了
  • 慢开始/慢启动
  • 指数规律增长:每次都翻倍,指数增长,增长速度是很快,慢开始,就算开始的很慢,也不打紧,在接下来的几轮之内,窗口就会变得很大。在指数增长的过程中,如果达到阀值ssthresh,就要从指数增长,变成线性增长。
  • 网络拥塞(丢包):一旦触发丢包,就把窗口大小再缩小,重新进行前面的 慢开始-》指数增长—》线性增长,并且会根据当前丢包的窗口大小,重新指定线性增长阈值(为了避免指数增长,一下就达到了丢包的极限)

举个例子:谈了对象,就可以知道了。

后面又对TCP进行了改进。改进措施,不是一下让速度归零,而是仍然有一定的初始速度(省略了指数增长的过程)

流量控制和拥塞控制,都是在限制发送方的发送窗口的大小,最终时机发送的窗口大小,是取 流量控制 和 拥塞控制 中的窗口的较小值。


🎈延时应答

  • A把数据传给B ,B就会立即返回ACK给A 【正常】
  • 也有的时候,A传输给B ,此时B等一会再返回ACK给A 【延时应答】

本质上也是为了提高传输效率,发送方的窗口大小,就是传输效率的关键。(所以提高传输效率,就是扩大窗口大小,怎么扩大窗口大小呢?就需要延时应答)

流量控制这里,就是根据接收缓冲区的剩余空间,来决定发送速率的。如果能够有办法,让这个流量控制得到了窗口更大些,发送速率就更快点。(大点的前提,能够让接收方还是能处理过来的)。

延时返回ACK,给接收方更多的时间,来读取接收缓冲区的数据,此时接收方读了这个数据之后,缓冲区剩余空间,变大了,返回的窗口大小也就更大了。

初始情况下,接收缓冲区剩余空间是10kb,如果立即返回ack,返回了10kb,这么大的窗口如果延时个200ms,再返回,这200ms的过程中,接收方的应用程序的又读了2kb,此时,返回的ack,就可以返回12kb的窗口了。就比如老师评作业,那个作业是在作业系统中完成的,发现某个学生,10次作业都没做,然后老师给同学发了信息,然后这个同学看了消息,如果立即回复,意味着它这边还是欠10次作业,很虚,如果这个同学看到消息之后,开始狂补作业,一个下午,补了5次,再给我回信息,就剩下5个作业了,延时应答。

延时应答,也促进了 四次挥手能够三次挥完。


🎈捎带应答

在延时应答的基础上,进一步的提高效率,网络通信中,往往是这种”一问一答“这样通信模型。

ack是内核立即返回的,response则是应用程序代码来返回的,这两者时机是不同的。

由于tcp引入了延时应答,上面的ack,不一定是立即返回的,可能要等一会,在等一会的过程中,B就正好把response给计算好了,计算好了之后,就会把response返回,于此,同时顺便把刚才要返回的ack也带上了。

俩种机制,肯定是第二种捎带应答效率更高。俩个数据合并成一个数据了。

本来是需要传输俩个tcp数据包(封装分用2遍),目前通过上述操作,就把俩个包合并成一个了,此时就可以得到更高效的效果。


🎈面向字节流 

❗粘包问题

粘包问题不是TCP独有的,而是面向字节流的机制都有类似的情况。

此时”包“应用层数据包,如果同时有多个应用层数据包被传输过来,此时就容易出现粘包问题。

目前接收缓冲区中,这三个应用层数据包的数据,就是以字节的形式紧紧挨在一起。接收方的应用程序,读取数据的时候,可以一次读一个字节,也可以读俩个字节,也可以读N个字节。

但是最终的目标是为了得到完整的应用层数据包,B应用程序,就不知道,缓冲区里的数据,从哪里到哪里是一个完整的应用数据包了。

相比之下,像UDP这样的面向数据报的通信方式,就没有上述问题,UDP的接收缓冲区中,相当于是一个一个的DatagramPacket对象,应用程序读的时候,就能明确知道哪里到哪里是一个完整的数据。


👩🏻‍💻如何解决粘包问题

核心思路:通过定义好应用层协议,明确应用层数据包之间的边界。

  • 引入分隔符
  • 引入长度

一个简单的例子,使用\n作为分隔符。


一、

这个时候,应用程序,读取数据的时候,就可以一直持续读数据,知道读到\n为止。


二、

应用程序读数据的时候,就可以先读两个字节,得到数据包的长度,再根据这个长度,继续读取对应字节的个数。这样就读出来了一个数据包。

自定义应用层协议的格式,xml,json,protobuffer本身都是明确了包的边界的。


🎈异常情况的处理

如果在使用tcp过程中,出现意外,如何处理?

❗进程崩溃

进程崩溃:

进程没了,异常终止了,文件描述符表,也就释放了,相当于调用socket.close()。此时就会触发FIN,对方收到之后,自然就会返回FIN和ACK,这边再进行ACK(正常的四次挥手断开连接的流程)

TCP的连接,可以独立于进程存在(进程没了,TCP连接不一定没有),这里我们要知道一个概念那就是,TCP连接是否存在和进程是否存在是没有必然联系的,因为TCP连接是独立于进程存在的,这是为什么呢?

因为连接的构建本质上是内核进行完成的,就像我们用电话,和别人打电话,当我们的电话挂断后我们的电话线依然是存在的,而我们在写代码的时候也确实并没有去在代码里建立连接我们是直接调用了接口然后把参数传给操作系统内核让内核去建立连接。我们可以理解为,连接的建立是两个操作系统建立的,而进程只是利用这个连接进行通信的。因此进程结束并不一定意味着操作系统会删除对方信息,从而断开连接。因此,TCP连接具有独立性和持久性。但是当通信的两端进程都终止后,TCP连接也会随之关闭。


❗主机关机 

主机关机(正常流程)

在进行关机的时候,就是会触发强制终止进程操作,此时就就会触发FIN,对方收到之后,自然就会返回FIN和ACK,不仅仅是进程没了,整个系统也可能关闭了,如果在系统关闭之前,对端返回的ACK和FIN到了,此时系统还是可以返回ACK,进行正常的四次挥手的,如果系统已经关闭了,ACK和FIN迟到了,无法进行后续ack的响应,站在对端的角度,对端以为是自己Fin丢包了,重传FIN,重传几次都没有响应,自然就会放弃连接(把持有的对端的信息就删了)


❗主机掉电 

主机掉电(非正常)

此时,是一瞬间的事情,来不及杀进程,也来不及发送FIN,主机直接就停机了。站在对端的角度,对端不一定知道这个事情怎么弄。

  • 1.如果对端在发送数据(接收方掉电),发送的数据就会一直等待ack,触发超时重传,触发TCP连接重置功能,发起”复位报文段RST“,如果复位报文段发过去之后,也没有效果,此时就会释放连接了。(超时重传(继续再发一遍)->连接重置功能(重新和接收方建立连接)->复位报文段RST->单方面的释放连接)

  • 2.如果对端在接收数据(发送方掉电),对端还在等待数据到达,等了半天没信息,此时其实是无法区分的,是对端没发信息,还是对端挂了。(此时就可以试探性的发信息,这就叫做心跳包机制)
👩🏻‍💻心跳包

TCP就是提供了 心跳包 机制

接收方也会周期性的给发送方发起一个特殊的,不携带业务数据的数据包,并且期望对方返回一个应答,如果对方没有应答,并且重复了多次之后,仍然没有,就视为对方挂了,此时就可以单方面的释放连接了。


❗网线断开

网线断开,和刚才的主机掉电时非常类似的。

A对B发送数据,一旦网线断开。

  • A就相当于触发了超时重传,然后连接重置,然后单方面释放连接;
  • B就会触发心跳包,发现对端没有响应,单方面的释放连接。

我们这里介绍了十大机制,不代表TCP就只有十机制,还有更多机制,只不过这些是最主要的。


🚩TCP和UDP之间的对比

  • TCP的优势 ”可靠传输“  用于绝大部分的场景
  • UDP的优势 ”更高效率“  UDP更适合于 ”可靠不敏感,性能敏感 场景(也就是不可靠的,但是效率很高,不可靠说明往往会出错)

UDP适用场景

 局域网内部(同一个机房)的主机之间通信。

同一个机房内部,网络结构简单,带宽重组,交换机/路由器网络设备负载程度也不出现丢包的概率也不大,往往也希望机器之间数据传输能更快。

TCP运用场景

投屏功能,(手机和电视得在同一个局域网下,手机这边触发投屏功能,就会往局域网发起广播数据包,询问一下,你们谁是电视?谁能够接收到投屏??

此时电视就会回应,我i是电视,并且把自己ip端口啥都告诉了手机,手机就可以完成后续的投屏了。

经典面试题:如何基于UDP实现可靠传输!!本质上是考察TCP

  • 确认应答
  • 引入序号,确认序号
  • 超时重传
  • 滑动窗口
  • 流量控制
  • 拥塞控制
  • 延时应答 
  • 捎带应答
  • ........

 我想,生命这卷书落款应是亭亭常青树。

  • 12
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值