TCP/IP协议就是这样设计的,是不可避免的。主要有两个原因:
1)可靠地实现TCP全双工连接的终止
TCP协议在关闭连接的四次握手过程中,最终的ACK是由主动关闭连接的一端(后面统称A端)发出的,如果这个ACK丢失,对方(后面统称B端)将重发出最终的FIN,因此A端必须维护状态信息(TIME_WAIT)允许它重发最终的ACK。如果A端不维持TIME_WAIT状态,而是处于CLOSED 状态,那么A端将响应RST分节,B端收到后将此分节解释成一个错误(在java中会抛出connection reset的SocketException)。
因而,要实现TCP全双工连接的正常终止,必须处理终止过程中四个分节任何一个分节的丢失情况,主动关闭连接的A端必须维持TIME_WAIT状态 。
2)允许老的重复分节在网络中消逝
TCP分节可能由于路由器异常而“迷途”,在迷途期间,TCP发送端可能因确认超时而重发这个分节,迷途的分节在路由器修复后也会被送到最终目的地,这个迟到的迷途分节到达时可能会引起问题。在关闭“前一个连接”之后,马上又重新建立起一个相同的IP和端口之间的“新连接”,“前一个连接”的迷途重复分组在“前一个连接”终止后到达,而被“新连接”收到了。为了避免这个情况,TCP协议不允许处于TIME_WAIT状态的连接启动一个新的可用连接,因为TIME_WAIT状态持续2MSL,就可以保证当成功建立一个新TCP连接的时候,来自旧连接重复分组已经在网络中消逝。
在建立TCP连接的三次握手中,前两个步骤完成后,此时server像client发送ack包后,等待client的ack包,这种连接状态叫做半连接,server会将这个半连接加入到队列中,不停的重复发送ack包,直到超时才将这个半连接从队列中删除。SYN攻击就是通过发送大量的半连接,并配合假IP来欺骗server端,消耗server端的资源。
netstat -n -p TCP | grep SYN_RECV
通过netstat查看tcp连接信息,如果有大量连接处于SYN_RECV状态,并且IP随机,则应该注意可能是SYN攻击。
SynAttackProtect保护机制、SYN cookies技术、增加最大半连接和缩短超时时间等
修改/etc/sysctl.conf,开启syncookies
#表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭
net.ipv4.tcp_syncookies = 1
netstat -n -p TCP | grep SYN_RECV | wc -l
如果出现大量TIME_WAIT状态的连接,是因为关闭连接时TIME_WAIT还要存在2MSL时间。解决办法是修改系统配置,快速回收TIME_WAIT的连接。
修改/etc/sysctl.conf
#对于一个新建连接,内核要发送多少个 SYN 连接请求才决定放弃,不应该大于255,默认值是5,对应于180秒左右时间
net.ipv4.tcp_syn_retries=2
#net.ipv4.tcp_synack_retries=2
#表示当keepalive起用的时候,TCP发送keepalive消息的频度。缺省是2小时,改为300秒
net.ipv4.tcp_keepalive_time=1200
net.ipv4.tcp_orphan_retries=3
#表示如果套接字由本端要求关闭,这个参数决定了它保持在FIN-WAIT-2状态的时间
net.ipv4.tcp_fin_timeout=30
#表示SYN队列的长度,默认为1024,加大队列长度为8192,可以容纳更多等待连接的网络连接数。
net.ipv4.tcp_max_syn_backlog = 4096
#表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭
net.ipv4.tcp_syncookies = 1
#表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭
net.ipv4.tcp_tw_reuse = 1
#表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭
net.ipv4.tcp_tw_recycle = 1
##减少超时前的探测次数
net.ipv4.tcp_keepalive_probes=5
##优化网络设备接收队列
net.core.netdev_max_backlog=3000
出现大量CLOSE_WAIT的原因在于代码中,当主动方发起关闭连接后,被动方仍然还在这个连接上读或者写数据,被动方一直没有关闭连接,才会使主动方停滞在TIME_WAIT状态。
解决办法就是检查代码,在主动方发起关闭连接后,不要无限制的继续读写这个连接,及时关闭。
golang写两个例子测试一下:
// 这是client/main.go,client会发起连接并在3秒后关闭
package main
import (
"fmt"
"net"
"time"
)
func check(err error) {
if err != nil {
panic(err)
}
}
func handle(conn net.Conn) {
for {
var buffer = make([]byte, 256)
conn.Read(buffer)
fmt.Println(string(buffer))
}
}
func main() {
client, err := net.Dial("tcp", ":8818")
check(err)
client.Write([]byte("Connect success"))
time.Sleep(time.Second * 3)
client.Close()
}
// server/main.go
package main
import (
"net"
"fmt"
)
func check(err error) {
if err != nil {
panic(err)
}
}
func handle(conn net.Conn) {
for {
var buffer = make([]byte, 256)
n, _ := conn.Read(buffer)
if n > 0 {
fmt.Println(string(buffer))
}
}
}
func main() {
server, err := net.Listen("tcp", ":8818")
check(err)
for {
conn, err := server.Accept()
check(err)
go handle(conn)
}
}
注意第18行,我们刻意忽略了在已经关闭的连接上的错误检查,client关闭后运行netstat -n -p TCP | grep :8818
检查连接状态
λ netstat -n -p TCP | grep :8818
TCP 127.0.0.1:8818 127.0.0.1:12365 CLOSE_WAIT
TCP 127.0.0.1:12365 127.0.0.1:8818 FIN_WAIT_2
可以看到,这个TCP连接处于CLOSE_WAIT的FIN_WAIT_2的状态,这是主动方关闭了连接,但是被动方还在死循环中不停的读,没有关闭连接导致的,这个连接会一直存直到超时
接下来改一改server的代码,在读到EOF时,会产生一个EOF的error,此时server主动的关闭这个连接
func handle(conn net.Conn) {
for {
var buffer = make([]byte, 256)
n, err := conn.Read(buffer)
if err != nil {
conn.Close()
}
if n > 0 {
fmt.Println(string(buffer))
}
}
}
λ netstat -n -p TCP | grep :8818
TCP 127.0.0.1:12584 127.0.0.1:8818 TIME_WAIT
结果顺利的进入了TIME_WAIT状态,这个状态在2MSL后变为CLOSED状态。