网络连接状态

网络连接状态非常重要这里既包含三次握手中的也包括四次断开中的,所以要熟悉。

状态
说明
LISTEN
首先服务器需要打开一个socket进行监听,监听来自远方TCP端口的连接请求
SYN_SENT
表示主动连接,客户端能通过应用程序调用connect()函数进行active open。于是客户端TCP发送一个SYN以请求建立一个连接,之后状态为SYN_SEND,表示已发送一个SYN到服务器端,等待SYN 1+ACK 0响应。
SYN_RECV
服务器端收到客户端的SYN 1+ACK 0,然后状态变为SYN_RECV。表示服务器收到了客户端发来的SYN,然后自己也响应了给客户端一个SYN 1+ACK 1,然后等待客户端确认。 这时候客户端过来的连接(属于半连接状态)被放在一个SYN队列里面,SYN泛洪攻击也是这样的,就是服务器响应了SYN+ACK之后,客户端就不在发送ACK了,然后继续发送SYN,直到把服务器的最大连接数量耗尽。 半连接队列长度是由内核参数tcp_max_syn_backlog来决定的。
ESTABLISHED
代表一个打开的连接,客户端收到服务器发送的SYN 1+ACK 1,就变为这个状态,然后向服务器发送ACK,如果服务器收到这个ACK,那么它也变为这个状态。这个状态就是表示连接以及建立,正在或即将传输数据。服务器收到ACK以后就会把半连接从上面提到的SYN队列中删除,然后放到ACCEPT队列中,这时这个半连接的状态就变成了ESTABLISHED。
FIN_WAIT1
主动关闭端(可以是服务器也可以是客户端)应用程序调用了close,于是其TCP发出FIN主动关闭请求,也就是四次断开的第一次,之后就进入了FIN_WAIT1状态,等待远程主机的ACK请求。
CLOSE_WAIT

被动关闭端(可以是服务器有可以是客户端)收到了对方发来的FIN后,进入该状态,然后发出ACK+1以回应FIN请求(它的接收也作为文件结束符传递给上层应用程序)。这个状态实际上是说客户端告诉服务器我没有请求或者数据要发送了,等待看看服务器或者说是进程还有没有数据要发送,如果有则继续发送,如果没有的话,就发送反向关闭指令。

如果服务器大量连接是这个状态就要去查看程序,很有可能是程序设计的问题。

FIN_WAIT2
主动关闭端收到ACK+1后,就进入的FIN_WAIT2状态,也就等服务器是否还有数据发来,如果服务器没有数据了,那么服务器就发送的反向关闭指令。也就是反向关闭连接指令FIN1+ACK1。实际上是告诉客户端我的数据发送完了,可以关闭连接了。
LAST_ACK
被动关闭端,发送反向结束连接请求FIN 1+ACK 1,然后进入LAST_ACK状态,等待主动关闭端发送ACK。
TIME_WAIT

主动关闭端收到FIN 1 +ACK 1后,并进入TIME_WAIT状态,然后发送ACK+1,等待一段时间(2MSL)以确保服务器收到了ACK+1,然后自己进入CLOSED状态。这个阶段主要是客户端为了再次确认一下服务器是否可以关闭连接,因为网络毕竟是不可靠的。

对于服务器有大量TIME_WAIT这个问题通常调整sysctl来解决。

CLOSING
比较少见,表示等待远程TCP对连接中断的确认。
CLOSED

被动关闭端在收到ACK包以后,就进入closed状态,连接结束。

UNKNOWN
未知的Socket状态


三次握手过程

TCP/IP之(二)三次握手_半连接

客户端:发送SYN=1请求建立连接,此时客户端进入SYN_SENT状态等待服务器响应

服务器:收到客户端的SYN=1后发送ACK=1SYN=1表示收到建立连接请求,然后自己进入SYN_RECV状态进行等待客户端的最后确认

客户端:收到服务器发来的SYN=1ACK=1然后发送ACK=1表示收到之前确认,然后自己进入ESTABLISHED状态表示自己处于连接建立状态

服务器:收到客户端的ACK以后则自己进入ESTABLISHED状态,此时双方都处于连接建立状态,之后进行数据传送。


三次握手的目的是为了告诉对方SEQ然后服务器回复SEQ+1,这样发送端就知道包没有丢;另外握手的目的是交换信息,比如:

MSS:最大传输包(不含TCP/IP头),MMS+包头就是MTU,如果MTU过大传输就会卡死。

