TCP 是面向连接的、可靠的、基于字节流的传输层通信协议,处于OSI模型的第四层传输层。
我们来看看 RFC 793 是如何定义「连接」的:
Connections: The reliability and flow control mechanisms described above require that TCPs initialize and maintain certain status information for each data stream. The combination of this information, including sockets, sequence numbers, and window sizes, is called a connection.
简单来说就是,用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括 Socket、序列号和窗口大小称为连接。
所以我们可以知道,建立一个 TCP 连接是需要客户端与服务端达成上述三个信息的共识。
TCP 四元组可以唯一的确定一个连接,四元组包括如下:
源地址和目的地址的字段(32 位)是在IP头部
中,作用是通过 IP 协议发送报文给对方主机。
源端口和目的端口的字段(16 位)是在TCP头部
中,作用是告诉 TCP 协议应该把报文发给哪个进程。
服务端通常固定在某个本地端口上监听,等待客户端的连接请求。
因此,客户端 IP 和端口是可变的,其理论值计算公式如下:
最大TCP连接数 = 客户端的IP数 * 客户端的端口数
对 IPv4,客户端的 IP 数最多为2^32
,客户端的端口数最多为2^16
,也就是服务端单机最大 TCP 连接数,约为 2^48
。
当然,服务端最大并发 TCP 连接数远不能达到理论上限,会受以下因素影响:
cat /proc/sys/fs/file-max
查看;cat /etc/security/limits.conf
查看;cat /proc/sys/fs/nr_open
查看;OOM
。我们假设客户端发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达服务端。
本来这是一个早已失效的报文段。但服务端收到此失效的连接请求报文段后,就误认为是客户端再次发出的一个新的连接请求。于是就向客户端发出确认报文段,同意建立连接。
假设不采用“三次握手”,那么只要服务端发出确认,新的连接就建立了。由于现在客户端并没有发出建立连接的请求,因此不会理睬服务端的确认,也不会向服务端发送数据。但服务端却以为新的运输连接已经建立,并一直等待客户端发来数据。这样,服务端的很多资源就白白浪费掉了。
所以,采用“三次握手”的办法可以防止上述现象发生。例如刚才那种情况,客户端不会向服务端的确认发出确认。服务端由于收不到确认,就知道客户端并没有要求建立连接。
TCP 三次握手跟现实生活中的人与人打电话是很类似的:
三次握手:
“喂,你听得到吗?”
“我听得到呀,你听得到我吗?”
“我能听到你,今天 balabala……”
经过三次的互相确认,大家就会认为对方对听的到自己说话,并且愿意下一步沟通,否则,对话就不一定能正常下去了。在TCP连接建立时同样需要这三次的牵手。
TCP 是面向连接的协议,所以使用 TCP 前必须先建立连接,而建立连接是通过三次握手来进行的。三次握手的过程如下图:
CLOSE
状态。先是服务端主动监听某个端口,处于 LISTEN
状态;client_isn
),将此序号置于 TCP 首部的「序列号」字段中,同时把 SYN
标志位置为 1
,表示 SYN
报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于 SYN-SENT
状态。SYN
报文后,首先服务端也会生成自己的随机初始化的序号(server_isn
),将此序号填入 TCP 首部的「序列号」字段中,其次把 TCP 首部的「确认应答号」字段填入 client_isn + 1
, 接着把 SYN
和 ACK
标志位置为 1
。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于 SYN-RCVD
状态。ACK
标志位置为 1
,其次「确认应答号」字段填入 server_isn + 1
,最后把报文发送给服务端,这次报文可以携带客户到服务端的数据,之后客户端处于 ESTABLISHED
状态。ESTABLISHED
状态。从上面的过程可以发现第三次握手是可以携带数据的,前两次握手是不可以携带数据的。一旦完成三次握手,双方都处于 ESTABLISHED
状态,此时连接就已建立完成,客户端和服务端就可以相互发送数据了。
在 RFC 793 中指出了 TCP 连接使用三次握手的首要原因:
The principle reason for the three-way handshake is to prevent old duplicate connection initiations from causing confusion.
简单来说,三次握手的首要原因是为了防止旧的重复连接初始化造成混乱。
我们考虑一个场景,客户端先发送了 SYN(seq = 90)
报文,然后客户端宕机了,而且这个 SYN
报文还被网络阻塞了,服务端并没有收到。接着客户端重启后,又重新向服务端建立连接,发送了 SYN(seq = 100)
报文
注意!不是重传
SYN
,重传的SYN
的序列号是一样的
看看三次握手是如何阻止历史连接的:
客户端连续发送多次 SYN
(都是同一个四元组)建立连接的报文,在网络拥堵情况下:
旧SYN报文
」比「最新的SYN报文
」早到达了服务端,那么此时服务端就会回一个 SYN + ACK
报文给客户端,此报文中的确认号是 91(90+1)
。100 + 1
,而不是 90 + 1
,于是就会回 RST
报文。RST
报文后,就会释放连接。SYN
抵达了服务端后,客户端与服务端就可以正常的完成三次握手了。其中「旧 SYN 报文
」称为历史连接,TCP 使用三次握手建立连接的最主要原因就是防止「历史连接」初始化了连接。
那么如果是两次握手或一次握手会发生什么呢?
答案是在两次握手或一次握手时,TCP建立连接无法避免历史连接的发生。主要是因为在两次握手的情况下,服务端没有中间状态给客户端来阻止历史连接,导致服务端可能建立一个历史连接,造成资源浪费。
大家可以尝试模拟这么一个场景,在两次握手的情况下,服务端在收到 SYN
报文后,就进入 ESTABLISHED
状态,意味着这时可以给对方发送数据,但是客户端此时还没有进入 ESTABLISHED
状态,假设这次是历史连接,客户端判断到此次连接为历史连接,那么就会回 RST
报文来断开连接,而服务端在第一次握手的时候就进入 ESTABLISHED
状态,所以它可以发送数据的,但是它并不知道这个是历史连接,它只有在收到 RST
报文后,才会断开连接。
可以看到,如果采用两次握手或一次握手建立 TCP 连接的场景下,服务端在向客户端发送数据前,并没有阻止掉历史连接,导致服务端建立了一个历史连接,又白白发送了数据,妥妥地浪费了服务端的资源。
因此,要解决这种现象,最好就是在服务端发送数据前,也就是建立连接之前,要阻止掉历史连接,这样就不会造成资源浪费,而要实现这个功能,就需要三次握手。
TCP 协议的通信双方, 都必须维护一个「序列号」, 序列号是可靠传输的一个关键因素,它的作用:
可见,序列号在 TCP 连接中占据着非常重要的作用,所以当客户端发送携带「初始序列号」的 SYN
报文的时候,需要服务端回一个 ACK
应答报文,表示客户端的 SYN
报文已被服务端成功接收,那当服务端发送「初始序列号」给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。
而对于四次握手,也能够实现可靠的同步双方的初始化序号,但由于第二步和第三步可以优化成一步,本着能省则省的持家本领,所以就成了「三次握手」。但是两次握手就完全实现不了这个需求了,一次就更别想了。
如果只有「两次握手」,当客户端发生的 SYN
报文在网络中阻塞,客户端没有接收到 ACK
报文,就会重新发送 SYN
,由于没有第三次握手,服务端不清楚客户端是否收到了自己回复的 ACK
报文,所以服务端每收到一个 SYN
就只能先主动建立一个连接,这会造成什么情况呢?
如果客户端发送的 SYN
报文在网络中阻塞了,重复发送多次 SYN
报文,那么服务端在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。
即两次握手会造成消息滞留情况下,服务端重复接受无用的连接请求 SYN
报文,而造成重复分配资源。
TCP 建立连接时,通过三次握手能防止历史连接的建立,能减少双方不必要的资源开销,能帮助双方同步初始化序列号。序列号能够保证数据包不重复、不丢弃和按序传输。
不使用「两次握手」和「四次握手」的原因:
对了,比四次还多的握手就没那个分析的必要了,总不能一个
ACK
还要发个几百遍对吧,握个手握这么多遍就不礼貌了哈。
当客户端想和服务端建立 TCP 连接的时候,首先第一个发的就是 SYN
报文,然后进入到 SYN_SENT
状态。
在这之后,如果客户端迟迟收不到服务端的 SYN-ACK
报文(第二次握手),就会触发「超时重传」机制,重传 SYN
报文,而且重传的 SYN 报文的序列号都是一样的。
不同版本的操作系统可能超时时间不同,有的1秒的,也有3秒的,这个超时时间是写死在内核里的,如果想要更改则需要重新编译内核,比较麻烦。
当客户端在1秒后没收到服务端的 SYN-ACK
报文后,客户端就会重发 SYN
报文,那到底重发几次呢?
在 Linux 里,客户端的 SYN
报文最大重传次数由 tcp_syn_retries
内核参数控制,这个参数是可以自定义的,默认值一般是 5。
# cat /proc/sys/net/ipv4/tcp_syn_retries
5
通常,第一次超时重传是在1秒后,第二次超时重传是在2秒,第三次超时重传是在4秒后,第四次超时重传是在8秒后,第五次是在超时重传16秒后。没错,每次超时的时间是上一次的 2 倍。
当第五次超时重传后,会继续等待32秒,如果服务端仍然没有回应 ACK
,客户端就不再发送 SYN
包,然后断开 TCP 连接。
所以,总耗时是 1+2+4+8+16+32=63
秒,大约1分钟左右。
举个例子,假设 tcp_syn_retries
参数值为3,那么当客户端的 SYN 报文一直在网络中丢失时,便会发生以下流程:
SYN
报文后,由于 tcp_syn_retries
为3,已达到最大重传次数;2倍
);SYN-ACK
报文),那么客户端就会断开连接。当服务端收到客户端的第一次握手后,就会回 SYN-ACK
报文给客户端,这个就是第二次握手,此时服务端会进入 SYN_RCVD
状态。
第二次握手的 SYN-ACK
报文其实有两个目的 :
ACK
, 是对第一次握手的确认报文;SYN
,是服务端发起建立 TCP 连接的报文;所以,如果第二次握手丢了,就会发生比较有意思的事情,具体会怎么样呢?
因为第二次握手报文里是包含对客户端的第一次握手的 ACK
确认报文,所以,如果客户端迟迟没有收到第二次握手,那么客户端就觉得可能自己的 SYN
报文(第一次握手)丢失了,于是客户端就会触发超时重传机制,重传 SYN 报文。
然后,因为第二次握手中包含服务端的 SYN
报文,所以当客户端收到后,需要给服务端发送 ACK
确认报文(第三次握手),服务端才会认为该 SYN
报文被客户端收到了。
那么,如果第二次握手丢失了,服务端就收不到第三次握手,于是服务端这边会触发超时重传机制,重传 SYN-ACK 报文。
在 Linux 下,SYN-ACK
报的最大重传次数由 tcp_synack_retries
内核参数决定,默认值是 5。
# cat /proc/sys/net/ipv4/tcp_synack_retries
5
因此,当第二次握手丢失了,客户端和服务端都会重传:
SYN 报文
,也就是第一次握手,最大重传次数由 tcp_syn_retries
内核参数决定;SYN-ACK
报文,也就是第二次握手,最大重传次数由 tcp_synack_retries
内核参数决定。举个例子,假设 tcp_syn_retries
参数值为1,tcp_synack_retries
参数值为2,那么当第二次握手一直丢失时便会发生以下流程:
SYN
报文后,由于 tcp_syn_retries
为1,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的2倍
),如果还是没能收到服务端的第二次握手(SYN-ACK
报文),那么客户端就会断开连接;SYN-ACK
报文后,由于 tcp_synack_retries
为2,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的2倍
),如果还是没能收到客户端的第三次握手(ACK
报文),那么服务端就会断开连接。客户端收到服务端的 SYN-ACK
报文后,就会给服务端回一个 ACK
报文,也就是第三次握手,此时客户端状态进入到 ESTABLISH
状态。
因为这个第三次握手的 ACK
是对第二次握手的 SYN
的确认报文,所以当第三次握手丢失了,如果服务端那一方迟迟收不到这个确认报文,就会触发超时重传机制,重传 SYN-ACK
报文,直到收到第三次握手,或者达到最大重传次数。
注意,ACK 报文是不会有重传的,当 ACK 丢失了,就由对方重传对应的报文。
举个例子,假设 tcp_synack_retries
参数值为 2,那么当第三次握手一直丢失时便会发生以下流程:
SYN-ACK
报文后,由于 tcp_synack_retries
为2,已达到最大重传次数;2倍
);ACK
报文),那么服务端就会断开连接。天下没有不散的宴席,对于 TCP 连接也是这样, TCP 断开连接是通过四次挥手方式。
四次挥手即终止 TCP 连接,就是指断开一个 TCP 连接时,需要客户端和服务端总共发送4个包以确认连接的断开。由于 TCP 连接是全双工的,因此,每个方向都必须要单独进行关闭。
双方都可以主动断开连接,断开连接后主机中的「资源」将被释放,四次挥手的过程如下:
FIN
标志位被置为 1
的报文,也即 FIN
报文,之后客户端进入 FIN_WAIT_1
状态。ACK
应答报文,接着服务端进入 CLOSE_WAIT
状态。ACK
应答报文后,之后进入 FIN_WAIT_2
状态。FIN
报文,之后服务端进入 LAST_ACK
状态。FIN
报文后,回一个 ACK
应答报文,之后进入 TIME_WAIT
状态ACK
应答报文后,就进入了 CLOSE
状态,至此服务端已经完成连接的关闭。2MSL
一段时间后,自动进入 CLOSE
状态,至此客户端也完成连接的关闭。可以看到,每个方向都需要一个 FIN
和一个 ACK
,因此通常被称为四次挥手。
有一点值得注意是:主动关闭连接的,才有 TIME_WAIT
状态。
再来回顾下四次挥手双方发 FIN
包的过程,就能理解为什么需要四次了。
FIN
时,仅仅表示客户端不再发送数据了但是还能接收数据。FIN
报文时,先回一个 ACK
应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN
报文给客户端来表示同意现在关闭连接。从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK
和 FIN
一般都会分开发送,因此是需要四次挥手。
但也有特殊情况会变成三次分手:当被动关闭方(上图的服务端)在 TCP 挥手过程中,「没有数据要发送」并且「开启了 TCP 延迟确认机制」,那么第二和第三次挥手就会合并传输,这样就出现了三次挥手。
断开连接的过程顶破天也就三次或者四次了,太少的话达不到分手的目的,太多的话分手的开销也太大了。
当客户端(主动关闭方)调用 close
函数后,就会向服务端发送 FIN
报文,试图与服务端断开连接,此时客户端的连接进入到 FIN_WAIT_1
状态。
正常情况下,如果能及时收到服务端(被动关闭方)的 ACK
,则会很快变为 FIN_WAIT2
状态。
如果第一次挥手丢失了,那么客户端迟迟收不到被动方的 ACK
的话,也就会触发超时重传机制,重传 FIN
报文,重发次数由 tcp_orphan_retries
参数控制。
当客户端重传 FIN
报文的次数超过 tcp_orphan_retries
后,就不再发送 FIN
报文,则会在等待一段时间(时间为上一次超时时间的2倍
),如果还是没能收到第二次挥手,那么直接进入到 close
状态。
举个例子,假设 tcp_orphan_retries 参数值为 3,当第一次挥手一直丢失时便会发生以下流程:
FIN
报文后,由于 tcp_orphan_retries 为
3,已达到最大重传次数;2倍
);ACK
报文),那么客户端就会断开连接。当服务端收到客户端的第一次挥手后,就会先回一个 ACK
确认报文,此时服务端的连接进入到 CLOSE_WAIT
状态。
在前面我们也提了,ACK
报文是不会重传的,所以如果服务端的第二次挥手丢失了,客户端就会触发超时重传机制,重传 FIN
报文,直到收到服务端的第二次挥手,或者达到最大的重传次数。
举个例子,假设 tcp_orphan_retries
参数值为2,当第二次挥手一直丢失时便会发生以下流程:
FIN
报文后,由于 tcp_orphan_retries
为2,已达到最大重传次数;2倍
);ACK
报文),那么客户端就会断开连接。这里提一下,当客户端收到第二次挥手,也就是收到服务端发送的 ACK
报文后,客户端就会处于 FIN_WAIT2
状态,在这个状态需要等服务端发送第三次挥手,也就是服务端的 FIN
报文。
对于 close
函数关闭的连接,由于无法再发送和接收数据,所以FIN_WAIT2
状态不可以持续太久,而 tcp_fin_timeout
控制了这个状态下连接的持续时长,默认值是 60秒
。
这意味着对于调用 close
关闭的连接,如果在 60秒
后还没有收到 FIN
报文,客户端(主动关闭方)的连接就会直接关闭,如下图:
但是注意,如果主动关闭方使用 shutdown
函数关闭连接,指定了只关闭发送方向,而接收方向并没有关闭,那么意味着主动关闭方还是可以接收数据的。
此时,如果主动关闭方一直没收到第三次挥手,那么主动关闭方的连接将会一直处于 FIN_WAIT2
状态(tcp_fin_timeout
无法控制 shutdown
关闭的连接)。如下图:
当服务端(被动关闭方)收到客户端(主动关闭方)的 FIN
报文后,内核会自动回复 ACK
,同时连接处于 CLOSE_WAIT
状态,顾名思义,它表示等待应用进程调用 close
函数关闭连接。
此时,内核是没有权利替代进程关闭连接,必须由进程主动调用 close
函数来触发服务端发送 FIN
报文。
服务端处于 CLOSE_WAIT
状态时,调用了 close
函数,内核就会发出 FIN
报文,同时连接进入 LAST_ACK
状态,等待客户端返回 ACK
来确认连接关闭。
如果迟迟收不到这个 ACK
,服务端就会重发 FIN
报文,重发次数仍然由 tcp_orphan_retries
参数控制,这与客户端重发 FIN
报文的重传次数控制方式是一样的。
举个例子,假设 tcp_orphan_retries
= 3,当第三次挥手一直丢失时便会发生以下流程:
tcp_orphan_retries
为3,达到了重传最大次数,于是再等待一段时间(时间为上一次超时时间的2倍
),如果还是没能收到客户端的第四次挥手(ACK
报文),那么服务端就会断开连接;close
函数关闭连接的,处于 FIN_WAIT_2
状态是有时长限制的,如果 tcp_fin_timeout
时间内还是没能收到服务端的第三次挥手(FIN
报文),那么客户端就会断开连接。当客户端收到服务端的第三次挥手的 FIN
报文后,就会回 ACK
报文,也就是第四次挥手,此时客户端连接进入 TIME_WAIT
状态。
在 Linux 系统,TIME_WAIT
状态会持续 2MSL
后才会进入关闭状态。
然后,服务端(被动关闭方)没有收到 ACK
报文前,还是处于 LAST_ACK
状态。
如果第四次挥手的 ACK
报文没有到达服务端,服务端就会重发 FIN 报文,重发次数仍然由前面介绍过的 tcp_orphan_retries
参数控制。
举个例子,假设 tcp_orphan_retries
为2,当第四次挥手一直丢失时便会发生以下流程:
tcp_orphan_retries
为2, 达到了最大重传次数,于是再等待一段时间(时间为上一次超时时间的2倍
),如果还是没能收到客户端的第四次挥手(ACK
报文),那么服务端就会断开连接。TIME_WAIT
状态,开启时长为 2MSL
的定时器,如果途中再次收到第三次挥手(FIN
报文)后,就会重置定时器,当等待 2MSL
时长后,客户端就会断开连接。