TCP

1. TCP的基本认识

1.1 头部格式

img

  • 源端口号/目标端口号:分别表示发送方和接收方

  • 序列号:用来保证传输的可靠性,原数据是随机生成的随机数,之后服务端和客户端每交互一次就叠加一次。用来解决网络收发包的乱序问题

  • 确认应答号:下一次收到数据期望的序列号,可以认为此前的序列号都传输成功,用来解决网络丢包问题

  • 首部:表示TCP报文头部长度,按比例缩小进行存储。

  • 控制位:

    • URG(Urgent,紧急标志):表示包中有需要紧急处理的数据,优先处理。
    • ACK(Acknowledgment,确认标志):表示确认数据包已经收到。
    • PSH(Push,推送标志):PSH为1,立即发送数据。PSH为0,先进行缓存。
    • RST(Reset,重置标志):RST标志用于中止连接,用于解决网络连接问题。
    • SYN(Synchronization,同步标志):这个位标志用于发起一个连接,建立连接并设置初始序列号。
    • FIN(Finish,完成标志):表示终止TCP连接,用于数据传输完毕,此段连接不会再有数据往来。
  • 窗口大小:用于流量控制,分别表表明自己能够接受的流量大小。

  • 校验和:确保数据传输没有被篡改,重新通过某种算法计算后得到的数。

  • 紧急指针:当URG标志位为1的时候才存在该字段,表示该部分数据为紧急数据。

  • 选项:用于优化TCP传输性能,对TCP功能做出解释,也是TCP总长度可变的原因。

有的文章保留字段为4比特,标志位为8比特,有的文章保留字段为6比特,标志位为6比特。

1.2 TCP的意义和作用

TCP的面向连接的,可靠的,基于字节流的传输。

ip层是不可靠的,其只负责将数据传输到对应的主机,保证数据在网络传输之间不丢失,但是能够按序到达数据内容的完整无法保证。所以tcp层的出现正是为了保证数据的安全性。

  1. 面向连接:只有建立起来连接,才能进行数据的传输。即只有一对一的连接。保证了数据之间传输的安全
  2. 可靠性:更加偏向于一个目的的描述,保证数据包发送之后能可靠的到达目的地。
  3. 基于字节流:应用程序对数据的发送和接收是没有边界限制的,为了保证其有序性,同时也可以针对其特性建立缓存区,将传输的若干数量包拼装完成后再接收。

RFC 793定义的连接:

Connections: The reliability and flow control mechanisms described above require that TCPs initialize and maintain certain status information for each data stream. The combination of this information, including sockets, sequence numbers, and window sizes, is called a connection.

从中我们可以看出一个TCP连接中主要包含着三个关键信息:

  • sockets:由ip地址和端口号组成,用于确认地址信息
  • sequence numbers:随机序列号,用来确保传输的有序和安全
  • window sizes:窗口大小,用来进行流量控制

1.3 如何确定一个TCP连接

TCP四元组可以确定一个唯一的TCP连接:

  • 源地址
  • 源端口
  • 目的地址
  • 目的端口

求一个端口能够监听的最大TCP连接数

最大TCP连接数 = 客户端IP地址数量 * 客户端端口数量

1.4 TCP和UDP的区别

img

这里要注意包长度指的是整个UDP的长度,包含数据。校验和为了防止收到损坏的数据包。

  1. 连接
  • TCP是面向连接的协议,他需要经过确定连接之后才可以进行数据的传输。
  • UDP是无连接协议,数据即刻传输
  1. 服务对象
  • TCP只支持一对一的服务,这是他面向连接的特点决定的
  • UDP支持一对一,一对多,多对多的数据传输
  1. 可靠性
  • TCP的可靠性较高,数据丢包和被篡改的风险较低下
  • UDP的可靠性较低,但其也有其改进版本增强其可靠性,如QUIC协议。
  1. 拥塞控制
  • TCP通过头部的窗口大小字段告诉传输方自己最大承受数据的能力,是有限制的。
  • UDP则因为其无连接特性,数据包可以在网络连接中停留拥塞,故可无后顾之忧的发送
  1. 头部开销
  • TCP的头部长度为20个字节加上选项
  • UDP的头部长度为8个字节,且固定
  1. 传输方式
  • TCP采用流式传输方式,保证有序及可靠
  • UDP采用数据包传输方式,是有边界的,可能会导致传输的乱序问题。
  1. 分片方式
  • TCP 的数据大小如果大于 MSS 大小,则会在传输层进行分片,目标主机收到后,也同样在传输层组装 TCP 数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片。
  • UDP 的数据大小如果大于 MTU 大小,则会在 IP 层进行分片,目标主机收到后,在 IP 层组装完数据,接着再传给传输层。
  1. 应用场景

