TCP和UDP
TCP报文格式
TCP首部的报文格式如下:
宏观上来看如下:
此图来源于:https://zhuanlan.zhihu.com/p/144273871 知乎-腾讯技术-TCP拥塞控制详解
-
来源连接端口(16位长)-识别发送连接端口
-
目的连接端口(16位长)-识别接收连接端口
-
序列号(seq,32位长):
-
- 如果含有同步化旗标(SYN),则此为最初的序列号;第一个数据比特的序列码为本序列号加一。
- 如果没有同步化旗标(SYN),则此为第一个数据比特的序列码。
-
确认号(ack,32位长)— 期望收到的数据的开始序列号。也即已经收到的数据的字节长度加1。
-
数据偏移(4位长)— 以4字节为单位计算出的数据段开始地址的偏移值。
-
保留(3比特长)— 须置0
-
标志符(9比特长)
-
- NS—ECN-nonce。ECN显式拥塞通知(Explicit Congestion Notification)是对TCP的扩展,定义于RFC 3540(2003)。ECN允许拥塞控制的端对端通知而避免丢包。ECN为一项可选功能,如果底层网络设施支持,则可能被启用ECN的两个端点使用。在ECN成功协商的情况下,ECN感知路由器可以在IP头中设置一个标记来代替丢弃数据包,以标明阻塞即将发生。数据包的接收端回应发送端的表示,降低其传输速率,就如同在往常中检测到包丢失那样。
- CWR—Congestion Window Reduced,定义于RFC 3168(2001)。
- ECE—ECN-Echo有两种意思,取决于SYN标志的值,定义于RFC 3168(2001)。
- URG—为1表示高优先级数据包,紧急指针字段有效。
- ACK—为1表示确认号字段有效
- PSH—为1表示是带有PUSH标志的数据,指示接收方应该尽快将这个报文段交给应用层而不用等待缓冲区装满。
- RST—为1表示出现严重差错。可能需要重新创建TCP连接。还可以用于拒绝非法的报文段和拒绝连接请求。
- SYN—为1表示这是连接请求或是连接接受请求,用于创建连接和使顺序号同步
- FIN—为1表示发送方没有数据要传输了,要求释放连接。
-
窗口(WIN,16位长)—表示从确认号开始,本报文的发送方可以接收的字节数,即接收窗口大小。用于流量控制。
-
校验和(Checksum,16位长)—对整个的TCP报文段,包括TCP头部和TCP数据,以16位字进行计算所得。这是一个强制性的字段。
-
紧急指针(16位长)—本报文段中的紧急数据的最后一个字节的序号。
-
选项字段—最多40字节。每个选项的开始是1字节的kind字段,说明选项的类型。
-
-
0:选项表结束(1字节)
-
1:无操作(1字节)用于选项字段之间的字边界对齐。
-
2:最大报文段长度(4字节,Maximum Segment Size,MSS)通常在创建连接而设置SYN标志的数据包中指明这个选项,指明本端所能接收的最大长度的报文段。通常将MSS设置为(MTU-40)字节,携带TCP报文段的IP数据报的长度就不会超过MTU(MTU最大长度为1518字节,最短为64字节),从而避免本机发生IP分片。只能出现在同步报文段中,否则将被忽略。
-
3:窗口扩大因子(3字节,wscale),取值0-14。用来把TCP的窗口的值左移的位数,使窗口值乘倍。只能出现在同步报文段中,否则将被忽略。这是因为现在的TCP接收数据缓冲区(接收窗口)的长度通常大于65535字节。
-
4:sackOK—发送端支持并同意使用SACK选项。
-
5:SACK实际工作的选项。
-
8:时间戳(10字节,TCP Timestamps Option,TSopt)
-
- 发送端的时间戳(Timestamp Value field,TSval,4字节)
- 时间戳回显应答(Timestamp Echo Reply field,TSecr,4字节)
-
参考:https://baike.baidu.com/item/TCP%E6%8A%A5%E6%96%87%E6%A0%BC%E5%BC%8F/53100036?fr=aladdin
在了解 TCP 的拥塞控制之前,先来看看 TCP 的首部格式和一些基本概念。
TCP 头部标准长度是 20 字节。包含源端口、目的端口、序列号、确认号、数据偏移、保留位、控制位、窗口大小、校验和、紧急指针、选项等。
1.1 数据偏移(Data Offset)
该字段长 4 位,单位为 4 字节。表示为 TCP 首部的长度。所以 TCP 首部长度最多为 60 字节。
1.2 控制位
目前的 TCP 控制位如下,其中 CWR 和 ECE 用于拥塞控制,ACK、RST、SYN、FIN 用于连接管理及数据传输。
CWR:用于 IP 首部的 ECN 字段。ECE 为 1 时,则通知对方已将拥塞窗口缩小。
ECE:在收到数据包的 IP 首部中 ECN 为 1 时将 TCP 首部中的 ECE 设置为 1,表示从对方到这边的网络有拥塞。
URG:紧急模式
ACK:确认
PSH:推送,接收方应尽快给应用程序传送这个数据。没用到
RST:该位为 1 表示 TCP 连接中出现异常必须强制断开连接。
SYN:初始化一个连接的同步序列号
FIN:该位为 1 表示今后不会有数据发送,希望断开连接。
1.3 窗口大小(Window)
该字段长度位 16 位,即 TCP 数据包长度位 64KB。可以通过 Options 字段的 WSOPT 选项扩展到 1GB。
1.4 选项(Options)
受 Data Offset 控制,长度最大为 40 字节。一般 Option 的格式为 TLV 结构:
常见的 TCP Options 有,SACK 字段就位于该选项中。:
1.5 SACK 选项
SACK 包括了两个 TCP 选项,一个选项用于标识是否支持 SACK,是在 TCP 连接建立时发送;另一种选项则包含了具体的 SACK 信息。
- SACK_Permitted 选项,该选项只允许在 TCP 连接建立时,有 SYN 标志的包中设置,也即 TCP 握手的前两个包中,分别表示通信的两方各自是否支持 SACK。
TCP SACK-Permitted Option:
Kind: 4
Length: Variable
+----------+----------+
| Kind=4 | Length=2 |
+----------+----------+
- SACK(选择性确认) 选项位于 Options 中。该选项参数告诉对方已经接收到并缓存的不连续的数据块,发送方可根据此信息检查究竟是哪些块丢失,从而发送相应的数据块。受 TCP 包长度限制,TCP 包头最多包含四组 SACK 字段。
TCP SACK Option:
Kind: 5
Length: Variable+--------+--------+| Kind=5 | Length |+--------+--------+--------+--------+| Left Edge Of lst Block |+--------+--------+--------+--------+| Right Edge Of lst Block |+--------+--------+--------+--------+| . . . |+--------+--------+--------+--------+| Left Edge Of nth Block |+--------+--------+--------+--------+| Right Edge Of nth Block |+--------+--------+--------+--------+
- SACK 的工作原理
如下图所示, 接收方收到 500-699 的数据包,但没有收到 300-499 的数据包就会回 SACK(500-700) 给发送端,表示收到 500-699 的数据。
WireShark抓包TCP三次握手
- 第一次握手:客户端向服务端发送SYN包到服务器,并进入SYN_SNET状态,等待服务器确认。
- 第二次握手:服务器收到SYN包之后,必须先确认客户端的SYN包,同时自己也发一个SYN包,即一次发送ACK+SYN的包给到客户端,此时服务器进入SYN_RECV状态
- 第三次握手:客户端收到服务器的ACK+SYN包之后,向服务器端发送ACK包。
第三次握手完成之后,客户端和服务器端都进入ESTABLISHED状态。
为什么客户端还要发送最后一次确认呢?
主要是为了防止已经失效的连接请求报文突然又传送到了服务端,因而产生错误。
所谓「已经失效的连接请求报文」是这样产生的:客户端发送的第一个连接请求报文段没有丢失,而是在某些网络结点长时间滞留了,导致延误到连接释放以后的某个时刻才到达服务端。本来这是一个已经失效的报文,但是服务端接收到此请求报文之后,被误认为是客户端又发出一次新的连接请求,于是又向客户端发送确认报文,同意建立连接。假设没有最后一次握手,那么只要服务端发出确认,新的连接就建立了。
由于现在客户端并没有发出建立连接的请求,因此不会理睬服务端的确认,也不会向服务端发送数据。但是服务端却以为新的连接已经建立了,并一直等待客户端发来数据,服务端的许多资源就这样白白浪费了。
什么是SYN泛洪攻击
TCP SYN泛洪发生在OSI第四层,这种方式利用TCP协议的特性,就是三次握手。攻击者发送TCP SYN,SYN是TCP三次握手中的第一个数据包,而当服务器返回ACK后,该攻击者就不对其进行再确认,那这个TCP连接就处于挂起状态,也就是所谓的半连接状态,服务器收不到再确认的话,还会重复发送ACK给攻击者。这样更加会浪费服务器的资源。攻击者就对服务器发送非常大量的这种TCP连接,由于每一个都没法完成三次握手,所以在服务器上,这些TCP连接会因为挂起状态而消耗CPU和内存,最后服务器可能死机,就无法为正常用户提供服务了。
SYN泛洪攻击的防范措施
对于SYN泛洪攻击的防范,优化主机系统设置是常用的手段。
-
降低SYN timeout时间,使得主机尽快释放半连接的占用;
-
采用SYN cookie设置,如果短时间内连续收到某个IP的重复SYN请求,则认为受到了该IP的攻击,丢弃来自该IP的后续请求报文。
-
限制SYN缓存队列,是从内存资源的角度进行预防的。客户端发来的SYN请求数据都会放到SYN队列中,当客户端发来ACK确认时,数据会放到Accept队列中,大量的SYN请求会使SYN队列激增,耗尽内存,因此我们可以限制SYN队列的大小,超过门限,直接拒绝请求;缺点很明显,正常的客户端ip也会被拒之门外,但至少可以保证服务端不崩溃;
-
防护系统隔离,原理是syn报文首先由DDOS防护系统 带上特定的seq(记为cookie)来响应syn_ack,并不分配储存连接的数据区(不对SYN的信息进行任何存储)。真实的客户端会返回一个ack 为cookie+1,此时再分配内存。 而伪造的客户端,将不会作出响应。这样我们就可以知道那些IP对应的客户端是真实的,将真实客户端IP加入白名单。未及时做出响应的,会作为黑用户屏蔽一段时间;
下次访问直接通过,而其他伪造的syn报文就被拦截。由于防护系统不需要分配储存连接的数据区,以及进入SYN-RCVD状态,因此不会被击溃;
此外合理地采用防火墙等外部网络安全设施也可缓解SYN泛洪攻击。
参考:https://zhuanlan.zhihu.com/p/451051795 「SYN泛洪攻击原理及预防策略详解」
WireShark抓包TCP四次挥手
准确来说,客户端和服务端都能作为主动方去发起断开链接,也就是说active close端既可以是我们平时认为的client端也可以是我们平时认为的server端,而对端就是passive close端。所以为了方便称呼,我们下面把主动断开的一方称呼为「客户端」,被动断开的一端称为「服务端」。这里称呼的「客户端」并不是指的我们认为的浏览器端、手机端等客户端,而是主动断开的一方(active close),只是为了方便称呼描述。
-
第一次挥手:客户端进程发出释放报文,并且停止发送数据。释放数据报文首部之后,向服务端发送FIN包到服务器,并进入FIN_WAIT_1状态,即半关闭状态,等待服务器确认,但这个时候客户端仍然可以收到服务端发送过来的数据,只是停止自己这边发送给服务器数据。
-
第二次挥手:服务器收到FIN包之后,确认了客户端想要释放连接,随后服务器端会发送一个ACK包给客户端确认自己收到了此次客户端的FIN包,并随之进入了CLOSE_WAIT状态。客户端收到了服务端发来的ACK确认报文之后就会进入FIN_WAIT_2状态。
-
第三次挥手:服务器端发送ACK确认报文之后,经过CLOSE_WAIT阶段之后,做好了释放服务器端到客户端的连接准备,再次向客户端发送一个FIN报文。随后服务器端结束CLOSE_WAIT阶段,进入了LAST_ACK状态。
-
第四次挥手:客户端收到了服务器端发出的FIN报文,确认了服务器端已经做好释放连接的准备,向服务器端发送确认报文将ACK置为1之后进入TIME_WAIT阶段。
-
客户端在TIME_WAIT阶段之后经过2MSL时间段之后就进入了CLOSED阶段,服务器端在收到了客户端发送来的ACK确认报文段之后也进入了CLOSED阶段。
为什么要客户端要等待2MSL呢?
为的是
- (1)为了保证客户端发送的最后一个ACK报文段能够到达服务端。
- (2)确保由于网络堵塞等原因迟到且客户端重发的消息在本次连接中作废掉,而不会进入下一次连接中。
当客户端发出最后的ACK确认报文时,并不能确定服务器端能够收到该段报文。所以客户端在发送完ACK确认报文之后,会设置一个时长为2MSL的计时器。MSL指的是Maximum Segment Lifetime:一段TCP报文在传输过程中的最大生命周期。2MSL即是服务器端发出为FIN报文和客户端发出的ACK确认报文所能保持有效的最大时长。
服务器端在1MSL内没有收到客户端发出的ACK确认报文,就会再次向客户端发出FIN报文;
如果客户端在2MSL(Maximum Segment Lifetime 最长报文段寿命)内,再次收到了来自服务器端的FIN报文,说明服务器端由于各种原因没有接收到客户端发出的ACK确认报文。客户端再次向服务器端发出ACK确认报文,计时器重置,重新开始2MSL的计时;否则客户端在2MSL内没有再次收到来自服务器端的FIN报文,说明服务器端正常接收了ACK确认报文,客户端可以进入CLOSED阶段,完成“四次挥手”。
所以,客户端要经历时长为2SML的TIME-WAIT阶段;这也是为什么客户端比服务器端晚进入CLOSED阶段的原因。
当有大量的close_wait的时候服务端会出现什么情况?怎么去解决?
当有大量close_wait的时候,会占用服务器的fd,而一个服务器能打开的fd是有限的,如果无法分配fd了,就无法建立连接了。
那么如何去避免客户端断开异常的时候服务端close_wait呢?
我们可以针对性的去看那些没有关闭的close()的fd,比如我们mysql的连接一直没有close掉,那么我们需要在写代码的时候注意到
如果是用的Mybatis框架的时候,用的连接池,那么当一条连接用完之后就会自动返回连接池,减少连接的开启和关闭的时间。
如果用的是传统的JDBC的话,一定要记得写完后要记得调用close()方法关掉,如果客户端断开异常的时候容易造成服务端产生大量的close_wait。
参考:https://www.zhihu.com/question/298214130/answer/1090787813 「服务端close-wait或者time-wait状态过多会导致什么样的后果?」,参考了知乎博主「果冻虾仁」的回答。具体的可以点击链接访问他的论述。
TIME_WAIT会带来哪些问题呢?
TIME_WAIT 带来的问题注意是源于:一个连接进入 TIME_WAIT 状态后需要等待 2*MSL(一般是 1 到 4 分钟)那么长的时间才能断开连接释放连接占用的资源,会造成以下问题:
- 作为服务器,短时间内关闭了大量的 Client 连接,就会造成服务器上出现大量的 TIME_WAIT 连接,占据大量的 tuple,严重消耗着服务器的资源。
- 作为客户端,短时间内大量的短连接,会大量消耗的 Client 机器的端口,毕竟端口只有 65535 个,端口被耗尽了,后续就无法在发起新的连接了。
由于上面两个问题,作为客户端需要连本机的一个服务的时候,首选 UNIX 域套接字而不是 TCP。TIME_WAIT 很令人头疼,很多问题是由 TIME_WAIT 造成的,但是 TIME_WAIT 又不是多余的不能简单将 TIME_WAIT 去掉,那么怎么来解决或缓解 TIME_WAIT 问题呢?可以进行 TIME_WAIT 的快速回收和重用来缓解 TIME_WAIT 的问题。有没一些清掉 TIME_WAIT 的技巧呢?
什么是RST攻击?
RST:(Reset the connection)用于复位因某种原因引起出现的错误连接,也用来拒绝非法数据和请求。如果接收到RST位时候,通常发生了某些错误。假设有一个合法用户(1.1.1.1)已经同服务器建立了正常的连接,攻击者构造攻击的TCP数据,伪装自己的IP为1.1.1.1,并向服务器发送一个带有RST位的TCP数据段。服务器接收到这样的数据后,认为从1.1.1.1发送的连接有错误,就会清空缓冲区中建立好的连接。这时,如果合法用户1.1.1.1再发送合法数据,服务器就已经没有这样的连接了,该用户就必须重新开始建立连接。
RST攻击 这种攻击只能针对tcp,对udp无效。RST:(Reset the connection)用于复位因某种原因引起出现的错误连接,也用来拒绝非法数据和请求。如果接收到RST位时候,通常发生了某些错误。
产生原因
有四个条件可以产生RST包:
-
建立连接的SYN到达某端口,但是该端口上没有正在监听的服务
如:IP为192.168.1.33的主机上并没有开启WEB服务(端口号为0x50),这时我们通过IE去访问192.168.1.33,通过Wireshark抓包,可以看到,对此SYN包的回复为RST。说明此服务器(即IP192.168.1.33)是存在的,不过其上并没有运行WEB Server(如apache)的程序
-
向处于TIME_WAIT状态的端口发起连接请求时
当客户端向服务器的某个端口发起连接,而服务器的该端口处于TIME_WAIT状态时,客户端会收到RST段
-
异常终止连接
初始时,A和B已经建立了连接
(1) 但是A使用socket选项SO_LINGER向B发送RST段,此时A缓冲区中所有排队等待的数据将都会丢弃(详细请看socket套接字选项详解的SO_LINGER)
(2) B收到A发送来的RST段后,将异常的断开A和B之间的连接 -
处理半打开状态
(1) A关闭或者异常终止了连接,但是B没有收到FIN段(比如网络发生了故障)。此时AB双方还维持着原来的连接,而A即使重启,也没有该连接的任何信息,将这种状态称为半打开状态。处于半打开状态的连接称为半打开连接。
(2) B向A中写入数据,则A将回复RST段给B
总结一句话:向处于半打开状态的连接写入数据,则对方将回应一个RST段。
似乎挺恐怖,然而关键是。怎样能伪造成A发给B的包呢?这里有两个关键因素。源port和序列号。
一个TCP连接都是四元组。由源IP、源port、目标IP、目标port唯一确定一个连接。所以。假设C要伪造A发给B的包。要在上面提到的IP头和TCP头。把源IP、源port、目标IP、目标port都填对。这里B作为server,IP和port是公开的,A是我们要下手的目标。IP当然知道,但A的源port就不清楚了,由于这可能是A随机生成的。
当然。假设可以对常见的OS如windows和linux找出生成source port规律的话,还是可以搞定的。
序列号问题是与滑动窗体相应的,伪造的TCP包里须要填序列号,假设序列号的值不在A之前向B发送时B的滑动窗体内,B是会主动丢弃的。所以我们要找到能落到当时的AB间滑动窗体的序列号。这个能够暴力解决,由于一个sequence长度是32位,取值范围0-4294967296。假设窗体大小像上图中我抓到的windows下的65535的话,仅仅须要相除,就知道最多仅仅须要发65537(4294967296/65535=65537)个包就能有一个序列号落到滑动窗体内。RST包是非常小的,IP头+TCP头也才40字节,算算我们的带宽就知道这实在仅仅须要几秒钟就能搞定。
对付这种攻击也可以通过防火墙简单设置就可以了。建议使用防火墙将进来的包带RST位的包丢弃就可以了。
参考:https://baike.baidu.com/item/RST%E6%94%BB%E5%87%BB/5791572?fr=aladdin
参考:https://blog.csdn.net/weixin_36750623/article/details/84953067
TCP和UDP的差异点:
TCP的主要特点:
- TCP是面向连接的传输层协议。
- 每一条TCP连接只能有两个端点,每一条TCP连接都只能是点对点的(一对一)
- TCP提供可靠交付的服务。通过TCP连接传送的数据无差错、不丢失、不重复,且按序到达。
- TCP提供全双工通信。TCP允许通信双方的应用进程在任何时候都能发送数据。
- TCP是面向字节流的。TCP中的“流”指的是流入到进程或者从进程中流出的字节序列。
UCP的主要特点:
- UDP是无连接的,即发送数据之前是不建立连接的。
- UCP不保证可靠交付。
- UDP是面向报文的。
- UDP没有拥塞控制
- UCP支持一对一、一对多、多对一和多对多的交互通信。
- UDP首部的开销小,只有8个字节。比TCP的20个字节的首部要短。
四次挥手能不能变成三次挥手呢?
答案是可能的。TCP 是全双工通信,Cliet 在自己已经不会在有新的数据要发送给 Server 后,可以发送 FIN 信号告知 Server,这边已经终止 Client 到对端 Server 那边的数据传输。但是,这个时候对端 Server 可以继续往 Client 这边发送数据包。于是,两端数据传输的终止在时序上是独立并且可能会相隔比较长的时间,这个时候就必须最少需要 2+2= 4 次挥手来完全终止这个连接。但是,如果 Server 在收到 Client 的 FIN 包后,在也没数据需要发送给 Client 了,那么对 Client 的 ACK 包和 Server 自己的 FIN 包就可以合并成为一个包发送过去,这样四次挥手就可以变成三次了(似乎 linux 协议栈就是这样实现的)
Peer 两端,哪一端会进入 TIME_WAIT 呢?为什么?
相信大家都知道,TCP 主动关闭连接的那一方会最后进入 TIME_WAIT。那么怎么界定主动关闭方呢?是否主动关闭是由 FIN 包的先后决定的,就是在自己没收到对端 Peer 的 FIN 包之前自己发出了 FIN 包,那么自己就是主动关闭连接的那一方。对于疑症(4)中描述的情况,那么 Peer 两边都是主动关闭的一方,两边都会进入 TIME_WAIT。为什么是主动关闭的一方进行 TIME_WAIT 呢,被动关闭的进入 TIME_WAIT 可以不呢?我们来看看 TCP 四次挥手可以简单分为下面三个过程:
- 过程一.主动关闭方发送 FIN;
- 过程二.被动关闭方收到主动关闭方的 FIN 后发送该 FIN 的 ACK,被动关闭方发送 FIN;
- 过程三.主动关闭方收到被动关闭方的 FIN 后发送该 FIN 的 ACK,被动关闭方等待自己 FIN 的 ACK。
问题就在过程三中,据 TCP 协议规范,不对 ACK 进行 ACK,如果主动关闭方不进入 TIME_WAIT,那么主动关闭方在发送完 ACK 就走了的话,如果最后发送的 ACK 在路由过程中丢掉了,最后没能到被动关闭方,这个时候被动关闭方没收到自己 FIN 的 ACK 就不能关闭连接,接着被动关闭方会超时重发 FIN 包,但是这个时候已经没有对端会给该 FIN 回 ACK,被动关闭方就无法正常关闭连接了,所以主动关闭方需要进入 TIME_WAIT 以便能够重发丢掉的被动关闭方 FIN 的 ACK。
TIME_WAIT 状态是用来解决或避免什么问题呢?
TIME_WAIT 主要是用来解决以下几个问题:
1)上面解释为什么主动关闭方需要进入 TIME_WAIT 状态中提到的: 主动关闭方需要进入 TIME_WAIT 以便能够重发丢掉的被动关闭方 FIN 包的 ACK。如果主动关闭方不进入 TIME_WAIT,那么在主动关闭方对被动关闭方 FIN 包的 ACK 丢失了的时候,被动关闭方由于没收到自己 FIN 的 ACK,会进行重传 FIN 包,这个 FIN 包到主动关闭方后,由于这个连接已经不存在于主动关闭方了,这个时候主动关闭方无法识别这个 FIN 包,协议栈会认为对方疯了,都还没建立连接你给我来个 FIN 包?,于是回复一个 RST 包给被动关闭方,被动关闭方就会收到一个错误(我们见的比较多的:connect reset by peer,这里顺便说下 Broken pipe,在收到 RST 包的时候,还往这个连接写数据,就会收到 Broken pipe 错误了),原本应该正常关闭的连接,给我来个错误,很难让人接受。
2)防止已经断开的连接 1 中在链路中残留的 FIN 包终止掉新的连接 2(重用了连接 1 的所有的 5 元素(源 IP,目的 IP,TCP,源端口,目的端口)),这个概率比较低,因为涉及到一个匹配问题,迟到的 FIN 分段的序列号必须落在连接 2 的一方的期望序列号范围之内,虽然概率低,但是确实可能发生,因为初始序列号都是随机产生的,并且这个序列号是 32 位的,会回绕。
3)防止链路上已经关闭的连接的残余数据包(a lost duplicate packet or a wandering duplicate packet) 干扰正常的数据包,造成数据流的不正常。这个问题和 2)类似。
清掉 TIME_WAIT 的奇技怪巧
可以用下面两种方式控制服务器的 TIME_WAIT 数量:
【1】修改 tcp_max_tw_buckets tcp_max_tw_buckets 控制并发的 TIME_WAIT 的数量,默认值是 180000。如果超过默认值,内核会把多的 TIME_WAIT 连接清掉,然后在日志里打一个警告。官网文档说这个选项只是为了阻止一些简单的 DoS 攻击,平常不要人为的降低它。
【2】利用 RST 包从外部清掉 TIME_WAIT 链接 根据 TCP 规范,收到任何的发送到未侦听端口、已经关闭的连接的数据包、连接处于任何非同步状态(LISTEN,SYS-SENT,SYN-RECEIVED)并且收到的包的 ACK 在窗口外,或者安全层不匹配,都要回执以 RST 响应(而收到滑动窗口外的序列号的数据包,都要丢弃这个数据包,并回复一个 ACK 包),内核收到 RST 将会产生一个错误并终止该连接。我们可以利用 RST 包来终止掉处于 TIME_WAIT 状态的连接,其实这就是所谓的 RST 攻击了。
TCP 的延迟确认机制
按照 TCP 协议,确认机制是累积的,也就是确认号 X 的确认指示的是所有 X 之前但不包括 X 的数据已经收到了。确认号(ACK)本身就是不含数据的分段,因此大量的确认号消耗了大量的带宽,虽然大多数情况下,ACK 还是可以和数据一起捎带传输的,但是如果没有捎带传输,那么就只能单独回来一个 ACK,如果这样的分段太多,网络的利用率就会下降。为缓解这个问题,RFC 建议了一种延迟的 ACK,也就是说,ACK 在收到数据后并不马上回复,而是延迟一段可以接受的时间,延迟一段时间的目的是看能不能和接收方要发给发送方的数据一起回去,因为 TCP 协议头中总是包含确认号的,如果能的话,就将数据一起捎带回去,这样网络利用率就提高了。
延迟 ACK 就算没有数据捎带,那么如果收到了按序的两个包,那么只要对第二包做确认即可,这样也能省去一个 ACK 消耗。由于 TCP 协议不对 ACK 进行 ACK 的,RFC 建议最多等待 2 个包的积累确认,这样能够及时通知对端 Peer,我这边的接收情况。Linux 实现中,有延迟 ACK 和快速 ACK,并根据当前的包的收发情况来在这两种 ACK 中切换。一般情况下,ACK 并不会对网络性能有太大的影响,延迟 ACK 能减少发送的分段从而节省了带宽,而快速 ACK 能及时通知发送方丢包,避免滑动窗口停等,提升吞吐率。关于 ACK 分段,有个细节需要说明一下,ACK 的确认号,是确认按序收到的最后一个字节序,对于乱序到来的 TCP 分段,接收端会回复相同的 ACK 分段,只确认按序到达的最后一个 TCP 分段。TCP 连接的延迟确认时间一般初始化为最小值 40ms,随后根据连接的重传超时时间(RTO)、上次收到数据包与本次接收数据包的时间间隔等参数进行不断调整。
TCP 拥塞控制
谈到拥塞控制,就要先谈谈拥塞的因素和本质。
本质上,网络上拥塞的原因就是大家都想独享整个网络资源,对于 TCP,端到端的流量控制必然会导致网络拥堵。这是因为 TCP 只看到对端的接收空间的大小,而无法知道链路上的容量,只要双方的处理能力很强,那么就可以以很大的速率发包,于是链路很快出现拥堵,进而引起大量的丢包,丢包又引发发送端的重传风暴,进一步加剧链路的拥塞。
另外一个拥塞的因素是链路上的转发节点,例如路由器,再好的路由器只要接入网络,总是会拉低网络的总带宽,如果在路由器节点上出现处理瓶颈,那么就很容易出现拥塞。由于 TCP 看不到网络的状况,那么拥塞控制是必须的并且需要采用试探性的方式来控制拥塞,于是拥塞控制要完成两个任务:[1]公平性;[2]拥塞过后的恢复。
TCP 发展到现在,拥塞控制方面的算法很多,其中 Reno 是目前应用最广泛且较为成熟的算法,下面着重介绍一下 Reno 算法(RFC5681)。介绍该算法前,首先介绍一个概念 duplicate acknowledgment(冗余 ACK、重复 ACK)一般情况下一个 ACK 被称为冗余 ACK,要同时满足下面几个条件(对于 SACK,那么根据 SACK 的一些信息来进一步判断)。 [1] 接收 ACK 的那端已经发出了一些还没被 ACK 的数据包; [2] 该 ACK 没有捎带 data; [3] 该 ACK 的 SYN 和 FIN 位都是 off 的,也就是既不是 SYN 包的 ACK 也不是 FIN 包的 ACK; [4] 该 ACK 的确认号等于接收 ACK 那端已经收到的 ACK 的最大确认号; [5] 该 ACK 通知的窗口等接收该 ACK 的那端上一个收到的 ACK 的窗口。
Reno 算法包含 4 个部分:
- [1]慢热启动算法 – Slow Start;
- [2]拥塞避免算法 – Congestion Avoidance;
- [3]快速重传 - Fast Retransimit;
- [4]快速恢复算法 – Fast Recovery。
TCP 的拥塞控制主要原理依赖于一个拥塞窗口(cwnd)来控制,根据前面的讨论,我们知道有一个接收端通告的接收窗口(rwnd)用于流量控制;加上拥塞控制后,发送端真正的发送窗口=min(rwnd,cwnd)。关于 cwnd 的单位,在 TCP 中是以字节来做单位的,我们假设 TCP 每次传输都是按照 MSS 大小来发送数据,因此你可以认为 cwnd 按照数据包个数来做单位也可以理解,下面如果没有特别说明是字节,那么 cwnd 增加 1 也就是相当于字节数增加 1 个 MSS 大小。
【1】慢热启动算法 – Slow Start
慢启动体现了一个试探的过程,刚接入网络的时候先发包慢点,探测一下网络情况,然后在慢慢提速。不要一上来就拼命发包,这样很容易造成链路的拥堵,出现拥堵了在想到要降速来缓解拥堵这就有点成本高了,毕竟无数的先例告诫我们先污染后治理的成本是很高的。慢启动的算法如下(cwnd 全称 Congestion Window):
- 1)连接建好的开始先初始化 cwnd = N,表明可以传 N 个 MSS 大小的数据;
- 2)每当收到一个 ACK,++cwnd; 呈线性上升;
- 3)每当过了一个 RTT,cwnd = cwnd*2; 呈指数让升;
- 4)还有一个慢启动门限 ssthresh(slow start threshold),是一个上限,当 cwnd >= ssthresh 时,就会进入"拥塞避免算法 - Congestion Avoidance"。
根据 RFC5681,如果 MSS > 2190 bytes,则 N = 2;如果 MSS < 1095 bytes,则 N =4;如果 2190 bytes >= MSS >= 1095 bytes,则 N = 3;一篇 Google 的论文《An Argument for Increasing TCP’s Initial Congestion Window》建议把 cwnd 初始化成了 10 个 MSS。Linux 3.0 后采用了这篇论文的建议。
【2】拥塞避免算法 – Congestion Avoidance
慢启动的时候说过,cwnd 是指数快速增长的,但是增长是有个门限 ssthresh(一般来说大多数的实现 ssthresh 的值是 65535 字节)的,到达门限后进入拥塞避免阶段。在进入拥塞避免阶段后,cwnd 值变化算法如下:
- 1)每收到一个 ACK,调整 cwnd 为 (cwnd + 1/cwnd) * MSS 个字节;
- 2)每经过一个 RTT 的时长,cwnd 增加 1 个 MSS 大小,即cwnd++;
TCP 是看不到网络的整体状况的,那么 TCP 认为网络拥塞的主要依据是它重传了报文段。前面我们说过 TCP 的重传分两种情况:
1)出现 RTO 超时,重传数据包。这种情况下,TCP 就认为出现拥塞的可能性就很大,于是它反应非常’强烈’ [1] 调整门限 ssthresh 的值为当前 cwnd 值的 1/2; [2] reset 自己的 cwnd 值为 1; [3] 然后重新进入慢启动过程。
2)在 RTO 超时前,收到 3 个 duplicate ACK 进行重传数据包。这种情况下,收到 3 个冗余 ACK 后说明确实有中间的分段丢失,然而后面的分段确实到达了接收端,因为这样才会发送冗余 ACK,这一般是路由器故障或者轻度拥塞或者其它不太严重的原因引起的,因此此时拥塞窗口缩小的幅度就不能太大,此时进入快速重传。
【3】快速重传 - Fast Retransimit 做的事情有:
快重传使用了冗余ACK来检测丢包的发生,当发送方连续收到三个重复的ACK报文时,直接重传对方尚未收到的报文段,而不必等待那个报文段设置的重传计时器超时
-
1) 调整门限 ssthresh 的值为当前 cwnd 值的 1/2;
-
2) 将 cwnd 值设置为新的 ssthresh 的值;
-
3) 重新进入拥塞避免阶段。
在快速重传的时候,一般网络只是轻微拥堵,在进入拥塞避免后,cwnd 恢复的比较慢。针对这个,“快速恢复”算法被添加进来,当收到 3 个冗余 ACK 时,TCP 最后的[3]步骤进入的不是拥塞避免阶段,而是快速恢复阶段。
【4】快速恢复算法 – Fast Recovery :
快速恢复的思想是“数据包守恒”原则,即带宽不变的情况下,在网络同一时刻能容纳数据包数量是恒定的。当“老”数据包离开了网络后,就能向网络中发送一个“新”的数据包。既然已经收到了 3 个冗余 ACK,说明有三个数据分段已经到达了接收端,既然三个分段已经离开了网络,那么就是说可以在发送 3 个分段了。于是只要发送方收到一个冗余的 ACK,于是 cwnd 加 1 个 MSS。快速恢复步骤如下(在进入快速恢复前,cwnd 和 sshthresh 已被更新为:sshthresh = cwnd /2,cwnd = sshthresh):
- 1)把 cwnd 设置为 ssthresh 的值加 3,重传 Duplicated ACKs 指定的数据包;
- 2)如果再收到 duplicated Acks,那么 cwnd = cwnd +1;
- 3)如果收到新的 ACK,而非 duplicated Ack,那么将 cwnd 重新设置为【3】中 1)的 sshthresh 的值。然后进入拥塞避免状态。
细心的同学可能会发现快速恢复有个比较明显的缺陷就是:它依赖于 3 个冗余 ACK,并假定很多情况下,3 个冗余的 ACK 只代表丢失一个包。但是 3 个冗余 ACK 也很有可能是丢失了很多个包,快速恢复只是重传了一个包,然后其他丢失的包就只能等待到 RTO 超时了。超时会导致 ssthresh 减半,并且退出了 Fast Recovery 阶段,多个超时会导致 TCP 传输速率呈级数下降。出现这个问题的主要原因是过早退出了 Fast Recovery 阶段。
为解决这个问题,提出了 New Reno 算法,该算法是在没有 SACK 的支持下改进 Fast Recovery 算法(SACK 改变 TCP 的确认机制,把乱序等信息会全部告诉对方,SACK 本身携带的信息就可以使得发送方有足够的信息来知道需要重传哪些包,而不需要重传哪些包),具体改进如下:
- 1)发送端收到 3 个冗余 ACK 后,重传冗余 ACK 指示可能丢失的那个包 segment1,如果 segment1 的 ACK 通告接收端已经收到发送端的全部已经发出的数据的话,那么就是只丢失一个包,如果没有,那么就是有多个包丢失了;
- 2)发送端根据 segment1 的 ACK 判断出有多个包丢失,那么发送端继续重传窗口内未被 ACK 的第一个包,直到 sliding window 内发出去的包全被 ACK 了,才真正退出 Fast Recovery 阶段。
我们可以看到,拥塞控制在拥塞避免阶段,cwnd 是加性增加的,在判断出现拥塞的时候采取的是指数递减。为什么要这样做呢?这是出于公平性的原则,拥塞窗口的增加受惠的只是自己,而拥塞窗口减少受益的是大家。这种指数递减的方式实现了公平性,一旦出现丢包,那么立即减半退避,可以给其他新建的连接腾出足够的带宽空间,从而保证整个的公平性。
至此,TCP 的疑难杂症基本介绍完毕了,总的来说 TCP 是一个有连接的、可靠的、带流量控制和拥塞控制的端到端的协议。TCP 的发送端能发多少数据,由发送端的发送窗口决定(当然发送窗口又被接收端的接收窗口、发送端的拥塞窗口限制)的,那么一个 TCP 连接的传输稳定状态应该体现在发送端的发送窗口的稳定状态上,这样的话,TCP 的发送窗口有哪些稳定状态呢?TCP 的发送窗口稳定状态主要有上面三种稳定状态:
参考:https://zhuanlan.zhihu.com/p/199284611 腾讯技术工程「彻底搞懂TCP协议:从 TCP 三次握手四次挥手说起」