目录
系统调用 listen() 的 backlog 参数指的是什么
P2P 下载加速理论上的加速比是多少呢?
TCP 的发送窗口有哪些稳定状态呢?
TCP 是端到端的协议,也就是 TCP 对端 Peer 只看到对方,看不到网络上的其他点,那么 TCP 的两端怎么对网络情况做出反映呢?发生拥塞的时候,拥塞控制机制是如何的?
TCP 两端 Peer 的处理能力不对等的时候,比如发送方处理能力很强,接收方处理能力很弱,这样发送方是否能够不管接收方死活狂发数据呢?如果不能,流量控制机制的如何的?
假如发送一个包,一直都没收到确认呢?什么时候重传呢?超时机制的怎样的?
TCP的可靠性是怎么确认的?是收到一个包就马上确认,还是可以稍等一下在确认呢?
清理掉TIME_WAIT的技巧?
TIME_WAIT 带来占用大量连接的问题怎么解决?
为什么主动关闭的一方会进入time_wait?
Client 和 Server 同时发起断开连接的 FIN 包会怎么样呢,TCP 状态是怎么转移的?
假如Client 发送一个 SYN 包给 Server 后就挂了或是不管了,这个时候这个连接处于什么状态呢?会超时吗?为什么呢?
初始化序列号 X、Y 是可以是写死固定的吗,为什么不能呢?
为什么要进行 TIME_WAIT 呢(超时设置是 2*MSL,RFC793 定义了 MSL 为 2 分钟,Linux 设置成了 30s)?
Client 和 Server 同时发起断开连接的 FIN 包会怎么样呢,TCP 状态是怎么转移的?
三次挥手?
四次握手来建立连接?
要说明 backlog 参数的含义,首先需要说一下 Linux 的协议栈维护的 TCP 连接的两个连接队列:[1]SYN 半连接队列;[2]accept 连接队列。
[1]SYN 半连接队列:Server 端收到 Client 的 SYN 包并回复 SYN,ACK 包后,该连接的信息就会被移到一个队列,这个队列就是 SYN 半连接队列(此时 TCP 连接处于 非同步状态 )
[2]accept 连接队列:Server 端收到 SYN,ACK 包的 ACK 包后,就会将连接信息从[1]中的队列移到另外一个队列,这个队列就是 accept 连接队列(这个时候 TCP 连接已经建立,三次握手完成了)。
用户进程调用 accept()系统调用后,该连接信息就会从[2]中的队列中移走。相信不少同学就 backlog 的具体含义进行争论过,有些认为 backlog 指的是[1]和[2]两个队列的和。而有些则认为是 backlog 指的是[2]的大小。其实,这两个说法都对,在 linux kernel 2.2 之前 backlog 指的是[1]和[2]两个队列的和。而 2.2 以后,就指的是[2]的大小,那么在 kernel 2.2 以后,[1]的大小怎么确定的呢?
两个队列的作用分别是什么呢?
【1】SYN 半连接队列的作用
对于 SYN 半连接队列的大小是由(/proc/sys/net/ipv4/tcp_max_syn_backlog)这个内核参数控制的,有些内核似乎也受 listen 的 backlog 参数影响,取得是两个值的最小值。当这个队列满了,Server 会丢弃新来的 SYN 包,而 Client 端在多次重发 SYN 包得不到响应而返回(connection time out)错误。但是,当 Server 端开启了 syncookies,那么 SYN 半连接队列就没有逻辑上的最大值了,并且/proc/sys/net/ipv4/tcp_max_syn_backlog 设置的值也会被忽略。
【2】accept 连接队列
accept 连接队列的大小是由 backlog 参数和(/proc/sys/net/core/somaxconn)内核参数共同决定,取值为两个中的最小值。当 accept 连接队列满了,协议栈的行为根据(/proc/sys/net/ipv4/tcp_abort_on_overflow)内核参数而定。如果 tcp_abort_on_overflow=1,server 在收到 SYN_ACK 的 ACK 包后,协议栈会丢弃该连接并回复 RST 包给对端,这个是 Client 会出现(connection reset by peer)错误。如果 tcp_abort_on_overflow=0,server 在收到 SYN_ACK 的 ACK 包后,直接丢弃该 ACK 包。这个时候 Client 认为连接已经建立了,一直在等 Server 的数据,直到超时出现 read timeout 错误。
[1]首先,节点 N1 文件分成 3 个片段 FS2,FS3,FS4,接着将 FS2 发送给 N2,FS3 发送给 N3,FS4 发送给 N4,耗时 FS/(3*BW);[2]然后,N2,N3,N4 执行“人人为我,我为人人”的精神,将自己拥有的 F2,F3,F4 分别发给没有的其他的节点,这样耗时 FS/(3*BW)完成交换。当 K 趋于无穷大的时候,文件进行无限细分的时候,耗时变成了 FS/(3*BW),也就是当节点是 n+1 的时候,加速比是 n。这就是理论上的最大加速比了,最大加速比是 P2P 网络节点个数减 1。
【1】接收端拥有大窗口的经典锯齿状
因为,一般情况下机器的处理速度就是比较快,这样 TCP 的接收端都是拥有较大的窗口,这时发送端的发送窗口就完全由其拥塞窗口 cwnd 决定了;网络上拥有成千上万的 TCP 连接,它们在相互争用网络带宽,TCP 的流量控制使得它想要独享整个网络,而拥塞控制又限制其必要时做出牺牲来体现公平性。于是在传输稳定的时候 TCP 发送端呈现出下面过程的反复
【2】接收端拥有小窗口的直线状态
这种情况下是接收端非常慢速,接收窗口一直很小,这样发送窗口就完全有接收窗口决定了。由于发送窗口小,发送数据少,网络就不会出现拥塞了,于是发送窗口就一直稳定的等于那个较小的接收窗口,呈直线状态。
【3】两个直连网络端点间的满载状态下的直线状态
Peer 两端直连,并且只有位于一个 TCP 连接,那么这个连接将独享网络带宽,这里不存在拥塞问题,在他们处理能力足够的情况下,TCP 的流量控制使得他们能够跑慢整个网络带宽。
学生时代下载慢,被迅雷占用了网速就是因为迅雷利用TCP公平性多开了tcp连接,在通信链路带宽固定(假设为 W)下载一个大小为 FS 的文件,那么最快需要的时间是 FS/W,难道就没办法加速了吗?
答案是有的,这样因为网络是网状的,一个节点是要和很多几点互联的,这就存在多个带宽为 W 的通信链路,如果我们能够将要下载的文件,一半从 A 通信链路下载,另外一半从 B 通信链路下载,这样整个下载时间就减半了为 FS/(2W),这就是 p2p 加速。相信大家学生时代在下载爱情动作片的时候也遇到过这种情况,明明外网速度没这么快的,自己下载的爱情动作片的速度却达到几 M/s,那是因为,你的左后或右后的宿友在帮你加速中。我们都知道 P2P 模式下载会快,并且越多人下载就越快
谈到拥塞控制,就要先谈谈拥塞的因素和本质。本质上,网络上拥塞的原因就是大家都想独享整个网络资源,对于 TCP,端到端的流量控制必然会导致网络拥堵。这是因为 TCP 只看到对端的接收空间的大小,而无法知道链路上的容量,只要双方的处理能力很强,那么就可以以很大的速率发包,于是链路很快出现拥堵,进而引起大量的丢包,丢包又引发发送端的重传风暴,进一步加剧链路的拥塞。
由于 TCP 看不到网络的状况,那么拥塞控制是必须的并且需要采用试探性的方式来控制拥塞,
于是拥塞控制要完成两个任务:[1]公平性;[2]拥塞过后的恢复。
Reno 算法包含 4 个部分:
[1]慢热启动算法 – Slow Start;
慢启动体现了一个试探的过程,刚接入网络的时候先发包慢点,探测一下网络情况,然后在慢慢提速。不要一上来就拼命发包,这样很容易造成链路的拥堵,出现拥堵了在想到要降速来缓解拥堵这就有点成本高了,毕竟无数的先例告诫我们先污染后治理的成本是很高的。(就是看到的那条线,先线性,再指数级,再进入ssthresh限制,再进入“拥塞避免算法”)
一篇 Google 的论文《An Argument for Increasing TCP’s Initial Congestion Window》建议把 cwnd 初始化成了 10 个 MSS。Linux 3.0 后采用了这篇论文的建议。
[2]拥塞避免算法 – Congestion Avoidance;
1)出现 RTO 超时,重传数据包。这种情况下,TCP 就认为出现拥塞的可能性就很大,于是它反应非常'强烈' [1] 调整门限 ssthresh 的值为当前 cwnd 值的 1/2;[2] reset 自己的 cwnd 值为 1;[3] 然后重新进入慢启动过程。
2)在 RTO 超时前,收到 3 个 duplicate ACK 进行重传数据包。这种情况下,收到 3 个冗余 ACK 后说明确实有中间的分段丢失,然而后面的分段确实到达了接收端,因为这样才会发送冗余 ACK,这一般是路由器故障或者轻度拥塞或者其它不太严重的原因引起的,因此此时拥塞窗口缩小的幅度就不能太大,此时进入“快速重传”。
[3]快速重传 - Fast Retransimit;
1) 调整门限 ssthresh 的值为当前 cwnd 值的 1/2;2) 将 cwnd 值设置为新的 ssthresh 的值;3) 重新进入拥塞避免阶段。
当收到 3 个冗余 ACK 时,TCP 最后的[3]步骤进入的不是拥塞避免阶段,而是快速恢复阶段。
[4]快速恢复算法 – Fast Recovery。
拥塞控制在拥塞避免阶段,cwnd 是加性增加的,在判断出现拥塞的时候采取的是指数递减。为什么要这样做呢?
这是出于公平性的原则,拥塞窗口的增加受惠的只是自己,而拥塞窗口减少受益的是大家。这种指数递减的方式实现了公平性,一旦出现丢包,那么立即减半退避,可以给其他新建的连接腾出足够的带宽空间,从而保证整个的公平性。
TCP 的窗口(window)是一个 16bit 位字段,它代表的是窗口的字节容量,也就是 TCP 的标准窗口最大为 2^16-1=65535 个字节。另外在 TCP 的选项字段中还包含了一个 TCP 窗口扩大因子,option-kind 为 3,option-length 为 3 个字节,option-data 取值范围 0-14。窗口扩大因子用来扩大 TCP 窗口,可把原来 16bit 的窗口,扩大为 31bit。
这个窗口是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。
1)发送端是怎么做到比较方便知道自己哪些包可以发,哪些包不能发呢?
发送方要知道那些可以发,哪些不可以发,一个简明的方案就是按照接收方的窗口通告,发送方维护一个一样大小的发送窗口就可以了,在窗口内的可以发,窗口外的不可以发,窗口在发送序列上不断后移,这就是 TCP 中的滑动窗口。
2)如果接收端通知一个零窗口给发送端,这个时候发送端还能不能发送数据呢?如果不发数据,那一直等接收端口通知一个非 0 窗口吗,如果接收端一直不通知呢?
为解决 0 窗口的问题,TCP 使用了 Zero Window Probe 技术,缩写为 ZWP。发送端在窗口变成 0 后,会发 ZWP 的包给接收方,来探测目前接收端的窗口大小,一般这个值会设置成 3 次,每次大约 30-60 秒(不同的实现可能会不一样)。于是攻击者会并发大量的这样的请求,把 Server 端的资源耗尽。
3)如果接收端处理能力很慢,这样接收端的窗口很快被填满,然后接收处理完几个字节,腾出几个字节的窗口后,通知发送端,这个时候发送端马上就发送几个字节给接收端吗?发送的话会不会太浪费了,就像一艘万吨油轮只装上几斤的油就开去目的地一样。对于发送端产生数据的能力很弱也一样,如果发送端慢吞吞产生几个字节的数据要发送,这个时候该不该立即发送呢?还是累积多点在发送?
解决这个问题的思路有两种,1)接收端不通知小窗口,2)发送端积累一下数据在发送。
Nagle 算法和延迟确认(Delayed Acknoledgement)一起,当出现(write-write-read)的时候会引发一个 40ms 的延时问题???????
【1】TCP 的重传超时计算:
为了使我们的重传机制更高效,如果我们能够比较准确知道在当前网络状况下,一个数据包从发出去到回来的时间 RTT——Round Trip Time,那么根据这个 RTT 我们就可以方便设置 TimeOut——RTO(Retransmission TimeOut)了。
RTT算法,为了避免采样期间不准确,使用了忽略重传,再后来为了防止忽略重传造成的灾难(如果在某一时间,网络闪动,突然变慢了,产生了比较大的延时,这个延时导致要重转所有的包(因为之前的 RTO 很小),于是,因为重转的不算,所以,RTO 就不会被更新,这是一个灾难。),加入了变化率函数
【2】TCP 的重传机制
定时重传:在TCP层面做一个定时重传时钟,每次根据返回的情况更新时钟,但是不能大于RTO上限
Fast Retransmit(快速重传)
Selective Acknowledgment(SACK,选择确认)
D-SACK,也就是利用第一块 SACK 数据中描述重复接收的不连续数据块的序列号参数,其他 SACK 数据则描述其他正常接收到的不连续数据。
按照 TCP 协议,确认机制是累积的,也就是确认号 X 的确认指示的是所有 X 之前但不包括 X 的数据已经收到了。
ACK 在收到数据后并不马上回复,而是延迟一段可以接受的时间,延迟一段时间的目的是看能不能和接收方要发给发送方的数据一起回去,因为 TCP 协议头中总是包含确认号的,如果能的话,就将数据一起捎带回去,这样网络利用率就提高了。
由于 TCP 协议不对 ACK 进行 ACK 的,RFC 建议最多等待 2 个包的积累确认,这样能够及时通知对端 Peer,我这边的接收情况。Linux 实现中,有延迟 ACK 和快速 ACK,并根据当前的包的收发情况来在这两种 ACK 中切换。一般情况下,ACK 并不会对网络性能有太大的影响,延迟 ACK 能减少发送的分段从而节省了带宽,而快速 ACK 能及时通知发送方丢包,避免滑动窗口停等,提升吞吐率。
ACK 的确认号,是确认按序收到的最后一个字节序,对于乱序到来的 TCP 分段,接收端会回复相同的 ACK 分段,只确认按序到达的最后一个 TCP 分段。TCP 连接的延迟确认时间一般初始化为最小值 40ms,随后根据连接的重传超时时间(RTO)、上次收到数据包与本次接收数据包的时间间隔等参数进行不断调整。
【1】修改 tcp_max_tw_buckets:tcp_max_tw_buckets 控制并发的 TIME_WAIT 的数量,默认值是 180000。如果超过默认值,内核会把多的 TIME_WAIT 连接清掉,然后在日志里打一个警告。官网文档说这个选项只是为了阻止一些简单的 DoS 攻击,平常不要人为的降低它。
【2】利用 RST 包从外部清掉 TIME_WAIT 链接:server 收到 RST 包然后就释放连接 connect1 提前终止 TIME_WAIT 状态了。提前终止 TIME_WAIT 状态是可能会带来(问题二)中说的三点危害,具体的危害情况可以看下 RFC1337。RFC1337 中建议,不要用 RST 过早的结束 TIME_WAIT 状态。
【1】TIME_WAIT 快速回收:linux 下开启 TIME_WAIT 快速回收需要同时打开 tcp_tw_recycle 和 tcp_timestamps(默认打开)两选项
问题:在一个 NAT 后面的所有 Peer 机器在 Server 看来都是一个机器,NAT 后面的那么多 Peer 机器的系统时间戳很可能不一致,有些快,有些慢。
所以,在是否开启 tcp_tw_recycle 需要慎重考虑了
【2】TIME_WAIT 重用:[1]. 新连接 SYN 告知的初始序列号比 TIME_WAIT 老连接的末序列号大;[2]. 如果开启了 tcp_timestamps,并且新到来的连接的时间戳比老连接的时间戳大。
问题同上
tcp_tw_reuse 和 SO_REUSEADDR 选项,认为是相关的一个东西,其实他们是两个完全不同的东西,可以说两个半毛钱关系都没。tcp_tw_reuse 是内核选项,而 SO_REUSEADDR 用户态的选项,使用 SO_REUSEADDR 是告诉内核,如果端口忙,但 TCP 状态位于 TIME_WAIT,可以重用端口。如果端口忙,而 TCP 状态位于其他状态,重用端口时依旧得到一个错误信息,指明 Address already in use”。
问题同上
据 TCP 协议规范,不对 ACK 进行 ACK,如果主动关闭方不进入 TIME_WAIT,那么主动关闭方在发送完 ACK 就走了的话,如果最后发送的 ACK 在路由过程中丢掉了,最后没能到被动关闭方,这个时候被动关闭方没收到自己 FIN 的 ACK 就不能关闭连接,接着被动关闭方会超时重发 FIN 包,但是这个时候已经没有对端会给该 FIN 回 ACK,被动关闭方就无法正常关闭连接了,所以主动关闭方需要进入 TIME_WAIT 以便能够重发丢掉的被动关闭方 FIN 的 ACK。
如果没有time_wait,那么server在没有收到ACK的情况下就会给client重发FIN包,这个时候主动关闭方无法识别这个 FIN 包,协议栈会认为对方疯了,都还没建立连接你给我来个 FIN 包?,于是回复一个 RST 包给被动关闭方,被动关闭方就会收到一个错误(我们见的比较多的:connect reset by peer,这里顺便说下 Broken pipe,在收到 RST 包的时候,还往这个连接写数据,就会收到 Broken pipe 错误了),原本应该正常关闭的连接,给我来个错误,很难让人接受。
防止链路上已经关闭的连接的残余数据包(a lost duplicate packet or a wandering duplicate packet) 干扰正常的数据包,造成数据流的不正常。例如,server给client最后一次的FIN包被暂存网络某个路径中,过段时间被新的连接接收到
Peer 在 FIN_WAIT1 状态下首先收到对端 Peer 的 FIN 包的话,那么该 Peer 在确认已经收到了对端 Peer 全部的 Data 数据包后,就响应一个 ACK 给对端 Peer,然后自己进入 CLOSEING 状态,Peer 在 CLOSEING 状态下收到自己的 FIN 包的 ACK 包的话,那么就进入 TIME WAIT 状态。
TCP 的 Peer 两端同时发起 FIN 包进行断开连接,那么两端 Peer 可能出现完全一样的状态转移 FIN_WAIT1——>CLOSEING——->TIME_WAIT,也就会 Client 和 Server 最后同时进入 TIME_WAIT 状态。
这个需要server用一个超时时间来将这个没有建立的TCP链接断开,超时时间是按照:*2进行5次
Linux 下默认会进行 5 次重发 SYN-ACK 包,重试的间隔时间从 1s 开始,下次的重试间隔时间是前一次的双倍,5 次的重试时间间隔为 1s,2s, 4s, 8s,16s,总共 31s,第 5 次发出后还要等 32s 都知道第 5 次也超时了,所以,总共需要 1s + 2s +4s+ 8s+ 16s + 32s =63s,TCP 才会把断开这个连接。
由于,SYN 超时需要 63 秒,那么就给攻击者一个攻击服务器的机会,攻击者在短时间内发送大量的 SYN 包给 Server(俗称 SYN flood 攻击),用于耗尽 Server 的 SYN 队列。对于应对 SYN 过多的问题,linux 提供了几个 TCP 参数:tcp_syncookies、tcp_synack_retries、tcp_max_syn_backlog、tcp_abort_on_overflow 来调整应对。
在第一次建立的TCP连接中,要是client给server发送了10个包,传递中这10个包被链路中路由器给缓存了,这个时候client挂掉
然后第二次建立TCP连接,重新发送数据包,要是数据包的序列号是<10的,就会出问题了
RFC793 中,建议 ISN 和一个假的时钟绑在一起,这个时钟会在每 4 微秒对 ISN 做加一操作,直到超过 2^32,又从 0 开始,这需要 4 小时才会产生 ISN 的回绕问题,这几乎可以保证每个新连接的 ISN 不会和旧的连接的 ISN 产生冲突。
这种递增方式的 ISN,很容易让攻击者猜测到 TCP 连接的 ISN,现在的实现大多是在一个基准值的基础上进行随机的。
Server 端的 ACK 确认包能和接下来的 FIN 包合并成一个包发送,就出现三次挥手(似乎 linux 协议栈就是这样实现的)
当 Peer 两端同时发起 SYN 来建立连接的时候,就出现了四次握手来建立连接(对于有些 TCP/IP 的实现,可能不支持这种同时打开的情况)
参考文章:https://blog.csdn.net/dog250/article/details/6612496