由于 TCP 是面向连接,能保证数据的可靠性交付,因此经常用于:

  • FTP 文件传输;
  • HTTP / HTTPS;

由于 UDP 面向无连接,它可以随时发送数据,再加上 UDP 本身的处理既简单又高效,因此经常用于:

  • 包总量较少的通信,如 DNS 、SNMP 等;
  • 视频、音频等多媒体通信;
  • 广播通信;

1.5 TCP和UDP可共用一个端口号吗

可以的。

传输层端口号的作用是:用于区分同一台主机上不同应用程序的数据包

UDP和TCP是传输层的两个完全不一样的传输协议。

主机接收到数据包之后,首先会根据头部的格式判断出到底是UDP协议还是TCP协议,再交由相应的软件板块进行传输。

2. TCP的连接确立

三次握手是TCP连接最最最重要,也是最最最基础的点。但是也要记住,这个过程是数据传输的准备工作,其目的只是为客户端和服务端建立起连接,确保其能无误收发数据。

2.1 三次握手总流程图

img

  • 第一次握手:

服务端没有收到请求的时候,处于listen状态,同时的,客户端发起请求,发送标志位为1的synclient_isn,表示请求连接,客户端自身进入syn_send状态。

  • 第二次握手:

本次由服务端首先发起请求,其发送了标志位为1的ack状态码,表示对上次请求同意的回复,同时回复的递增1的序列号。至此,服务端告诉客户端自己能够收到他的信息。之后,发送标志位为1的synservice_isn用于确认客户端能否收到自己的信息,同时自身进入syn_revd状态。

  • 第三次握手:

本次握手由客户端发送,表示自己收到了服务端的请求并告知,发送了标志位为1的ack状态码,表示对上次请求同意的回复,同时回复的递增1的序列号。其后,进入Established状态,表示准备就绪,可以进行数据传输,服务端收到客户端发送的Ack,也同时进入Established状态,至此,三次握手完成。

2.2 握手的数量为3的原因

The principle reason for the three-way handshake is to prevent old duplicate connection initiations from causing confusion.

可以看到,三次握手的主要原因是为了防止旧的连接重复初始化导致连接混乱。

通过头部中的序列号和确认应答号可以校验连接双方是否昔日故人,如果是再进行数据传输。少掉任意一次连接,都会造成客户端或者服务端无法对对方身份的确认。这三次连接实质上是不可或缺的两问两答

而连接混乱造成的后果则是资源的浪费。用旧的无意义的连接传输资源,最后得到的也是无法使用的资源。

2.3 初始序列号ISN意义及如何生成

起始 ISN 是基于时钟的,每 4 微秒 + 1,转一圈要 4.55 个小时。

RFC793 提到初始化序列号 ISN 随机生成算法:ISN = M + F(localhost, localport, remotehost, remoteport)

  • M 是一个计时器,这个计时器每隔 4 微秒加 1。
  • F 是一个 Hash 算法,根据源 IP、目的 IP、源端口、目的端口生成一个随机数值。要保证 Hash 算法不能被外部轻易推算得出,用 MD5 算法是一个比较好的选择。

可以看到,随机数是会基于时钟计时器递增的,基本不可能会随机成一样的初始化序列号。

  • 意义:防止旧的报文被新的连接接收

2.4 握手报文丢失,双方怎么办

总而言之,简单来讲,超时重传,超过了一定的时间还收不到回应,那么则断开连接。

注意,重新发送的只有SYN,ACK不会重新发送

等待时间逐倍的递增。

在 Linux 下,SYN-ACK 报文的最大重传次数由 tcp_synack_retries内核参数决定,默认值是 5。

2.5 SYN攻击是什么,如何防范

在 TCP 三次握手的时候,Linux 内核会维护两个队列,分别是:

  • 半连接队列,也称 SYN 队列,对应着三次握手中的syn_revd状态;
  • 全连接队列,也称 accept 队列,对应着三次握手中的Established状态;

