TCP
ack是接收端期望接收到的数据序号,等于已接收到序号+1。
1. 有连接的:建立和终止
三次握手
- SYN ACK+SYN ACK
- 被动连接端的ACK通过SYN
捎带
- 交换双方的: 初始序列号 / MSS / 窗口大小(?)
四次挥手
- FIN ACK FIN ACK
- 单向关闭
- 半关闭。通过半关闭向另一端告知数据发送完毕。
状态变换:
主动关闭方的状态:
- FIN_WAIT_1 :发送 FIN 后等待对方的 ACK
- FIN_WAIT_2 :收到对方对自己 FIN 的 ACK 后等待对方的 FIN
- TIME_WAIT :收到对方的 FIN 并发送 ACK 后进入该状态
MSS
根据物理层的MTU计算,避免IP层分段
2MSL(TIME_WAIT)状态
- 主动关闭端在发送对FIN的最后一个ACK后,等待2MSL时间。MSL, max segment lifetime,一个segment在网络中存活的最长时间。
原因:
- 被动关闭端可能没有收到ACK而重发FIN,如果不等待,这个重发的FIN可能将新的连接关闭掉;
- 网络中可能还有在传输的旧的重传报文段,可能被当做新连接的数据。
等2MSL:一个ACK+FIN的最大时间,如果在这个时间段内没有收到重发的FIN,认为对方已经收到ACK正确关闭了。
- 处于2MSL状态的端口不能被使用。客户端每次连接随机选一个端口,因此没有影响;而服务器端固定端口,因此在服务器端主动关闭,必须等待一段时间才能重新绑定到原端口。设置选项
SO_REUSEADDR
可以取消这个设定。 - 在此期间拒绝任何收到的数据。
RST
- TCP模块会在自己认为的异常时刻发送RST;
- 不会对RST发ACK,立即丢弃缓冲区中的数据;
- 用RST而不是FIN关闭连接,称为
abortive release
(相对orderly release
);api会通知应用层连接是异常关闭的。 - 常见的产生时机:
- 连接到/发送数据到没有使用的端口,收到RST
- 关闭连接时接收缓冲区还有数据没被消费,将发送RST
- 向一个已经close()的socket发送数据,将收到RST —- close和shutdown虽然都是发送FIN关闭连接,但前者的语义是关闭读写,后者是关闭写,但依然能读。
同时打开
两端都是主动打开。SYN SYN+ACK / SYN SYN+ACK。4个segment。
同时关闭
两端都是主动关闭,FIN ACK / FIN ACK,两端最后都进入2MSL状态。
并发TCP服务器
Listen状态的socket在接收连接请求后,内核会创建一个新的established状态的socket;原socket依然是listen状态;二者在服务器端使用同一个端口。
backlog队列
- 服务器端的TCP模块内有一个连接队列,保存所有已被TCP接受(三次握手完成),但未被应用层接受的连接;队列的长度由应用层指定,称为backlog;
- TCP连接的建立 和 应用层得到一个已经建立的连接,是一个生产者消费者模型;应用层无法拒绝客户端的连接请求。
- 客户端发起连接成功后,该连接在服务器端可能只在backlog队列中,此时客户端如果发送数据,将被缓存在TCP接收buffer中;
- backlog队列满后,不会回应发来的FIN,让客户端重试。如果backlog队列一直满,客户端终将超时。
2. 滑动窗口
urgent mode
URG标志位为1时,说明报文段是紧急数据,需要尽快被应用进程接收和处理。普通数据在接收端是按序交付给应用进程的,紧急模式在两端间建立了一个独立于普通数据的逻辑信道,接收端通常将普通数据和紧急数据分开存放,二者之间不遵守有序的规则,允许越过普通数据直接读取后面的紧急数据。这种数据也被称为”带外数据“(out-of-band data),但实际上,带外数据和普通数据是共享物理信道传输的。
delayed ack
接收端不立即发送ack,而是等待一段时间,和数据一起发送,或和另一个ack合并成一个发送。
滑动窗口的工作方式
窗口通知:
发送端维护发送窗口大小(不在包中传输),接收端在ACK中告知接收窗口大小;
发送窗口初始是发送缓冲区大小,接收窗口初始是接收缓冲区大小;缓冲区决定窗口的最大值;
发送窗口一般包括3个部分,从左到右:
- 已发送但未收到ACK的数据
- 可以立即发送的数据
- 空闲空间;
接收窗口就是接收缓冲区还剩多少空间,接收端处理能力越强,从缓冲区提取数据的速度就越快,接收窗口就越大;
发送窗口大小由接收窗口决定,发送端收到ACK后:
- 丢弃缓存中对应的数据,左沿向右移动;(收缩)
- 根据ACK告知的接收窗口看是否需要移动右沿;已发送未 ACK 的数据 + 可立即发送数据 + 空闲 = 接收窗口;(扩张)
作用:
- 提高效率,可以同时发送多个数据;
- 流量控制,适配不同处理能力的发送端和接收端
最优窗口大小(即发送/接收缓冲区大小)的计算:
- 尽量将两端之间的信道填满;
- 填满时,在信道上传输的数据 = 带宽(数据传输速度) * RTT,两个缓冲区应至少为这个大小
问题1:零窗口
接收端接收缓冲区满时,ACK中接收窗口为0,阻止发送端发送数据。发送方需要在接收方缓冲区空出来时得到通知,因此在发现零窗口后会进行窗口探测,即定时发送含有1字节内容的segment,通过其ACK查询接收方的接收窗口。
问题2:糊涂窗口综合症 (silly window syndrome)
现象:大量的小segment被传输(payload太小),造成网络利用率低下
发送端和接收端都有可能引起这种情况:
- 发送端每次只发送少量数据;
- 接收端的处理能力不够或应用层没有即时从接收缓冲区中取数据,接收窗口一直很小,发送端只能发小segment。
解决:
* 发送端: nagle算法 *
- 只针对
小segment
的stop-wait
协议,大segment不受影响; - 在前一个 小segment(小于MSS)的ack未到来前,缓存并合并其他要发送的小segment;
- 如果ack回来的很快,合并不了多少数据;
- ack通常是delayed,会导致数据发送的延时;不适合实时性的应用(可以取消);
- 目的是减少segment数目。
* 接收端: *
在接收端,当接收窗口小于一定阈值(如MSS一半)时,无论是数据确认ACK,还是对窗口探测的回应ACK,都宣告接收窗口为0,阻止发送端发送小报文段。
3. 可靠性:超时重传
每个segment发送后都会有一个计时器负责接收ACK,如果超时,则重新发送该segment。
首先要预测当前发送的segment的RTT:
R = αR + (1-α)M,M是测量到的RTT,R是估计的RTT。
然后计算RTO(Retransmission TimeOut):
简单版:RTO = R*β
复杂版,将RTT的估计值,RTT的偏差的估计值也考虑在内:
Err = M - R #Err是实际RTT和估计RTT的差值
R = R + g*Err = (1-g)*R + g*M #计算估计RTT,和之前的算法一模一样
D = D + h*(|Err| - D) # 计算估计偏差,”估计偏差“指的是 估计RTT 和 真实RTT 的差值。这个算法和估计RTT的计算方法是一样的
RTO = R + 4D #
多次重传时RTO的exponential backoff(指数退避)
4. 顺序性
数据乱序到达,接收端可以不丢弃,而是缓冲起来,组装成有序后交付给应用进程。
快速重传
接收端在接收到乱序segment时,用重复ACK提醒发送端空洞的存在,使之发送丢失数据,保证可靠性。
例:发送端发送了5/6/7/8/9,假如接收端按顺序接收5/9/7/8,6丢失了,则在接收到9/7/8后均会发送ACK=6;当发送端收到3个重复的值为6的ack时认为6丢失了,打断6的超时timer并立即重新发送。
为什么阈值是3呢?这是一个权衡,重复ACK可能是由段丢失引起的,但也可能只是段乱序到达了而已,比如5/7/8/6/9,在7和8处会发送两个重复的ACK,但是紧接着6就到了,接收端重组后一切正常,此时也不会触发发送端的快速重传。
优点:接收端利用空洞后的segment比超时更快地检测到丢失片段,效率更高。
5. 拥塞控制
问题:堵塞 –> 数据丢失 –> 超时重传 –> 更堵塞
解决办法:
- 探测堵塞;
- 发现堵塞时控制自己发送数据的速度。
探测堵塞
上文已经提到了各种推测segment丢失的办法:超时 / 重复ACK,一旦出现段丢失则认为堵塞。
但是注意,这两种情况下的阻塞程度不一样:
- 超时:严重阻塞,发送了若干段但是一个ACK都没回来;
- 重复ACK:轻微阻塞,或者没有阻塞。因为后续的段到达了,只有中间某些段没有到达。
控制速度
单纯的滑动窗口中,发送方通过接收方通告的接收窗口大小调整发送窗口,从而控制发送速度;为了拥塞控制,TCP还会在发送端维护一个拥塞窗口,真实发送窗口= min(接收窗口,拥塞窗口)
。
拥塞控制有3个阶段:
- 慢启动(slow start)
- 拥塞避免(congestion avoidance)
- 快速恢复(相对快速重传而言)
// cwnd: 拥塞窗口大小
慢启动
系统从slow start阶段开始。在这一阶段cwnd被置为1,只能发送1个segment;每收到1个ack,都给拥塞窗口+1,即下次可以发送2个段,下下次可以发送4个段,即乘性倍增。
拥塞避免
当cwnd增长到某个阈值ssthresh(slow start threshhold)时,拥塞窗口进入congestion avoidance状态。此时发送窗口在每个窗口中所有片段都传输完毕后,将拥塞窗口+1,线性缓慢增长。
在congestion avoidance状态下,一旦出现探测到拥塞,ssthresh 立即更新为当前cwnd的一半,随后分两种情况:如果是ACK超时检测到的拥塞,则进入slow start;如果是重复ACK,则进入快速恢复阶段。
快速恢复
cwnd增加3个segment的长度(加3的原因是因为收到3个重复的ACK,表明有3个“老”的数据包离开了网络),以后每收到一个重复的ACK,就再增加1个segment长度。这是因为一个重复ACK说明有一个段已被发送出去,可以发送另一个段了;
当收到一个非重复ACK时,说明对方的数据接收完整了,发送端将cwnds复原为sthresh(已经减半了),重新回到congestion avoidance状态。
分开处理的原因正如前所述,两种情况表达的拥塞程度不同。对超时表达的严重堵塞,需严厉地立刻降低发送速率,因此直接进入慢启动阶段;对重复ACK,说明双方仍然有数据流动,我们不希望执行slow start突然减少数据流。
如果在slow start阶段就出现丢失数据,则slow start立刻开始,ssthresh更新为cwnd的一半。
总体来说,拥塞窗口的状态变迁是一个试探性的过程。slow start阶段起步速度低,不太可能出现拥塞,因此发送速度可以快速攀升;congestion avoidance阶段速度接近饱和,采用更保守的方式,速度缓慢增长逼近极限。一旦探测到拥塞,就将slow start的拐点降低为当前拥塞窗口的一半,且如果是严重堵塞,则立即重置,避免网络负担;如果是轻微拥塞,则快速恢复。