Bootstrap

TCP 三次握手、四次断开

目录

1、三次握手

1.1  三次握手流程图

1.2 三次握手过程详解

1.3  三次握手中需要注意的

1.4  三次握手中知识细节

1、半连接队列(syn queue)

 2、全连接队列(accept queue)

3、如何查看连接状态

1.5 三次握手中的常见问题

2、四次挥手---断开连接

2.1 四次挥手流程图

 2.2 四次挥手过程详解

1、四次挥手

2.3 四次挥手中的常见问题

1、三次握手

TCP建立连接的过程就是三次握手(Three-way Handshake),在建立连接的过程实际上就是客户端和服务端之间总共发送三个数据包。进行三次握手主要是就是为了确认双方都能接收到数据包和发送数据包,而客户端和服务端都会指定自己的初始化序列号,就是为了后面数据传输的可靠性做好准备。实质上也就是客户端在连接服务器端的时候指定端口,建立TCP连接,并同步连接双方的序列号(seq)和确认号(ack),交换TCP的窗口大小信息。

1.1  三次握手流程图

1.2 三次握手过程详解

最开始建立连接的时候,客户端最开始是处于closed关闭状态,服务器是要处于listen监听状态。

当开始连接的时候,客户端会主动打开,打开端口。然后进行三次握手。

发送第一个SYN的一端将执行主动打开(active open),接收这个SYN并发发回下一个SYN的另一端执行被动打开(passive open)。在socket编程中,客户端执行connect()时,将触发三次握手。

第一次握手:客户端给服务端发一个 SYN 标志位的数据包(TCP中有6个标志位,SYN标志位在倒数第15位,二进制位为1,发送第一个SYN包之后,SYN=1)请求建立连接,并指明客户端的初始化序列号 seq(初始序列号是随机产生的,是为了网络安全)。此时客户端处于SYN_SENT 状态。首部的同步位SYN=1,初始序号seq=x,SYN=1的报文段不能携带数据,但要消耗掉一个序号。

第二次握手:服务端收到客户端的SYN标志位数据包之后,也会给客户端发送一个SYN标准位数据包(SYN是为了告诉客户端,客户端到服务端的通道是没问题的)作为应答,表示同意建立连接。还指明了自己的初始化序列号seq。并且会把客户端的seq+1(x+1)作为ACK的值,(ACK是用来验证服务端到客户端的通道没有问题),表明自己已经接收到了客户端的SYN,此时的服务器处于SYN_RCVD的状态。

在确认标志位数据包的ACK=1,确认号ack=x+1(需要回复的数据包ack会在发送过来的序列号上加1),服务器端的初始序号seq=y。

第三次握手:客户端收到SYN标志位数据包之后,会发送一个ACK标志位数据包,也会把服务端的初始序列号seq+1(y+1)作为自己的确认号ACK的值,表示收到了服务端的SYN标志位数据包。此时,客户端处于ESTABLISHED状态(完成连接)。

在确认标志位数据包的ACK=1,确认号ack=y+1,客户端的序列号seq=x+1(初始为seq=x,第二个报文段所以要+1)。ACK可以携带数据,不携带数据则不消耗序号。

最后: 服务器收到确认标志位数据包,服务器状态由syn_received变为ESTABLISHED(完成连接)。这样双方就建立起了连接。

最后一个发送数据包的时候,服务器端不需要回复数据包,所以ack就不会占用序列号。

一般来说,ACK会携带序列号,但是一般不占用序列号,下一个包还可以从ACK序列号开始。

1.3  三次握手中需要注意的

① 三次握手的标志位:SYN、SYN和ACK。

第一次握手的时候客户端回复的是SYN标志位数据包。第二次握手的时候服务端回复的是SYN和ACK标志位。第三次客户端回复的是ACK标志位。

② 看状态:当服务器回复的时候客户端会变成SYN_SENT状态,当客户端发送数据包过去之后,服务器端会变成SYN_RCVD状态。