SACK_PERM:是否支持Selective ack(用户优化重传效率),比如客户端发送5个包给服务器,中途丢了2号包,服务器回复的时候只能回复2,表示2号前面的都收到了,请求重传2号包,可是客户端并不知道2后面的345是否收到没有,如果支持SACK的话,那么服务器请求重传2的时候就可以同时告诉345已经收到,这样客户端只需要重传2,如果没有SACK机制,那么客户端就会重传2345,这样效率就低了。

WS:窗口计算指数


半连接和全连接

TCP/IP之(二)三次握手_半连接_02

未完成连接队列:客户端发送SYN到服务器,服务器正在等待完成三次握手,此时就会把客户端发起的这个连接请求放在该队列里,也就是sync队列。这个队列由net.ipv4.tcp_max_syn_backlog参数决定, 系统默认2048,服务器端口状态为 SYNC_RCVD。

cat /proc/sys/net/ipv4/tcp_max_syn_backlog

TCP/IP之(二)三次握手_内核参数_03


已完成连接队列已经完成握手的连接从SYN队列移动到这个队列,也就是accept队列,默认128(其实这个队列最终的大小是由SOMAXCONN和使用listen函数传入参数的两者取最小值决定的),服务器端口状态为ESTABLISHED,在Linux内核2.4.25之后在/etc/sysctl.conf中net.core.somaxconn = 128直接修改。

cat /proc/sys/net/core/somaxconn

TCP/IP之(二)三次握手_半连接_04


TCP的三次握手第一步服务器收到客户端的SYN后,把该请求放在半连接队列中,之后回复SYN+ACK,当客户端收到这个信号并发送ACK之后并且服务器正常收到和处理后就把该请求从半连接队列移动到ACCEPT队列,进入这个队列才能从Listen变成accept。

比如syn泛洪攻击就是针对syn队列的,攻击方不同的建立连接,但是只做连接的第一步,当攻击者收到SYN+ACK后直接丢弃,导致受攻击的服务器上这个队列满了然后其他正常请求就无法进入。


常见问题:客户端在发送完最后一个ACK之后服务器端如果收到正常情况下应该把该链接从SYNC队列移动到ACCEPT队列,如果ACCEPT队列满了,默认服务器丢弃不会响应,所以从客户端角度来看三次握手已经完成,但服务器没有响应这个链接,这种情况经常出现在服务器同时收到很多链接请求的时候。如何确定这个问题?使用如下命令:

netstat -s | egrep "listen|LISTEN"

如果出现

xxxxx times the listen queue of a socket overflowed(全连接队列溢出次数)

xxxxx SYNs to LISTEN sockets ignored (半连接队列溢出次数)

TCP/IP之(二)三次握手_半连接_05

这两个值有时你会看到一样多,但是通常半连接溢出次数会大于等于全连接溢出次数。就说明可能会有这个问题。因为如果这个数值一直在增加那么就要注意了。如果想再次确认,那么你需要修改内核参数

echo '1' > /proc/sys/net/ipv4/tcp_abort_on_overflow

TCP/IP之(二)三次握手_内核参数_06

该参数默认为0,参数含义看后面。修改之后客户端再次发起连接就会收到reset信号,如果抓包收到这个信号,就证明服务器端的accept队列满了,你需要进行调整。比如JAVA中默认socket的backlog值大小是50.

ss -lnt

TCP/IP之(二)三次握手_半连接_07

Send-Q:表示LISTEN端口上的全连接队列最大为多少

Recv-Q:为全连接队列当前使用了多少

全连接队列大小取决于:min(backlog, somaxconn),前一个是在socket创建时传入的(listen函数),somaxconn是OS级别的参数,这个somaxconn的含义请查看后面的内涵参数说明

半连接队列大小取决于:/proc/sys/net/ipv4/tcp_max_syn_backlog 这个内核参数

Nginx默认的accept队列是511,而且是多个进程同时监听一个端口;Tomcat的accept队列是100,默认短连接。

# 查看Accept队列溢出情况,如果当前没有溢出则没有任何返回值
netstat -s | grep TCPBacklogDrop


思考:

如果客户端发出ACK之后刚好服务器ACCEPT队列满了,也就是客户端认为连接成功建立而实际上服务器端连接没有准备好,而这时客户端认为建立好了而强行发送数据会怎么办呢?客户端发送之后肯定会得不到响应,因为服务器丢弃了,然后客户端认为丢失所以进行重传,一定次数之后客户端认为异常,然后一直到超时最后断开。