SYN攻击指的是对于服务器的SYN队列进行ddos攻击,发送大量的请求,这时候服务端会产生大量的ACK+SYN加以应答,但是因为客户端是不存在的,所以服务器始终等不到客户端发送过来的ACK该批请求也就无法由SYN队列进入到accept队列,随着SYN队列被占满,服务器再也无法接受请求。

避免 SYN 攻击方式,可以有以下四种方法:

  • 调大 netdev_max_backlog;
  • 增大 TCP 半连接队列;
  • 开启 tcp_syncookies;
  • 减少 SYN+ACK 重传次数

3. TCP断开连接

3.1 四次挥手总流程图

img

  • 首先,客户端服务端双方起始都是可以传输数据的Established状态,正式进入四次挥手断开连接
  • 第一次挥手:客户端发送带有FIN标志位的报文,表示想终止这次连接,之后进入FIN_WAIT1阶段。
  • *第二次挥手:*服务端收到客户端发送的FIN报文,发送带有ACK头部的确认报文给客户端,表示自己已经收到。随后进入Closed_Wait阶段,表示等待连接的关闭,客户端接收到ACK确认报文后进入FIN_WAIT2阶段
  • 第三次挥手:服务端将自己想要发送给客户端的信息处理完之后发送FIN报文,表示服务端已没有信息需要发送给客户端。随后进入Last_Ack阶段,表示等待客户端的最后一次回应。
  • *第四次挥手:*客户端接受到服务端的FIN报文后,发送ACK确认报文给服务端。之后进入为期**2MSL****TIME_WAIT**阶段,之后断开连接。服务端收到应答之后断开连接。

只有在主动发起断开连接的一方才会有time_wait阶段

3.2 服务端的SYN和ACK可否合并

在四次挥手的过程中,服务端在两个报文中间有一段closed_wait的阶段,那么该阶段可否跳过省略呢

为什么会有closed_wait的阶段

首先我们要知道服务端在closed_wait这段时间内做了什么,他主要的作用就是来处理数据,检查是否还有没发送给客户端的数据,如果有,就发送给客户端。FIN标志位的意思是所有数据发送完毕,想要断开请求,所以必须检查数据是否完毕。

他主要实现的原理是在收到客户端发送的FIN报文之后,将一个结束标识符EOF放在待处理数据的末尾,由于TCP的流传输特性,所以之后当服务端将所有数据处理完,才会读取到EOF,读取到EOF之后,才会向服务端发送FIN报文。

三次连接是否可以实现?

可以实现,条件是需要开启TCP延时确认机制并且服务端没有数据要发送

TCP延时确认机制

服务端当收到客户端发送过来的FIN信息之后,需要回复ACK报文,如果只单单传输一个20字节的报文头部不携带任何报文信息,而后续需要发送的报文又独立发送,就会造成网络资源的浪费(因为内容和头部是始终需要携带的)。那么为了提高网络资源的利用效率,TCP延时确认机制产生,它主要做了下面三件事:

  • 当有响应数据需要发送时,数据会随着ACK头部一起发送。
  • 当没有响应数据需要发送时候,服务端会等待一段时间,已确定有没有数据需要发送进而一起发送。
  • 当在等待响应数据时,若又有一个请求发送过来,那么ACK头就会立刻发送。

延迟等待时间由TCP_DELACK_MINTCP_DELACK_MAX决定。linux内核中默认为:

1
2
TCP_DELACK_MIN = HZ/5
TCP_DELACK_MIN = HZ/25

HZ与系统的时钟周期频率相关。

img

TCP延时确认机制是默认开启的

三次连接实现原理

当没有数据要发送的时候,那么服务端可以直接读取到EOF,准备发送带有FIN的报文,但是因为延时确认机制,原先的ACK报文还没有发送,那么就将FIN一起写到ACK中,之后,等待一段时间或者知道客户端再次发送请求(分别对应了其2,3情况),那么这个没有数据但是含有ACKFIN头部信息的报文就直接发送了。

3.3 挥手报文丢失,双方怎么办

总而言之,简单来讲,超时重传,超过了一定的时间还收不到回应,那么则断开连接。

注意,重新发送的只有SYN,ACK不会重新发送

等待时间逐倍的递增。

在 Linux 下,SYN-ACK 报文的最大重传次数由 tcp_synack_retries内核参数决定,默认值是 5。

close和shutdown方法的比较

两者都是用于对进程的关闭,但是shutdown关闭的是该进程的发送信息功能,而接受信息功能仍然存在,close则是两个功能都将其关闭。

