[Java EE] TCP 协议

Author:MTingle
major:人工智能


Build your hopes like a tower!



一. TCP 协议

• 源/⽬的端⼝号:表⽰数据是从哪个进程来,到哪个进程去;

• 32位序号/32位确认号:后文详解;

• 4位TCP报头⻓度:表⽰该TCP头部有多少个32位bit(有多少个4字节);所以TCP头部最⼤⻓度是15* 4 =60

• 6位标志位:

◦ URG:紧急指针是否有效

◦ ACK:确认号是否有效

◦ PSH:提⽰接收端应⽤程序⽴刻从TCP缓冲区把数据读⾛

 RST:对⽅要求重新建⽴连接;我们把携带RST标识的称为复位报⽂段 

◦ SYN:请求建⽴连接;我们把携带SYN标识的称为同步报⽂段

◦ FIN:通知对⽅,本端要关闭了,我们称携带FIN标识的为结束报⽂段

• 16位窗⼝⼤⼩:后文详解

• 16位校验和:发送端填充,CRC校验.接收端校验不通过,则认为数据有问题.此处的检验和不光包含 TCP⾸部,也包含TCP数据部分.

• 16位紧急指针:标识哪部分数据是紧急数据;

• 40字节头部选项:暂时忽略;

• 6位保留位: UDP这个协议,长度受到两个字节的限制,想要进行扩展,发现扩展不了,一旦改变了报头的长度,就会使机器发送的 UDP 数据报和其他机器不兼容,无法通信, TCP 在设置报头的时候,就提前准备了几个保留位,使用保留位可以避免数据不兼容的问题.

二. TCP 特性

1. 确认应答(ack)

"确认应答" 是 TCP 用来确保可靠性最核心的机制!

并且 TCP中,还引入了序号和确认序号对于数据进行编号,在应答报文里告诉对方,我应答的是哪个数据,举个简单的例子来理解这个场景:

但如果网络中出现了先发后至的状况就会变成了以下这种状况,误会就发生了~

但当我们引入了确认序号之后,就能成功解决上面的这个问题~

以上是一个简化版本的模型,实际情况会更加复杂,TCP是以字节为单位进行传输的,所以TCP的序号和确认序号都是以字节来进行编号的.

假设载荷中有1000个字节,由1000个序号,由于序号是连续的,于是我们只需要在报头中存储第一个字节的信号,后续字节的序号都很容易计算得到

            报头中的序号是1

主机B收到1 - 1000这些字节的数据之后,反馈一个应答报文,应答报文中的数据为1001,这个序号的意义就是:

1. < 1000的数据我已经收到了

2. 发送方接下来需要给我发1001的数据

2.  超时重传

如果传输过程中一切顺利,通过应答报文就可以告诉发送方,当前数据已经收到,凡是网络上可能出现丢包的状况,此时 ack 报文就无法让发送方收到了,此时我们就需要超时重传了.

发送方发送一个数据之后,进入等待,等待的时间里,收到了 ack(数据的传输需要时间),说明文件成功传输,若等了很久都没有收到 ack ,等待的过程有一个时间阈值,当超出了这个阈值后,此时发送方就会默认传出去的数据丢包了,此时,发送方就会把刚刚发出的数据包再传输一次.

网络上为什么会出现丢包

网络中的路由器 / 交换机要支持千千万万的主机进行通信,整个网络中,就可能存着某个路由器 / 交换机,某个时刻负载量很高,但是短时间内还有大量数据要经过这个设备转发,但是一台设备能够处理的数据量是有限的,很可能某个瞬间的高负载超出了这个设备能转发的数据的极限,此时多出来的部分就没了,这个情况下,就被设备丢包了.丢包的情况客观存在,什么时候丢包,难以预测.上面的过程中,认为没收到 ack 就是丢包了,其实这样的结论并不准确,也可能是数据没丢,传回发送方的 ack 数据包丢失了!

在情况二中,数据已经被 B 收到了,再传输一次同一份数据,B就会收到两次,那么这显然是有问题的,如果这个数据是扣款请求,那不是扣了对方两次钱吗!!!

针对这个问题, TCP socket 在内核中存在接收缓冲区(一块内核空间),发送方发来的数据,是首先要放到缓冲区当中的,然后应用程序调用 read/scanner.next 才能读到数据,这里的读操作实际上读的是接收缓冲区.

