文章目录
- 流量控制
- 滑动窗口
- 延迟应答
- 捎带应答
- 总结TCP
- 拥塞控制
-
- [拥塞控制的策略 -- 每台识别主机拥塞的机器都要做](#拥塞控制的策略 -- 每台识别主机拥塞的机器都要做)
- 面向字节流和粘包问题
- tcp连接异常
Tcp状态
建立连接时
断开连接时
三次握手
tcp三次握手时我们想看看双方状态变化的过程
今天把服务器的代码注释掉大部分,连连接都没有获取,只是创建套接字监听了
客户端创建套接字然后connect连接,失败的话还会重连
服务器启动,查到服务LISTEN状态
如果netstat -nltp 改为 -ntp不查监听状态的连接情况
客户端连接服务器
则站在服务器角度认为已经有客户端和我建立了连接,此时S状态ESTABLISHED
为什么local address 和我云服务器公网ip不一致?
公网IP被模拟出来的,local address才是真实内网IP
我们先不管
站在服务器角度已经有客户端来连我了,连接是双向的
查客户端连接情况
此时我们就看到了 在客户端机器上的已经建立好 的连接,服务器端也看到了来自客户端发起来的建立好的连接
所以
结论
1。连接建立成功和上层有没有accept没有关系
三次握手是双方操作系统自动完成的!
当我们一共起了三个客户端连接服务器时
查服务器连接状态发现有一个SYN_RECV
第三次时怎么客户端和S连接怎么没建立成功呢?
查客户端
客户端当前已经认为连接建立好,而S说我的连接还没有建立好,他的状态没有变为ESTABLISHED。
这不就是连接没有建立成功时,连接建立成本在客户端。
那么从第三次往后,在客户端连接时,除非上一次有链接退出,否则新链接一直都是SYN_RECV
如果今天建立连接时服务器没有进行accept的话连接是可以三次握手完成的
三次握手完成之后accept获取的是新链接
第二个现象
当建立第一次没问题,第二次也没问题,当建立第三次时客户端认为连接建立好了,但服务器会SYN RECV
结论
2。
listen的第二个参数
我们刚刚把它设置为1
所以底层能直接建立的连接数是2
左边一大堆客户端,右边服务器
如果一个客户端来向我建立连接时,如果三次握手能完成,服务器就在底层建立好连接,连接可以暂时不把他拿上去,底层是能够把链接建立好的,第二个客户端再来的时候呢,三次握手一旦完成,OS也一定要在建立连接,此时如果有第三个第四个,如果服务器允许你把链接建立好,那么S上一定会存在大量的已经建立好的连接,但是已经建立好的连接并没有被上层accept拿上去,所以我们的OS就必须起一个对已经建立好的连接进行临时维护的效果,要对这些没有被拿上去的连接管理,所以先描述在组织,它采用队列的形式来管理已经建立好的连接,
如果再有第三个客户端来了,此时不管listen第二个参数,此时OS创建好连接,这个链接要被上层accept的,这个连接继续放到队里中,以供上层进行调用accept来获取连接。
上层可以从队列中把链接拿走,三次握手每一次建立连接的时候它的本质就是形成连接并把这个链接连入到队列中。
accept的本质就是从这个队列中把链接取走,来和特定的文件关联起来给我们返回文件描述符。
这个过程。这个队列就像已经建立好的连接在底层维护的CP模型,上层取,底层帮我们入队列。
listen第二个参数表示用来描述这个队列当中允许的节点
listen第二个参数backlog+1 表示底层已经建立好的链接队列的最大长度
我们把这个队列叫做全连接队列
也就是说,底层未来无论怎么三次握手,握手成功的就必须入队列,不成功的不入队列,listen的第二个参数我们刚才设为1,这个队列个数最大是2,因为我们实验服务器并没调用accept,所以CP模型中没有消费者只有生产者,所以不断建立连接,前两个连接直接建立成功,第三个时客户端认为自己建立成功,但服务器并没有建立成功,第三次来队列已经被打满了,再第三个来了时候不让你入队列了,此时服务器的状态就是SYN RECV
底层OS就给我们维持了全连接队列。
第三次来的时候,即便是三次握手成功了,S也决不能把连接放到全连接队列里。
问题是
SYN RCVD 代表的是已经收到SYN,我把SYN+ACK给对方发出了这种情况,还是我只要收到一个SYN我就已经是SYN RCVD了?
从一个状态变到下一个状态是必须满足下一个状态的条件,从SYN RCVD进入ESTABUSHED必须我得收到ACK,而我当前对于S来讲他并没有变为ESTABUSHED,是不是意味着第三个ACK客户端没有发呢?
首先客户端第三个ACK一定发了,因为我们刚看到客户端连接已经建立成功ESTABUSHED,所以最后一个ACK一定发了,只不过因为listen第二个参数限制,我们不准服务器从SYN RCVD变为ESTABUSHED,怎么做到呢?
必须保证三次握手成功 才能变成ESTABUSHED,首先C一定发了第三个ACK,只不过S把第三个ACK直接丢弃了,因为S判定全连接队列已经满了就不允许你在建立连接成功了,你前两次握手允许你握手成功,但是客户端发第三个ACK时,我在收到ACK的时候发现全连接是满的,这个链接即便是进入ESTABUSHED他都无法握手完成,无法放到全连接队列里,无法被上层拿走。此时要求S把最后一个ACK直接丢掉。S三次握手不完成也就无法从一个状态变迁为下一个状态,所以S一直都是SYN RCVD
我们过了两分钟再查一次S连接情况,发现SYN RCVD不见了
我们发现建立好的连接维持的时间会特别久,可是SYN RCVD状态因为上层不取走,所以全连接队列一直是满的,所以S端此时不能长时间一直待在SYN RCVD的状态。
为什么?
客户端还认为自己三次握手成功,他的连接一直存在
结论3
S端,不会长时间维护SYN RCVD,如果三次握手被建立连接的一方一直处于SYN RCVD的时候,这种连接称为半连接,半连接也要有队列,半连接的节点,不会长时间维护.
结论4
客户端和服务器双方连接建立不一致的问题
现在能不能理解三次握手的过程和accept是没有关系的,代码里accept都被注释掉了
刚刚实验中,S一直处于半连接,C认为连接建立好了,我们C在发数据其实数据没有发,因为三次握手根本没完成,C在发数据反而重新三次握手,会有一些情况导致连接进行RST,比如S挂了,但C还认为连接建立好直接发数据然后S就会发送RST
总结
listen第二个参数+1 是底层全连接长度
如果全连接长度已经满了,在想服务器发起连接,S要维护半连接队列,处于SYN RCVD状态的半连接不会长时间维护,最后释放。
所以客户端依旧给我发送大量SYN,服务器如果也给他SYN + ACK,即便我们没有把他放到全连接队列里,我们也会临时把这些连接放到半连接队列里,半连接队列也会消耗资源,有人说这个半连接队列也有长度限制不会维持很久。这个没问题。
他最大的问题其实不再是消耗服务器资源问题了。
我们发现我们的连接要进入全连接必须得先进入半连接
而半连接队列也有长度限制,所以你给我发送大量SYN也不怕,但是我最害怕的不是他消耗我资源,而是他消耗我半连接本身的长度,他给我发送大量的SYN时可能把半连接队列打满了,非法连接连着我又不跟我通信,把半连接打满了,此时来一个正常的客户端想连进来他是连不进来的,因为半连接已经满了,虽然半连接维护时间不久,但也架不住C始终一直发SYN,最后导致客户端直接连接不上的问题
但此时服务虽然没挂,但新来客户端,因为所以全连接和半连接都被恶意连接连着呢,导致正常用户连不上,半连接进不来,进不来就无法进入全连接,无法进全就无法被上层accept,就无法被处理,所以此时客户端也连不上。
这个叫做真正意义上的SYN洪水
所以抢票时发现浏览器直接卡死了,你去访问时半连接全连接都被打满时,所以进不去。
解决方案
tcp选项序号机制,cookie机制保证非法连接禁掉
listen第二个参数为什么不能太长
因为全连接队列如果太长了可能会导致S存在有些链接来不及被上层处理但依旧要在OS内长时间维持,服务器非常忙如果把全连接队列打的很满,服务器如果非常忙,没有时间不断把链接拿上去,可是不断有新的连接到来,所以我才很忙,如果全连接队列又维护的太长,注定在S非常忙的时候,你反而还有大量的资源被占用,而且只是单纯被占用不创造价值。
这个队列设置的短一点,资源就被腾出来了供上层使用。
listen第二个参数为什么不能没有?
既然忙了我把全连接资源全部 释放,全力支持上层,让上层拿所有资源处理,可是很尴尬的是,万一上层处理完了,突然服务器闲置了,此时可以有能力获取一个链接,所以维持一个链接队列上层就直接能把链接拿上来。
但如果你不维持队列,此时上层走了你想再拿取决于现在突然有没有客户端连,没有人连accept就阻塞了,服务器就不满载了,则服务器资源也就没有充分利用。
比如海底捞生意火爆,他会在门口摆几张凳子,刚好里面的人走了的话,外面的坐着的就能进入了。
如果你不给椅子 里面的人走了你就补不上客,一天就损失很多钱了。
那么队列长应该是多少呢??
服务器是什么样的,计算机硬件如何,都会影响队列应该维持多少个。
完全按情况来定,5,10,32,64,128
我们设置为10,16,32就足够了。
如上三次握手的情况 状态变化全部谈完。
四次挥手
我们上面实验应用层都没有主动close
C端断开连接发送FIN,进程退出了文件描述符也关了,服务器也给对方ACK了但服务器并没有调用close,所以服务器无法从CLSOE WAIT 变迁到LAST ACK,所以服务器查连接情况发现CLOSE WAIT,所以这个状态维持时间还挺久的
重点是C的TIME WAIT状态
理解TIME WAIT状态
此时客户端再连接服务器
无论是C还是S,主动断开连接的一方要进入TIME WAIT状态
服务器主动关闭,服务器先发FIN然后C也断开发送FIN,此时就想看到服务器的TIME WAIT
但是没查到,我们只能改改代码了,因为我们没有把链接拿上来也就是accept,连接在全连接队列里。
则S端先手关闭文件描述符调用close,C没退并给S发ACK所以S的连接状态变为FIN WAIT2
然后让C再进行退出,C也就自动底层进行剩下两次挥手
此时S的连接进入到TIME WAIT
客户端再查连接情况,就查不到了
前提是把链接从底层获取上来,如果你没有获取上来直接断开,看不到现象
左边当成服务器,右边今天是客户端
我们直接服务器关闭连接FIN,然后C响应ACK,S端进行FIN WAIT2,接着我让C Ctrl+c 了直接发送FIN,底层自动发的,因为FD生命周期随进程,直接LAST ACK发送FIN,然后S再给他最后ACK,最后ACK的时候对于C来讲他收到第四个ACK连接断开了没毛病,但是对于主动断开连接的一方呢此时他会处于TIME WAIT状态,可是S方四次挥手完成后主动断开连接的一方会处于TIME WAIT。
过了一段时间TIME WAIT自动消失了
5。主动断开连接的一方,在四次挥手完成之后,要进入time wait 状态,等待若干时长,之后,自动释放
有时候服务器的端口没法重新立马绑定。
因为主动断开连接的一方,如果是服务器方,就要主动进入time wait 状态 ,连接没有被彻底断开而服务器已经挂掉了,我想立即重启,可是连接没有断开意味着IP和端口正在被上一个进程使用,如果你想立即重启S进程,根据端口号无法被两个不同的进程同时绑定。所以无法立即绑定了。
我们不能让 服务器因为time wait的问题无法立即重启
所以套接字属性设置成允许地址服用
level 层级都是SOL SOCKET
设置允许地址复用后,time wait的状态S端依旧存在,但是允许地址复用,立即重启服务器。
那客户端怎么没这个问题呢?
因为C用的是随机端口
那TIME WAIT 时间是多久?
为什么?
TCP协议规定,主动关闭连接的一方要处于TIME_ WAIT状态,等待两个MSL(maximum segment lifetime)
的时间后才能回到CLOSED状态
最大存在时长MSL
MSL:报文在网络中存在的最大时长
两倍MSL
存在时长可能会因为网络阻塞存在很久
正常网络发送报文的情况下
最大传送时长:就是报文一来一回的时长,从一端到另一端(正常传送ms)
那这个时间应该是多长?没个定数,都是秒级的。
理由
可能我自己历史上发了很多数据,突然S或者C就想把连接关掉了,导致S或者C进入连接断开的历程,可是网络中还有残留的数据存在,所以为什么要等,我要等历史数据在双方网络信道中消散。
前两次挥手时,挥手失败有机会去矫正他,因为前两次挥手双方四次挥手双方连接不会彻底释放,即便前两次挥手都丢包了,双方可以补发,可是最后一个ACK丢失,如果主动断开连接的一方立即释放了,另一方收不到这个ACK,就会一直处于LAST ACK,他还会对FIN做补发,你再发也没意义了,所以我们让主动断开连接的一方维持TIME WAIT,万一最后两次握手时ACK丢了,在我TIME WAIT等待期间还有机会收到补发的FIN,我还可以重新再给你ACK,这样确保我们正确的四次挥手
总结主要原因为什么要等待呢?
1。让通信双方历史数据得以消散
2。让我们断开连接,4次挥手,具有较好的容错性
历史数据可能姗姗来迟,可是tcp不是有按序到达,超时重传吗,如果报文在路由器阻塞很长时间我早就补发了,为什么还要等数据消散呢?
断开连接的时间点,历史消散的意义不是让S端把报文收到,而是让S端把报文丢掉。
为什么?
因为会存在极端情况,比如C和S断开了瞬间C又想和S重连,双方IP和端口都一样,如果有历史报文存在就有可能对后续通信过程产生影响。
如果第三个FIN丢了这种情况谁也没办法,你给我发FIN不过来,补发过不去,说明网络出问题了,我等是应该的,你补发也是应该的。
如上就是TCP连接管理部分
流量控制
流量控制不是在你接受缓冲区满了才控制的,他是在给你发的时候从最开始就已经开始控制了。至少刚开始发绝对不可能给你打满。
TCP 首部中的 "窗口大小" 字段, 通过ACK端通知发送端
问题一
第一次的时候,怎么保证发送数据量是合理的
三次握手完成然后开始发数据,第一次发我可能并不知道对方接受能力,此时我应该挟带数据量多大?
你不确定对方接受缓冲区会不会溢出
不要理解三次握手,只是三次握手,三次握手期间双方也交换了报文!他们已经协商了双方的接受能力!
双方能交换报头里的窗口大小,在三次握手期间
问题二
必须三次握手成功之后,我们才能消息发送
第三次的时候,我们可以携带数据!
前两次不准携带数据,因为前两次 三次握手并没完成。
既然第三次可以携带数据,也就决定了双方协商通信接受能力一定是在前两次协商的。
这个也可以叫做捎带应答,可以提高效率
比如我连续发了三个报文时,对方的接受窗口变为0了,根据流量控制,所以A主机就不发了,他就得等,A总不能一直等吧,等B主机接受缓冲区有空间。
A等的时候超过重传时间还没有窗口更新的通知时,A会发送窗口探测报文,因为tcp发送报文都必须得确认,确认应答嘛,所以发一个窗口探测,B就必须应答,只要应答就必须携带16位窗口,所以A就得知B的窗口有没有更新了
主机A发送窗口探测时,会不会挟带数据呢?
不会,不需要,不敢
他并不清楚B的窗口是否更新了,所以他只是发了个tcp报头
万一A等的时候,A到B 网络出问题了,则A也无法窗口探测了,同时也为了提高效率,B的窗口更新了也会立即向A推送更新报头。
A收到更新之后,根据更新的窗口继续发数据
1。双方的策略哪一个快就用哪一个,提高效率
2。双方互相通知可以对网络单向出现问题,一方无法给对方发送报文,这种情况也能提供很好的容错性。
探测和更新通知都失败则网络出现问题了
确认双方不可能再通信了,则通信双方一定要主动关闭连接。
窗口大小是16位的,tcp面向字节流,也就是说双方三次握手,握手协商出来的窗口大小最大是2^16 = 65536的字节 = 64K左右
换句话说双方tcp接受缓冲区也就是65536字节吗?
默认是这样的。
但是tcp首部里除了有tcp默认标准字段,还有选项字段包含扩大因子M,双方tcp协商时如果你想把本地缓冲区扩大,你可以设置tcp窗口扩大因子,会让16位窗口数字左移,让 数据double一下。
总结
问题
流量控制 属于可靠性还是属于效率
直接数据可靠性的,防止正常的报文丢包。
其实变相的也有提高效率。
所以不会对正常报文丢包了,也不会对正常报文重传了,不重传也就不会有超时重传。
滑动窗口
为了能够有效解决tcp发送数据进行串行化的发送确认
我们把发送数据方式更改为右图的方式
已经发出去,但是暂时没有收到应答的报文,要被tcp暂时保存起来
tcp发出去的报文可能是多个,不再是一个了(左边)
已经发出去,但是暂时没有收到应答,可能会在发送方存在多个报文
确认应答,发一个收一个效率太差
所以收发的时候都是多发多收
已经发出去,但是暂时没有收到应答的多个报文,会被保存到哪里呢????
tcp没必要单独给你保存一份
tcp发送缓冲区和接受缓冲区(暂时不谈)
对于C来讲,他要发送的数据本来就在发送缓冲区里,所以我只要把数据发出去,发出去的本质不就是我把数据从内存,或者tcp发送缓冲区里把对应的一部分数据交给网卡,我的发送就完成了。
即便已经把数据发出去了,但是数据还是在发送缓冲区里。我要做的无非就是把缓冲区想办法做个区域划分就可以了。
我可以把缓冲区划分成三部分
已发送 已确认 | 已发送 未确认 | 待发送
为什么待发送只到箭头位置,后面不是还有吗?
之前说发送缓冲区就是个框框
后续又说发送缓冲区当成一个数组
今天又说 缓冲区可以划分成一个个区域
一旦划分好,我们未来发送的数据,像已经发送已确认的数据可以被覆盖。
被上层用户覆盖,将来用户想交给发送缓冲区的数据是可以直接往这个区域写的。
这部分数据叫做从tcp缓冲区移除它了!
所以从系统,到文件系统,到网络
计算机不存在真正意义的数据清空。
第二个区域中,凡是在这个窗口里叫做已发送,也并不一定这个窗口里的所有数据都发了,他表示的含义是可以直接发/已经发,但是尚未收到应答的区域
我不管你发还是没发,反正就是尚未收到应答的区域就在这里
所以这个区域的数据已经发了,发完没有收到应答时,这部分区域我们不让他做任何变化,不把它纳入到已发送已确认中
这部分内容就一直是有效的,所以已经发的数据我没有收到应答,我就把这个区域维持着,此时我们就叫做把数据暂时保存,只要我收到应答了,我要做的无非就是把收到应答报文纳入到前一个区域,这个报文就被清掉了。
所以我们把已发送但尚未收到确认的区域叫做滑动窗口。
1.滑动窗口 在哪里啊??
是我们发送缓冲区的一部分!!
对我们这个已发送未确认的区域最大是多少?
凡是在滑动窗口区域里的内容是可以直接推送给接收方的,意味着因为有滑动窗口的存在呢所以才支撑了tcp一次向对方发送过去大量的tcp报文!
所以滑动窗口最大是多少呢?
我一次给对方赛数据一定要考虑对方的接受能力,所以
2.滑动窗口的范围大小,是对方的接受窗口(目前)
假设接收方的接受窗口剩余空间会更新给对方,对方只要合适的把自己的窗口大小调成合适的大小,他就可以一次给对方发送大量数据并且还不超过接受范围。
也就是流量控制和滑动窗口是有勾连的。
3 . 如何理解区域划分?通过指针/下标来进行区分即可!
那你说把发送缓冲区想象成数组,你所谓的区域划分是怎么做到的?
发送缓冲区和接受缓冲区一个道理
把发送缓冲区想象成数组来看的话,所谓的数组存放的一个一个字节,所谓的已发送已确认无非是一段区域,已发送未确认也是一段区域,未来想衡量一部分区域我们只需要有一个数组的下标就可以了
win start win end
所以区域划分其实只要在双方的tcp协议里面维护几个整数,然后限定出开始的区域和结束的区域,此时范围不就出来了吗,win start 指针前的叫做已发送已确认,win end之后的叫做待发送,中间的叫做已发送未确认。
这就是在算法里看到的双指针。
所以所谓的 收到一个正常的ACK确认
,部分数据确认到了,只不过是把win start指针向后移动,表明这部分数据已经确认了
确认之后呢,这就叫做窗口向右进行滑动,如果对方接受能力变大了,无非就是把win end向待发送区域扩展,让他可以发更多数据,这个也叫做窗口滑动了,所以窗口滑动本质上就是指针右移。
不管怎么讲滑动窗口时,只需记住这三个点,保你理解滑动窗口主节奏不乱。
对于滑动窗口来讲最终就是在发送缓冲区里对双指针进行相关的移动。
为什么很多教材里都喜欢把滑动窗口分成一块一块的呢?
我把滑动窗口直接弄成一个大块,打一个大报文直接发给对方,单次IO数据量大效率高,而且滑动窗口范围大小,是对方的接受窗口也不会出问题
这个问题和硬件有关系,我们有这么大数据块为什么要分批发送呢?
你直接把四个报文干成一个直接发过去,以前说过IO中单个数据块越大效率越高,为什么要把他分成小块呢?
硬件不允许通过网卡发送大块数据
所以即便你的 滑动窗口可能很大,一次可以同时给对方赛很多数据,但对不起我们的数据报也是一段一段发的。
目前认为:至少滑动窗口的大小,不能超过对方的接受缓冲区的剩余空间的大小,即应答报文的窗口大小
问题
滑动窗口处理异常的情况下该如何理解?
滑动窗口只能向右滑动,能不能向左?
滑动窗口可不可以为0呢,变大,变小呢?
如果都丢包了怎么理解滑动窗口?
我们不谈是ACK丢还是数据丢,就认为是超时了,因为是一个概念最终都在发送方超时
如果2001-3000的报文丢了,窗口其他的块对方收到了还给我应答了,此时窗口要不要向右滑动呢?
窗口左边1001-2000收到了应答所以肯定窗口往右滑动的
但是2001-3000没有收到ACK,好像要在这里停下来,可是不要忘记了我们对于确认序号的定义:确认序号是x, x之前的报文我们全部收到了!允许少量的ACK丢失
意味着如果中间报文丢失了,如果收到了4001和5001应答,其实只不过是3001这个应答丢了,2001-3000的数据收到了。
所以对于滑动窗口来讲根本不会在红色箭头处停下来,直接会干到这里
所以目前为止根本就不担心中间部分的任何报文丢失
只要收到了最后一个,他的确认序号直接填的干到5001,所以滑动窗口直接就能最左侧干到最右侧代表全部收到了。
那要是最后一个丢失了呢,5001丢失了
5001丢了我们也不怕,之前的应答就收到了,收到后窗口更新到这里就可以了,
这个 报文我们就静等他超时然后补发就行了
如果这几个报文ACK全丢失了呢?全丢失了窗口大小不更新,我们收不到应答,那就全部都超时重新补发就行了。
对于滑动窗口来讲有确认序号的定义呢,凡是收到的报文一定能线性确认报文哪些都已经收到了,我们都是ACK丢了,无论是任何一个ACK丢了,或者中间的ACK丢了,还是两头的ACK丢了,都不影响,我们都可以保证对应的滑动窗口,左指针向右进行滑动的时候一点都不担心,因为可以根据确认序号的含义来随时正确的更新对应左侧的下标。
所以ACK丢失我们不怕。
那如果是数据丢了呢?
我还以2001-3000的报文丢失了
除了这个其他三个都收到了应答,问题是如果是中间报文丢了,那么其中4001和5001的ACK里确认序号应该填几?
根据确认序号的定义表示的是确认序号之前的报文都收到了。
4001和5001的ACK里确认序号不能填写4001或5001,因为确认序号的定义就是X之前的报文全收到了,所以4001,5001报头的ACK里确认序号只能填2001。
所以主机A窗口向右滑动时是不会出现越过丢包的情况,直接到4000到5000不会,他直接在窗口更新位置时,直接只能把窗口移动到2001。
所以我们根本不怕中间某一个报文丢失的问题,导致滑动窗口越过这个报文。
所以确认序号的定义保证了滑动窗口,线性的连续的向后更新,不会出现跳跃的情况。
那有人又说了要是2000的报文要是丢了呢?
3000和4000和5000的报文我都收到了,没关系3000,4000,5000的ACK只能1001
那要是5000的报文丢了呢,你确认序号最后收到的报文只能是4001
所以滑动窗口在左侧移动时,自始至终都不会跳过任何报文,无论是数据丢了还是应答丢了,他都能保证这一点。
有人就问了,就是2001-3000的丢了,2000,4000,5000收到了
此时主机A一次会批量发送大量数据,A是不是应该进入超时等待时间,未来把报文做补发啊?
他该怎么补发这个报文呢?
快重传
假如A批量化发送一批数据,其中1001-2000数据丢了,但是后续的报文都被收到了也有响应ACK,但是ACK里写的都是1001,如果连续收到3个同样的确认应答都是1001,则立即进行重发1001-2000的这个报文,如果重发成功则返回的就是7001
如果我们没有连续收到三个相同ACK,我们也可以等他超时
所以采用滑动窗口+快重传可以对A向B发送数据出现丢包的情况我们也能hold住
问题
已经有了快重传,为什么还要有超时重传?
快重传既快又能重传,还要超时重传干嘛呢?
只有是因为快重传是有条件的。
收到三个同样的确认应答才会快重传。
后面数据通信接近末期,没那么多数据发送也没那么多数据应答了,此时你的快重传就不会被触发。
所以快重传本意是为了提高效率的。
即便有了快重传也不能摒弃超时重传,因为超时重传给我们做兜底的。
根据我们刚才的分析,如果中间的报文有出现丢失的情况,窗口的左侧指针指向的报文一定是丢失的报文一个或多个中序号最小的报文,所以最小的报文后续可能还有很多报文意味后续有很多应答,所以这样的报文能触发尽快的进行重传,通信末期也可以采用超时重传做补发。
则应答和数据丢了都不怕
所以整个滑动窗口有了确认序号定义规则,所以整个滑动窗口才能准确的向后滑动
如果中间丢了两个,也就是1001-2000和4001-5000丢了呢
1001-2000补发后的响应就改为4001,4001-5000这个报文就如同1001-2000的处理方法。
问题2:滑动窗口向左移动?向右移动?移动的时候,大小会变化吗?怎么变化?会为0吗?
向左移动
不可以,因为滑动窗口本身划分的数据区域左侧是已经发送并且已经收到确认的,不会向左移动,所以不管是左指针还是右指针只会一味的++。
向右移动
会向右移动
分情况
移动的时候,大小会变化吗?
滑动窗口是固定大小吗?
根据刚刚对于滑动窗口的理解,目前认为滑动窗口的大小是不能超过对方接受缓冲区剩余空间大小的,即应答报文的窗口大小。
所以假设A给B发消息,B有自己的接受缓冲区,可是B主机不把数据取走,B剩余空间4000个字节,所以你这刚好有四个数据段4000字节
主机A再给B发报文,连续把4个报文全发过去了,所以B缓冲区被打满了,一段一段发的时候ACK的窗口大小从3000,2000,到1000,最后ACK就为0 了,所以滑动窗口的大小一定要照顾主机 B的接受能力,滑动窗口定义叫做可以直接发暂时不用收到应答的,什么叫直接发,直接发前提条件得保证也要处理流量控制,所以如果上层不取走,滑动窗口大小发送第一个报文时收到ACK右移,因为接收方上层一个数据都没取走,所以接收方给我通告的窗口只会越来越小,最后小到窗口大小直接就被干到了0
所以移动的时候,窗口大小会变化吗?
答案是 窗口大小是会动态变化的!
动态变化涉及到
变大
变小
不变
所以基于滑动窗口向右移动,而且滑动窗口必须动态变化要么变大变小不变
所以针对提出向右移动的三种方式。
我们上面说的接受方不取走数据的情况
假设最开始滑动窗口大小就是接收方缓冲区剩余空间大小
右不变,左移动
这叫做窗口变小的情况
窗口有没有可能变大呢?
比如接收方上层把缓冲区全部数据都拿走了,窗口大小直接干到16KB,四个报文全部被对方收到了,左侧直接移动到最右侧,
同时窗口大小ACK出来的就是16KB,右侧指针直接继续向右移动,所以窗口变大了。
左右都移动,范围扩大
范围到底扩多大,右侧不是直接干到最右侧,有可能数据只剩这一点了你更新那么大有什么用呢?
左右都移动,可以范围扩大,也可以范围缩小
假设我们发了四个报文对方也只剩四个地方,此时窗口是0,因为对方缓冲区满了,发送方窗口之前是4,然后对方拿走了2个,此时发送方收到更新了,此时更新窗口是2
大小也可以不变
刚开始是这样子
对四个报文确认
四个报文同时给我ACK,发送任何一个ACK时上层把4000字节也取走了,则右指针也会向后移动4个
此时窗口还是4
窗口整体向右移动,不会向左移动,移动的时候大小可能变大,可能变小,也可能不变,完全取决于对方接受能力变化,会变为0吗。会,代表对方接受能力为0 了。
如何用计算机语言表述
假设序号就是数组下标
int start = 根据确认序号,start就等于确认序号
int end = 确认序号+对方确认时给我窗口大小
start 永远都是确认序号,end范围的移动只要收到了ACK的win大小,我们直接从新序号的开始加上对应的长度,直接就更新到end
所以每收到一个报文的应答,我们就对窗口大小做调整,此时start 和end下标就不断进行右移,因为序号是默认增长的,所以start下标就会越来越大,因为end 等于确认序号+win大小,win大小为0,说明start = end,窗口就为0。但win大小有更新,取决于对方接收能力变大了,我们的窗口就变大,对方的接受能力变小了,我们的窗口就变小
一共要发4000,对方缓冲区还剩4000
先发了1001-2000,对方响应确认序号是2001
则start就是2001,此时的响应窗口大小是3000,2001+3000 = 5001,则end=5001
这就是end不变的情况
也可以你接收方给我ACK的时候窗口大小一直变大,接收方上层一下取走所有缓冲区数据,所以他给我更新出非常大的窗口,并且把历史所有报文全确认了,我们让strart一更新,再让end一加窗口大小,此时窗口大小自然变大了。
主机A不想给B发消息,B的窗口变化无意义
主机A想给B发消息,B的窗口变化才是有意义
主机A只要一直发数据,他就能很及时的拿到主机B的缓冲区剩余空间。
除非窗口为0,双方才会探测,start end 指向同一个位置,A就探测,问了如果不变那就还是0,如果更新了,对方窗口更新到5000,此时start 不变 end直接加5000,所以滑动窗口瞬间就有了。
就是问题二
如何理解滑动窗口对应的变化
所以流量控制是怎么实现的?
通过滑动串口实现的!
滑动窗口表示的是可以给对方直接发送暂时不需要应答的数据,可以直接发OS不一定发了还是没发,是可以发,意味着你不用担心你发错,对方能接受。
通过滑动窗口大小的控制来控制给对方发送数据的量多少
所以流量控制本身和滑动窗口是息息相关的。
流量控制可以很慢,当然也可以发的很快
不要觉得流量控制只是为了考虑对方主机来不及接受,万一接受能力非常强,此时流量控制可以通过把滑动窗口调的非常大,进而让主机A可以向主机B发送更多数据。
发送缓冲区应该分为4部分,最后一部分有可能根本上层就没写数据,所以是空
空也就相当于已确认区,能直接被覆盖
也就是win大小正确认识应该是min(win,有效数据),也就是滑动窗口大小不应该超过有效数据,对方窗口更新是5000,可是我只有2000有效数据则窗口也就是2000
问题3
滑动窗口向右滑动的时候,发送缓冲区是一个线性的,万一滑动窗口移到外面怎么办?
一直往后移动不就越界了吗,他会不会越界呢??
tcp采用了类似环状算法,基于数组的环形缓冲区。
物理上是线性,逻辑上想象成一个环形结构
这样双指针移动就不担心它出现溢出的问题
就相当于是取余运算
再聊聊起始随机序号
刚开始双方对应的序号基本都是随机的
三次握手会协商双方随机序号使用较小的
比如双方通信时采用的起始序号1234,我们把他理解成数组的下标就可以了,
新链接获取老数据的情况可能性就变得很小了。
今天把数据拷贝下来,每个数据都有数组下标,所以我们最终的序号可以理解成1234+数组下标,随着你不断发送序号也不断变化
发出去的时候给数组下标+1234,回来的时候 用确认序号 - 1234 = 下次发送数据在缓冲区中的位置
延迟应答
原理
客户端给S端发消息的时候,S是不是要给C应答
发送方一次发送更多的数据,发送的效率就越高
发送方一次发送更多的数据,取决于对方告诉我他能接受更多的数据
如果接收方,给发送方通告ACK的报文中更新一个更大的窗口大小(tcp报头)
如何让接收方给对方通告一个更大的窗口呢??
S收到了一个报文,本来呢我立即要进行应答,但是我可以等一等,或者收到第二个报文时再给应答。
收到报文不着急应答。
再不着急也不能让他超时。
为什么接收方等一等就能通告一个更大的窗口呢,在我等的时候上层就有较大概率来把数据取走,此时缓冲区的剩余空间就变大了,我再给你通告我的窗口不就变大了吗
我们把收到报文不着急应答的策略叫做延迟应答
是不是只要延迟了通信效率一定能提高?
肯定不是。还是博概率
所以未来写tcp服务器时,每次都尽快通过read,recv尽快的把数据全部从内核中拿上来!
我使劲读,对方就能使劲写,给对方更新出更大的窗口,通信效率就高了
接收端等一会不是应用层等,而是接收端tcp协议层来等一会再应答。
延迟应答采用的策略
这个时间肯定比超时重传时间要短
所以延迟应答是为了保证可靠性还是效率?
提高发送效率
tcp不是只设计了可靠,他也考虑了效率问题
捎带应答
把ACK和数据合在一起叫做捎带应答
有效减少数据量,提高了效率
总结TCP
校验和:保证交上去的数据是对的,没有比特位翻转等问题
三次握手:
1.建立连接
2协商起始序号
3.协商双方的接受缓冲区大小
几乎所有的策略,起作用的都是在两端机器上的!
数据包在网络中,不光考虑左右两端机器用什么策略,也要对网络信道有所评估
tcp还替我们考虑了网络状况
引入了拥塞控制
拥塞控制
如果发送数据,出现问题,不仅仅是对方主机出现问题吧,也可能是网络出现了问题!
1.如果通信的时候,出现了少量的丢包?
2.如果通信的时候,出现了大量的丢包?
少量丢包,常规情况
大量丢包,网络出现问题(
硬件设备出问题
数据量太大,引起阻塞)
tcp如何得知网络出现了问题呢?
如果通信双方出现了大量的数据丢包问题, tcp会判断网络出问题了(网络拥塞了)
发送方怎么知道有大量数据丢包了呢?
发送过程中,大量的数据都超时了
我们发送方,应该怎么办?? ?
我们不能立即对报文进行超时重发!!
为什么?
因为会加重网络的拥塞,网络本来已经很拥塞了,你还继续补发
所以该怎么办呢?
等等,发送少量数据
因为我少发了就能让网络缓一缓吗?
我发了就会加重网络拥塞,我这么厉害吗?
网络资源是共享的。
每一台机器都使用tcp/ip协议,关键不在于你少发,而是大家瞬间识别出网络拥塞的主机每一个人都应该减少自己发送数据量,所以一瞬间网络数据包急剧下降。
用TCP协议实现了多主机面对网络出现拥塞是的"共识"
在两台毫不相干的机器上也有共识,不仅你不发,我也不发,大家都不发
是不是所有主机一旦网络拥塞,所有主机都可以同时判断出来网络拥塞呢?
比如A主机可能网络拥塞前只发了一点数据,所以他丢包也就丢两个。
网络这种情况
1。博概率
2。拥塞严重程度不同影响是自适应的。
以上 拥塞 是什么?怎么判定他。
以及如何理解为什么不能对报文立即重发。
那发送方应该怎么办?
拥塞控制的策略 -- 每台识别主机拥塞的机器都要做
TCP引入 慢启动 机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据
先发一个,再发两个,指数级增长
我们当时认为滑动窗口的大小,不能应答报文的窗口大小,即对方接受缓冲区剩余空间大小
在计算机中,真正的滑动窗口大小 = min(窗口大小,拥塞窗口);
1。窗口大小考虑的是对方主机的接受能力
可是对方嗷嗷待哺,接受能力非常强,你一定就能给对方发送大量数据吗?
学习网络不仅要考虑对方接受能力问题,还要考虑网络情况。
所以拥塞窗口考虑的是动态的,网络的,接收能力。
如果拥塞窗口非常大,就按照窗口大小来发,如果拥塞窗口非常小,但接受能力很强,只能按照拥塞窗口来设置滑动窗口大小
所以tcp在传输控制时,既考虑了网络又考虑了对方的接受能力。
网络通信里要正确理解,我们有三种窗口,分别叫做滑动窗口,接收窗口,拥塞窗口
三个窗口互相配合就可以在tcp传输时既考虑网络问题,又考虑接受能力问题
所以重谈滑动窗口算法该进一步改一下了
int end = 确认序号 + min(应答报文窗口大小,有效数据,拥塞窗口)
拥塞窗口 定义:主机判断网络健康程度的指标,如果数据量超过拥塞窗口,会引发网络拥塞,否则不会
拥塞窗口评估网络状态,而网络是动态的,拥塞窗口本身肯定不能是静态的!
那你怎么能控制主机A发送数据的多少呢?
一次发一个,第二次发两个
不就是滑动窗口 = min(窗口大小,拥塞窗口)
拥塞窗口影响了滑动窗口大小。
我们把这种方式称为慢启动
坑
1。指数级增长怎么能叫慢启动?
不代表他前期增长很快。
发明慢启动的人就是个天才,慢启动指的是初始的时候慢但增长速度很快。
初始慢本来就符合网络拥塞情况,可是后面增长的太快了,又怎么理解呢?
网络出现拥塞了,发送少量的报文,如果都OK有正确应答,此时可以判断网络已经趋于健康了,我们发送的策略应该尽快恢复正常通信了!!
当然如果少量报文没有正确应答我就调整策略
实际机器发送的数据量,会一直指数增长吗??
绝对不会
因为此时网络非常好,拥塞窗口非常大,此时就以对方接受能力为标准了
我们也不能让拥塞窗口这个整数一直指数增长,所以引入慢启动的阈值,超过此阈值就会按照线性方式增长。
慢启动的阈值:最近一次发生网络拥塞时,拥塞窗口大小/2
一开始发一个两个,到达阈值16就线性增长,继续发送直到引发网络拥塞,此时乘法减小让24/2=12称为新的阈值,并且拥塞窗口变为1,继续发送
图中未考虑对方接受能力窗口大小
那为什么阈值要乘1/2?
实验是检验真知的唯一标准
这里是测出来的。
所以网络通信时,网络必然拥塞吗?
不一定
虽然图上是这么画的。
只有真正触发网络拥塞,才会执行这样的算法
所以整个过程都是在拥塞窗口其效果的前提条件下,这个图是生效的。
一旦按照对方接受能力进行数据更新时,这里限定的拥塞窗口也就没有意义了,甚至拥塞窗口大小也就不用更新了。
TCP拥塞控制这样的过程,就好像热恋的感觉
比如一开始你们俩很火热,感情迅速升温,但是关系好到一定程度,时间一久感情趋于平淡了,突然两人吵架了,感情迅速跌入冰点了,后来你有贱兮兮的给人发在吗,你们俩又慢慢升温,继续循环。
我们上面讲的所有tcp的东西全部都有双方OS自主完成
用户只关心应用层协议,不关心网络通信细节!!
提问
我接受到了服务器的窗口大小,但是服务器的窗口收到了其它客户端的数据使窗口变小了,我再发送就可能会比服务器就收窗口大,会不会有问题?
每个连接 , 一对接受和发送缓冲区,你发你的,我发我的,不影响!
面向字节流和粘包问题
创建一个TCP的socket, 同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区;
读写次数上不需要完全匹配的,这就叫做面向字节流
数据从C发送到S,可能上层正在忙,OS接受缓冲区已经接受到很多数据了,当上层读取时,可能读一个,可能读一个半,可能读三个半,反正内核收到的数据被对方上层取走,一次取多少由用户决定。
C端上层以为的4个请求,但是内核层只认识多个字节数据
内核tcp协议不关心上层协议是什么,在我看来你的所有数据不就是二进制不就是字节流吗,所以tcp不关心这是四个请求,假设一个请求40字节,tcp这里只认识是160字节,tcp的任务是尽可能安全高效的把数据发给对面,在对方的tcp层也只认收到了对应的多个字节,此时上层读取时对160个字节,4个请求进行解析分离,整个工作由用户层自主决定。
在tcp协议层根本不关心上层协议,不关心上层报文格式
所以tcp只有字节的概念
因为tcp是在tcp传输控制策略下不断的把数据从发送方发送缓冲区拷贝发送至接收方接受缓冲区,因为tcp只认识字节,所以发送数据时字节信息就不断进入发送缓冲区,出发送缓冲区,进入接受缓冲区,出接受缓冲区,所以在双方缓冲区就有了字节被流动起来的概念,所以叫面向字节流!
假设发过来了4个请求报文,160字节。
可是站在内核层面上,他认为就是160字节他不对报文做任何区分,上层读取时经过read读的时候,你定义了用户缓冲区一次读100字节200字节,可实际上接受缓冲区能让上层读多少那是不一定的,所以我们代码中需要让用户层不断去读,解析成功了就拿到一个请求,解析不成功就继续下次再读。
有没有可能发送方发送缓冲区有一个报文40字节,可是对方接受缓冲区通告给发送方的窗口大小只有20字节,所以tcp发送数据时,有没有可能把报文二分之一的数据发送给对方,即便你上层读上去也解析不了。
所以在接收方上层对收到数据拼接到用户层缓冲区里,在应用层来决定如何对它解析。
就如同我们当年写http定义了一个大缓冲区来进行decode对请求解析
所以tcp叫做面向字节流
这就是为什么我们的tcp协议报头里面通篇只有首部长度,他没有整个报文单次携带的数据长度。
因为tcp报文里有序号能够保证数据段的按序性,而且tcp是保证可靠性的,当底层收到一个tcp报文时,他要做的工作不用区分有效载荷是不是一个完整的报文,他不区分,他只是把对应的报头有效载荷分开然后把数据放在缓冲区合适的位置就可以了。
这就是为什么tcp根本不带长度的原因,因为他不需要。
一切的传输,从建立连接开始,从断开连接不在通信了,从连接开始我们只有字节流的概念,你给我发什么我就收什么,把对应数据按照序号拼在一起,你曾经缓冲区里有什么我接受缓冲区就有什么,双方对缓冲区的数据不做任何区分直接交给应用层,由应用层统一处理。
这就叫面向字节流,他特别像家里的自来水,水厂的水由河里的,有雨水,反正合成一个水流发给你家,你可用桶装,也可用盆装完全根据需求来定。
tcp面向字节流,我们读上来的报文有可能是一个半或者4,八个的,在用户层对报文进行处理必须一个一个处理,因为对方给我发的是4个,在用户层才有报文的概念,必须要求用户层处理将字节流变成一个一个完整的请求。
可是用户你怎么知道底层读上来的数据是一个完整的报文,是一个半还是10个
,有没有可能对方发了一个半都有可能,
所以上层读上来的数据可能并不是我们想要的一个完整的报文。
如果用户写代码时要求从接收缓冲区里读,要么不读,要么必须读上来一个完整的请求,所以上层就对报文边读边解析,但如果用户层不做约定标准,单纯的读,读上来的就会出现半个报文的情况,假设用户必须读到完整报文才能处理而且他没有完整的机制把读上来的报文暂时保存继续去读,他直接将半个报文丢弃了,此时就会影响后续报文解析。
所以当用户进行读取时,在用户层不对报文一个一个报文的分离,在报文处理时可能多处理或少处理对应的请求,这种情况把他称为数据包粘包问题
用户层读上来的是半个或者是一个半这种情况称为粘包问题
我们一般是把接受缓冲区的数据全拿上来在用户层缓冲区再去处理。
分析完之后就能给对方通知更大的窗口,效率比较高
就比如蒸包子,所有包子粘在一起来,你想吃一个但是你会多拿上来半个包子,多拿上来的部分就叫做粘包问题
因为tcp是面向字节流的,所以tcp根本不存在粘包不粘包的问题,所以粘包问题他是相对于用户层的概念
我们要解决粘包问题,必须得解决,你解决不了就没办法把收到的字节流变成一个一个完整的请求,解析不成一个请求就不能再用户层编写代码逻辑进行一个个请求响应。
解决粘包:订协议!
所以这就是当年Encode和Decode原因!!
因为要解决粘包
如何解决用户层的粘包问题??
核心思想:在应用层通过协议,明确报文和报文之间的边界
- 定长报文
- 使用特殊字符
- 使用自描述字段(描述有效载荷多长)+定长报头(八字节定长报头---像UDP)
- 使用自描述字段(content length描述有效载荷)+特殊字符(空行结尾---像HTTP)
网络版本计算器用的就是4
面向字节流衍生出来的问题就会需要用户去把字节流数据反向的转化成一个个请求,在转化 过程中,如果转化不当造成了报文残缺,或者报文剩余,少了要么多了,此时这种情况叫做粘包问题,为了解决粘包问题使用特定的方案来把报文和报文再进行分离。
分离之后然后才有反序列化,变成结构化数据,此时用户才算真正能处理这个请求。
这套思想要保证自己读到一个完整的报文,其实不仅是应用层要做,下层也要考虑,因为系统中他的字节也是一个一个拷贝的,所以tcp,ip,udp他的tcp报头里面都是定长报头,如果是变长的他也一定要有一部分定长的然后再加他的报头长度,所以UDP报头是八字节,TCP的报头定长20,还有40字节的选项。
所以这种思想在底层和应用层都是通用的。
tcp连接异常
双方三次握手建立好了双方的连接
两台机器中间经过网络
进程终止
如果突然进程终止了,C或者S挂掉了,曾经建立好的连接该怎么办?
连接本身和进程没有直接关系,连接是和文件是直接相关的!
文件的生命周期是随进程的,意味着连接本身是随进程的。
所以最终结论是当你的进程不管是正常终止还是异常终止 ,OS会不会说这个进程挂了,这个连接怎么办让他也异常把?
OS决不能把一个模块出现问题波及到另一个模块,进程无论正常终止还是异常终止,在连接角度就是进程退出了,在OS层面就是单纯的进程退出了,你语言上除零崩溃进程退出了,OS里关掉一个正常进程和杀掉一个异常进程没有任何区别。
它无非就是把你占有的PCB,地址空间,页表给你free掉,而且OS不希望这种异常去波及到其他模块,所以在连接层面上连接建立三次握手的过程是由用户驱动connect,然后OS自动完成的!
所以一个进程终止了,对于当前两个连接来讲他们都认为进程结束了。我们的连接就进行正常的四次挥手即可!
进程终止,连接正常自动断开
机器重启
关机之前先要杀掉所有的进程
这种情况等价于进程终止情况
机器掉电/网线断开
c端网线拔了,此时双方的连接会怎么办啊?
C没有机会和S四次挥手了,S此时并不清楚C已经网线挂了,S正常维护他的连接,C断电就不说了OS都没了,C能在应用层识别到网络断开了,连接维护也就没必要了,连接可以关掉了,当你把网线连上时,再给S发消息,这就叫连接不一致的问题。
对于S认为连接还存在,对于C认为连接早就没有了,所以S会给C发送RST,让C重新建立连接,S直接关闭连接。
那如果C断网了再也不给S发消息了,那S的连接一直维持吗,原则上连接要一直在,但是tcp内部有保活机制,如果S端发现C长时间不给我发消息,他有保活定时器,给客户端推消息询问,发现客户端都没有应答,此时S立马意识到连接断开了,所以S会把连接关掉的。