# TCP/IP
# TCP 协议报文格式
SPORT
DPORT
Seq Num:
1
2
3
4
5
6
7
8 序号(Sequence Number)的语义与SYN控制标志(Control Bits)的值有关。根据控制标志(Control Bits)中的SYN是否为1,序号(Sequence
Number)表达不同的含义:
(1)当SYN = 1时,当前为连接建立阶段,此时的序号为初始序号ISN((Initial Sequence Number),通过算法来随机生成序号;
(2)当SYN = 0时在数据传输正式开始时,第一个报文的序号为 ISN + 1,后面的报文的序号,为前一个报文的SN值+TCP报文的净荷字节数(不包含TCP头)。比如,如果发送端发送的一个TCP帧的净荷为12byte,序号为5,则发送端接着发送的下一个数据包的时候,序号的值应该设置为5+12=17。
在数据传输过程中,TCP协议通过序号(Sequence Number)对上层提供有序的数据流。发送端可以用序号来跟踪发送的数据量;接收端可以用序号识别出重复接收到的TCP包,从而丢弃重复包;对于乱序的数据包,接收端也可以依靠序号对其进行排序。
Ack Num
1 确认序号(Acknowledgment Number)标识了报文接收端期望接收的字节序列。如果设置了ACK控制位,确认序号的值表示一个准备接收的包的序列码,注意,它所指向的是准备接收的包,也就是下一个期望接收的包的序列码。![]()
1
2 只有控制标志的ACK标志为1时,数据帧中的确认序号ACK
Number才有效。TCP协议规定,连接建立后,所有发送的报文的ACK必须为1,也就是建立连接后,所有报文的确认序号有效。如果是SYN类型的报文,其ACK标志为0,故没有确认序号。header length
1 该字段占用4位,用来表示TCP报文首部的长度,单位是4bit位。其值所表示的并不是字节数,而是头部的所含有的32bit的数目(或者倍数),或者4个字节的倍数,所以TCP头部最多可以有60字节(4*15=60)。没有任何选项字段的TCP头部长度为20字节,所以其头部长度为5,可以通过20/4=5计算得到。reverse
1 头部长度后面预留的字段长度为6位,作为保留字段,暂时没有什么用处。control flags
1 控制标志(Control Bits)共6个bit位,具体的标志位为:URG、ACK、PSH、RST、SYN、FIN。
标志位 说明 URG 占 1 位,表示紧急指针字段有效。URG 位指示报文段里的上层实体(数据)标记为 “紧急” 数据。当 URG=1 时,其后的紧急指针指示紧急数据在当前数据段中的位置 (相对于当前序列号的字节偏移量),TCP 接收方必须通知上层实体。 ACK 占 1 位,置位 ACK=1 表示确认号字段有效;TCP 协议规定,接建立后所有发送的报文的 ACK 必须为 1;当 ACK=0 时,表示该数据段不包含确认信息。当 ACK=1 时,表示该报文段包括一个对已被成功接收报文段的确认序号 Acknowledgment Number,该序号同时也是下一个报文的预期序号。 PSH 占 1 位,表示当前报文需要请求推(push)操作;当 PSH=1 时,接收方在收到数据后立即将数据交给上层,而不是直到整个缓冲区满。 RST 占 1 位,置位 RST=1 表示复位 TCP 连接;用于重置一个已经混乱的连接,也可用于拒绝一个无效的数据段或者拒绝一个连接请求。如果数据段被设置了 RST 位,说明报文发送方有问题发生。 SYN 占 1 位,在连接建立时用来同步序号。当 SYN=1 而 ACK=0 时,表明这是一个连接请求报文。对方若同意建立连接,则应在响应报文中使 SYN=1 和 ACK=1。 综合一下,SYN 置 1 就表示这是一个连接请求或连接接受报文。 FIN 占 1 位,用于在释放 TCP 连接时,标识发送方比特流结束,用来释放一个连接。当 FIN = 1 时,表明此报文的发送方的数据已经发送完毕,并要求释放连接。 窗口大小
1 长度为16位,共2个字节。此字段用来进行流量控制。流量控制的单位为字节数,这个值是本端期望一次接收的字节数。Checksum
1 长度为16位,共2个字节。对整个TCP报文段,即TCP头部和TCP数据进行校验和计算,接收端用于对收到的数据包进行验证。首先,把伪首部、TCP 报头、TCP 数据分为 16 位的字,如果总长度为奇数个字节,则在最后增添一个位都为 0 的字节。把 TCP 报头中的校验和字段置为 0
其次,用反码相加法累加所有的 16 位字(进位也要累加)。
最后,对计算结果取反,作为 TCP 的校验和。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 unsigned short
checksum(unsigned short * addr, int count)
{
long sum = 0;
/*
计算所有数据的16bit对之和
*/
while( count > 1 ) {
/* This is the inner loop */
sum += *(unsigned short*)addr++;
count -= 2
}
/* 如果数据长度为奇数,在该字节之后补一个字节(0),
然后将其转换为16bit整数,加到上面计算的校验和
中。
*/
if( count > 0 ) {
char left_over[2] = {0};
left_over[0] = *addr;
sum += * (unsigned short*) left_over;
}
/* 将32bit数据压缩成16bit数据,即将进位加大校验和
的低字节上,直到没有进位为止。
*/
while (sum>>16)
sum = (sum & 0xffff) + (sum >> 16);
/*返回校验和的反码*/
return ~sum;
}紧急指针
1 长度为16位,2个字节。它是一个偏移量,和SN序号值相加表示紧急数据最后一个字节的序号。Option&pedding
1
2
3 可选项和填充部分的长度为4n字节(n是整数),该部分是根据需要而增加的选项。如果不足4n字节,要加填充位,使得选项长度为32位(4字节)的整数倍,具体的做法是在这个字段中加入额外的零,以确保TCP头是32位(4字节)的整数倍。
最常见的选项字段是MSS(Maximum Segment Size最长报文大小),每个连接方通常都在通信的第一个报文段(SYN标志为1的那个段)中指明这个选项字段,表示当前连接方所能接受的最大报文段的长度。由于可选项和填充部分不是必须的,所以 TCP 报文首部最小长度为 20 个字节。
data
1 数据部分是可选的。在一个连接建立和一个连接终止时,双方交换的报文段仅有TCP首部。如果一方没有数据要发送,也使用没有任何数据的首部来确认收到的数据,比如在处理超时的过程中,也会发送不带任何数据的报文段。
# TCP 可靠性
(1)应用数据分割成 TCP 认为最适合发送的数据块。这部分是通过 MSS(最大数据包长度)选项来控制的,通常这种机制也被称为一种协商机制,MSS 规定了 TCP 传往另一端的最大数据块的长度。值得注意的是,MSS 只能出现在 SYN 报文段中,若一方不接收来自另一方的 MSS 值,则 MSS 就定为 536 字节。一般来讲,MSS 值还是越大越好,这样可以提高网络的利用率。
(2)重传机制。设置定时器,等待确认包,如果定时器超时还没有收到确认包,则报文重传。
(3)对首部和数据进行校验。
(4)接收端对收到的数据进行排序,然后交给应用层。
(5)接收端丢弃重复的数据。
(6)TCP 还提供流量控制,主要是通过滑动窗口来实现流量控制。
# TCP 三次握手
当服务端调用操作系统的 bind () 函数和 listen () 函数后,服务端就会处于 LISTENING 状态。
![]()
(1)第一次握手:Client 进入 SYN_SENT 状态,发送一个 SYN 帧来主动打开传输通道,该帧的 SYN 标志位被设置为 1,同时会带上 Client 分配好的 SN 序列号,该 SN 是根据时间产生的一个随机值。除此之外,SYN 帧还会带一个 MSS(最大报文段长度)可选项的值,表示客户端发送出去的最大数据块的长度。
这个报文段并不包括确认号,也没有定义窗口大小。只有当一个报文段中包含了确认时,定义窗口大小才有意义。这个报文段还可以包含一些选项。SYN 报文段是一个控制报文段,它不包含任何数据,但是它消耗了一个序号。
当数据传送开始时,序号就应该加 1。也就是说,SYN 报文段并不包含真正的数据,但是它要消耗一个序号。
(2)第二次握手:Server 端在收到 SYN 帧之后,会进入 SYN_RCVD 状态,同时返回 SYN+ACK 帧给 Client,主要目的在于通知 Client,Server 端已经收到 SYN 消息,现在需要进行确认。Server 端发出的 SYN+ACK 帧的 ACK 标志位被设置为 1,其确认序号 AN(Acknowledgment
Number)值被设置为 Client 的 SN+1;SYN+ACK 帧的 SYN 标志位被设置为 1,SN 值为 Server 端生成的 SN 序号;SYN+ACK 帧的 MSS(最大报文段长度)表示的是 Server 端的最大数据块长度。 因为这个报文段包含了确认,所以他还需要定义接收窗口大小 ——rwnd。
SYN+ACK 报文段若携带数据,则消耗一个序号,否则不消耗。
(3)第三次握手:Client 在收到 Server 的第二次握手 SYN+ACK 确认帧之后,首先将自己的状态会从 SYN_SENT 变成 ESTABLISHED,表示自己方向的连接通道已经建立成功,Client 可以发送数据给 Server 端了。然后,Client 发 ACK 帧给 Server 端,该 ACK 帧的 ACK 标志位被设置为 1,其确认序号 AN(Acknowledgment Number)值被设置为 Server 端的 SN 序列号 + 1。
(4)Server 端在收到 Client 的 ACK 帧之后,会从 SYN_RCVD 状态会进入 ESTABLISHED 状态,至此,Server 方向的通道连接建立成功,Server 可以发送数据给 Client,TCP 的全双工连接建立完成。
# TCP 四次挥手
TCP 连接开始断开(或者拆接)的过程,在这个过程中连接的每个端的都能独立地、主动的发起,断开的过程
![]()
(1)第一次挥手:主动断开方(可以是客户端,也可以是服务器端),向对方发送一个 FIN 结束请求报文,此报文的 FIN 位被设置为 1,并且正确设置 Sequence Number(序列号)和 Acknowledgment Number(确认号)。发送完成后,主动断开方进入 FIN_WAIT_1 状态,这表示主动断开方没有业务数据要发送给对方,准备关闭 SOCKET 连接了。
(2)第二次挥手:正常情况下,在收到了主动断开方发送的 FIN 断开请求报文后,被动断开方会发送一个 ACK 响应报文,报文 Acknowledgment Number(确认号)值为断开请求报文的 Sequence Number(序列号)加 1,该 ACK 确认报文的含义是:“我同意你的连接断开请求”。之后,被动断开方就进入了 CLOSE-WAIT(关闭等待)状态,TCP 协议服务会通知高层的应用进程,对方向本地方向的连接已经关闭,** 对方已经没有数据要发送了,若本地还要发送数据给对方,对方依然会接受。** 被动断开方的 CLOSE-WAIT(关闭等待)还要持续一段时间,也就是整个 CLOSE-WAIT 状态持续的时间。
主动断开方在收到了 ACK 报文后,由 FIN_WAIT_1 转换成 FIN_WAIT_2 状态。
(3)第三次挥手:在发送完成 ACK 报文后,被动断开方还可以继续完成业务数据的发送,待剩余数据发送完成后,或者 CLOSE-WAIT(关闭等待)截止后,被动断开方会向主动断开方发送一个 FIN+ACK 结束响应报文,表示被动断开方的数据都发送完了,然后,被动断开方进入 LAST_ACK 状态。
(4)第四次挥手:主动断开方收在到 FIN+ACK 断开响应报文后,还需要进行最后的确认,向被动断开方发送一个 ACK 确认报文,然后,自己就进入 TIME_WAIT 状态,等待超时后最终关闭连接。处于 TIME_WAIT 状态的主动断开方,在等待完成 2MSL 的时间后,如果期间没有收到其他报文,则证明对方已正常关闭,主动断开方的连接最终关闭。
被动断开方在收到主动断开方的最后的 ACK 报文以后,最终关闭了连接,自己啥也不管了。
处于 TIME_WAIT 状态的主动断开方,在等待完成 2MSL 的时间后,才真正关闭连接通道
2MSL 翻译过来就是两倍的 MSL。MSL 全称为 Maximum Segment Lifetime,指的是一个 TCP 报文片段在网络中最大的存活时间,具体来说,2MSL 对应于一次消息的来回(一个发送和一个回复)所需的最大时间。如果直到 2MSL,主动断开方都没有再一次收到对方的报文(如 FIN 报文),则可以推断 ACK 已经被对方成功接收,此时,主动断开方将最终结束自己的 TCP 连接。所以,TCP 的 TIME_WAIT 状态也称为 2MSL 等待状态。
# 脑溢血环节
为什么关闭连接的需要四次挥手,而建立连接却只要三次握手呢?
1
2
3
4
5 关闭连接时,被动断开方在收到对方的FIN结束请求报文时,很可能业务数据没有发送完成,并不能立即关闭连接,被动方只能先回复一个ACK响应报文,告诉主动断开方:“你发的FIN报文我收到了,只有等到我所有的业务报文都发送完了,我才能真正的结束,在结束之前,我会发你FIN+ACK报文的,你先等着”。所以,被动断开方的确认报文,需要拆开成为两步,故总体就需要四步挥手。
而在建立连接场景中,Server端的应答可以稍微简单一些。当Server端收到Client端的SYN连接请求报文后,其中ACK报文表示对请求报文的应答,SYN报文用来表示服务端的连接也已经同步开启了,而ACK报文和SYN报文之间,不会有其他报文需要发送,故而可以合二为一,可以直接发送一个SYN+ACK报文。所以,在建立连接时,只需要三次握手即可。
简单的说,断开连接时可能还有数据报文的交互,不能直接断开,要等数据发送完再说。建立时没有其他的数据报文,可以把两个合二为一发送为什么连接建立的时候是三次握手,可以改成两次握手吗?
1
2
3 三次握手完成两个重要的功能:一是双方都做好发送数据的准备工作,而且双方都知道对方已准备好;二是双方完成初始SN序列号的协商,双方的SN序列号在握手过程中被发送和确认。三是确定客户端和服务端的MSS
如果把三次握手改成两次握手,可能发生死锁。两次握手的话,缺失了Client的二次确认ACK帧,假想的TCP建立的连接时二次挥手,可以如下图所示:![]()
1 在假想的TCP建立的连接时二次握手过程中,Client发送Server发送一个SYN请求帧,Server收到后发送了确认应答SYN+ACK帧。按照两次握手的协定,Server认为连接已经成功地建立了,可以开始发送数据帧。这个过程中,如果确认应答SYN+ACK帧在传输中被丢失,Client没有收到,Client将不知道Server是否已准备好,也不知道Server的SN序列号,Client认为连接还未建立成功,将忽略Server发来的任何数据分组,会一直等待Server的SYN+ACK确认应答帧。而Server在发出的数据帧后,一直没有收到对应的ACK确认后就会产生超时,重复发送同样的数据帧。这样就形成了死锁。为什么主动断开方在 TIME-WAIT 状态必须等待 2MSL 的时间?
原因之一:主动断开方等待 2MSL 的时间,是为了确保两端都能最终关闭。假设网络是不可靠的,被动断开方发送 FIN+ACK 报文后,其主动方的 ACK 响应报文有可能丢失,这时候的被动断开方处于 LAST-ACK 状态的,由于收不到 ACK 确认被动方一直不能正常的进入 CLOSED 状态。在这种场景下,被动断开方会超时重传 FIN+ACK 断开响应报文,如果主动断开方在 2MSL 时间内,收到这个重传的 FIN+ACK 报文,会重传一次 ACK 报文,后再一次重新启动 2MSL 计时等待,这样,就能确保被动断开方能收到 ACK 报文,从而能确保被动方顺利进入到 CLOSED 状态。只有这样,双方都能够确保关闭。反过来说,如果主动断开方在发送完 ACK 响应报文后,不是进入 TIME_WAIT 状态去等待 2MSL 时间,而是立即释放连接,则将无法收到被动方重传的 FIN+ACK 报文,所以不会再发送一次 ACK 确认报文,此时处于 LAST-ACK 状态的被动断开方,无法正常进入到 CLOSED 状态。
原因之二:防止 “旧连接的已失效的数据报文” 出现在新连接中。主动断开方在发送完最后一个 ACK 报文后,再经过 2MSL,才能最终关闭和释放端口,这就意味着,相同端口的新 TCP 新连接,需要在 2MSL 的时间之后,才能够正常的建立。2MSL 这段时间内,旧连接所产生的所有数据报文,都已经从网络中消失了,从而,确保了下一个新的连接中不会出现这种旧连接请求报文。
如果已经建立了连接,但是 Client 端突然出现故障了怎么办?
1 TCP还设有一个保活计时器,Client端如果出现故障,Server端不能一直等下去,这样会浪费系统资源。每收到一次Client客户端的数据帧后,Server端都的保活计时器会复位。计时器的超时时间通常是设置为2小时,若2小时还没有收到Client端的任何数据帧,Server端就会发送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没反应,Server端就认为Client端出了故障,接着就关闭连接。如果觉得保活计时器的两个多小时的间隔太长,可以自行调整TCP连接的保活参数。源端连续接收到三个相同 ack 怎么办
连续收到三个 ack 咋办当 TCP 源端收到 3 个相同的 ACK 确认时,即认为有数据包丢失,则源端重传丢失的数据包,而不必等待 RTO (Retransmission Timeout) 超时。
七个包中某个包丢了该怎么办呐
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48 0x00 TCP第一次握手SYN包丢包
重传第一个包,重传次数由/proc/sys/net/ipv4/tcp_syn_retries决定,每次重传的时间翻倍上涨的,直至达到tcp_syn_retries决定,不在重传,开始摆烂
1 2 4 8 16 32 酱紫翻倍的
0x01 TCP第二次握手SYN包丢包
客户端无法收到synack包,会重传syn。
服务端收到客户的SYN包后,就会回SYN、ACK包,但是客户端一直没有回ACK,服务端在超时后,重传了
SYN、ACK 包,接着一会,客户端超时重传的SYN包又抵达了服务端,服务端收到后,超时定时器就重新
计时,然后回SYN、ACK包,所以相当于服务端的超时定时器只触发了一次,又被重置了;
当第二次握手的SYN、ACK丢包时,客户端会超时重发SYN包,服务端也会超时重传SYN、ACK包。同时客户端重传次数由tcp_syn_retries决定
0x02
TCP第三次握手SYN包丢包
如果第三次握手的ACK,服务端无法收到,则服务端就会短暂处于SYN_RECV状态,并且依次等待3秒、6秒、12秒后重新发送SYN+ACK包,而客户端会处于 ESTABLISHED 状态。由于服务端一直收不到TCP第三次握手的ACK,则会一直重传SYN、ACK包,直到重传次数超过tcp_synack_retries,直至重传次数超过tcp_synack_retries
0x03
// 第一次握手重传次数限制
cat /proc/sys/net/ipv4/tcp_syn_retries
// 第二次握手重传次数限制
cat /proc/sys/net/ipv4/tcp_synack_retries
// 数据包最大重传次数限制
cat /proc/sys/net/ipv4/tcp_retries2
参考链接:https://juejin.cn/post/6844904181795389454
师傅在文章里还有详细截图,看不懂我写的可以去看他写的~
0x04 TCP第一次挥手FIN包丢包
client发的FIN包丢了,对于client,因为没收对应的ACK包,应当一直重传(像普通包一样),直至到达上限次数,直接关闭连接;对于server,它应该无任何感知;
0x05 TCP第二次挥手ACK包丢包
server回client的ACK包丢了,对于client,将执行(1),对于server将像丢普通的ack一样,再次收到FIN后,再发一个ACK包;
0x06 TCP第三次挥手FIN包丢包
如果client收到ACK后,server直接跑路。client将永远停留在这个状态(半打开状态,就像client关闭了输出一样)。linux有tcp_fin_timeout这个参数,设置一个超时时间 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看,默认60s。
server发的FIN包丢了,对于server,像丢普通的包一样,重传。若此时client早已跑路且与其他人建立的连接,client应会不认识这个FIN包,直接回个RST包给server。如若client没跑路,且没收到server的FIN包,如(3)描述;
0x07 TCP第三次挥手ACK包丢包
防止回复的ACK包丢失(丢失后,server因为没收FIN的ACK,所以会再发一个FIN),将等待2MSL(最大报文存活时间)
syn = 1丢了 计时器超时后重传第一个包(syn=1)
syn = 1, ack = 1丢了 重传第一个(syn=1)和第二个包(syn=1,ack=1)
ack = 1丢了 等待3秒、6秒、12秒后重新发送第二个(syn=1,ack=1)包
客户端fin = 1丢了 重传第一个包(fin=1)
服务端ack = 1丢了 重传第一个(fin=1)和第二个包(ack=1)
服务端fin = 1丢了 服务端重传第三个包(fin=1),client行为分类讨论,如果已经和别人建立了连接,那么回RST,如果没跑路,那么client将永远停留在这个状态(fin-wait)
客户端ack = 1丢了 服务端重传第三个包(fin=1),客户端等待Time-wait,即2MSL
# backlog
backlog 的值即为未连接队列和已连接队列的和
未完成连接队列 (so_q0len)
一个 SYN 已经到达,但三次握手还没有完成的连接中的数量
已完成连接队列 (so_q1len)
三次握手已完成,内核正等待进程执行 accept 的调用中的数量
简单理解,在 TCP 进行三次握手时,Liunx 会为其维护两个队列:
- 半连接队列,也叫未完成连接队列,简称为 syn 队列
- 全连接队列,也叫已完成连接队列,简称为 accept 队列
而 backlog 参数, 指定了内核为此套接口排队的最大连接个数。
在 C 语言中,通过下面的方式,在开始新连接监听的时候,设置 backlog
int listen(int fd,int backlog);
# TCP 半连接队列和全连接队列
TCP 面向连接:客户端和服务端之间通过创建相应的数据结构,来维护双方的状态,并通过这样的数据结构来保持面向连接的特性。
服务端收到客户端发起的 SYN 请求后,内核会把该连接存储到半连接队列,并向客户端响应 SYN+ACK,
接着客户端会返回 ACK,
服务端收到第三次握手的 ACK 后,内核会把连接从半连接队列移除,然后创建新的完全的 socket 连接,并将其添加到 accept 队列,等待进程调用 accept 函数时把连接取出来。
第一次握手时,服务端先创建一个轻量版本的 request_sock,
第三次握手时,才会创建 sock,这样可以减少资源的消耗。
三次握手后,将新 sock 插入 accept_queue 全连接队列,等待 accept。
# 为什么要存在半连接队列
因为根据 TCP 协议的特点,会存在半连接这样的网络攻击存在,叫做 syn 攻击
即不停的发 SYN 包,而从不回应 SYN_ACK。
如果发一个 SYN 包就让 Kernel 建立一个消耗极大的 sock,那么很容易就内存耗尽。
我们只需要一直对服务端发送 syn 包,但是不回 ack 回应包,
这样就会使得服务端有大量请求处于 syn_recv 状态,这就是所谓的 syn flood 洪泛,syn 攻击,DDos 攻击
半连接队列,是解决 syn flood 洪泛的 一个关键措施。
所以内核在三次握手成功之前,只分配一个占用内存极小的 request_sock,以防止这种攻击的现象,
再配合 syn_cookie 机制,尽量抵御这种半连接攻击的风险。
1./proc/sys/net/ipv4/tcp_max_syn_backlog # 1024
2./proc/sys/net/core/somaxconn # 1024
listen 的 backlog 参数最大不能超过 somaxconn。它用于限制 tcp 的全连接队列和半连接队列的长度。
全连接队列的最大值是 min (somaxconn, backlog);
tcp_max_syn_backlog 在系统配置中设置,用于检查 syn 半连接队列健康情况,
当服务端接收到一定数量的 syn 包,要检查新旧包是否有冲突,如有冲突就丢弃新包,这样就要避免洪水攻击。
当 tcp_max_syn_backlog> min (somaxconn, backlog) 时, 半连接队列最大值 max_qlen_log = min (somaxconn, backlog) * 2;
当 tcp_max_syn_backlog<min (somaxconn, backlog) 时, 半连接队列最大值 max_qlen_log = tcp_max_syn_backlog * 2;# 全队列溢出
当服务端的全连接队列过小时,容易发生全连接队列溢出。
发生全连接队列溢出,后续的请求就会别丢弃。
Linux 有个参数,可以指定 TCP 全连接队列满了,会使用什么策略来回应客户端
- 可以丢弃客户端的 ack 报文,当然, 只是 liunx 的默认行为,
- 可以向客户端发送 RST 报文,终止连接,告诉客户端连接失败
tcp_abort_on_overflow 共有两个值分别是 0 和 1
- 0:如果全连接队列满了,那么服务端丢弃 ack 报文
- 1:如果全连接队列满了,那么服务端会向客户端发送 RST 报文,终止这个握手连接
如果设置 tcp_abort_on_overflow 为 0 的话,此时服务端全连接队列满了,客户端发送过来的 ack 报文,服务端丢弃。
而此时客户端还会继续重传,如果此时服务端的全连接队列有空闲,那么就会接受重传的 ack 包,这样就能直接建立连接了。
而设置 tcp_abort_on_overflow 为 1 的话,还需要重新连接
# 防御 TCP syn 攻击
1.tcp_syncookies
syncookies 在接收到客户端的 syn 报文时,计算出一个 cookies 值,放到 syn+ack 报文中发出。
cookies 值通过调用 secure_tcp_syn_cookie 函数,根据报文中的源 / 目的 IP 地址,TCP 源 / 目的端口号,TCP 序号和 MSS 索引值,计算出来
当客户端返回 ack 报文时,取出该值验证,成功则建立连接
开启 tcp_syncookies , 服务端根本不创建 request_sock 对象,也不建立 sock 连接
如果启用了 syncookies,服务端将客户端 SYN 报文中的一些信息保存在了序号中,就不需要保留此连接的 request_sock 结构了,
在发送完 SYN+ACK 报文之后,将已经申请的资源释放,降低 DDos 攻击时的资源消耗
2. 增大半连接队列
由于全连接队列里面保存的是占用内存很大的普通 sock,所以 Kernel 给其加了一个最大长度的限制。
这个限制为下面三者中的最小值
1.listen 系统调用中传进去的 backlog
2./proc/sys/net/ipv4/tcp_max_syn_backlog
3./proc/sys/net/core/somaxconn
即 min (backlog, tcp_ma_syn_backlog, somaxcon)
所以不能只增大 tcp_max_syn_backlog, 还需要一同增大 somaconn 和 backlog,也就是增大全连接队列
3. 减少 ack+syn 报文重传次数
因为我们在收到 syn 攻击时,服务端会重传 syn+ack 报文到最大次数,才会断开连接。
针对 syn 攻击的场景,我们可以减少 ack+syn 报文的重传次数,使处于 syn_recv 状态的它们更快断开连接
/proc/sys/net/ipv4/tcp_synack_retries
4. 半连接队列
# UDP 报文结构

源端口:这个字段占据 UDP 报文头的前 16 位,通常包含发送数据报的应用程序所使用的 UDP 端口。接收端的应用程序利用这个字段的值作为发送响应的目的地址。这个字段是可选的,所以发送端的应用程序不一定会把自己的端口号写入该字段中。如果不写入端口号,则把这个字段设置为 0。这样,接收端的应用程序就不能发送响应了。
目的端口:接收端计算机上 UDP 软件使用的端口,占据 16 位。
Length 占用 2 个字节,标识 UDP 头的长度,包括首部长度和数据长度。可以有 65535 字节那么长。但是一般网络在传送的时候,一次一般传送不了那么长的协议(涉及到 MTU 的问题),就只好对数据分片。
Checksum : 校验和,包含 UDP 头和数据部分。这是一个可选的选项,并不是所有的系统都对 UDP 数据包加以检验和数据 (相对 TCP 协议的必须来说),但是 RFC 中标准要求,发送端应该计算检验和。UDP 的首部,数据部分,伪首部都会参与检验和的计算,各字段是按照 16 比特为单位进行计算的,因此数据部分是要保证是 16 比特的倍数,不够用 0 填充。
将 UDP 伪头部、UDP 头部和数据部分全部用 16 进制数表示。
将第一个 16 进制数与第二个 16 进制数相加,得到一个 32 位的数,如果 32 位数的高 16 位大于 0,需要将高 16 位与低 16 位再相加,得到一个 32 位的数,直到高 16 位为 0,得到这一次相加的结果。
将上一步得到的 16 位数与第三个数 16 进制的数相加,重复第二步,直到累加完所有的 16 进制数,并且得到的结果为 16 进制数。
将累加最后得到的 16 进制数取反,得到校验和。
1
2
3
4
5
6
7
8
9
10
unsigned short check_sum(unsigned short *data, int num){
unsigned long sum = 0;
for(int i = 0;i < num; i++){
sum += *data++; //将2个16进制数相加
sum = (sum>>16) + (sum&0xffff); //取相加结果的低16位与高16位相加
}
return ~sum; //对最后的结果取反
}
# 伪首部
1. 长度为 12B
2. 伪首部不是 UDP 的真正首部,只在计算校验和时用到
3. 伪首部既不向下传送也不向上递交,只是为了计算校验和

发送方或接收方根据 IP 报文首部获得 8 字节的源地址 + 目的地址、2 字节的 0 字段 + UDP 协议字段、2 字节的数据长度,得到 12 字节伪首部,临时添加在首部前面。
发送方将计算完毕的校验和填入首部的校验和字段后,去除伪首部发送 UDP 报文。
# UDP 适用场景
UDP 协议一般作为流媒体应用、语音交流、视频会议所使用的传输层协议,还有许多基于互联网的电话服务使用的 VOIP(基于 IP 的语音)也是基于 UDP 运行的,实时视频和音频流协议旨在处理偶尔丢失的数据包,因此,如果重新传输丢失的数据包,则只会发生质量略有下降,而不是出现较大的延迟。
我们大家都知道的 DNS 协议底层也使用了 UDP 协议,这些应用或协议之所以选择 UDP 主要是因为以下这几点
速度快,采用 UDP 协议时,只要应用进程将数据传给 UDP,UDP 就会将此数据打包进 UDP 报文段并立刻传递给网络层,然而 TCP 有拥塞控制的功能,它会在发送前判断互联网的拥堵情况,如果互联网极度阻塞,那么就会抑制 TCP 的发送方。使用 UDP 的目的就是希望实时性。
无须建立连接,TCP 在数据传输之前需要经过三次握手的操作,而 UDP 则无须任何准备即可进行数据传输。因此 UDP 没有建立连接的时延。
无连接状态,TCP 需要在端系统中维护连接状态,连接状态包括接收和发送缓存、拥塞控制参数以及序号和确认号的参数,在 UDP 中没有这些参数,也没有发送缓存和接受缓存。因此,某些专门用于某种特定应用的服务器当应用程序运行在 UDP 上,一般能支持更多的活跃用户
分组首部开销小,每个 TCP 报文段都有 20 字节的首部开销,而 UDP 仅仅只有 8 字节的开销。
# UDP flood
UDP 洪水是一种拒绝服务攻击,攻击者将大量用户数据报协议 (UDP) 数据包发送到目标服务器,旨在让该设备的处理和响应能力无力承担。由于 UDP 洪水攻击,保护目标服务器的防火墙也可能不堪重负,导致对正常流量拒绝服务。
UDP 洪水的工作原理主要是利用服务器响应发送到其端口之一的 UDP 数据包时所采取的步骤。在正常情况下,服务器在特定端口上收到 UDP 数据包时,将通过以下两个步骤进行响应:
服务器首先检查是否有任何当前侦听指定端口请求的程序正在运行。
如果该端口上没有程序正在接收数据包,则服务器将以 ICMP (ping) 数据包作为响应,以告知发送方目标不可达。
由于目标服务器利用资源来检查并响应每个接收到的 UDP 数据包,当收到大量 UDP 数据包时,目标资源会很快耗尽,从而导致对正常流量拒绝服务。
# UDP 和 TCP 区别
- TCP 面向连接;UDP 是无连接的,即发送数据之前不需要建立连接。
- TCP 提供可靠的服务;UDP 不保证可靠交付。
- TCP 面向字节流,把数据看成一连串无结构的字节流;UDP 是面向报文的。
- TCP 有拥塞控制;UDP 没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如实时视频会议等)。
- 每一条 TCP 连接只能是点到点的;UDP 支持一对一、一对多、多对一和多对多的通信方式。
- TCP 首部开销 20 字节;UDP 的首部开销小,只有 8 个字节。
# IP 报文结构