当数据到达缓冲区的时候,接收方首先会判定一下,这个数据是否在缓冲区,或是否在缓冲区存在过,如果在缓冲区或已经存在过,就会将这个数据丢弃,就会确保应用程序不会读到重复的数据.

接收方如何判断"重复数据"?

1.数据还在缓冲区,没有被读走,此时只需要把拿到的数据和缓冲区已有的数据进行对比即可.

2.数据已经被应用程序读走,此时新来的数据序号直接无法在缓冲区中查到,但是要注意,应用程序读数据的时候,是按照序号的先后顺序,连续读取的,一定是先读序号小的,再读序号大的,socket的api中会记录上次读取的数据的序号是多少,比如上次最后一个字节是3000,此时传来的数据序号是1001,可以确定1001已经读过了,就可以将数据包丢弃了.

超时重传,重传也不是无限的重传,也是有一定策略的.

1.重传的次数是有上限的,重传到一定程度,还没有 ack ,就会尝试重置连接,如果重置失败,就直接诶放弃连接.

2.重传的超时时间阈值也不是固定不变的,随着重传次数的增加而增大,重传的频率越来越低,经历了重传之后仍然丢包,大概率是网络出现问题了,再怎么重传也是白费劲,但是仍然是要重传的,只不过可以省点力气,少传几次.

假设网络中丢包的概率是10%,重传一次,两次都丢包的概率是10%*10%=1%,随着重传次数的增加,到达的几率也会增大,连续三四次还丢包,说明当前丢包的概率太大了,远远不止10%,此时意味着网络出现了严重故障,在重传也于事无补,只能放弃重传.

3. 连接管理

三次握手

内核是怎样完成上述"建立连接"的过程的呢? 这个过程称为三次握手

上述过程从流程上来讲,是有四次交互的,但是实际过程中,其中的两次交互能够合二为一,这就最终形成了"三次握手"

建立连接的过程,本质上就是通信双方各自给对方发起一个syn,各自给对方回应一个ack,虽然第一次握手,客户端已经把自己的信息告诉给了服务器没大会师服务器是否确定存储这个信息还得观望,等所有握手环节完成,服务器才会最终保存客户端的相关信息.

1.客户端给服务器打电话:喂喂喂,你能听到吗?

2.服务器给客户端回:我能听到.

3.客户端给服务器回:我知道了~~

对于1,客户端给服务器发送信息,此时他是不知道服务器是否能收到信息的

对于2,服务器回复客户端,说明他收到了信息,并且尝试去告诉客户端他收到了信息,能够通信

对于3:客户端收到了服务器信息,说明他能够正常收到信息通信,此时他要告诉服务器,你这边发出的信息我也能正常收到~

syn这样的数据包,不携带载荷,没有应用层数据,也就不代表任何应用层数据的业务逻辑,syn起到的作用就只是类似"打招呼",所以把这个动作形象的称为"握手",打完招呼之后,双方就能执行业务逻辑了.

三次握手的意义:

1. 三次握手,可以先针对通信路径,进行投石问路,初步的确认一下通信链路是否畅通(可靠性的前提)地铁,每天晚上就关门了.第二天一大早,开始运营~~ 也是为了投石问路空车跑一趟就是为了验证一下,这个路线是否畅通,是否存在一些问题,如果能成,再谈"可靠传输

2. 三次握手,也是在验证通信双方发送能力和接收能力是否正常.

3.三次握手的过程中也会协商一些必要的参数,通信是客户端服务器两方的事情,其中的有些内容需要保持一致.

TCP一次通信的过程中,序号不是从0或者1开始的,而是先选择一个比较大的数字来进行计算,即使是同一个客户端和服务器,每次连接开始的序号都是不同的,这样可以避免一个情况"前朝得见,斩本朝的官".

第一次连接的过程中,传输的有一个数据包,在路上堵车了迟迟没有到达对端.等到终于到了对端的时候,已经改朝换代了,之前的连接早都没了,现在是新的连接了!!此时,这份数据,就应该被丢弃!!

数据报时按照 ip+端口 进行识别的第一个连接,是用客户端A 来连的,第二个连接用客户端B 来连的,(恰好是同一个端口的话,客户端概率是比较低,服务器概率很大)此时数据到达这一边,早已物是人非,这个时候的话,再来进行处理这个数据就不合适了此时,丢弃这个数据包是一个上策.

如何识别出,当前的数据是"前朝”的数据包呢??就可以通过序号来区分!

四次挥手