关于Backlog

TCP连接客户端connect()返回并不代表TCP连接成功,有可能是服务器接收队列满了,系统会丢弃后续的ACK请求,
客户端以为建立了连接,然后就执行后续操作,然后就等待到超时。服务器则会等待ACK超时,会重传SYN。

TCP队列的一些问题

  1. 客户端通过connect向服务器发出SYN包,客户端会维护一个socket等待队列,而服务器则会维护一个SYN队列

  2. 此时是半连接状态,如果socket等待队列满了,服务器则会丢弃,而客户端会返回超时。只要客户端没有收到SYN+ACK,3秒后客户端会再次发送,然后依然没有收到,9秒后再继续发送。

  3. 半连接SYN队列长度由tcp_max_syn_backlog决定

  4. 当服务器收到客户端SYN后,会返回SYN+ACK包,客户端的TCP协议栈会唤醒socket等待队列,发出connect调用

  5. 客户端返回ACK后,服务器会进入一个新的叫做accept的队列,这个队列长度为min(backlog,somaxconn)默认情况下somaxconn是128,表示最多有129的ESTAB的连接等待accept(),而backlog的值由int listen(int sockfd,int backlog)中的第二个参数指定,其含义是设置listen()函数最多允许多个网络连接同时处于挂起状态,大部分平台都是511

  6. 当accept队列满了之后,即是客户端继续向服务器发送ACK包,也不会得到响应,此时服务器通过tcp_abort_on_overflow来决定如何返回,0表示直接丢弃,1表示发送RST通知;客户端则会分别返回read timeout或者connection reset by peer。从上面可以看到有2个队列,一个保存SYN_SEND以及SYN_RECV,另外一个accept队列保存ESTAB的状态。

比如客户端通Nginx通信,Nginx立即返回ACK,但是3秒后才返回响应数据,Nginx同后端通信,发送SYN请求等待3秒后端才响应,就可能是backlog值设置过小,导致accept queue溢出,SYN被丢弃导致3s重传。


关于ss命令中Recv-Q和Send-Q的含义?

这两个指标在不同场景含义不同。一个是状态处于LISTEN状态、一个是非LISTEN的其他状态。

LISTEN状态

TCP/IP之(二)三次握手_半连接_08

这里的含义就是上面说的Recv-Q是当前全连接队列使用量;Send-Q是当前对应进程SOCKET套接字最大blacklog的数量,也就是全连接队列最大长度

非LISTEN状态

TCP/IP之(二)三次握手_半连接_09

Recv-Q:数据已经接收到本地缓存,还有多少没有被程序取走,单位bytes

Send-Q:要发送的数据有多少还在本地缓冲区对方未确认,如果不是0可能是本地发送数据过快或者对方接收数据过慢,单位bytes

上述两个值在非LISTEN状态下都应该保持0或者瞬间不为0,如果长期不为0则可能有问题。


# 统计各种状态的值
netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
# 统计特定进程的TCP状态
netstat -ntap | grep '3141' | awk '{++S[$6]} END {for(a in S) print a, S[a]}'

TCP/IP之(二)三次握手_内核参数_10


Linux内核中的TCP/IP参数

tcp_abort_on_overflow 默认为0

TCP全连接队列也就是accept队列满了之后如何处理,默认是0,也就是丢弃,可以改为1,表示如果队列满了这时候有客户端建立连接则发送一个reset包给客户端,表示废除这个握手。


net.core.netdev_max_backlog 默认为128

表示当每个网络接口接收数据包的速率比内核处理这些包的速率快时,允许发送到队列的数据包最大数目。就是说当接口接收包的速度比内核处理的快时,那么多出来的数据包要存放到队列中,那么这个队列最大可以放多少个呢?就是这个参数设置的。
 
net.ipv4.tcp_max_orphans
用于设定系统中最多允许存在多少TCP套接字不被关联到任何一个用户文件句柄上,如果超过这个值,那么没有与用户文件句柄关联的TCP套接字就会被复位,同时给出警告信息。这个值主要是为了防止DOS攻击。一般在系统内存比较大的情况下可以调大。

net.ipv4.tcp_max_syn_backlog
用于记录尚未收到客户端ACK信息的连接请求最大值。内存比较多可以设置大一点。也就是半连接的队列,表示服务器收到了客户端的SYN包同时服务器也发送了ACK+SYN,但是还没有收到客户端返回的ACK包,此时连接处于SYN_RECV状态,当服务器收到客户端的ACK包时,则删除该半连接条目,服务器进入ESTABLISHED状态,这时候把该连接放入Accept队列。修改这个值可以增加更多的网络连接,但是过大容易受到SYN泛洪攻击。