版本(version):4 bits,表示 IP 的版本,分为 IPV4 和 IPV6
报头长度(header length):4 bits,标识 IP 报头的长度(不包括数据部分),单位是 32bits (也就是 4 字节)。由于 4 位二进制的最大值为 15,所以 IP 报头最大就是 15 x 4 = 60 字节。IP 报文的最小长度就是 20 字节。
差分服务字段(different services field): 8bits, 标识数据包的优先级,主要用于 QOS 服务中。
总长度(total length) :16 bits 标识 IP 报文的总长度(包括数据部分),单位是 1 byte。报头的长度和数据部分的长度之和。
所以一个 IP 报文的最大长度是 65535 字节。
标识符(identifier):16 bits,数据包的一个 ID 编号,用于标识数据包,特别是用于数据包分片技术中。通常每发送一个报文,它的值加一。当 IP 报文长度超过传输网络的 MTU(最大传输单元)时必须分片,这个标识字段的值被复制到所有数据分片的标识字段中,使得这些分片在达到最终目的地时可以依照标识字段的内容重新组成原先的数据。
标志位 1(reserve bit):1 bit, 保留位
标志位 2(dont fragment): 1 bit,不可分片位(DF),如果置 1,则代表此数据位不可分片
标志位 3(more fragment): 1 bit ,更多分片位(MF)
如果数据包被分片,则除了最后一个分片报文的 MF 位是 0 外,其他分片报文的 MF 位是 1。
分段偏移(fragment offset):13 bit,单位是 byte,标识分片报文相对于原始报文起始位置的偏移量
生存时间(time to live): 8 bit , 初始数据包会设置一个特定的 TTL 值,在传输过程中,每经过一次路由,TTL 就会减 1,所以 TTL 代表了数据包被路由的次数,网络中一般称为跳数。
TTL 一旦减到 0,则网络设备会丢弃此数据包。
协议号(protocol):8bit,标识了网络层之上使用了何种网络协议。其中 TCP = 6,UDP = 17
报头检验和(header checksum):16 bit, 针对 IP 报头的纠错字段,只校验 IP 报头
源 IP 地址(source address):32bit,数据包发送方 IP 地址
目的 IP 地址(destination address) : 32bit,数据包接收方 IP 地址。
可选项 & 填充项:(option & padding):0-40 字节,一些可选项,主要用于测试,不足 32bit 则用 0 补充。
# 分片
# MSS
- 在建立 TCP 连接的同时,也可以确定发送数据包的单位,我们也可以称其为 “最大消息长度”(MSS)。最理想的情况是,最大消息长度正好是 IP 中不会被分片处理的最大数据长度。
- TCP 在传送大量数据时,是以 MSS 的大小将数据进行分割发送。进行重发时也是以 MSS 为单位。
- MSS 在三次握手的时候,在两端主机之间被计算得出。两端的主机在发出建立连接的请求时,会在 TCP 首部中写入 MSS 选项,告诉对方自己的接口能够适应的 MSS 的大小。然后会在两者之间选择一个较小的值投入使用。
Frame = Ethernet Header + IP Header + TCP Header + TCP Segment Data
(1)Ethernet Header = 14 Byte = Dst Physical Address(6 Byte)+ Src Physical Address(6 Byte)+ Type(2 Byte),以太网帧头以下称之为数据帧。
(2)IP Header = 20 Byte(without options field),数据在 IP 层称为 Datagram,分片称为 Fragment。
(3)TCP Header = 20 Byte(without options field),数据在 TCP 层称为 Stream,分段称为 Segment(UDP 中称为 Message)。
(4)54 个字节后为 TCP 数据负载部分(Data Portion),即应用层用户数据。
Ethernet Header 以下的 IP 数据报最大传输单位为 MTU(Maximum Transmission Unit,Effect of short board),对于大多数使用以太网的局域网来说,MTU=1500。
TCP 数据包每次能够传输的最大数据分段为 MSS,为了达到最佳的传输效能,在建立 TCP 连接时双方协商 MSS 值,双方提供的 MSS 值的最小值为这次连接的最大 MSS 值。MSS 往往基于 MTU 计算出来,通常 MSS=MTU-sizeof (IP Header)-sizeof (TCP Header)=1500-20-20=1460。
这样,数据经过本地 TCP 层分段后,交给本地 IP 层,在本地 IP 层就不需要分片了。但是在下一跳路由(Next Hop)的邻居路由器上可能发生 IP 分片!因为路由器的网卡的 MTU 可能小于需要转发的 IP 数据报的大小。这时候,在路由器上可能发生两种情况:
(1). 如果源发送端设置了这个 IP 数据包可以分片(May Fragment,DF=0),路由器将 IP 数据报分片后转发。
(2). 如果源发送端设置了这个 IP 数据报不可以分片(Don’t Fragment,DF=1),路由器将 IP 数据报丢弃,并发送 ICMP 分片错误消息给源发送端。
# 粘包和半包
在网络通信中,当发送方连续发送多个小数据包时,接收方可能会将它们合并成一个大的数据包,这就是粘包问题;而当发送方发送的数据包长度大于接收方的缓冲区长度时,接收方无法完整接收数据包,导致数据的接收不完整,这就是半包问题。
粘包 就是多个数据混淆在一起了,而且多个数据包之间没有明确的分隔,导致无法对这些数据包进行正确的读取。
半包 就是一个大的数据包被拆分成了多个数据包发送,读取的时候没有把多个包合成一个原本的大包,导致读取的数据不完整。
这种问题产生的原因可能有多种因素,从应用层到链路层中都有可能引起这个问题。
# 滑动窗口
TCP 协议是一种可靠性传输协议,所以在传输数据的时候必须要等到对方的应答之后才能发送下一条数据,这种显然效率不高。
TCP 协议为了解决这个传输效率的问题,引入了滑动窗口。滑动窗口就是在发送方和接收方都有一个缓冲区,这个缓冲区就是 "窗口",假设发送方的窗口大小是
0~100KB, 那么发送数据的时候前 100KB 的数据不需要等到对方 ACK 应答即可全部发送。如果发送的过程中收到了对方返回某个数据包的 ACK,那么这个窗口会对应的向后滑动。比如刚开始的窗口大小是
0~100KB,收到前 20KB 数据包的 ACK 之后,这个窗口就会滑动到20~120KB的位置,以此类推。这里还有一个小问题,如果发送方一直未接收到前 20KB 的 ACK 消息,那么在发送完0~100KB的数据之后,窗口就会卡在那里,这就是经典的队头阻塞问题# 窗口变化
发送窗口的位置由窗口前沿和后沿的位置共同确定。发送窗口后沿的变化情况有两种,即不动 (没有收到新的确认) 和前移 (收到了新的确认)。发送窗口后沿不可能向后移动,因为不能撤销已收到的确认
发送窗口前沿通常是不断向前移动,但也有可能不动。这对应于两种情况:
- 一是没有收到新的确认,对应通知的窗口大小也不变
- 二是收到了新的窗口但对方通知的窗口缩小了,使得发送窗口前沿正好不动
发送窗口前沿也有可能
向后收缩。这发生在对方通知的窗口缩小了。但 TCP 的标准强烈不赞成这样做。因为很可能发送方在收到这个通知以前已经发生了窗口中的许多数据,现在又要收缩窗口,不让发送这些数据,这样就会产生一些错误要描述一个发送窗口的状态需要三个指针:P1,P2,P3。指针都指向字节的序号。这三个指针指向的几个部分的意义如下:
小于 P1 的是已发送并已收到确认的部分,而大于 P3 的是不允许发送的部分
P3 - P1 = A 的发送窗口
P2 - P1 已发送但尚未收到确认的字节数
P3 - P2 允许发送但当前尚未发送的字节数 (又称为
可用窗口或有效窗口)
# Nagle 算法
有这么一种情况,每次发送的数据包都非常小,比如只有 1 个字节,但是 TCP 的报文头默认有 40 个字节,数据 + 报文头一共是 41 字节。如果这种较小的数据包经常出现,会造成过多的网络资源浪费。比如有 1W 个这样的数据包,那么总数据量中有 400MB 都是报文头,只有 10MB 是真正的数据。
所以 TCP 中引入了一种叫做 Nagle 的算法,如若连续几次发送的数据都很小, TCP 会根据这个算法把多个数据合并成一个包发出,从而优化传输效率,避免网络资源浪费。
# 应用层的接收缓冲区和发送缓冲区
对于操作系统的 IO 函数而言,网络数据不管是发送或者接收,都不会去逐个读取,而是会先把接收 / 发送的数据放入到一个缓冲区中,然后批量进行操作。当然,发送和接收各自会对应有一个缓冲区。
假设现在要发送 1234567 这组数据,操作系统的 IO 函数会挨个将他们写入到发送缓冲区。接收方也是这样,会将他们挨个从接收缓冲区中读取出来。
# 产生原因
搞清楚上面几个概念之后,我们再来分析一下为什么会产生粘包或者半包的问题
粘包:发送ABCD、EFGHIJK两个数据包,被接收成ABCDEFGHIJK一个数据包,多个包粘在一起。
- 应用层:接收方的接收缓冲区太大,导致读取多个数据包一起输出。
- TCP 滑动窗口:接收方窗口较大,导致发送方发出的多个数据包处理不及时造成粘包
- Nagle 算法:由于发送方的单个数据包体积太小,导致多个包合并成一个包发送
半包:发送
ABCDEFG一个数据包,被接收成ABC、DEFG两个数据包,一个包被拆成了多个。
- 应用层:接收方缓冲区太小,无法存放发送发的单个数据包,因此拆开读取。
- 滑动窗口:接收方的窗口太小,无法一次性放下完整的数据包,只能读取其中的一部分。
- MSS 限制:发送方的单个包大小超出了 MSS 限制,被拆分成了多个包
# 解决方案
1. 短链接
所谓短连接就是一次性把数据发完,然后就断开连接。客户端断开连接之后,服务端会接收到一个 - 1 的状态码,可以以这个 - 1 作为每个数据包的边界。
- 定长帧解码器
定长帧其实就是固定每次数据包的大小,比如固定每个包的大小为 8 个字节,那么发送方每次最多发送 8 个字节的数据,不够的自动补齐,然后接收方在接收的时候,每次也只接收 8 个字节大小的数据,这样就可以有效避免粘包半包问题。
# 拥塞控制
防止过多的数据注入到网络中。 几种拥塞控制方法:慢开始 (slow-start)、拥塞避免 ( congestion avoidance )、快重传 ( fast retransmit ) 和快恢复 ( fast recovery )。