连接本质上是通信双方保存对方的信息,每个客户端服务器都需要保存很多对端的信息,一旦数据多了,就要使用"数据结构"来进行组织;

断开连接的本质,就是把对端的信息,从数据结构中删除掉 / 释放掉.此处谈到的四次挥手,指的就是和离这种状况,对于单方面的状况,四次挥手不一定适用,就会有其他方式释放这里的连接,不意味着说,断开连接一定是四次挥手.

四次挥手不一定是客户端先发 fin ,服务器也可能先发 fin ,和三次握手不一样,三次握手一定得是客户端主动,四次挥手中,谁先发都是可以的,主要看代码实现.

那么四次挥手能否像三次挥手一样,把中间两次交互合二为一呢?

有的时候能够合并,有的时候不能够合并,不像三次握手一样,能够100%合并.因为三次握手合并(syn和ack)是因为这两次操作触发时机是完全一致的.

若 fin 和 ack 当前时间间隔很小,是有可能合并的,但是也可能二者中间需要执行许多逻辑,两次操作时间间隔比较长,此时就不会合并.

TCP状态

LISTEN状态: 表示服务器创建好了 severSocket 了,并且绑定端口号完成

ESTABLISHED状态: 已确立的,客户端和服务器连接已经建立完毕(三次握手完成了)

CLOSE WAIT状态: 谁被动断开连接,谁进入 CLOSE WAIT 状态,该状态表示,接下来代码中需要调用 close 来主动发起 fin ,收到对方的 fin 后进入这个状态

TIME_WAIT: 表示本端给对方发起 FIN 之后,对端也给本端发 FIN 此时本端就进入了TIME_WAIT.谁主动断开连接,谁进入 TIME_WAIT.

TIME_WAIT 存在意义: 为了防止最后一个ack丢包.

客户端如果在TIME_WAIT,把 TCP 连接释放掉,此时意味着重传的 FIN 就无法被返回 ACK 了.(保存对端信息的数据结构存在,才能给这个连接提供各种操作,才能返回 ACK)

此处的 TIME WAIT 等待也不是无休止的等待,最多等 2 MSL (MSL 是一个系统内核的配置项,表示客户端到服务器之间,消耗的最长时间.这个时间一般都是拍脑门出来的一个非常大的时间,比如常见的设置值是 1min)

4 滑动窗口

TCP 的可靠传输机制付出了较大的代价,它的传输效率是比较差的,单位时间传输的数据量小.确认应答机制下,每次发送方收到一个ack才会发下一个数据,大量的时间都消耗在等待 ack 上.此处等待消耗的时间成本是非常大的,如果是短连接,效率就会更加低下.,滑动窗口的提出就是为了解决上述问题,滑动窗口就可以在确保可靠传输的基础下提高效率.

引入了滑动窗口,我们可以实现批量传输,之前发了一个数据, 等待 ack,再发下一条数据.现在先发一个数据,不等 ack,再发下一个,继续再往下发.连续发了一定数据之后,统一等一波 ack,把多次请求的等待时间,使用同一份时间来等了.减少了总的等待时间,

那么"滑动"的含义是什么呢?

当我们批量发送四个数据,就会对应四个 ack ,这四个 ack 的到达有先有后,那我们什么时候进行发送下一波数据呢?答案是,等回来一个 ack 就往后发送一组.在视觉上就启动了一个"滑动"的效果.

滑动窗口与可靠性: 如果出现了丢包,滑动窗口会咋样?

1). ack 丢了

如果 1001 的 ack 丢了,但是数据被成功接收了,当2001的 ack 返回之后,就代表告诉发送方,前面的数据我已经全部成功接收了.这就像生活中,一个人说他连孩子都有了,一般就说明他结过婚,谈过恋爱.

2). 数据丢了

如图所示,若接收方一直没有收到1-1000的数据,他就会一直发送1001的ack,反复索要,告诉发送方他没有收到1-1000的数据,需要他重传,当发送方重传之后,由于后面的数据都成功接收,发送方会返回一个7001的 ack 来表示发送方接下来要发送的数据编号.

反复索要,也相当于在给 1001 的到达留有等待时间,连续多次索要,发现 1001 还没到, 应该就是丢了就相当于"超时时间"判定了.

在上述重传的过程中,整体的效率是非常高的.这里的重传做到了“针对性"的重传.哪个丢了就重传哪个,已经收到的数据,是不必重复发送的整体的效率没有额外损失的,就把这种重传成为"快速重传"