net.core.somaxconn
表示用于调节系统同时发起的TCP连接数,一般为128,当高并发的情况下,如果这个值比较小,就会导致连接超时或者重传现象。Nginx服务器中定义的NGX_LISTEN_BACKLOG默认是511,所以需要调整这个参数。当服务器收到ACK包之后,就会进入一个叫accept的队列这个队列的最大长度就是由这个参数决定的。表示最多可有多少个ESTAB的连接等待accept()。这个值表示已客户端和服务器已完成三次握手的已建立连接的队列大小。

net.ipv4.tcp_timestamps
该参数用于设置时间戳,可以避免序列号重复,在一个端口速率比较大的网卡下,遇到重复的序列号的概率还是比较大的。如果设置为0表示禁用对TCP时间戳的支持。默认情况下,系统是允许重复的。但是对于Nginx来说还是建议关闭。

net.ipv4_tcpsynack_retries
用于设置内核放弃TCP连接之前向客户端发送ACK+SYN包的数量,也就是重试次数。这个参数主要影响三次握手中的第二次,也就是服务器向客户端发送SYN+前一个SYN的ACK。一般设置为1,表示内核放弃连接之前发送一次SYN+ACK包。比如客户端发来SYN,然后服务器回复ACK+SYN,这时候客户端断线了,之后会怎么办呢?服务器会进行重发ACK+SYNC,Linux中默认重试5次,每次时间间隔为上一次的一倍,1s-2s-4s-8s-16s之后再等一个32s如果还没有客户端响应,则服务器断开这个连接。

net.ipv4.syn_retries

参数和上一个类似,这是这次是设置内核放弃建立连接之前发送SYN包的数量。也建议设置为1. 


net.ipv4.tcp.syncookies

修改此参数可以有效防范syn flood攻击。原理是在TCP服务器收到SYN包后,攻击者就下线,这样默认服务器需要等待63秒之后才会断开这个连接(中间服务器要重试几次),这样服务器的SYN队列很快就满了。这个参数的目的就是为了解决这个问题,当SYN队列满了,服务器根据预源端口、目的IP和时间戳生产一个序列号(可以叫做cookie)发送出去,如果是攻击者它是不会响应的,如果是真实请求则会返回这个cookie,然后服务器根据这个Cookie来建立连接就算你不在SYN队列中也可以。默认为0,1表示开启。对于连接请求很大的服务器不要开启这个参数,因为它并不严谨。你应该设置三个参数来变相解决这个问题:net.ipv4_tcpsynack_retriesnet.ipv4.tcp_max_syn_backlog和tcp_abort_on_overflow也就是,也就是减少重试次数、增大SYN队列长度和如果处理不过来就拒绝。


net.ipv4.tcp_tw_reuse
表示开启重用。允许将TIME_WAIT状态的sockets重新用于新的TCP连接,因为大量处于TIME_WAIT状态很浪费资源,占用文件描述符,默认为0,表示关闭,设置为1表示开启;

net.ipv4.tcp_tw_recycle
表示开启TCP连接中TIME_WAIT sockets的快速回收,默认为0,表示关闭。设置为1表示开启。

net.ipv4.tcp_fin_timeout
表示如果套接字由本端要求关闭,这个参数决定了它保持在FIN_WAIT-2状态的时间。默认为2MSL。不建议修改,如果要修改可以根据实际情况而定。

net.ipv4.tcp_keepalive_time
TCP keepalive心跳包机制,用于检测连接是否已经断开,这个值就是设置检测频率的。表示当keepalive起用的时候,TCP发送keepalive消息的频度。缺省是2小时,改为20分钟。

net.ipv4.ip_local_port_range = 1024 65000
表示用于向外连接的端口范围。缺省情况下很小,改为1024到65000。

net.ipv4.tcp_max_tw_buckets = 5000
表示系统同时保持TIME_WAIT套接字状态的最大数量,如果超过这个数字,TIME_WAIT套接字将立刻被清除并打印警告信息。默认为180000,改为5000。对于Apache、Nginx等服务器,上几行的参数可以很好地减少TIME_WAIT套接字数量,但是对于Squid,效果却不大。此项参数可以控制TIME_WAIT套接字的最大数量,避免Squid服务器被大量的TIME_WAIT套接字拖死。