ACK报文是用来应答的,SYN报文是用来同步的

LISTEN:倾听来自远方TCP端口的连接请求。

SYN_SENT:在发送连接请求后等待匹配的连接请求。

SYN_RECEIVED:在收到和发送一个连接请求之后等待对连接请求的确认。

ESTABLISHED:代表一个打开的连接,数据可以传送给用户。

FIN-WAIT-1:等待远程TCP的连接中断请求,或先前的连接中断请求的确认。

FIN-WAIT-2:从远程TCP等待连接中断请求的确认。

CLOSING:等待远程TCP对连接中断的确认。

LAST-ACK:等待原来发向远程的TCP的连接中断请求的确认。

TIME-WAIT:等待足够的时间以确保远程TCP接收到连接中断请求的确认。

CLOSED:没有任何连接状态。

③ 思考:在第三次握手的时候,如果因为网络问题,客户端发送给服务端的ack包一直没收到怎么办?

这时就引入了超时重传计时器,当ack包迟迟未收到,服务器就会重新发送数据包:SYN+ACK的数据包。

超时重传计时器:超时重传数据包的时候,计时器会清零。

当ack包迟迟未收到,服务器端检测超时,服务器就会重新发送数据包:SYN+ACK的数据包。

SACK:(Selective Acknowledgment):是一个TCP的选项,来运行TCP单独确认非连续的片段,用于告知真正丢失的包,只重传丢失的片段。要使用SACK,2个设备必须同时支持SACK才可以,建立连接的时候需要使用SACK PerMitted的option,如果允许,后续的传输过程中TCP segment中的可以携带SACK option,这个option内容包含一系列的非连续的没有确认的数据的seq range,这些SYN包中的SACK Permitted选项,双方都支持才对。

④ 思考:那么服务器发送数据包的次数是无限制的吗?

那当然不是的,我们可以去修改TCP中的参数的设置,修改的tcp中的syn+ack包发送的次数:

在centos系统中,默认发送syn + ack包 5次,存放这个文件的路径为:/proc/sys/net/ipv4/

tcp_synack_retries。

内核参数配置文件:/proc/sys/net/ipv4

其中的proc是表示进程;sys表示的是系统;vm表示的是虚拟内核;kernel表示的是内核;net表示的是网络。

[root@nginx-kafka01 html]# cd /proc/sys
[root@nginx-kafka01 sys]# ls
abi  crypto  debug  dev  fs  kernel  net  sunrpc  user  vm
[root@nginx-kafka01 ipv4]# pwd

/proc/sys/net/ipv4

①临时修改的文件为:/proc/sys/net/ipv4/ tcp_synack_retries

[root@nginx-kafka01 ipv4]# cat tcp_synack_retries

5

②修改内核参数(调优)永久修改:vim /etc/sysctl.conf

修改完之后会生效到/proc/sys/net/ipv4/ tcp_synack_retries中。

[root@nginx-kafka01 ipv4]# vim /etc/sysctl.conf

  1 # sysctl settings are defined through files in
  2 # /usr/lib/sysctl.d/, /run/sysctl.d/, and /etc/sysctl.d/.
  3 #
  4 # Vendors settings live in /usr/lib/sysctl.d/.
  5 # To override a whole file, create a new file with the same in
  6 # /etc/sysctl.d/ and put new settings there. To override
  7 # only specific settings, add a file with a lexically later
  8 # name in /etc/sysctl.d/ and put new settings there.
  9 #
 10 # For more information, see sysctl.conf(5) and sysctl.d(5).
 11 #
 12 net.ipv4.ip_forward=10
 13 vm.swappiness=10
~                     

③ 刷新生效或者重启

刷新生效:systemctl -p

重启:init 6

查看当前内核参数:

systemctl -a

1.4  三次握手中知识细节

服务启动的时候会初始化两个队列:半连接队列和全连接队列。

1、半连接队列(syn queue)