在第二次挥手中,客户端shutdown了

这是一个十分有趣的情况,第二次挥手是由服务端对于客户端的FIN标志的回应,发送了ACK确认标志,如果服务端始终不发送过来第三次挥手,正常情况下客户端是需要重发FIN请求的,但是因为此时客户端的shutdown,使得其无法发送,那么最后造就的结果便是客户端一直在FIN_WAIT2这个阶段卡住,无法关闭。服务端重发请求到达上限时间次数便会自己断开连接。

3.4 TIME_WAIT状态

TIME_WAIT状态的时间是多少

2MSL(Maximum Segment Lifetime,报文最大生存时间)。非常巧合的是,我们发现,这个时间便是一次客户端和服务端之间交互一来一回的时间。

TIME_WAIT状态存在的原因

  • 确保新的相同的四元组TCP连接不要接收到旧的报文信息。

主要是为了避免第四次挥手的丢失,客户端发送完成第四次挥手ACK之后就断开连接,那么万一服务端没有收到ACK确认,又重发了一次FIN,此时客户端无法收到他的请求。此时恰好又有一个相同的四元组TCP连接建立了起来,这时候旧的FIN或者数据就会发送到新的连接。会客户端收到错误的信息或者造成新的连接无法将其信息发送完整就断开连接。

  • 保证被动关闭的一方能够***正常关闭***

还是基于第四次挥手的丢失,服务端重新发送FIN报文之后,因为客户端已经关闭,所以无法收到他的ACK报文,那么最后关闭只能是因为超时被动断开连接了。这样既浪费了服务器资源,又对于服务器是有危害的。

TIME_WAIT状态过多的危害

TIME_WAIT状态其实是一种阻塞的表现,等待至少2MSL的时间,而这个时间对端口号来说是占用的。如果网络不通畅,那么端口号资源就被占用的极多,就会无法对于服务端建立连接了。会占用系统资源,比如文件描述符、内存资源、CPU 资源、线程资源等。

如何优化 TIME_WAIT?

  • 打开 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_timestamps 选项;
  • net.ipv4.tcp_max_tw_buckets
  • 程序中使用 SO_LINGER ,应用强制使用 RST 关闭。

第一二种方法主要是通过控制TIME_WAIT的时间加以控制,第三种方法则是直接跨过了TIME_WAIT。都存在着一定上诉提到的风险。

long-alive 长连接状态

这个状态我们可以在请求报文的头部信息中看到,开启状态表示一次请求交互完成之后不立刻断开,也就表示着后面还有请求需要进行处理。那么如果每次请求都进行握手挥手网络资源消耗量极大,于是用长连接状态表示不立刻断开。如果交互确实进行完成,那么只要有一方发送断开连接请求,那么就会来到四次挥手的状态。

什么场景下服务端会主动断开连接呢?

  • 第一个场景:HTTP 没有使用长连接
  • 第二个场景:HTTP 长连接超时
  • 第三个场景:HTTP 长连接的请求数量达到上限

3.5 服务器出现大量 CLOSE_WAIT 状态的原因有哪些?

说明没有发出FIN报文,无法到达下一步LAST_ACK。那么也就是说无法调用closed方法。

通常是代码的问题,主要排查方向是为何服务端调用不到closed方法。

3.6 建立连接后一方发生故障会怎么样?

保活机制

通过开启SO_KEEPALIVE 选项生效。

在一个时间段内,如果任何连接相关的传输的活动都没有产生,那么每隔一个时间段就发送一次探测报文,如果均没有得到回应,就断定该TCP连接已经死亡。

linux中默认参数如下:

1
2
3
net.ipv4.tcp_keepalive_time=7200 //没有活动的时间长度
net.ipv4.tcp_keepalive_intvl=75 //发送探测报文的相隔时间段
net.ipv4.tcp_keepalive_probes=9 // 发送探测报文的次数

也就是说在 Linux 系统中,最少需要经过 2 小时 11 分 15 秒才可以发现一个「死亡」连接。

进程突然被closed

如前文所讲,挥手报文丢失的情况。

主机突然发生宕机

客户端主机崩溃了,服务端是无法感知到的,在保活机制时间激活之前,服务端的 TCP 连接将会一直处于 ESTABLISHED 连接状态,直到服务端重启进程。

所以,我们可以得知一个点,在双方不传输数据的情况下,一方的 TCP 连接处在 ESTABLISHED 状态,并不代表另一方的连接还一定正常。