5. 流量控制

通过滑动窗口可以提高传输效率.,窗口大小越大,更多的数据复用同一块时间等待,效率就更高.批量传多少数据不需要等待 ack,此时数据的量就称为"窗口大小"

那么窗口可以无限大吗?如果接收方处理不过来,接收方的缓冲区满了,发送方继续发送数据,就会出现丢包问题,这就像我们小学学过的蓄水池问题,如果蓄水速度大于防水速度,那么水池一段时间后就会被装满,水会溢出.

与其等到接收方满了,不如提前就感知到,减缓发送速度,让发送方和接收方能够步调一致,让接收方反过来影响发送方的发送速度,这就是流量控制.于是, TCP 就通过16为窗口大小来给发送方反馈发送速度,这个字段在普通报文中没有意义,在 ack 报文中才有意义,通过这个大小反馈给发送方接下来要发送的窗口设置成多少合适.接收方会按照自己接收缓冲区的剩余空间大小,作为 ack 中的窗口大小的数值.下一步发送方就会根据这个数值调整窗口大小.TCP 报头的选项中,还包含了一个参数,叫做窗口扩展因子,实际上的真实要设置的窗口大小是:16位窗口大小*2^窗口扩展因子.

6.拥塞控制

拥塞控制的目的也是为了限制发送方发送数据的速率.

如果当前接收方处理数据的速度很快,但是中间的通信路径出现了问题,某个地方出现了"堵车",发送数据再快也没用,这就像木桶效应,一个马桶能装多少水,取决于最短的板,针对这种状况,核心思路就是,将所有的中间路径经过的设备视为一个整体,找出一个比较合适的传输速率.

如果按照某个窗口大小发送数据,出现了丢包,就视为中间路径出现了拥堵,就减少窗口大小,如果没有出现丢包,就视为中间路径不存在拥堵,就增大窗口.

上述方案,一方面简化了问题,另一方面,也能够很好的适应,当前网络环境的复杂性中间这些节点,啥时候出现拥堵,啥时候不拥堵,就都是"随机”的.此时按照上述策略,就可以让发送速率,动态变化.

总的来说,流量控制和拥塞控制,谁产生的窗口更小,谁说了算,只不过一个是站在了接收方的角度,发送方的角度.

那么这个窗口大小是怎么试出来的呢?

1).慢启动.,刚开始传输的数据, 速率是比较小的,采用的窗口大小也就比较小.此时,网络的拥堵情况未知,如果一上来就搞很大,可能就让本来不富裕的网络带宽,就雪上加霜了

2). 如果上述传输的数据,没有出现丢包,说明网络还是畅通的,就要增大窗口大小,此时,增大方式是按照指数来增长,由于使用慢启动,开始的时候,窗口大小非常小,也有可能网络上就是很畅通,通过指数增长可以让上述的窗口大小快速变大这样就可以保证传输的效率.

3). 指数增长,不会一直持续保持的,可能会增长太快, 一下就导致网络拥堵.这里引入了一个"阈值”,当拥塞窗口达到阈值之后, 此时,指数增长就成了线性增长.线性增长能够使当下的窗口持久的保持在一个比较高的速率,并且也不容易一下就造成丢包.

4). 线性增长也是一直在增长, 积累一段时间之后, 传输的速度可能太快,此时还是会引起丢包.一旦出现丢包, 就把拥塞窗口重置成较小的值, 回到最初的慢启动过程(又要重新指数增长)并且这里也会根据刚才丢包时的窗口大小,重新设置指数增长到线性增长的阈值.

全新版本中,后续就没有指数增长的过程了,而是回到了线性增长的位置,之所以做出这样的改变,是由于当前的网络环境对比 TCP 诞生时通信质量的稳定性得到了巨大的提升,当年的时候网络环境经常会出现大规模的网络波动~~一旦出现拥塞,此时很可能网络带宽就非常捉襟见肘了,按照比较小的速度发送是更稳健的做法,现在网络环境更稳定了,路也更宽了.相比之下之前的问题就不存在

7. 延时应答

也是基于滑动窗口,结合流量控制和滑动窗口,通过延时应答 ack 的方式,将反馈的窗口大小提高.接收方接收到数据之后,不会立刻返回 ack ,而是稍微等一下,等一会在返回 ack ,这里的等一会相当于给接收方的应用程序更多的时间来消费这里的数据,此时接收缓冲区的空闲区会增大,对应的,返回的窗口大小也会增加.