会给客户端发送过来的数据存放在一个空间,服务器将该连接状态为syn_recvd连接信息放到半连接队列。

处于半连接队列如果出现服务端一直没收到客户端的ack报文的情况,也就是连接满了的情况下

很可能出现的问题是:

(1)半连接队列配置参数小了

当半连接满了之后,修改配置参数大小:/proc/sys/net/ipv4/tcp_max_syn_backlog  对应来扩大连接队列。

[root@nginx-kafka01 ipv4]# cat tcp_max_syn_backlog 
128

(2)syn flood攻击(洪泛攻击)

半连接满了,如果修改了连接数据还是不行,那就是遭受到syn flood攻击。检测 SYN 攻击非常的方便,当看到服务器上存在大量的半连接状态时,特别是源IP地址是随机的,基本上可以断定这是一次SYN攻击。开启dmess日志中可以看到syn flood攻击的现象。在 Linux/Unix 上可以使用系统自带的 netstats 命令来检测 SYN 攻击。

① 什么是syn flood攻击:syn flood攻击是指利用TCP协议的缺陷,发送大量伪造的TCP连接请求,常用假冒的IP或IP号段发送大量的请求连接的第一个握手包(SYN包),被攻击的服务器发送第二个握手包(SYN+ACK包,因为对方是假冒的IP,对方也无法收到包而且也不会回应第三个握手包。就会导致被攻击的服务器保持着大量的SYN_RECV状态的“半连接”,并且会重试默认的5次回应第二个握手包,大量的随机的恶意的syn占满了未完成连接队列,导致正常合法的syn排不上队列,就会导致正常的业务请求连接不过来。SYN 攻击是一种典型的 DoS/DDoS 攻击。

【服务器端的资源分配是在二次握手时分配的,而客户端的资源是在完成三次握手时分配的,所以服务器容易受到SYN洪泛攻击】

② Syn-Flood攻击成立的关键在于服务器资源是有限的,而服务器收到请求会分配资源。通常来说,服务器用这些资源保存此次请求的关键信息,包括请求的来源和目(五元组),以及TCP选项,如最大报文段长度MSS、时间戳timestamp、选择应答使能Sack、窗口缩放因子Wscale等等。当后续的ACK报文到达,三次握手完成,新的连接创建,这些信息可以会被复制到连接结构中,用来指导后续的报文收发。

MSS:TCP 最大报文长度 传输层一次最大可以传输多少数据---不包括头部字段

MSS一般来说最大值为: 1500 - ip 头(20字节)- tcp 头部(最少20字节) = 1460 最大报文长度

因为有了MSS tcp包在ip层一般来说网络层不需要分片的 ,mss中还有选项

MTU:最大传输单元 网络层 最大为1500个字节

③  如何解决syn flood:

a、缩短超时(SYN Timeout)时间

b、增加最大连接数

c、过滤网关防护

d、SYN cookie技术

使用syn cookie是为了解决syn flood攻击,tcp建立连接的时候,双方的起始报文序号是可以任意的。SYN cookies利用这一点,就可以构造初始序列号。当客户端收到此SYN+ACK报文后,根据TCP标准,它会回复ACK报文,且报文中ack = n + 1,那么在服务器收到它时,将ack - 1就可以拿回当初发送的SYN+ACK报文中的序号了。服务器巧妙地通过这种方式间接保存了一部分SYN报文的信息。服务器需要对ack - 1这个序号进行检查。

那么如何启用syn cookie:/proc/sys/net/ipv4/tcp_syncookies  值为1表示是开启的状态。

[root@nginx-kafka01 ipv4]# cat tcp_syncookies

1

 2、全连接队列(accept queue)

服务器连接进入establish状态,表示是进入了全连接队列。

3、如何查看连接状态

① netstat -a

② netstat -tanpl

③netstat -anplut

③ ss -lnt 需要安装

[root@nginx-kafka01 ipv4]# netstat -a
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State      
tcp        0      0 0.0.0.0:mysql           0.0.0.0:*               LISTEN     
tcp        0      0 0.0.0.0:sunrpc          0.0.0.0:*               LISTEN     
tcp        0      0 0.0.0.0:http            0.0.0.0:*               LISTEN     
tcp        0      0 0.0.0.0:ssh             0.0.0.0:*               LISTEN     
tcp        0      0 localhost:smtp          0.0.0.0:*               LISTEN     
tcp        0      0 nginx-kafka01:ssh       192.168.2.135:52102     ESTABLISHED
tcp6       0      0 [::]:sunrpc             [::]:*                  LISTEN     
tcp6       0      0 [::]:hbci               [::]:*                  LISTEN     
tcp6       0      0 localhost:smtp          [::]:*                  LISTEN     
udp        0      0 0.0.0.0:sunrpc          0.0.0.0:*                          
udp        0      0 0.0.0.0:757             0.0.0.0:*                          
udp        0      0 localhost:323           0.0.0.0:*                          
udp6       0      0 [::]:sunrpc             [::]:*                             
udp6       0      0 [::]:757                [::]:*                             
udp6       0      0 localhost:323           [::]:*                             
Active UNIX domain sockets (servers and established)

[root@nginx-kafka01 ipv4]# netstat -tanpl
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 0.0.0.0:3306            0.0.0.0:*               LISTEN      7117/mysqld         
tcp        0      0 0.0.0.0:111             0.0.0.0:*               LISTEN      6102/rpcbind        
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      6669/nginx: master  
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      6640/sshd           
tcp        0      0 127.0.0.1:25            0.0.0.0:*               LISTEN      7284/master         
tcp        0      0 192.168.2.152:22        192.168.2.135:52102     ESTABLISHED 7879/sshd: root@pts 
tcp6       0      0 :::111                  :::*                    LISTEN      6102/rpcbind        
tcp6       0      0 :::3000                 :::*                    LISTEN      7840/grafana-server 
tcp6       0      0 ::1:25                  :::*                    LISTEN      7284/master      


[root@nginx-kafka01 ipv4]# netstat -anlut
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State      
tcp        0      0 0.0.0.0:3306            0.0.0.0:*               LISTEN     
tcp        0      0 0.0.0.0:111             0.0.0.0:*               LISTEN     
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN     
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.1:25            0.0.0.0:*               LISTEN     
tcp        0      0 192.168.2.152:22        192.168.2.135:52102     ESTABLISHED
tcp6       0      0 :::111                  :::*                    LISTEN     
tcp6       0      0 :::3000                 :::*                    LISTEN     
tcp6       0      0 ::1:25                  :::*                    LISTEN     
udp        0      0 0.0.0.0:111             0.0.0.0:*                          
udp        0      0 0.0.0.0:757             0.0.0.0:*                          
udp        0      0 127.0.0.1:323           0.0.0.0:*                          
udp6       0      0 :::111                  :::*                               
udp6       0      0 :::757                  :::*                               
udp6       0      0 ::1:323                 :::*            


[root@nginx-kafka01 ipv4]# ss -lnt
State      Recv-Q Send-Q              Local Address:Port                             Peer Address:Port              
LISTEN     0      50                              *:3306                                        *:*                  
LISTEN     0      128                             *:111                                         *:*                  
LISTEN     0      128                             *:80                                          *:*                  
LISTEN     0      128                             *:22                                          *:*                  
LISTEN     0      100                     127.0.0.1:25                                          *:*                  
LISTEN     0      128                            :::111                                        :::*                  
LISTEN     0      128                            :::3000                                       :::*                  
LISTEN     0      100                           ::1:25                                         :::*         

recv0Q-- send-Q

netstat和ss 看到的值是不一样的

netstat:其中,接收队列(Recv-Q)和发送队列(send-Q)需要你特别关注。

Recv-Q:Established: The count of bytes not copied by the user program connected to this socket. Listen‐:接收方已经发送了ack,但是用户程序还没有去拿:连接到此套接字的用户程序未复制的字节数。

Send-Q:发送的数据包。

syn backlog:是 TCP 协议栈中的半连接队列长度。

它们通常应该是0.当你发现它们不是0时,说明网络包的堆积发生。

1.5 三次握手中的常见问题

1、三次握手为什么是三次而不是两次?

为了保证数据能达到目标,TCP采用三次握手策略。三次握手完成两个重要的功能,既要双方做好发送数据的准备工作(双方都知道彼此已经准备好),也要允许双方就初始序列号进行协商,这个序列号在握手过程中被发送和确认。最主要的目的就是双方都需要确认自己与对方的发送与接收都是正常的。

第一次握手:客户端:发送数据包,服务端收到了。服务端就能得知:客户端的发送能力、服务端的接收能力都是正常的。

第二次握手:服务端发送数据包,客户端收到了。客户端就能得出:服务端的接收、发送能力,客户端的接收、发送能力都是正常的。

第三次握手:客户端发送数据包,服务端接收到了。服务端就能得出:客户端的接收、发送能力正常,服务器自己的发送、接收能力也是正常的。

因此,需要三次握手才能确认双方的接收和发送能力是否正常。

现在假设将三次握手改为仅需要两次握手的话,死锁是可能发生的。作为例子,考虑计算机S和C之间的通信,假定C给S发送一个连接请求分组,S收到了这个分组,并发送了确认应答分组。按照两次握手的协定,S认为连接已经是成功建立了,可以开始发送数据分组。可是,C在S的应答分组在传输中被丢失的情况下,将不知道S是否已经准备好,不知道S建立什么样的序列号,C甚至怀疑S是否收到自己的连接请求分组。在这种情况下,C认为连接还未建立成,将会忽略S发来的任何数据分组,只等待连接确认应答分组。而S在发出的分组超时后,重复发送同样的分组。这样就形成了死锁。

2、如果已经建立了连接,但是客户端突然故障了怎么办?

TCP还设有一个保活计时器,显然,客户端如果出现了故障,服务器不能一直等下去,白白浪费资源。服务器每次收到一次客户端的请求之后都会重新复位这个计时器,时间通常是设置为2个小时,如果2小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒钟发送一次。如果一连发送10个探测报文仍然没有反应,服务器就认为客户端出现了故障,接着就会关闭连接。

3、为什么三次握手,返回时,ack值是seq加1(ack=x+1)?

假设对方接收到数据,比如sequence number = 1000,TCP Payload = 1000,数据第一个字节编号为1000,最后一个为1999,回应一个确认报文,确认号为2000,意味着2000前的字节接收完成,准备接收编号为2000及更多的数据。

确认收到的序列,并且告诉发送端下一次发送的序列号从哪里开始(便于接收方数据排序,便于选择重传)

4、三次握手过程中可以携带数据吗?

其实第三次握手的时候,是可以携带数据的。但是。第一次、第二次握手不可以携带数据。因为如果第一次握手可以携带数据的话,如果有人要恶意攻击服务器,那他每次都在第一次握手中的SYN报文中放入大量的数据。因为攻击者根本就不理服务器的接收、发送能力是否正常,然后就疯狂重复的发送着SYN报文的话,这会让服务器花费很多时间、内存空间来接收这些报文。也就是,第一次握手不可以放数据,其中的一个原因就是会让服务器更加容易受到攻击了。而对于第三次的话,此时客户端已经处于ESTABLISHED状态,对于客户端来说,他已经建立起连接了,并且也知道服务器的接收、发送能力是正常的了,所以携带数据是可以的。

2、四次挥手---断开连接

建立一个连接需要进行三次握手,而终止一个连接需要经过四次挥手(断开连接),这由TCP的半关闭(half-close)造成的。所谓的半关闭,其实就是TCP提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力。这个连接可以是客户端主动断开的,也可以是因为超过连接时间了,服务端主动断开的

2.1 四次挥手流程图

 2.2 四次挥手过程详解

刚开始的时候,双方都处于established状态。假如是客户端先发起关闭连接请求。

1、四次挥手

第一次挥手:客户端发送一个FIN的标准位数据包(FIN=1,连接释放报文段)给服务器端,会在报文中会指定一个序列号seq=u,并停止发送数据,主动关闭TCP连接。此时的客户端处于FIN_WAIT1的状态,等待服务端的确认。

第二次挥手:服务端收到FIN标志位数据包之后,会给客户端发送ACK报文(ACK=1:确认报文段),表明已经收到客户端的报文了,这个报文中是把客户端的序列号值进行加一(u+1)作为确认ACK报文的序列号值(ack=u+1:确认序列号),同时也会生成一个初始序列号seq=v。此时的服务器端处于 CLOSE_WAIT (关闭等待)状态。

此时的TCP处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入FIN_WAIT2(终止等待2)状态,等待服务端发出的连接释放报文段。

第三次挥手:如果服务器也想断开连接了(服务端没有要向客户端发出的数据),就会和客户端的第一次挥手一样,会给客户端发送一个FIN报文(FIN=1:连接释放报文,ACK=1),并且随机指定一个序列号seq=w(确认号为ack=u+1)。此时的服务端处于 LAST_ACK (最后确认)的状态,等待客户端的确认。

第四次挥手:客户端收到FIN报文(连接释放报文段)之后,一样的发送一个ACK报文(ACK=1,seq=u+1:确认报文段)作为应答,并且把服务端的序列号值+1(ack=w+1)作为自己ACK报文的序列号值。此时的客户端处于TIME_WAIT (时间等待)状态,(此时的TCP没有被释放掉)需要经过一段时间(等待时间计时器设置的时间2MSL)以确保服务端收到自己的ACK报文之后才会进入CLOSED状态,服务器端收到ACK报文之后,就处于关闭连接了,处于CLOSED状态。

收到一个FIN只意味着在这一方向上没有数据流动。客户端执行主动关闭并进入TIME_WAIT是正常的,服务端通常执行被动关闭,不会进入TIME_WAIT状态。

2.3 四次挥手中的常见问题

1、timewait过多的情况

一般是客户端,nginx作为反向代理,它既是服务器又是客户端,可能出现。

(1)timewait多的原因是:访问量过大,客户机在访问新的页面。

(2)解决timewait过多情况:①增加机器。②设置防火墙 nginx limit限制  ③修改内核参数:将等待时间减小 。提高资源利用率,在等待时间不处理其他事情。

2、为什么连接的时候是三次握手,关闭的时候却是四次挥手?

因为建立连接的时候,服务器在LISTEN状态下,当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文发送给客户端。其中ACK报文是用来应答的,SYN报文是用来同步的。

但是关闭连接时,当Server端收到对方发送的FIN报文时,告诉Client端,“你发的报文我收到了”。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。

3、为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?

有些材料显示2msl的最大等待时间为2分钟。

虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可能最后一个ACK丢失,所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。在Client发送出最后的ACK回复,但该ACK可能丢失;Server如果没有收到ACK,将不断重复发送FIN片段。所以Client不能立即关闭,它必须确认Server接收到该ACK。Client会在发送出ACK之后进入到TIME_WAIT状态。Client会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。所谓的2MSL是两倍的MSL(Maximum Segment Lifetime)。MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接。

4、服务器端会有一个TIME_WAIT状态吗?如果是服务器端主动断开连接呢?

发起连接的主动方基本都是客户端,但是断开连接的主动方服务器端和客户端都可以充当,也就是说,只要是主动断开连接的,就会有 TIME_WAIT状态。

四次挥手是指断开一个TCP连接时,需要客户端和服务器端总共发送4个确认包以确认连接的断开。在socket编程中,这一过程由客户端或服务端任一方执行close来触发。

由于TCP连接时全双工的,因此,每个方向的数据传输通道都必须要单独进行关闭。

;