客户端的网络连接突然断开

TCP 连接在 Linux 内核中是一个名为 struct socket 的结构体,该结构体的内容包含 TCP 连接的状态等信息。当拔掉网线的时候,操作系统并不会变更该结构体的任何内容,所以 TCP 连接的状态也不会发生改变。

换而言之,TCP连接一旦建立,就生成了两个独立的结构存储在本地,和传输层的连接关系已然不大。

之后,发送就如同进程被closed的状态,直至重新连上网络,方可恢复正常。

4. TCP的四个机制

4.1 重传机制

超时重传

顾名思义,超时重传指的是当发送方超过一定时间没有收到回复的时候,重新发送数据报文。

  • RTT(Routing-trip time)往返时延,指的是一个数据包从发送到接受到回应所需要经过的时间长度
  • **RTO (**Retransmission Timeout ) 超时发送时间,指的是超时重传的时间。

一般在超时重传机制中,RTO会略大于RTT。

RTO太大或者太小的坏处:

  • 太大浪费网络资源,网络收发包效率低下,影响用户体验。首先发送等待请求的时候,发送方就无法给其他主机发送信息,RTO过长会导致发送方资源的浪费。而且响应的时间变慢,网络传输效率低下,影响了用户的体验。
  • 太小浪费了传输的资源,增加网络阻塞。也许是回应包还没有发送到,就重新发送,这样会导致发送了两个相同的数据包,对传输资源是一种浪费。

超时重传的弊端:时间过久,影响用户体验。

快速重传

快速重传的工作原理是收到三个相同的ACK报文时,再发送一次ACK报文下一个的请求报文。

因为TCP连接中基于字节流的特性,报文的传输是有序的,因此响应的ACK所发送过来的序列号就表示该序列号之前的所有报文都已经接收完成。但是后面的响应生死未卜。所以快速重传机制会认为对方没有接收到后续的请求报文,所以重新发送一遍。

但是这种做法存在着问题,你后面的数据包有多少是没有收到的,发送方要重新发送多少数据包,这些是快速重传无法得知的,所以我们有了SACK重传机制。

SACK && D-SACK

SACK(select-acknowledge) 选择性确认,接收方发送ACK响应的时候,会告诉发送方自己接受到了哪些数据。这能够解决快速重传机制存在的问题。D-SACK(Duplicate SACK),D-SACK告诉发送方那些数据是重复发送的。

4.2 滑动窗口

TCP头部信息中的窗口大小(16位)指的就是这个窗口的大小。指的是接受数据的能力。在未接受到接受方的应答(数据还没有被处理)的时候,发送方能发送的数据总容量大小。通常该属性由接收方的窗口大小决定。

发送方的窗口大小参数

  • SND.WND:表示发送窗口的大小(大小是由接收方指定的);
  • SND.UNA(Send Unacknoleged):是一个绝对指针,它指向的是已发送但未收到确认的第一个字节的序列号,也就是 #2 的第一个字节。
  • SND.NXT:也是一个绝对指针,它指向未发送但可发送范围的第一个字节的序列号,也就是 #3 的第一个字节。
  • 指向 #4 的第一个字节是个相对指针,它需要 SND.UNA 指针加上 SND.WND 大小的偏移量,就可以指向 #4 的第一个字节了。可用窗口大小 = **SND.WND -(SND.NXT - SND.UNA)**

4.3 流量控制

TCP一般会使用滑动窗口机制来进行流量控制,简单来讲,就是接收方通过发送窗口的大小来告诉发送方,我还有多少地方可以承受你发的数据,如果太多的话,会造成丢包的情况。

丢包情况

但是在业务繁忙的时候,丢包情况也会发生。很简单的一个例子,当接收方窗口大小发生改变的时候,他会立刻发送报文告知发送方,但是发送方在接收到接收方发送的报文之前,就已经将一条数据容量超过窗口大小的数据发送给接收方,那么这时候就会产生丢包的现象。

死锁情况

这里指的是双方都无从得知对方的状态,从而进入了相对隔离的,无法更新对方信息的状态。那么造成这种结果的原因是:接收方更新窗口大小为非0的报文丢失在了网络中,这种情况下,发送方不知道对方已经处理完成信息了,会一直等待,而接收方也在一直等待发送方发送过来的信息。

  • 解决方法:

