1.Tcp基本认识
1.1.TCP 头格式有哪些?
- 序列号:在建立连接时由计算机生成的随机数作为其初始值,通过
SYN包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题。 - 确认应答号:指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决丢包的问题。
- 控制位:
ACK:该位为 1 时,「确认应答」的字段变为有效,TCP规定除了最初建立连接时的SYN包之外该位必须设置为 1 。RST:该位为 1 时,表示TCP连接中出现异常必须强制断开连接。SYN:该位为 1 时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换FIN位为 1 的TCP段。
1.2.什么是 TCP ?
TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。
- 面向连接:一定是「一对一」才能连接,不能像 UDP 协议可以一个套接字同时向多个目标套接字发送消息。
- 可靠的:无论的网络链路中出现了怎样的链路变化,数据以有序,无重复,无丢失方式被对方接收。
- 字节流:连接上传输以字节流形式的数据。
建立一个 TCP 连接是需要客户端与服务端达成上述三个信息的共识。
- Socket:由 IP 地址和端口号组成
- 序列号:用来解决乱序问题等
- 窗口大小:用来做流量控制
1.3.TCP 连接建立
TCP 三次握手过程是怎样的?
SYN标志占据一个数据序号。Seq Num表示包内信息对应的首个数据序号。ACK代表预期接收的下个数据序号。- 第三次握手是可以携带数据的,前两次握手是不可以携带数据的。
为什么是三次握手?不是两次、四次?
1.避免历史连接
两次连接下,服务端收到SYN后,即进入ESTABLISHED状态。
我们考虑一个场景,客户端先发送了SYN(seq = 90)报文,然后客户端宕机了,而且这个SYN报文还被网络阻塞了,服务端并没有收到,接着客户端重启后,又重新向服务端建立连接,发送了SYN(seq = 100)报文。
如采用二次握手,服务端收到SYN(seq = 90)报文,即进入连接。
采用三次握手,服务端收到SYN(seq = 90)报文,回复ACK+SYN,客户端收到并分析后,反馈RST,阻止连接建立。
2.同步双方初始序列号
第一次握手丢失了,会发生什么?
SYN丢失,客户端无法收到ACK,会超时重传。
在Linux里,客户端的SYN报文最大重传次数由tcp_syn_retries内核参数控制,这个参数是可以自定义的,默认值一般是 5。
#cat/proc/sys/net/ipv4/tcp_syn_retries5第二次握手丢失了,会发生什么?
客户端由于无法收到SYN的ACK会重传SYN,重传控制参数如上所述。
服务端由于无法收到SYN的ACK也会重传SYN+ACK,重传控制参数:
#cat/proc/sys/net/ipv4/tcp_synack_retries5什么是 SYN 攻击?如何避免 SYN 攻击?
我们都知道TCP连接建立是需要三次握手,假设攻击者短时间伪造不同IP地址的SYN报文,服务端每接收到一个SYN报文,就进入SYN_RCVD状态,但服务端发送出去的ACK + SYN报文,无法得到未知IP主机的ACK应答,久而久之就会占满服务端的半连接队列,使得服务端不能为正常用户服务。
在TCP三次握手的时候,Linux内核会维护两个队列,分别是:
- 半连接队列,也称
SYN队列; - 全连接队列,也称
accept队列;
我们先来看下Linux内核的SYN队列(半连接队列)与Accpet队列(全连接队列)是如何工作的?
不管是半连接队列还是全连接队列,都有最大长度限制,超过限制时,默认情况都会丢弃报文。
SYN攻击方式最直接的表现就会把TCP半连接队列打满,这样当TCP半连接队列满了,后续再在收到SYN报文就会丢弃,导致客户端无法和服务端建立连接。
避免SYN攻击方式,可以有以下四种方法:
- 调大
netdev_max_backlog; - 增大
TCP半连接队列; - 开启
tcp_syncookies; - 减少
SYN+ACK重传次数
1.调大netdev_max_backlog
当网卡接收数据包的速度大于内核处理的速度时,会有一个队列保存这些数据包。控制该队列的最大值如下参数,默认值是1000,我们要适当调大该参数的值,比如设置为10000:
2.增大TCP半连接队列
增大 TCP 半连接队列,要同时增大下面这三个参数:
- 增大
net.ipv4.tcp_max_syn_backlog - 增大
listen()函数中的backlog - 增大
net.core.somaxconn
3.开启net.ipv4.tcp_syncookies
开启syncookies功能就可以在不使用SYN半连接队列的情况下成功建立连接,相当于绕过了SYN半连接来建立连接。
具体过程:
- 当 「
SYN队列」满之后,后续服务端收到SYN包,不会丢弃,而是根据算法,计算出一个cookie值; - 将
cookie值放到第二次握手报文的「序列号」里,然后服务端回第二次握手给客户端; - 服务端接收到客户端的应答报文时,服务端会检查这个
ACK包的合法性。如果合法,将该连接对象放入到「Accept队列」。 - 最后应用程序通过调用
accept()接口,从「Accept队列」取出的连接。
net.ipv4.tcp_syncookies参数主要有以下三个值:
0值,表示关闭该功能;1值,表示仅当SYN半连接队列放不下时,再启用它;2值,表示无条件开启功能;
那么在应对SYN攻击时,只需要设置为1即可。
$ echo1>/proc/sys/net/ipv4/tcp_syncookies4.减少SYN+ACK重传次数
当服务端受到SYN攻击时,就会有大量处于SYN_REVC状态的TCP连接,处于这个状态的TCP会重传SYN+ACK,当重传超过次数达到上限后,就会断开连接。
那么针对SYN攻击的场景,我们可以减少SYN-ACK的重传次数,以加快处于SYN_REVC状态的TCP连接断开。
SYN-ACK报文的最大重传次数由tcp_synack_retries内核参数决定(默认值是5次),比如将tcp_synack_retries减少到2次:
$ echo2>/proc/sys/net/ipv4/tcp_synack_retries1.4.TCP 连接断开
TCP 四次挥手过程是怎样的?
- 发出
FIN表示此后自身不会继续发送数据。 FIN占用一个数据序号。
第一次挥手丢失了,会发生什么?
如果第一次挥手丢失了,那么客户端迟迟收不到被动方的ACK的话,也就会触发超时重传机制,重传FIN报文,重发次数由tcp_orphan_retries参数控制。
第二次挥手丢失,即ACK丢失,同样会引发FIN的重传。
close/shutdown区别
close执行时会发出FIN,原则上套接字还可持续接收来自对端数据,但close关闭下,应用无法继续从套接字接收数据。所以FIN_WAIT2状态不可以持续太久,而tcp_fin_timeout控制了这个状态下连接的持续时长,默认值是60秒。
如果主动关闭方使用shutdown函数关闭连接,指定了只关闭发送方向,而接收方向并没有关闭,那么意味着主动关闭方还是可以接收数据的。
此时,如果主动关闭方一直没收到第三次挥手,那么主动关闭方的连接将会一直处于FIN_WAIT2状态(tcp_fin_timeout无法控制shutdown关闭的连接)。如下图:
第三次挥手丢失了,会发生什么?
服务端就会重发FIN报文,重发次数仍然由tcp_orphan_retries参数控制,这与主动关闭侧重发FIN报文的重传次数控制方式是一样的。
第四次挥手丢失了,会发生什么?
在Linux系统,TIME_WAIT状态会持续2MSL后才会进入关闭状态。
然后,服务端(被动关闭方)没有收到ACK报文前,还是处于LAST_ACK状态。
如果第四次挥手的ACK报文没有到达服务端,服务端就会重发FIN报文,重发次数仍然由前面介绍过的tcp_orphan_retries参数控制。
客户端在收到第三次挥手后,就会进入TIME_WAIT状态,开启时长为2MSL的定时器,如果途中再次收到第三次挥手(FIN报文)后,就会重置定时器,当等待2MSL时长后,客户端就会断开连接。
为什么 TIME_WAIT 等待的时间是 2MSL?
比较合理的解释是: 网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间。
为什么需要 TIME_WAIT 状态?
- 防止历史连接中的数据,被后面相同四元组的连接错误的接收;
2MSL时长,这个时间足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的。 - 保证「被动关闭连接」的一方,能被正确的关闭;
如果服务端没有收到ACK,那么就会触发TCP重传机制,服务端会重新发送一个FIN,这样一去一来刚好两个 MSL 的时间。
TIME_WAIT 过多有什么危害?
- 第一是占用系统资源,比如文件描述符、内存资源、
CPU资源、线程资源等; - 第二是占用端口资源,端口资源也是有限的,一般可以开启的端口为
32768~61000,也可以通过net.ipv4.ip_local_port_range参数指定范围。
如何优化 TIME_WAIT?
- 打开
net.ipv4.tcp_tw_reuse和net.ipv4.tcp_timestamps选项;
如下的 Linux 内核参数开启后,则可以复用处于 TIME_WAIT 的 socket 为新的连接所用。
有一点需要注意的是,tcp_tw_reuse功能只能用客户端(连接发起方),因为开启了该功能,在调用connect()函数时,内核会随机找一个time_wait状态超过1秒的连接给新的连接复用。
net.ipv4.tcp_tw_reuse=1使用这个选项,还有一个前提,需要打开对 TCP 时间戳的支持,即
net.ipv4.tcp_timestamps=1(默认即为1)这个时间戳的字段是在TCP头部的「选项」里,它由一共8个字节表示时间戳,其中第一个4字节字段用来保存发送该数据包的时间,第二个4字节字段用来保存最近一次接收对方发送到达数据的时间。
由于引入了时间戳,我们在前面提到的2MSL问题就不复存在了,因为历史数据包会因为时间戳过期被自然丢弃。
net.ipv4.tcp_max_tw_buckets
这个值默认为18000,当系统中处于TIME_WAIT的连接一旦超过这个值时,系统就会将后面的TIME_WAIT连接状态重置,这个方法比较暴力。
- 程序中使用
SO_LINGER,应用强制使用RST关闭。
我们可以通过设置socket选项,来设置调用close关闭连接行为。
structlingerso_linger;so_linger.l_onoff=1;so_linger.l_linger=0;如果l_onoff为非 0, 且l_linger值为 0,那么调用close后,会立该发送一个RST标志给对端,直接关闭。
不过这是一个非常危险的行为,不值得提倡。
服务器出现大量 TIME_WAIT 状态的原因有哪些?
HTTP没有使用长连接
关闭HTTP长连接机制后,每次请求都要经历这样的过程:建立TCP-> 请求资源 -> 响应资源 ->释放连接。
当服务端出现大量的TIME_WAIT状态连接的时候,可以排查下是否客户端和服务端都开启了HTTP Keep-Alive,因为任意一方没有开启HTTP Keep-Alive,都会导致服务端在处理完一个HTTP请求后,就主动关闭连接,此时服务端上就会出现大量的TIME_WAIT状态的连接。
HTTP长连接超时
HTTP 长连接的特点是,只要任意一端没有明确提出断开连接,则保持 TCP 连接状态。
web 服务软件一般都会提供一个参数,用来指定 HTTP 长连接的超时时间,比如 nginx 提供的 keepalive_timeout 参数。
假设设置了HTTP长连接的超时时间是60秒,nginx就会启动一个「定时器」,如果客户端在完后一个HTTP请求后,在60秒内都没有再发起新的请求,定时器的时间一到,nginx就会触发回调函数来关闭该连接,那么此时服务端上就会出现TIME_WAIT状态的连接。
2.Udp
2.1.概述
3.Udp和Tcp差异
- 连接
TCP是面向连接的传输层协议,传输数据前先要建立连接。一个套接字和另一个套接字进行双向数据收发。UDP是不需要连接,即刻传输数据。一个套接字可以向多个套接字发送数据,也能接收来自多个套接字发出的数据。
- 可靠性
TCP是可靠交付数据的,数据可以无差错、不丢失、不重复、按序到达。UDP是不可靠的传输协议,不保证可靠交付数据,数据发送之后,丢失了就丢失了。
- 拥塞控制、流量控制
Tcp支持拥塞控制,流量控制。Udp不支持。
- 首部开销
TCP首部在没有使用「选项」字段时是 20 个字节,如果使用了「选项」字段则会变长的。UDP首部只有 8 个字节,并且是固定不变的,开销较小。
- 传输方式
TCP是字节流式传输,需要应用层处理包的边界识别。UDP是一个包一个包的发送,是有边界的,但可能会丢包和乱序。
- 分片不同
TCP的数据大小如果大于MSS大小,则会在传输层进行分片,目标主机收到后,也同样在传输层组装TCP数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片。UDP的数据大小如果大于MTU大小,则会在IP层进行分片,目标主机收到后,在IP层组装完数据,接着再传给传输层。
为什么
UDP头部没有「首部长度」字段,而TCP头部有「首部长度」字段呢?
Tcp首部长度可能因为首部选项而变化,Udp首部固定。
为什么 UDP 头部有「包长度」字段,而 TCP 头部则没有「包长度」字段呢?
先说说 TCP 是如何计算负载数据长度:
其中IP总长度 和IP首部长度,在IP首部格式是已知的。TCP首部长度,则是在TCP首部格式已知的,所以就可以求得TCP数据的长度。
UDP头部的包长度可以起到头部尺寸为4字节倍数效果。