T04BF
👋专栏: 算法|JAVA|MySQL|C语言
🫵 今天你敲代码了吗
目录
TCP
我们在前面的文章里面提过,TCP的特点是 有连接,可靠传输,面向字节流,全双工
但是对于可靠传输这样的特性,在代码中是无法直接观察到的,但是这个特性是 TCP 最核心的部分
我们来看看TCP协议段格式:
我们先来理解一下协议格式里面的一些容易理解的:
首先就是16位源端口号和目的端口号
作为传输层协议首先就是要能够表示端口号信息
接着是 4位首部长度,这里的首部长度指的是 TCP报头的长度 ,而不是整个报文的长度
那么既然用一个变量来专门描述报头长度,就说明 和UDP不同的是,TCP报头长度是可变的
接下来看 "选项",实际上这里翻译成可选项更加合理,即表示可以选择 加还是不加
如果选项完全没有,那么tcp报头的长度就是20个字节
如果选项拉满,那么最长的时候就有60个字节
为什么是60个字节?? 实际上,这里的4位首部长度可以表示的范围是0 - 15,但是实际上他的单位是 "4字节",也就是4 * 15 = 60个字节的长度
因此最长的长度是60个字节,去掉20个固定的,剩下的选项最多就是40字节
接下来看 "保留(6位)",UDP报文的长度使用2个字节表示,太小了
而在TCP报头里,就提前申请好了一块空间,这个空间暂时用不上,但是说不定以后就能够用的上(万一以后TCP要扩展一些新的功能,就可以用这个保留位来表示了)
这样的空间就是保留位
接着是标志位
这是TCP报头中的6位标志位,是TCP的灵魂
对于16位校验和,此处的校验和和UDP的校验和是同一个东西
接下来我们重点理解TCP协议的几个核心机制
确认应答机制
所谓的可靠传输,不是说发送方能够将数据100%的传输给对方,而是尽力做到
此时尤为关键的就是,要让发送方能够知道,接收方是否已经收到数据
因此就引入了"确认应答机制"
如图所示,这样用于应答的数据,称为"应答报文",应答就是 acknowledge,在TCP6个标志位里面就有一个ACK,如果在报头里面的这个比特位为1,那么就表示 这是一个应答报文
但是如果就只是这样简单的,单纯的应答,在你批量发送数据的时候,就会出现问题
因为在网络传输过程中会存在"后发先至"的情况,这是网络通信中客观存在的,改变不了
但是只要我们能够对传输的数据进行编号,并且能够让应答报文的编号 和 发送数据的编号 对应起来,这样的话即使是出现"后发先至"的情况也不影响传输数据的意思表达
在TCP报头里,就存在 32位序号 和 32位确认序号
其中32位确认序号是给应答报文使用的,当ack标志位为1的时候,才是有效的
这样的数据就能够区分出,当前应答报文是在应答上面的哪个报文了
但是实际上真实的tcp的序号不是按照"一条两条"的方式来编号的,而是按照"字节"来编号的,即每个字节都会分配好一个序号
那么此处的1000个字节,每个字节都会有编号
假设第一个字节的编号是1,那么第二个字节就是2...即每个字节的编号都是连续递增的(第一个不一定从0开始)
此时在TCP报头里面写的序号的数值,就是载荷是第一个字节的编号,由于序号递增的特性,此时只要知道第一个字节的序号,后续的字节编号就都知道了
确认序号的设定也是非常有特点的,ack报文确认序号的取值,是在收到的数据的最后一个字节 的编号 + 1
我们可以有两种理解方式:
(1)对于B来说, 告诉A: < 1001的数据都已经确认收到的
(2)B在向A索要从 1001 开始的 数据
至于为什么是32位,实际上就是16位太紧张了,表示的数据范围太小了.
但是即使是32位,超了也无所谓,因为TCP会在最开始连接的时候,协商好起始的序号是多少,如果用完了,重新触发协商即可
TCP可靠传输之所以能够达成,主要就是依靠 "确认应答机制"
超时重传
上面讲到的确认应答机制,是通过应答报文 来通知发送方
这是一切顺利的情况
但是如果没哟那么顺利,而是数据在发送的过程中,出现丢包的情况呢??
丢包就是 数据在传输的过程中被丢弃了,无法到达对端了,也是客观存在的随机事件
主要就是因为在数据转发路上经过的路由器 / 交换机 转发能力是 有上限的,一旦某个设备需要转发的数量量超出了自身的极限,此时多出来的部分就会直接丢弃
这是客观存在的事实,也是无法预测的
网络环境本身就是不可靠的,而TCP就是要在不可靠的环境创建出 可靠的 通信方式
超时重传就是要用来应对 网络出现丢包的情况
正常情况下,TCP是通过确认应答 来知道数据是否被对方接受了
此时一旦出现丢包,A就课可以通过 是否 "收到了ACK" 来区分,传输的数据是否出现丢包
A从发送数据到正常接受到 ACK,肯定也是要经历一段时间的
A就会进行一定的等待,但是如果等待时间超过了某个阈值,还没有收到 ACK,此时就可以认为是 出现 丢包了
即使是收到的太晚了,也视为是 "丢包"
此时一旦发现丢包,就会触发**重传,**就是把刚发的数据再发一次
但是如果是B返回的ACK丢了呢??
实际上站在A的角度,是无法区分,是A传输的数据丢了,还是 B返回的ACK丢了
因此A能够做的事情就是,触发重传
但是很明显,此时一旦触发的重传此时B就会收到两份重复的数据,这是很不安全的
那么在TCP这边,就要针对收到的数据,进行去重操作
实际上在传输层面,收到重复的数据是无所谓的,只要保证应用层的应用程序在读取数据的时候,不能读取到重复的数据即可
在接收方操作系统内核里,实际上存在有一个数据结构,起到数据缓冲区的效果,类似于PriorityBlockingQueue
此处也类似于我们前面讲过的生产者消费者模型
发送方就是生产者,接收方的应用程序就是消费者
接受缓冲区的阻塞队列就是用来支撑这个过程的核心数据结构
当数据到达传输层的时候,就会将数据存在这个队列里面
此时就会根据当前这个数据的序号,在队列里面判定这个数据是否在队列里面存在(或者曾经在队列里面存在过,只是被应用程序消耗了),只要满足两种条件之一,这个新的数据就不会进入队列,而是直接丢弃
注意:即使B收到数据后,判定是重复数据,也是要继续返回ack的,因为B收到重复数据,就可以推断出 刚才的ack丢包了,因此再传输一遍ACK即可(如果不传输,A过一会还要继续重传一遍)
这个队列是怎么知道数据曾经在队列里面存在过??
实际上接受缓冲区除了去重之外,还有一个非常重要的功能,就是针对收到的数据进行排序
我们前面说过网络传输会先发后至,但是很多时候我们希望发送出去的消息能够有序的到达接收方
在接收缓冲区里面就会对接受到的数据进行排序,即使接受到的是乱序的,也会让序号小的在前面,序号大的在后面,并且数据和数据之间的序号始终是连续的
这样的话,假设队首元素的序号是1000,那么说明比1000小的数据都read过了
那么此时再收到<1000的数据,就可以认为是 重复数据了
但是实际上,丢包本身就是一个"概率性事件"
假设网络的丢包率是 10%,一次传输丢包的概率就是10%,触发重传后,重传也丢包的概率就是1%,此时就说明99%的概率是传输成功的
如果连续重传几次后都发现丢包,那么就说明网络是发生严重故障了
因此我们前面提到的等待超时重传的时间,实际上是动态变化的,会随着重传轮次的增加,变得越来越长
假设第一次传输数据,等待50ms,就会触发重传,那么重传之后就会等待100ms...(超时时间的间隔会越来越长,不一定是线性),换而言之,就是重传的概率会越来越低
这样设定实际上就是因为,出现重传多次的情况,大概率就是网络出现严重故障了,重传成功的可能性也比较低
那么此时就降低重传的时间间隔,如果是设定重传越来越快,非但得不到好的结果,而且会浪费很多系统资源
如果是网络出现了严重故障,重传若干次还是不成功,到达一定的次数阈值,就会尝试"重置连接"
就会触发一个"复位报文",尝试重置连接,相当于连接重新开始
当网络出现严重故障的时候,RST报文也是无法顺利完成的,此时重置也是失败了,就只能断开连接了
超时重传是 确认应答的重要补充
TCP的可靠传输,全靠 确认应答 和 超时重传这两个机制 支撑起来
连接管理(三次握手和四次挥手)
建立连接的过程:三次握手
断开连接的流程:四次挥手
三次握手
指的是两个机器一见面,就进行"打招呼","打招呼"的过程中,没有实际上的数据交互,只是为了打招呼而传输一些数据
即无论是握手还是挥手,传输的网络数据报,不携带任何业务上的数据
建立连接,就是通信双方各自保存对端的信息,而具体完成上述的过程,需要经过三次网络交互
三次握手的第一次,一定是客户端先发起的
此时这个数据报是不携带任何业务数据的,即载荷部分是空着的,只有TCP报头,这个TCP报头6个标志位中SYN这一位为1
三次握手的流程就是
上述流程,客户端和服务器各自给对方发送syn,再各自给对方回一个ack,其实是4次交互
关键在于,中间的交互ACK和SYN可以合并成一个网络数据
ACK是6个标志位的第二位,SYN是6个标志位的第5位.所谓合并,就是让这一个TCP数据报的报头中,同时将两个比特位都设置为1
3次交互实际是比4次交互的效率更高的
三次握手的时候,实际上就相当于双方各自让对方保存自己的信息,得是双方都把对方的信息保存好,连接才算是建立完成
我们写socket代码的时候,客户端new Socket对象,就在系统底层触发syn等三次握手的过程,而调用accept其实是三次握手已经完成了,应用程序将操作系统内核里面搞好的连接信息给取出来了
那么三次握手的意义是什么?? 解决的是什么问题??
(1)相当于是投石问路,在正式传输业务数据的之前,先确认一下人通信链路是否畅通(实际上也是可靠传输的一种保证)
(2)通过三次握手,来确认通信双方的发送能力和接收能力都是正常的
就很像我们平时打电话
那么试想一下,三次握手变成两次握手是否可行??
实际上是肯定不行的,因为如果只有前两次没有第三次,那么此时服务器这边对于发送能力以及客户端的接受能力的认知是不完整的,需要第三次交互,把客户端掌握的情况告知给服务器
(3)三次握手的过程中,还需要协商一些必要的参数
此时需要通信双方共同商量一下,有的参数不是单方面就能够确定,需要双方共同来确定
我们在上面讲过的 TCP通信时使用的序号,就是在这个阶段协商出来的
也就是说,序号不是单纯的从0/1开始
而第一次连接和第二次连接,协商出来的起始序号,是不一样的,往往差异很大
这里这么设定,是TCP考虑到一个情况,就是避免出现"前朝的剑,斩本朝的官".我们考虑一种场景:
此时就是通信双方先建立连接,进行若干次通信之后,断开连接,过一会后,又重新建立连接进行若干通信
但是可能会出现一种情况,就是在前一次连接的过程中,某一次数据报走了一个非常绕远的路线(不是丢包),等到这个数据报到达对方之后,此时已经断开连接了.是一个新的连接
那么服务器收到这个数据报,应该是直接丢弃还是按照正常逻辑来处理??
答案是直接丢弃,因为新的连接对应的服务器进程都可能不是同一个程序了
那么既然要丢弃,服务器是如何区分当前来的数据报,是否是"前朝的数据报"??
实际上通过序号就可以区分出来
每一次连接都是从一个新的数字开始作为起始的序号的,并且本次连接的数据之间的序号一定是沿着这个起始序号开始往下的序号(不会差别很大)
如果此时突然收到了一个数据报,序号与当前连接的起始序号相差很大,就可以认为是"前朝的数据"了
实际上这样设定的目的就是防止不同连接的数据互相干扰
我们在上面画的是三次握手的示意图实际上是一个草图,我们来看看详图
详图相比简图,就是多了,在应用层方面,三次握手对应的代码调用的原生api是哪些,以及tcp对应的状态变化
我们重点理解的是几个状态变化:
listen:是服务器出现的状态,当服务器绑定端口成功之后,就会进入到listen状态,此时就意味着,就随时可以有客户端连接上来了
established:表示建立完成,可以进行后续通信了
四次挥手
四次挥手是断开连接的过程
我们之前说过,三次握手,一定是客户端先发起的第一次请求
但四次挥手则不一定,客户端和服务器之间都可以主动发起.我们此处以客户端主动退出为例,即客户端代码里面调用socket.close方法 或者 客户端进程结束
FIN是finish的缩写,是6个标志位的最后一位
四次挥手的过程为:
断开连接就是,通信双方都把之前保存的对方的信息都删了.在TCP中就是希望达成的效果是:双方互删
那么四次挥手的过程中,能否像三次握手那样,将中间两次合并??
答案是常规情况下是不能合并的,特殊情况下是可以合并的
为什么不能合并??
实际上,我们在三次握手的过程中,ack和syn都是系统内核自动控制发送的,是同一时机
但是在我们四次挥手的过程中,就不一样了
此时中间ack和fin数据报的发送操作,不一定是同一时机,而是中间会隔很久
当服务器发现scanner没有next的时候,就结束循环了,就会进入close操作,客户端调用close / 客户端进程退出,此时fin数据报过来之后,服务器就能感知到scanner没有next了
但是实际上我们当前的服务器写的很简单,感知到scanner没有next就立即结束循环调用close了
但是如果代码更加复杂,在没有next 和 close之间,又写了很多代码??甚至都没有调用close
此时就指不定,close要什么时候执行了
由于这两个数据报发送触发的是不同的时机,因此就很难合并
但是在特殊情况下,上述两个操作是可以合并的
在TCP协议中还有一个机制,延时应答,即回复ack的时候不是马上,而是稍等一会,在这种情况下就能够合并
但是终究,上述的合并的情况是属于一种特殊情况,对于一般情况是不能合并的,所以还是将断开连接 称为 "四次挥手"
同样,我们也来看看四次挥手的详图
CLOSE_WAIT:被动一方进入的状态,表示等待代码调用close
当代码调用close得越及时,这里的状态就不越容易被看到
有的时候,可能会在服务器这边见到大量的CLOSE_WAIT => 说明代码大概率就有bug了,即代码很可能就忘记调用 close 或者close调用的不及时
TIME_WAIT:存在的意义就是为了应对最后一个ACK丢包这样的情况
客户端在收到 服务器返回的fin之后,不能立即释放tcp连接(如果立即释放了,后续一旦对端重传的fin,此时客户端就不能应对,即无法返回ack了)
那么此时在客户端这边,就需要有一个特殊的状态,来应对可能到来的重传的fin数据报
而TINE_WAIT状态不是持续的,而是有一定时间.在一定时间内,如果没有收到重传的fin,潜台词就是,最后一个ack已经被对方收到了,就不会重传fin,此时time_wait就可以释放了
那么也出现一种小概率的极端情况,就是对端一直不发剩下的fin,可能是bug / 对端没有调用close,那么等待一定时间之后,自己就会单方面将 保存B的信息删掉
注意:服务器调用close后不是真正的断开连接了,真正将对端信息互删 是在4次挥手完了之后,才断开连接
那么如果有时候发现服务器端出现大量的time_wait,如何处理??
出现大量的time_wait,说明服务器这边触发了大量的主动断开TCP连接的操作,
这样的情况对于服务器来说是很不科学的,因为一般都是 客户端主动断开的连接
总结:close_wait不一定是服务器处于的状态,time_wait也不一定是客户端处于的状态 而是:close_wait是被动接受断开连接的一方,time_wait是主动发起断开连接的一方
滑动窗口
我们前面讲过,确认应答,超市重传,连接管理,都保障了TCP的可靠传输
但是TCP在保证可靠传输的同时,也要考虑效率问题,也希望能够尽可能高效的完成数据传输
滑动窗口就是一种提高效率的机制
如果不引入滑动窗口,那么数据的传输过程就是这样的
就是A每收到一个ACK,才发送下一个数据
实际上这样是比较低效的,大量的时间都花费在等待ack上面
当引入滑动窗口
此时就是从之前的一条数据一条数据发送 => 批量发送
那么此时就可以将等待时间重叠了
所谓滑动窗口,窗口就是表示,不等待ack,批量发送是多少数据,这个数据的量就是窗口大小
如图所示,白色区域就是表示,当前批量发送了多这四组数据 ,等待这4组数据的ack
当收到了确认序号为2001的ack,此时就代表1000 - 2001的数据得到了确认,标记成灰色了;于此同时,也发送了新的数据(5001 - 6000)
此时还是继续等待4组数据的ack
引入滑动窗口后,批量发了4组数据之后,不是等待4组数据的ack都收到才发送新的数据,而是 收到一个ack,就往后发一个新的,就类似于窗口的滑动
那么在滑动窗口的情况下,出现丢包了怎么办
情况1:数据已经到达了,但是返回的ACK丢了
如图所示,此时序号为1001的ack丢了,但是实际上不需要做任何处理
下一个2001的ack实际上就是告诉发送方,当发送方收到2001的数据时候,此时2001之前的所有数据都已经收到了
同样,尽管3001 和 4001 的ack丢包了,但是当发送方收到5001的ack后,表示前面的3001 - 4000 4001 - 5000的数据也收到了
此时ack确认序号的规则,巧妙的解决了这里ack丢包的问题
情况2:数据报直接丢了
如图所示,虽然1001-2000的数据丢了,但是此时主机A还是仍然给B发送数据
但是接下来的每个数据,B返回的ack都是在索要 1001-2000这些数据
当A这边连续收到若干个1001这样的ack后,就明白了1001 - 2000数据报丢包了,就会重发这个数据报
此时重发1001 -2000被接受之后,B就是直接索要7001 - 8000的数据报
当某一处存在某个缺口的时候,返回的ack确认序号就是一直在索要缺口的数据
此时一旦缺口被补上,那么就直接从队列的最后一个数据的序号继续往后索要
上述针对丢包的处理实际上,整个过程是非常高效的
(1)对于ack丢失,不做任何处理
(2)对于数据丢失,只需要将缺失的数据重发即可,其他数据不必重传
这就是滑动窗口之下,搭配的丢包处理机制:快速重传
那么超时重传和快速重传,好像是两个不一样的重传机制??会不会出现冲突?
实际上不会.这两种机制实际上是不同情况下,采取的重传策略
可以认为,快速重传是 超时重传在滑动窗口下的特殊变种
因为如果你的tcp传输的数据较少,不频繁,此时就不会触发滑动窗口,得是你短时间内 传输大量的数据,超能触发滑动窗口
如果没有触发滑动窗口,那么此时还是 按照超时重传的传统方式来解决丢包问题
一旦触发了滑动窗口,此时才触发快速重传,按照ack反馈的次数来解决丢包问题
总的来说,滑动窗口,说是提升效率的机制.更加准确的说,是亡羊补牢的机制
因为TCP为了保证可靠传输,是牺牲了很多效率的复杂引入滑动窗口,就是为了让效率牺牲更小一点,但是还是存在牺牲的,同时也让协议变得更加复杂
再怎么使用滑动窗口,速度还是不可能比UDP这种没有可靠传输的机制快的
流量控制
在滑动窗口里面,我们涉及到一个关键概念,就是窗口的大小
实际上,这里的窗口大小是可以设定的
也就是说,我们可以通过改变窗口的大小,来控制发送方的发送速度
那么窗口越大,单位时间内传输的数据就越多,效率就越高
反之就越低
通常情况下,我们肯定是希望尽可能高效的传输
但是高效的前提一定是保可靠性
如果发送太快,接收方可能处理不过来,此时如果引起丢包,那更加得不偿失
那么接收方就要根据自身的处理能力,反向制约发送方的速度,使双方达到一个平衡
那么具体是怎么衡量接收方的处理速度呢??
实际上是通过接受方的接受缓冲区(阻塞队列)来判断的
实际上就是以空闲空间大小(未使用的空间大小)作为发送方发送数据的窗口大小
那么接受方就需要将这个空间的大小告诉发送方
在接收方给发送方返的ack报文里面,在报头里面就存在16位窗口大小
这个东西只会在ack报文里面生效,含义就是 接收方缓冲区里面空闲的空间大小
那么既然是16位,是否就意味着,最大的就是 64kb呢??
答案是否定的
在选项里面实际就包含有一个特殊属性,"窗口拓展因子"
有这个选项之后,此时的窗口大小就是 窗口大小 >> 窗口拓展因子
此时发送方就可以根据上述窗口大小,来决定下一轮数据发送的窗口大小了
在上图中,实际上没有考虑B应用程序消耗数据的过程.但是实际上B的应用程序,也在不停的消耗数据,使剩余空间变大
一旦缓冲区满了,就会告诉发送方,暂停发送
如果A暂停发送,B也就不会发对应的ack了,但是如果此时B缓冲区发生改变(应用程序消耗了一部分数据),那么如何告知A??
实际上当收到缓冲区满了的信号之后,此时A就会周期性的发送"窗口探测报文",也是不携带业务数据载荷的tcp数据报,主要目的就是为了触发ack,从而知道,B这边的缓冲区的情况
拥塞控制
与流量控制类型,都是和滑动窗口搭配的机制
流量控制是站在接收方的角度,影响发送方的速度
但实际上发送方发数据给接收方,中途会遇到很多的路由器和交换机,我们还是要考虑这些设备的性能瓶颈
链路上的任何一个节点,性能瓶颈都会制约发送方的发送速度
但流量控制,很容易就能够定量的来衡量接受缓冲区的剩余空间大小,用这个来作为 发送窗口的大小
但是考虑中间节点就比较复杂,实际上中间有多少设备是不清楚的,更何况 每次走的路径不一样,每个设备的处理能力,繁忙程度也不一样
实际上处理方法是,任凭你中间有多复杂,tcp都把他们视为一个整体,然后通过实验的方式来找到一个适合的窗口大小
刚开始,按照比较小的速度,小的窗口来发送速度,如果没有出现丢包,说明中间链路非常畅通,此时就可以增加速度,增加窗口大小
如果还是没有出现丢包,仍然很畅通,中间路径都是可以抗住的,那就继续增加速度,继续增加窗口大小
增加到一定程度,发送速度非常快了,此时可能某个设备就会达到瓶颈,出现丢包了
此时发送方立即减小窗口大小,继续尝试增加
如果还是丢包,就继续尝试减
不丢包就继续尝试加
这样就能找到一个合适的窗口大小的值,就可以不出现丢包,并且还能以比较快的速度来玩笑传输
那么在复杂的网络里面,就能够按照上述的方式来动态调整,随时应对,适应网络中的变化,达到动态平衡
那么拥堵控制这里的窗口,影响发送速度,流量控制这里窗口,也是影响发送速度的
那么听谁的?? 实际上就是 这两个窗口谁小,就听谁的
那么究竟拥塞控制,窗口的大小是如何变化的?? 是否有规律??
规律就是:
(1)刚开始以较小的窗口来传输数据(主要是因为刚开始不知道网络是否拥堵,先试试看)
(2)接着按照指数的方式来扩大窗口(*2)
(3)指数增长的过程中,达到某个阈值,就会变成线程增长(+n,这里的n也是可配置的)
(4)线性增长后,速度越来越快,增长到一定程度,就会出现丢包
此时发送方就大概知道,网络的大概能力是什么水平,此时就会立即将窗口变小
缩小有两种方式:
①直接缩小到低,回到的最初的慢启动的时候,接下来还是指数增长,线性增长
现在这种方式已经废弃了
②缩小 出现丢包情况的窗口大小的一半,接下来线性增长
这是目前tcp使用的方式
①这个方式比②方式更加低效,一下子缩得过火
延时应答
指的是,ack不会立即返回,而是等一会再返回
那么为什么要延时??
核心目的就是为了提高传输的效率
我们在前面说过,决定传输速度最关键的因素就在于 窗口大小
我们希望在接收能够承受的前提下,尽可能的提高窗口大小
通过延时,就能够使窗口大小得到提升
延时,就是为了给应用程序腾出更多的消费数据的时间
如图所示,假设收到1kb的数据之后,缓冲区的空间还剩下4kb,如果立即返回ack,那么ack指定的窗口大小就是4kb
但是如果延时一会再返回ack,此时在延时的时间里面,应用程序很可能就消费了刚刚1kb里面的一部分数据,那么此时的窗口大小就会对应变大
实际上这里的变大,其实也是和应用程序的处理能力直接相关的,是在合理范围内的变大
这里的延时,就有两种方式
(1)按照一定的时间来指定延时
(2)按照收到的数据量来指定延时
在实际上tcp连接中,这两策略实际上是结合使用的
捎带应答
这是建立在延时应答的基础之上,提升效率的机制
在日常开发中的通信,通常是"一问一答"这样的模型
正常来说,ack是内核接受请求之后,自动返回的
而响应数据,是应用程序代码,执行一段逻辑之后,返回的
那么由于延时应答的存在,ack不一定立即返回,在ack稍等一会之后,正好就要返回响应数据
此时就在响应数据里面,tcp报头中ack这一位设置上,将确认序号以及窗口大小都设置上
这样的效果也是为了提升效率,将两次传输,合并成了一次
面向字节流
在字节流读写数据的场景中,就会涉及到一个比较关键的问题:粘包问题
那么当服务器调用read来读取请求,由于字节流的特点,读的时候,怎么读都行,一次可以读一个字节,也可以若干个字节
在这个场景中,服务器就无法分辨,从哪里到哪里是一个完整的单词
在实际上,此处粘包粘的就是应用层的数据包
主要是需要区分,从哪里到哪里是一个完整的应用层数据包,就需要我们明确包之间的界限
(1)使用分隔符
可以定义任意字符,只要字符在请求数据中不存在
(2)约定包的长度
就是类似于达到这样的效果,这样服务器就知道一次需要读多少长度的数据了
异常情况
(1)网络连接时,其中某一个进程崩溃了
实际上进程崩溃也好,正常结束也好,操作系统 都能够回收对应的pcb,就可以释放里面的文件描述符表,此时也就相当于调用了close
此时人仍然会与对方进行正常的4次挥手操作
(2)某一个主机被关机(正常流程的关机)
对于这种正常流程的关机,操作系统会先尝试结束 所有的用户进程,然后再进入关机流程
这个过程也会和第一种情况一样,进行4次挥手
但是与上面不同的是
第一种的4次挥手可以挥完,虽然进程不在,但是操作系统仍然管理这着tcp的连接,可以顺利的和对方挥手挥完
但是对于第二种,就可能挥完了.也可能挥不完
即这个掉电的主机A主动触发了fin,但是之后的流程还没走完,系统就关机了
此时B收到了fin之后,就会返回ACK,再返回fin
但是由于A已经关机,那么这个fin就会反复重传几次,还是没有ack,此时还是会把A给删了,至于A都关机了,自然就会将保存B的信息删掉
(3)某个主机电源掉电
假设A和B之间通信,突然之间A就掉电了(不能做出任何反应),但是此时B还以为A还存在
就会分成两种情况:
(a)B是数据发送方
那么接下来B发送的数据就都不会有ack了,就会触发超时重传,重传几次之后,还是没有响应,就会发送复位报文(RST),RST也没有响应,就会单方面删除保存的A的信息
(b)B是接收方
接收方,是无法知道对方啥时候给他发数据的
有时候当A沉默一会,B也不知道A是挂了还是 暂时暂停一会
实际上,B在一定时间之内没收到A的数据之后,就会触发 "心跳包"
心跳的特点就是有周期,并且表示没有心跳就挂了
可以认为是一个没有载荷的数据报,只是为了触发对方的ACK
B给A发了一个心跳包,如果A响应了ACK,那么A就是正常的
如果A挂了,B不会收到任何回应
连续发了若干次后,A都没有响应,这时候B就认为 A挂了,就会单方面释放连接
实际上,虽然tcp里面内置了心跳包,但是这个心跳包的周期比较长,指望通过这个心跳发现对方挂了,往往需要分钟级别这样的时间
而在实际开发中,经常会实现应用层的心跳包,用更高频率,短周期(秒级/毫秒级别)的时去发送心跳包(ping - pong)
此时一旦某个设备挂了,就可以快速的发现问题
(4)网线断开
本质上就是第三种情况,A和B之间建立TCP连接,突然某一方网线断开了
感谢您的访问!!期待您的关注!!!
T04BF
🫵 今天记得敲代码