只要 TCP 连接一方收到对方的零窗口通知,就启动持续计时器。这个计时器会在没有收到窗口信息传递报文的一定时间内,发送窗口探测报文,而接收方一旦收到这个报文,就会将自己目前的窗口大小发送。通过外界的力量打破了两者之间相互隔离的状态。

糊涂窗口综合症

这里指的是发送方太过于急于求成,可以算是一种贪心算法。只要接收方告诉发送方自己目前有多少个字节的窗口,那么发送方就会毫不犹豫的发送这么多字节的包过去。

但是这样有一个缺点,万一接收方剩余的窗口很小,而TCP的头部是需要占掉很大一部分的,所以说有时候是一笔得不偿失的交易。

那么为了避免这种情况,发送方针对发送数据的大小做出了规定:

接收方采用的策略如下:

不发送小窗口给发送方

发送方通常的策略如下:

使用 Nagle 算法,该算法的思路是延时处理,只有满足下面两个条件中的任意一个条件,才可以发送数据:

  • 条件一:要等到窗口大小 >= MSS 并且 数据大小 >= MSS;
  • 条件二:收到之前发送数据的 ack 回包;

4.4 拥塞控制

在网络出现拥堵时,如果继续发送大量数据包,可能会导致数据包时延、丢失等,这时 TCP 就会重传数据,但是一重传就会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这个情况就会进入恶性循环被不断地放大….

拥塞窗口 cwnd是发送方维护的一个的状态变量,它会根据网络的拥塞程度动态变化的

拥塞窗口 cwnd 变化的规则:

  • 只要网络中没有出现拥塞,cwnd 就会增大;
  • 但网络中出现了拥塞,cwnd 就减少;

拥塞控制主要有如下四个算法:

  • 慢启动
  • 拥塞避免
  • 拥塞发生
  • 快速恢复

慢启动

慢启动的意思就是一点一点的提高发送数据包的数量,当发送方每收到一个 ACK,拥塞窗口 cwnd 的大小就会加 1。慢启动的增长为指数型增长。

有一个叫慢启动门限 ssthresh (slow start threshold)状态变量。

  • cwnd < ssthresh 时,使用慢启动算法。
  • cwnd >= ssthresh 时,就会使用「拥塞避免算法」。

一般来说 ssthresh 的大小是 65535 字节。

拥塞避免

每当收到一个 ACK 时,cwnd 增加 1/cwnd。它属于线性增长,增长较慢。

拥塞发生

就是在拥塞时,将窗口大小紧急下降。

TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,则 ssthresh 和 cwnd 变化如下:

  • cwnd = cwnd/2 ,也就是设置为原来的一半;
  • ssthresh = cwnd;
  • 进入快速恢复算法

快速恢复

进入快速恢复算法如下:

  • 拥塞窗口 cwnd = ssthresh + 3 ( 3 的意思是确认有 3 个数据包被收到了);
  • 重传丢失的数据包;
  • 如果再收到重复的 ACK,那么 cwnd 增加 1;
  • 如果收到新数据的 ACK 后,把 cwnd 设置为第一步中的 ssthresh 的值,原因是该 ACK 确认了新的数据,说明从 duplicated ACK 时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态;

5. TCP优化的连接

5.1 从三次握手的角度进行优化

因为三次握手是服务端和客户端之间建立联系的前提条件,不涉及到任何具体传输数据的传输,而只是进行了一些状态和校验的传输。因为正常情况下的用时基本可以忽略不记。所以我们可以将优化点放在具体传输的异常状态中,去通过调节参数来对三次握手进行加速。

5.1.1 超时重传的参数调节(client)

在网络状态堵塞的情况下,超时重传是不可避免的。但是超时了时候重传几次,采取什么策略。就会对于网络连接的效率起到一定的影响作用。

  • 超时重传的次数过少:可能会引发重复传输,影响网络的资源利用率。只是因为网络拥堵,数据包没有丢失只是到达的时间较慢。而在发送一次请求只会对于本就拥塞的网络进行加塞。
  • 超时重传的次数过多:可能没办法及时发现问题,让一个达不到目的地,没有接收到的连接一直占用着网络资源。导致网络资源利用率的下降。

如何调节超时重传次数:通过控制tcp_syn_retries参数来控制重传次数。在linx内核中默认值为5,每次超时重传的时间为上一次的两倍。初始值为1

5.1.2 半连接队列的参数调节(service)