慢开始
把拥塞窗口 cwnd 设置为一个最大报文段 MSS 的数值。而在每收到一个对新的报文段的确认后,把拥塞窗口增加至多一个 MSS 的数值。每经过一个传输轮次,拥塞窗口 cwnd 就加倍。 为了防止拥塞窗口 cwnd 增长过大引起网络拥塞,还需要设置一个慢开始门限 ssthresh 状态变量。
当 cwnd < ssthresh 时,使用慢开始算法。
当 cwnd > ssthresh 时,停止使用慢开始算法而改用拥塞避免算法。
当 cwnd = ssthresh 时,既可使用慢开始算法,也可使用拥塞控制避免算法。
拥塞避免
让拥塞窗口 cwnd 缓慢地增大,每经过一个往返时间 RTT 就把发送方的拥塞窗口 cwnd 加 1,而不是加倍。这样拥塞窗口 cwnd 按线性规律缓慢增长。
无论在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(其根据就是没有收到确认),就要把慢开始门限 ssthresh 设置为出现拥塞时的发送 方窗口值的一半(但不能小于 2)。然后把拥塞窗口 cwnd 重新设置为 1,执行慢开始算法。这样做的目的就是要迅速减少主机发送到网络中的分组数,使得发生 拥塞的路由器有足够时间把队列中积压的分组处理完毕。
快重传
有时个别报文段会在网络中丢失,但实际上网络并未发生拥塞。如果发送方迟迟收不到确认,就会产生超时,就会误认为网络发生了拥塞。这就导致发送方错误地启动慢开始,把拥塞窗口 cwnd 又设置为 1,因而降低了传输效率。
快重传算法可以避免这个问题。快重传算法首先要求接收方每收到一个失序的报文段后就立即发出重复确认,使发送方及早知道有报文段没有到达对方。
发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待重传计时器到期。由于发送方尽早重传未被确认的报文段,因此采用快重传后可以使整个网络吞吐量提高约 20%。
快恢复
当发送方连续收到三个重复确认,就会把慢开始门限 ssthresh 减半,接着把 cwnd 值设置为慢开始门限 ssthresh 减半后的数值,然后开始执行拥塞避免算法,使拥塞窗口缓慢地线性增大。
在采用快恢复算法时,慢开始算法只是在 TCP 连接建立时和网络出现超时时才使用。 采用这样的拥塞控制方法使得 TCP 的性能有明显的改进。
# 超时重传
TCP 为了实现可靠传输,实现了重传机制。最基本的重传机制,就是超时重传,即在发送数据报文时,设定一个定时器,每间隔一段时间,没有收到对方的 ACK 确认应答报文,就会重发该报文。
我们先来看下什么叫 RTT(Round-Trip Time,往返时间)。
RTT 就是,一个数据包从发出去到回来的时间,即数据包的一次往返时间。超时重传时间,就是 Retransmission Timeout ,简称 RTO。
- 如果 RTO 比较小,那很可能数据都没有丢失,就重发了,这会导致网络阻塞,会导致更多的超时出现。
- 如果 RTO 比较大,时间太长还是没有重发,那效果就不好了。
一般情况下,RTO 略大于 RTT,效果是最好的。
RTO 计算公式:Jacobson / Karels 算法
1. 先计算 SRTT(计算平滑的 RTT)
1 SRTT = (1 - α) * SRTT + α * RTT //求 SRTT 的加权平均2. 再计算 RTTVAR (round-trip time variation)
1 RTTVAR = (1 - β) * RTTVAR + β * (|RTT - SRTT|) //计算 SRTT 与真实值的差距3. 最终的 RTO
1 RTO = µ * SRTT + ∂ * RTTVAR = SRTT + 4·RTTVAR其中,
α = 0.125,β = 0.25, μ = 1,∂ = 4,这些参数都是大量结果得出的最优参数。超时重传缺点
- 当一个报文段丢失时,会等待一定的超时周期然后才重传分组,增加了端到端的时延。
- 当一个报文段丢失时,在其等待超时的过程中,可能会出现这种情况:其后的报文段已经被接收端接收但却迟迟得不到确认,发送端会认为也丢失了,从而引起不必要的重传,既浪费资源也浪费时间。
- TCP 有个策略,就是超时时间间隔会加倍。超时重传需要等待很长时间。因此,还可以使用快速重传机制。
快重传
- 第一份 Seq=1 先送到了,于是就 Ack 回 2;
- 第二份 Seq=2 也送到了,假设也正常,于是 ACK 回 3;
- 第三份 Seq=3 由于网络等其他原因,没送到;
- 第四份 Seq=4 也送到了,但是因为 Seq3 没收到。所以 ACK 回 3;
- 后面的 Seq=4,5 的也送到了,但是 ACK 还是回复 3,因为 Seq=3 没收到。
- 发送端连着收到三个重复冗余 ACK=3 的确认(实际上是 4 个,但是前面一个是正常的 ACK,后面三个才是重复冗余的),便知道哪个报文段在传输过程中丢失了,于是在定时器过期之前,重传该报文段。
- 最后,接收到收到了 Seq3,此时因为 Seq=4,5,6 都收到了,于是 ACK 回 7
但快速重传还可能会有个问题:ACK 只向发送端告知最大的有序报文段,到底是哪个报文丢失了呢?并不确定
是重传 Seq3 呢?还是重传 Seq3、Seq4、Seq5、Seq6 呢?因为发送端并不清楚这三个连续的 ACK3 是谁传回来的
带选择确认的重传(SACK)
SACK 机制就是,在快速重传的基础上,接收端返回最近收到的报文段的序列号范围,这样发送端就知道接收端哪些数据包没收到,酱紫就很清楚该重传哪些数据包啦。SACK 标记是加在 TCP 头部选项字段里面的。
发送端收到了三次同样的 ACK=30 的确认报文,于是就会触发快速重发机制,通过 SACK 信息发现只有
30~39这段数据丢失,于是重发时就只选择了这个30~39的 TCP 报文段进行重发。
# ACK
# dup ack