8.捎带应答

基于延时应答引入的机制,可以提高传输效率.捎带应答就是尽可能的把能够合并的数据包进行合并,从而起到提高效率的效果.ack 延时的这段时间里,可以把 ack 和应答的数据合并成一个 tcp 数据报,然后发送给对方.

要注意区别捎带应答和三次挥手:捎带应答携带的数据是有意义的数据包,而握手只是打招呼,不携带实际意义的数据包.因为延时应答和捎带应答的加持,后续的四次挥手可能合并成三次.

9.面向字节流与粘包问题

tcp 传输的数据到了接收方之后,接收方要根据 socket api 来 read 出来.read 出来的结果就是应用层数据包.由于整个 read 过程非常灵活,可能会使代码中无法区分出当前的数据从哪到哪是一个完整的应用数据包.应用程序通过read读出来的数据可以是一个a,也可以是aa,也可以是aaabb......多个应用层数据包混淆不清了,此称为"粘包".

我们要让系统读到数据之后就能将其转换为应用层数据包,这样才能让数据被正确的使用.你粘包问题不是 TCP 独有的问题,是所有面向字节流的,都会有同样的问题.解决问题的关键就是"明确包之间的边界".

1.通过特殊符号,作为分隔符. 见到分隔符,就视为是一个包结束了,使用任意字符作为分隔符都可以,需要确保当前这个分隔符不会在正式的数据中存在,

2.指定出包的长度,比如在包开始的位置,加上一个特殊的空间来表示整个数据的长度.

对应的, UDP 没有这个问题,UDP 传输的基本单位是 UDP 数据报.在UDP这一层,就已经分开了只要约定好,每个 UDP 数据报都只承载一个 应用层数据包, 就不需要额外的手段来进行区分了,UDP 的接收缓冲区就不是一个队列这样的结构,而是类似于链表.每个链表的节点都是一个 UDP 数据报通过代码来读取的时候,一次取一个,也就是一个应用层数据包了.

10. 异常情况

1). 一方进程崩溃

进程无论是正常结束,还是异常崩溃,都会触发到回收文件资源,关闭文件这样的效果 (系统自动完成的)就会触发四次挥手.

TCP 连接的生命周期,可以比进程更长一些,虽然进程已经退出了,但是 TCP 连接还在,仍然可以继续进行四次挥手虽然说是异常崩溃,实际上和正常的四次挥手结束,没啥区别,进程不再了,是通过系统中仍然持有的连接信息,完成后续的挥手过程的.

2). 一方关机

当有个主机,触发关机操作,就会先强制终止所有的进程,(类似于上述的 强杀进程)终止进程自然就会触发4次挥手点了关机之后,此时,四次挥手不一定能挥完,系统马上就关闭了.

如果挥的快,能够顺利挥完,此时, 本端和对端都能正确的删除掉保存的连接信息,这也是四次挥手的核心任务.

如果挥的不快,至少也能把第一个 fin 发给对端,至少能告诉对方,我这边要结束了.对端收到 fin 之后,对端也就要进入释放连接的流程了,返回 ack,并且也发 fin, 这里发的 fin 不会有 ack 了.所以此处会触发"超时重传".当重传几次之后,发现还是不行,还是没有 ack,这个时候,单方面的释放连接信息

3). 一方断电 / 网线断了

如果直接断电,机器瞬间关机,此时肯定来不及发送 fin ,同时这种操作是比较伤机器的,尤其是硬盘,磁头来不及归位,就会写坏硬盘,或是写入错误数据.

1. 断电的是接收方

发送方就会突然发现没有 ack 了,就有重传,重传几次还是不行, TCP 就会尝试复位连接(RST),相当于清除原来的 TCP 各种临时数据,重新开始,此时的 RST 也不会有 ack ,重置失败,单方面放弃连接

2. 断电的是发送方

接收方本来就是在阻塞等待发送方的消息,结果迟迟没来消息,这个情况下,接收方需要区分出,发送方是挂了,还是好着,暂时没发,TCP 中也是如此,接收方一段时间之后,没有收到对方的消息,就会触发 "心跳包” 来询问对方的情况,如果对端没心跳了,此时本端也就会尝试复位并且单方面释放连接了.

心跳包也是不携带应用层数据的特殊数据包,具有周期性的,如果对端没有心跳,视为对端挂了.


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值