半连接队列指的是服务端接受到客户端的第一次握手请求发送的时候,会将这个连接的中间态放到半连接队列中,主要用来存储准备启用但尚未启用的连接。那么对于三次握手的SYN攻击指的是通过不断发送只发送第一次请求的连接,将半连接队列占满,使得后面与服务端请求的连接无法获得通讯。那么怎么去尽量避免这种情况呢,有如下两种做法:

  • 调节syn队列的大小:通过调节somaxconn, backlog, tcp_max_syn_conn三个参数来共同调节syn队列的大小,其中,前两个参数是调整accept队列的大小,但是也能够同时调整到半连接队列的大小。
  • 通过设置synCookie参数来进行调节: 这个cookie的主要作用是存储半连接队列中连接的信息。将这个信息连同第二次握手一起发送,这样就可以替代半连接队列的作用了。但是也不能一直开启,一直开启就表示忽略半连接队列,将之前的信息全部再做一次传输,那么服务器的压力就会增大。

syn_cookie有三个值:0表示不开启,1表示当半连接队列满时开启,2表示一直开启。默认为1

5.1.3 全连接队列的参数调节(service)

tcp_abort_on_overflow 共有两个值分别是 0 和 1,其分别表示:

  • 0 :如果 accept 队列满了,那么 server 扔掉 client 发过来的 ack ;
  • 1 :如果 accept 队列满了,server 发送一个 RST 包给 client,表示废掉这个握手过程和这个连接;

accept队列的长度取决于 somaxconnbacklog 之间的最小值,也就是 min(somaxconn, backlog)

5.1.4 如何绕过三次握手

linux3.7 版本后,提供了tcp fast open的方式:

在客户端首次建立连接时的过程:

  1. 客户端发送 SYN 报文,该报文包含 Fast Open 选项,且该选项的 Cookie 为空,这表明客户端请求 Fast Open Cookie;
  2. 支持 TCP Fast Open 的服务器生成 Cookie,并将其置于 SYN-ACK 数据包中的 Fast Open 选项以发回客户端;
  3. 客户端收到 SYN-ACK 后,本地缓存 Fast Open 选项中的 Cookie。

支持 TCP Fast Open 的服务器会对收到 Cookie 进行校验:如果 Cookie 有效,服务器将在 SYN-ACK 报文中对 SYN 和「数据」进行确认,服务器随后将「数据」递送至相应的应用程序;如果 Cookie 无效,服务器将丢弃 SYN 报文中包含的「数据」,且其随后发出的 SYN-ACK 报文将只确认 SYN 的对应序列号;

节约了一个TTL的时间。

5.2 从四次挥手的角度进行优化

img

主动方的优化

主动发起 FIN 报文断开连接的一方,如果迟迟没收到对方的 ACK 回复,则会重传 FIN 报文,重传的次数由 tcp_orphan_retries 参数决定。

当主动方收到 ACK 报文后,连接就进入 FIN_WAIT2 状态,根据关闭的方式不同,优化的方式也不同:

  • 如果这是 close 函数关闭的连接,那么它就是孤儿连接。如果 tcp_fin_timeout 秒内没有收到对方的 FIN 报文,连接就直接关闭。同时,为了应对孤儿连接占用太多的资源,tcp_max_orphans 定义了最大孤儿连接的数量,超过时连接就会直接释放。
  • 反之是 shutdown 函数关闭的连接,则不受此参数限制;

当主动方接收到 FIN 报文,并返回 ACK 后,主动方的连接进入 TIME_WAIT 状态。这一状态会持续 1 分钟,为了防止 TIME_WAIT 状态占用太多的资源,tcp_max_tw_buckets 定义了最大数量,超过时连接也会直接释放。

当 TIME_WAIT 状态过多时,还可以通过设置 tcp_tw_reusetcp_timestamps 为 1 ,将 TIME_WAIT 状态的端口复用于作为客户端的新连接,注意该参数只适用于客户端。

被动方的优化

被动关闭的连接方应对非常简单,它在回复 ACK 后就进入了 CLOSE_WAIT 状态,等待进程调用 close 函数关闭连接。因此,出现大量 CLOSE_WAIT 状态的连接时,应当从应用程序中找问题。

当被动方发送 FIN 报文后,连接就进入 LAST_ACK 状态,在未等到 ACK 时,会在 tcp_orphan_retries 参数的控制下重发 FIN 报文。

5.3 传输过程中进行优化

img