[TCP dup ack XXX#X] 表示第几次重新请求某一个包,
XXX 表示第几个包(不是 Seq),X 表示第几次请求。
丢包或者乱序的情况下,会出现该标志。
server 收到了 3 和 8 号包,但是没有收到中间的 4/5/6/7,那么 server 就会 ack 3,如果 client 还是继续发 8/9 号包,那么 server 会继续发 dup ack 3#1 ; dup ack 3#2 来向客户端说明只收到了 3 号包,不要着急发后面的大包,把 4/5/6/7 给我发过来
# keep-alive ack
在 TCP 中有一个 Keep-alive 的机制可以检测死连接,原理很简单,TCP 会在空闲了一定时间后发送数据给对方:
1. 如果主机可达,对方就会响应 ACK 应答,就认为是存活的。
2. 如果可达,但应用程序退出,对方就发 RST 应答,发送 TCP 撤消连接。
3. 如果可达,但应用程序崩溃,对方就发 FIN 消息。
4. 如果对方主机不响应 ack, rst,继续发送直到超时,就撤消连接。这个时间就是默认的二个小时。
# tcp retransmission
超时重传,如果一个包的丢了,又没有后续包可以在接收方触发 [Dup Ack],或者 **[Dup Ack] 也丢失 ** 的情况下,TCP 会触发超时重传机制。
# tcp spurious retransmission
# tcp fast retransmission
一般快速重传算法在收到三次冗余的 Ack,即三次 [TCP dup ack XXX#X] 后,发送端进行快速重传。
为什么是三次呢?因为两次 duplicated ACK 肯定是乱序造成的,丢包肯定会造成三次 duplicated ACK。
TCP 半连接队列和全连接队列(史上最全) - 疯狂创客圈 - 博客园 (cnblogs.com)
TCP/IP 协议 (图解 + 秒懂 + 史上最全) - 疯狂创客圈 - 博客园 (cnblogs.com)
MSS 与 MTU 的关系 - 静之深 - 博客园 (cnblogs.com)
UDP 协议详解 - 知乎 (zhihu.com)
TCP 协议中的粘包和半包问题 - 知乎 (zhihu.com)
太厉害了,终于有人能把 TCP/IP 协议讲的明明白白了 - 知乎 (zhihu.com)
TCP/IP/ICMP 报文格式 - 知乎 (zhihu.com)
这可能是最全面的 TCP 面试八股文了 - 知乎 (zhihu.com)
TCP 协议详解 - 知乎 (zhihu.com)
面试必备!TCP 协议经典十五连问! - 知乎 (zhihu.com)
(63 条消息) IP 报文的结构 - CSDN 博客
(63 条消息) TCP/IP 协议专栏 —— 分片报文详解 —— 网络入门和工程维护必看_牛牛来了的博客 - CSDN 博客
linux ss 命令详解 - 腾讯云开发者社区 - 腾讯云 (tencent.com)
(95 条消息) TCP 报文( tcp dup ack 、TCP Retransmission)_ynchyong 的博客 - CSDN 博客
(95 条消息) TCP DUP ACK 抓包分析_造夢先森的博客 - CSDN 博客
(TCP keepalive)心跳包机制设计详解 - 知乎 (zhihu.com)





