TCP 连接过程 三次握手
图示
连接过程详解
- 服务端绑定ip,端口,开启监听,等待客户端连接
- 客户端发起连接请求携带
SYN
=1,客户端连接状态为SYN_SENT
- 服务端收到SYN请求,服务端的连接状态为
SYN_RCVD
,此时该连接会进入syns queue
(半连接队列),服务端返回响应ACK
,SYN
发起连接请求 - 客户端收到服务端响应的
ACK
与SYN
,客户端此时连接状态变为ESTABLISHED
- 此时从客户端角度来说,连接已建立,可以发送数据,实际上在服务端未收到客户端最后发送的
ACK
时,此时的连接状态依然是半连接状态 - 客户端响应服务端的
SYN
,发送ACK
到服务端 - 服务端收到客户端的
ACK
确认,将连接从syns queue
(半连接队列)中取出,服务端的连接状态修改为ESTABLISHED
,并将连接放入accept queue
(全连接队列),此时连接才真正建立 - 系统通过
accept
消费accept queue
(全连接队列)中的连接
以上是一个正确的,完整的连接过程
代码角度
TCP通信 不管上层怎么封装,最下面调用的始终是Socket 的一套接口
基本过程,Server调用 listen
监听端口, 客户端使用 connect
发起连接,然后Server使用 accept
获取已经到达 ESTABLISHED
状态的连接,然后进行读写
数据包角度
- Client: 发送
SYN
,连接状态进入SYN_SENT
- Server: 收到
SYN
, 创建连接状态为SYN_RCVD/SYN_RECV
的 Socket,响应SYN/ACK
- Client: 收到
SYN/ACK
,连接状态从SYN_SENT
变为ESTABLISHED
,响应ACK
- Server: 收到
ACK
,连接状态变为ESTABLISHED
此时,双方的Socket都进入了 ESTABLISHED
状态,可以开始交换数据了。
动态过程
连接过程中出现的问题(基于Centos7)
从连接过程图示中可知,在连接的过程中会出现两个队列,全连接队列与半连接队列,既然是队列,必然会有长度和溢出的问题
全连接队列
全连接队列长度 = min(backlog, somaconn)
backlog
为调用 listen
函数时传递的参数
somaxconn
是一个系统参数 /proc/sys/net/core/somaxconn
,默认值 128
全连接队列溢出
果全连接队满了又会怎么样?应用程序调用 accept
的速度太慢,或者请求太多,都会导致这个情况。
当系统收到三次握手中第三次的 ACK 包,正常情况下应该是从半连接队列中取出连接,更改状态,放入全连接队列中。此时如果全连接队列满了的话:
- 如果设置了
net.ipv4.tcp_abort_on_overflow
,那么直接回复RST
,同时,对应的连接直接从半连接队列中删除,客户端的响应就是 Connection Reset - 否则,直接忽略
ACK
,然后 TCP 的超时机制会起作用,一定时间以后,Server 会重新发送SYN/ACK
,因为在 Server 看来,它没有收到ACK
配置位置: /proc/sys/net/ipv4/tcp_abort_on_overflow
半连接队列
半连接队列大小由 三个参数控制
listen
时传入的 backlog/proc/sys/net/ipv4/tcp_max_syn_backlog
,默认为 1024/proc/sys/net/core/somaxconn
,默认为 128
计算方式:
backlog = min(somaxconn, backlog)
nr_table_entries = backlog
nr_table_entries = min(backlog, sysctl_max_syn_backlog)
nr_table_entries = max(nr_table_entries, 8)
// roundup_pow_of_two: 将参数向上取整到最小的 2^n
// 注意这里存在一个 +1
nr_table_entries = roundup_pow_of_two(nr_table_entries + 1)
max_qlen_log = max(3, log2(nr_table_entries))
max_queue_length = 2^max_qlen_log
半连接队列溢出 & SYN FLOOD
SYN Flood 的思路很简单,发送大量的 SYN
数据包给 Server,然后不回应 Server 响应的 SYN/ACK
,这样,Server 端就会存在大量处于 SYN_RECV
状态的连接,这样的连接会持续填充半连接队列,最终导致半连接队列溢出。
当半连接队列溢出时,Server 收到了新的发起连接的 SYN
:
- 如果不开启
net.ipv4.tcp_syncookies
:直接丢弃这个SYN
- 如果开启
net.ipv4.tcp_syncookies
: 如果全连接队列满了,并且
qlen_young
的值大于 1:丢弃这个 SYN- 否则,生成 syncookie 并返回
SYN/ACK
包
- 否则,生成 syncookie 并返回
qlen_young
表示目前半连接队列中,没有进行 SYN/ACK 包重传的连接数量。
这里涉及到一个 net.ipv4.tcp_syncookies
配置项,这是内核用于抵御 SYN Flood 攻击的一种方式,它的核心思想在于:攻击者对于我们返回的 SYN/ACK 包是不会回复的,而正常用户会回复一个 ACK 包。
通过生成一个 Cookie 携带在我们返回的 SYN/ACK 包中,之后我们收到了 ACK 包,我们可以验证 Cookie 是否正确,如果正确,则允许建立连接。详细的过比较复杂,这里不再讨论了。
当 net.ipv4.syncookies
开启的时候,即便半连接队列已经满了,正常用户依旧可以和服务器进行通信。
如果我们关闭 net.ipv4.tcp_syncookies
的话,当收到 SYN Flood 攻击时,系统会直接丢弃新的 SYN 包,也就是正常客户端将无法和服务器通信。
防御SYN FLOOD
tcp_max_syn_backlog 调大
tcp_synack_retries 服务端重连次数,适当调小
tcp_syncookies 开启cookies验证
为什么连接要三次握手
主要目的防止server端一直等待,浪费资源
为了防止服务器端开启一些无用的连接增加服务器开销以及防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。
由于网络传输是有延时的(要通过网络光纤和各种中间代理服务器),在传输的过程中,比如客户端发起了SYN=1创建连接的请求(第一次握手)。
如果服务器端就直接创建了这个连接并返回包含SYN、ACK和Seq等内容的数据包给客户端,这个数据包因为网络传输的原因丢失了,丢失之后客户端就一直没有接收到服务器返回的数据包。
客户端可能设置了一个超时时间,时间到了就关闭了连接创建的请求。再重新发出创建连接的请求,而服务器端是不知道的,如果没有第三次握手告诉服务器端客户端收的到服务器端传输的数据的话,
服务器端是不知道客户端有没有接收到服务器端返回的信息的。
TCP 断开过程 四次挥手
动图
TCP 四次挥手过程详解
TCP 的连接的拆除需要发送四个包,因此称为四次挥手(Four-way handshake),也叫做改进的三次握手。客户端或服务器均可主动发起挥手动作,在 socket 编程中,任何一方执行 close()
操作即可产生挥手操作。
- 第一次挥手(FIN=1,seq=x)
假设客户端想要关闭连接,客户端发送一个 FIN 标志位置为1的包,表示自己已经没有数据可以发送了,但是仍然可以接受数据。
发送完毕后,客户端进入 FIN_WAIT_1
状态。
- 第二次挥手(ACK=1,ack=x+1)
服务器端确认客户端的 FIN 包,发送一个确认包,表明自己接受到了客户端关闭连接的请求,但还没有准备好关闭连接。
发送完毕后,服务器端进入 CLOSE_WAIT
状态,客户端接收到这个确认包之后,进入 FIN_WAIT_2
状态,等待服务器端关闭连接。
- 第三次挥手(FIN=1,seq=y)
服务器端准备好关闭连接时,向客户端发送结束连接请求,FIN 置为1。
发送完毕后,服务器端进入 LAST_ACK
状态,等待来自客户端的最后一个ACK。
- 第四次挥手(ACK=1,ack=y+1)
客户端接收到来自服务器端的关闭请求,发送一个确认包,并进入 TIME_WAIT
状态,等待可能出现的要求重传的 ACK 包。
服务器端接收到这个确认包之后,关闭连接,进入 CLOSED
状态。
客户端等待了某个固定时间(两个最大段生命周期,2MSL,2 Maximum Segment Lifetime)之后,没有收到服务器端的 ACK ,认为服务器端已经正常关闭连接,于是自己也关闭连接,进入 CLOSED
状态。
断开连接过程中连接状态的变化
- FIN_WAIT_1: 这个状态要好好解释一下,其实FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。而这两种状态的区别是:FIN_WAIT_1状态实际上是当SOCKET在ESTABLISHED状态时,它想主动关闭连接,向对方发送了FIN报文,此时该SOCKET即进入到FIN_WAIT_1状态。而当对方回应ACK报文后,则进入到FIN_WAIT_2状态,当然在实际的正常情况下,无论对方何种情况下,都应该马上回应ACK报文,所以FIN_WAIT_1状态一般是比较难见到的,而FIN_WAIT_2状态还有时常常可以用netstat看到。(主动方)
- FIN_WAIT_2:上面已经详细解释了这种状态,实际上FIN_WAIT_2状态下的SOCKET,表示半连接,也即有一方要求close连接,但另外还告诉对方,我暂时还有点数据需要传送给你(ACK信息),稍后再关闭连接。(主动方)
- CLOSE_WAIT:这种状态的含义其实是表示在等待关闭。怎么理解呢?当对方close一个SOCKET后发送FIN报文给自己,你系统毫无疑问地会回应一个ACK报文给对方,此时则进入到CLOSE_WAIT状态。接下来呢,实际上你真正需要考虑的事情是察看你是否还有数据发送给对方,如果没有的话,那么你也就可以 close这个SOCKET,发送FIN报文给对方,也即关闭连接。所以你在CLOSE_WAIT状态下,需要完成的事情是等待你去关闭连接。(被动方)
- LAST_ACK: 这个状态还是比较容易好理解的,它是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,也即可以进入到CLOSED可用状态了。(被动方)
- TIME_WAIT: 表示收到了对方的FIN报文,并发送出了ACK报文,就等2MSL后即可回到CLOSED可用状态了。如果FINWAIT1状态下,收到了对方同时带FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。(主动方)
- CLOSED: 表示连接中断。
客户端为什么在TIME-WAIT阶段要等待2MSL?
为的是确认服务器端是否收到客户端发出的ACK确认报文
当客户端发出最后的ACK确认报文时,并不能确定服务器端能够收到该段报文。所以客户端在发送完ACK确认报文之后,会设置一个时长为2MSL的计时器。MSL指的是Maximum Segment Lifetime:一段TCP报文在传输过程中的最大生命周期。2MSL即是服务器端发出为FIN报文和客户端发出的ACK确认报文所能保持有效的最大时长。
服务器端在1MSL内没有收到客户端发出的ACK确认报文,就会再次向客户端发出FIN报文;
- 如果客户端在2MSL内,再次收到了来自服务器端的FIN报文,说明服务器端由于各种原因没有接收到客户端发出的ACK确认报文。客户端再次向服务器端发出ACK确认报文,计时器重置,重新开始2MSL的计时;
- 否则客户端在2MSL内没有再次收到来自服务器端的FIN报文,说明服务器端正常接收了ACK确认报文,客户端可以进入CLOSED阶段,完成“四次挥手”。
所以,客户端要经历时长为2SML的TIME-WAIT
阶段;这也是为什么客户端比服务器端晚进入CLOSED
阶段的原因
为什么建立连接是三次握手,断开连接是四次?
建立连接的时候, 服务器在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。
而关闭连接时,服务器收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,而自己也未必全部数据都发送给对方了,所以己方可以立即关闭,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送,从而导致多了一次。
连接已建立,客户端出现故障怎么怎么处理?
TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。
TCP报文
IP 协议只是一个地址协议,表明数据由哪里发向哪里,并不保证数据包的完整。
TCP 协议的作用是,保证数据通信的完整性和可靠性,防止丢包。
TCP报文首部 如图
参数解释
1、源端口号:源端口和IP地址的作用是标识报文的发出地址及返回地址。
2、目的端口:端口指明接收方计算机上的应用程序接口。
2、序号和确认号:是TCP可靠传输的关键部分。序号是本报文段发送的数据组的第一个字节的序号。在TCP传送的流中,每一个字节一个序号。e.g.一个报文段的序号为300,此报文段数据部分共有100字节,则下一个报文段的序号为400。所以序号确保了TCP传输的有序性。确认号,即ACK,指明下一个期待收到的字节序号,表明该序号之前的所有数据已经正确无误的收到。确认号只有当ACK标志为1时才有效。比如建立连接时,SYN报文的ACK标志位为0。
3、数据偏移/首部长度:4bits。由于首部可能含有可选项内容,因此TCP报头的长度是不确定的,报头不包含任何任选字段则长度为20字节,4位首部长度字段所能表示的最大值为1111,转化为10进制为15,15*32/8 = 60,故报头最大长度为60字节。首部长度也叫数据偏移,是因为首部长度实际上指示了数据区在报文段中的起始偏移值。
4、保留:为将来定义新的用途保留,现在一般置0。
5、控制位:URG ACK PSH RST SYN FIN,共6个,每一个标志位表示一个控制功能。
1)URG:紧急指针标志,为1时表示紧急指针有效,为0则忽略紧急指针。
2)ACK:确认序号标志,为1时表示确认号有效,为0表示报文中不含确认信息,忽略确认号字段。
3)PSH:push标志,为1表示是带有push标志的数据,指示接收方在接收到该报文段以后,应尽快将这个报文段交给应用程序,而不是在缓冲区排队。
4)RST:重置连接标志,用于重置由于主机崩溃或其他原因而出现错误的连接。或者用于拒绝非法的报文段和拒绝连接请求。
5)SYN:同步序号,用于建立连接过程,在连接请求中,SYN=1和ACK=0表示该数据段没有使用捎带的确认域,而连接应答捎带一个确认,即SYN=1和ACK=1。
6)FIN:finish标志,用于释放连接,为1时表示发送方已经没有数据发送了,即关闭本方数据流。
6、窗口:滑动窗口大小,用来告知发送端接受端的缓存大小,以此控制发送端发送数据的速率,从而达到流量控制。窗口大小时一个16bit字段,因而窗口大小最大为65535。
7、校验和:奇偶校验,此校验和是对整个的 TCP 报文段,包括 TCP 头部和 TCP 数据,以 16 位字进行计算所得。由发送端计算和存储,并由接收端进行验证。
8、紧急指针:只有当 URG 标志置 1 时紧急指针才有效。紧急指针是一个正的偏移量,和顺序号字段中的值相加表示紧急数据最后一个字节的序号。 TCP 的紧急方式是发送端向另一端发送紧急数据的一种方式。
9、选项和填充:最常见的可选字段是最长报文大小,又称为MSS(Maximum Segment Size),每个连接方通常都在通信的第一个报文段(为建立连接而设置SYN标志为1的那个段)中指明这个选项,它表示本端所能接受的最大报文段的长度。选项长度不一定是32位的整数倍,所以要加填充位,即在这个字段中加入额外的零,以保证TCP头是32的整数倍。
10、数据部分: TCP 报文段中的数据部分是可选的。在一个连接建立和一个连接终止时,双方交换的报文段仅有 TCP 首部。如果一方没有数据要发送,也使用没有任何数据的首部来确认收到的数据。在处理超时的许多情况中,也会发送不带任何数据的报文段。