文章目录
- 1. TCP头部包含哪些内容?
- 2. 为什么需要 TCP 协议? TCP 工作在哪一层?
- 3. 什么是 TCP ?
- 4. 什么是 TCP 连接?
- 5. 如何唯一确定一个 TCP 连接呢?
- 6. UDP头部大小是多少?包含哪些内容?
- 7. TCP与UDP的区别?
- 9. TCP 和 UDP 可以使用同一个端口吗?
- 10. TCP 三次握手过程是怎样的?
- 11. 如何在 Linux 系统中查看 TCP 状态?
- 12. 为什么是三次握手?不是两次、四次?
- 13. 为什么每次建立 TCP 连接时,初始化的序列号都要求不一样呢?
- 14. 初始序列号 ISN 是如何随机产生的?
- 15. 既然 IP 层会分片,为什么 TCP 层还需要 MSS 呢?
- 16. 第一次握手丢失了,会发生什么?
- 17. 第二次握手丢失了,会发生什么?
- 18. 第三次握手丢失了,会发生什么?
- 19. 什么是 SYN 攻击?如何避免 SYN 攻击?
- 20. TCP 四次挥手过程是怎样的?
- 21. 为什么挥手需要四次?
- 22. 第一次挥手丢失了,会发生什么?
- 23. 第二次挥手丢失了,会发生什么?
- 24. 第三次挥手丢失了,会发生什么?
- 25. 第四次挥手丢失了,会发生什么?
- 26. 为什么 TIME_WAIT 等待的时间是 2MSL?
- 27. 为什么需要 TIME_WAIT 状态?
- 28. 过多的TIME_WAIT 状态有什么危害?
- 29. 服务器出现大量 TIME_WAIT 状态的原因有哪些?
- 30. 如果已经建立了连接,但是客户端突然出现了宕机或者断电,怎么办?
- 31. 当服务器向客户端发送TCP保活的探测报文,后续会出现哪几种情况?
- 32. 如果已经建立了连接,但是服务端的进程崩溃会发生什么?
- 33. 针对 TCP 应该如何 Socket 编程?
- 34. listen 时候参数 backlog 的意义?
- 35. accept 发生在三次握手的哪一步?
- 36. 没有 accept,能建立 TCP 连接吗?
- 37. 没有 listen,能建立 TCP 连接吗?
- 38. 说说 TCP 如何保证数据的可靠性传输?
- 39. TCP针对数据包丢失的情况,有哪些重传机制?
- 40. 滑动窗口
- 41. 流量控制
- 42. 拥塞控制
- 43. 什么是TCP半连接队列和全连接队列?
- 44. 如何理解是 TCP 面向字节流协议?
- 45. 如何解决TCP粘包?
- 46. SYN 报文什么时候情况下会被丢弃?
- 47. 防御 SYN 攻击的方法有哪些?
- 48. 一个已经建立的 TCP 连接,客户端中途宕机了,而服务端此时也没有数据要发送,一直处于 Established 状态,客户端恢复后,向服务端建立连接,此时服务端会怎么处理?
- 49. 如何关闭一个 TCP 连接?
- 50. 在 FIN_WAIT_2 状态下,是如何处理收到的乱序 FIN 报文,然后 TCP 连接又是什么时候才进入到 TIME_WAIT 状态?
- 51. 在 TCP 正常挥手过程中,处于 TIME_WAIT 状态的连接,收到相同四元组的 SYN 后会发生什么?
- 52. TCP连接,客户端进程崩溃和客户端主机宕机发生后,有什么区别?
- 53. 拔掉网线后, 原本的 TCP 连接还存在吗?
- 54. HTTPS 中 TLS 和 TCP 能同时握手吗?
- 55. TCP Keepalive 和 HTTP Keep-Alive 是一个东西吗?
- 56. 多个 TCP 服务进程可以同时绑定同一个端口吗?
- 57. 如何解决服务端重启时,报错“Address already in use”的问题?
- 58. 客户端的端口可以重复使用吗?
- 59. 客户端 TCP 连接 TIME_WAIT 状态过多,会导致端口资源耗尽而无法建立新的连接吗?
- 60. 没有 accept,能建立 TCP 连接吗?
- 61. 为什么半连接队列要设计成哈希表?
- 62. 有没有一种方法可以绕过半连接队列?
- 63. 没有listen,为什么还能建立连接?
- 64. 介绍以下关闭连接的close函数和shutdown函数?
- 65. 什么是 TCP 延迟确认机制?
- 66. TCP 四次挥手,可以变成三次吗?
由于IP
层是不可靠的,它不保证网络包的交付、不保证网络包的按序交付、也不保证网络包中的数据的完整性。
为了保证网络数据包的可靠性传输,那么就需要由传输层的 TCP 协议来负责。因为 TCP 是一个工作在传输层的可靠数据传输的服务,它能确保接收端接收的网络包是无损坏、无间隔、非冗余和按序的。
TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。
用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括 Socket、序列号和窗口大小称为连接。
所以我们可以知道,建立一个 TCP 连接是需要客户端与服务端达成上述三个信息的共识。
TCP 四元组可以唯一的确定一个连接,四元组包括如下:源IP地址、源端口、目标IP地址、目标端口。
UDP头部占8个字节,包含:源端口号(16位)、目标端口号(16)位、包长度(16位)、校验和(16位)。
TCP
是面向连接的传输层协议,传输数据前先要建立连接。UDP
是不需要连接,即刻传输数据。TCP
是一对一的两点服务,即一条连接只有两个端点。UDP
支持一对一、一对多、多对多的交互通信。TCP
是可靠交付数据的,数据可以无差错、不丢失、不重复、按序到达。UDP
是尽最大努力交付,不保证可靠交付数据。但是我们可以基于 UDP 传输协议实现一个可靠的传输协议,比如 QUIC
协议。TCP
有拥塞控制和流量控制机制,保证数据传输的安全性。UDP
则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率。TCP
首部长度较长,会有一定的开销,首部在没有使用选项字段时是 20
个字节,如果使用了选项字段则会变长的。UDP
首部只有 8
个字节,并且是固定不变的,开销较小。TCP
是基于字节流进行数据传输,没有边界,但保证顺序和可靠。UDP
是一个包一个包的发送,是有边界的,但可能会丢包和乱序。TCP
的数据大小如果大于 MSS 大小,则会在传输层进行分片,目标主机收到后,也同样在传输层组装 TCP 数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片。UDP
的数据大小如果大于 MTU 大小,则会在 IP 层进行分片,目标主机收到后,在 IP 层组装完数据,接着再传给传输层。TCP
是面向连接,能保证数据的可靠性交付,因此经常用于:FTP 文件传输、HTTP / HTTPS等。UDP
面向无连接,它可以随时发送数据,再加上 UDP 本身的处理既简单又高效,因此经常用于:包总量较少的通信,如 DNS 、SNMP 等;视频、音频等多媒体通信;广播通信。可以的。传输层有两个传输协议分别是 TCP 和 UDP,在内核中是两个完全独立的软件模块。因此,TCP/UDP 各自的端口号也相互独立,如 TCP 有一个 80 号端口,UDP 也可以有一个 80 号端口,二者并不冲突。
一开始,客户端和服务端都处于 CLOSE
状态,先是服务端主动监听某个端口,处于 LISTEN
状态。
第1次握手
客户端会随机初始化序号(client_isn
),将此序号置于 TCP 首部的序号字段中,同时把 SYN
标志位置为 1
,表示 SYN 报文。接着把第一个 SYN
报文发送给服务端,表示向服务端发起连接,之后客户端处于 SYN-SENT
状态。
第2次握手
服务端收到客户端的 SYN
报文后,首先服务端也随机初始化自己的序号(server_isn
),将此序号填入 TCP
首部的序号字段中,其次把 TCP
首部的确认应答号字段填入 client_isn + 1
, 接着把 SYN
和 ACK
标志位置为 1
。最后把该报文发给客户端,之后服务端处于 SYN-RCVD
状态。
第3次握手
客户端收到服务端报文后,还要向服务返回最后一个应答报文,首先该应答报文 TCP
首部 ACK
标志位置为 1
,其次确认应答号字段填入 server_isn + 1
,最后把报文发送给服务端,这次报文可以携带客户到服务端的数据,之后客户端处于 ESTABLISHED
状态。服务端收到客户端的应答报文后,也进入 ESTABLISHED
状态。
注意⚠️:第三次握手是可以携带数据的,前两次握手是不可以携带数据的。
在 Linux 可以通过 netstat -napt
命令查看TCP的连接状态。
ISN = M + F(localhost, localport, remotehost, remoteport)。
如果在 TCP 的整个报文(头部 + 数据)交给 IP 层进行分片,那么当如果一个 IP 分片丢失,整个 IP 报文的所有分片都得重传。
因为当某一个 IP 分片丢失后,接收方的 IP 层就无法组装成一个完整的 TCP 报文(头部 + 数据),也就无法将数据报文送到 TCP 层,所以接收方不会响应 ACK 给发送方,因为发送方迟迟收不到 ACK 确认报文,所以会触发超时重传,就会重传整个 TCP 报文(头部 + 数据)。经过 TCP 层分片后,如果一个 TCP 分片丢失后,进行重发时也是以 MSS
为单位,只需要重传丢失的TCP分片,大大增加了重传的效率。
如果达到超时时间,客户端还没有收到服务端的 SYN-ACK 报文(第二次握手),就会触发超时重传机制,重传 SYN
报文,而且重传的 SYN 报文的序列号都是一样的,每次超时的时间是上一次的 2 倍。当客户端达到最大重传次数,再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到服务端的第二次握手(SYN-ACK 报文),那么客户端就会断开连接。
当第二次握手丢失了,客户端和服务端都会重传:
当达到最大重传次数,再等待一段时间(时间为上一次超时时间的 2 倍),如果还没收到对端的响应,就会断开连接。
当第三次握手丢失了,如果服务端那一方迟迟收不到这个确认报文,就会触发超时重传机制,重传 SYN-ACK 报文,直到收到第三次握手,或者达到最大重传次数,再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到客户端的第三次握手(ACK 报文),那么服务端就会断开连接。
SYN 攻击方式最直接的表现就会把 TCP 半连接队列打满,这样当 TCP 半连接队列满了,后续再在收到 SYN 报文就会丢弃,导致客户端无法和服务端建立连接。
最常见的就是攻击者短时间伪造不同 IP 地址的 SYN 报文,服务端每接收到一个 SYN 报文,就进入SYN_RCVD 状态,但服务端发送出去的 ACK + SYN 报文,无法得到未知 IP 主机的 ACK 应答,久而久之就会占满服务端的半连接队列,使得服务端不能为正常用户服务。
避免SYN攻击的方法:调大 netdev_max_backlog、增大 TCP 半连接队列、开启 tcp_syncookies、减少 SYN+ACK 重传次数。
第一次挥手
客户端打算关闭连接,此时会发送一个 TCP 首部 FIN
标志位被置为 1
的报文,也即 FIN
报文,之后客户端进入 FIN_WAIT_1
状态。
第二次挥手
服务端收到该报文后,就向客户端发送 ACK
应答报文,接着服务端进入 CLOSE_WAIT
状态。
客户端收到服务端的 ACK
应答报文后,之后进入 FIN_WAIT_2
状态。
第三次挥手
等待服务端处理完数据后,也向客户端发送 FIN
报文,之后服务端进入 LAST_ACK
状态。
第四次挥手
客户端收到服务端的 FIN
报文后,回一个 ACK
应答报文,之后进入 TIME_WAIT
状态。然后再经过 2MSL
时间后,自动进入 CLOSE
状态,至此客户端完成连接的关闭。
服务端收到了 ACK
应答报文后,就进入了 CLOSE
状态,至此服务端完成连接的关闭。
服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK 和 FIN 一般都会分开发送,因此是需要四次挥手。但是在特定情况下,四次挥手是可以变成三次挥手的。
当被动关闭方在 TCP 挥手过程中,如果没有数据要发送,同时没有开启 TCP_QUICKACK(默认情况就是没有开启,没有开启 TCP_QUICKACK,等于就是在使用 TCP 延迟确认机制),那么第二和第三次挥手就会合并传输,这样就出现了三次挥手。所以,出现三次挥手现象,是因为 TCP 延迟确认机制导致的。
如果第一次挥手丢失了,那么客户端迟迟收不到被动方的 ACK 的话,也就会触发超时重传机制,重传 FIN
报文,当客户端重传 FIN 报文的次数超过最大重传次数后,就不再发送 FIN 报文,则会再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到第二次挥手,那么直接进入到 close
状态。
由于第二次挥手的ACK 报文是不会重传的,所以如果服务端的第二次挥手丢失了,客户端就会触发超时重传机制,重传 FIN
报文,直到收到服务端的第二次挥手,或者达到最大的重传次数后,再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到第二次挥手,那么直接进入到 close
状态。
如果服务器迟迟收不到客户端的 ACK,也就会触发超时重传机制,重传 FIN
报文,当服务端重传 FIN 报文的次数超过最大重传次数后,就不再发送 FIN 报文,则会再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到第四次挥手,那么直接进入到 close
状态。
ACK
,也就会触发超时重传机制,当服务端重传第三次挥手报文达到了最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到客户端的第四次挥手(ACK 报文),那么服务端就会断开连接。TIME_WAIT
状态,开启时长为 2MSL
的定时器,如果途中再次收到第三次挥手(FIN
报文)后,就会重置定时器,当等待 2MSL
时长后,客户端就会断开连接。MSL
报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。
2MSL
时长 这其实是相当于至少允许报文丢失一次。比如,若 ACK 在一个 MSL 内丢失,这样被动方重发的 FIN 会在第 2 个 MSL 内到达,TIME_WAIT 状态的连接可以应对。
为什么不是 4MSL 或者 8MSL 的时长呢?你可以想象一个丢包率达到百分之一的糟糕网络,连续两次丢包的概率只有万分之一,这个概率实在是太小了,忽略它比解决它更具性价比。
在 Linux 系统里 2MSL
默认是 60
秒,那么一个 MSL
也就是 30
秒。Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒。
原因一:防止历史连接中的数据,被后面相同四元组的连接错误的接收
序列号是一个 32 位的无符号数,因此在到达 4G 之后再循环回到 0。初始化序列号可被视为一个 32 位的计数器,该计数器的数值每 4 微秒加 1,循环一次需要 4.55 小时。序列号和初始化序列号并不是无限递增的,会发生回绕为初始值的情况,这意味着无法根据序列号来判断新老数据。
为了防止历史连接中的数据,被后面相同四元组的连接错误的接收,因此 TCP 设计了 TIME_WAIT
状态,状态会持续 2MSL
时长,这个时间足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的。
原因二:保证被动关闭连接的一方,能被正确的关闭
主动断开连接的一方保持TIME-WAIT状态,等待足够的时间以确保最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭。
TIME_WAIT
状态连接过多的话,就会受端口资源限制,如果占满了所有端口资源,那么就无法再跟目的 IP+ 目的 PORT都一样的服务端建立连接了。TIME_WAIT
状态连接的时候,可以排查下是否客户端和服务端都开启了 HTTP Keep-Alive
,因为任意一方没有开启 HTTP Keep-Alive
,都会导致服务端在处理完一个 HTTP
请求后,就主动关闭连接,此时服务端上就会出现大量的 TIME_WAIT
状态的连接。TIME_WAIT
状态的连接时,如果现象是有大量的客户端建立完 TCP
连接后,很长一段时间没有发送数据,那么大概率就是因为 HTTP
长连接超时,导致服务端主动关闭连接,产生大量处于 TIME_WAIT
状态的连接。TIME_WAIT
状态的连接。发生这种情况的时候,如果服务端一直不会发送数据给客户端,那么服务端最初是无法感知到客户端宕机这个事件的,当达到保活时间以后,服务器会主动向客户端发送TCP保活的探测报文,每隔一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,服务器主动断开连接。
TCP
保活时间会被重置,等待下一个 TCP 保活时间的到来。RST
报文,这样很快就会发现 TCP 连接已经被重置。TCP 的连接信息是由内核维护的,所以当服务端的进程崩溃后,内核需要回收该进程的所有 TCP 连接资源,于是内核会发送第一次挥手 FIN 报文,后续的挥手过程也都是在内核完成,并不需要进程的参与,所以即使服务端的进程退出了,还是能与客户端完成 TCP 四次挥手的过程。
socket
,得到用于监听的文件描述符;bind
,将 socket
绑定在指定的 IP
地址和端口;listen
,进行监听;accept
,等待客户端连接;connect
,向服务端的地址和端口发起连接请求;accept
返回用于传输的 socket
的文件描述符;write
写入数据;服务端调用 read
读取数据;close
,那么服务端 read
读取数据的时候,就会读取到了 EOF
,待处理完数据后,服务端调用 close
,表示连接关闭。Linux内核中会维护两个队列:
SYN
队列):接收到一个 SYN
建立连接请求,处于 SYN_RCVD
状态;Accpet
队列):已完成 TCP 三次握手过程,处于 ESTABLISHED
状态;int listen (int socketfd, int backlog)
客户端 connect 成功返回是在第二次握手,服务端 accept 成功返回是在三次握手成功之后。
可以的。accpet 系统调用并不参与 TCP 三次握手过程,它只是负责从 TCP 全连接队列取出一个已经建立连接的 socket,用户层通过 accpet 系统调用拿到了已经建立连接的 socket,就可以对该 socket 进行读写操作了。
可以的。客户端是可以自己连自己的形成连接(TCP自连接),也可以两个客户端同时向对方发出请求建立连接(TCP同时打开),这两个情况都有个共同点,就是没有服务端参与,也就是没有 listen,也能建立TCP连接。
TCP主要提供了检验和、序列号/确认应答、重传机制、最大消息长度、滑动窗口控制、流量控制、拥塞控制等方法实现了可靠性传输。
第一种:超时重传
在发送数据时,设定一个定时器,当超过指定的时间后,没有收到对方的 ACK 确认应答报文,就会重发该数据,也就是我们常说的超时重传。
超时重传时间 RTO 的值应该略大于报文往返 RTT 的值。
如果超时重发的数据,再次超时的时候,又需要重传的时候,TCP 的策略是超时间隔加倍。
第二种:快速重传
快速重传(Fast Retransmit)机制,它不以时间为驱动,而是以数据驱动重传。
快速重传的工作方式是当收到三个相同的 ACK 报文时,会在定时器过期之前,重传丢失的报文段。
快速重传机制只解决了一个问题,就是超时时间的问题。但是它依然面临着另外一个问题,就是重传的时候,是重传一个,还是重传所有的问题。
第三种:SACK 方法
SACK
( Selective Acknowledgment), 选择性确认。
这种方式需要在 TCP 头部选项字段里加一个 SACK
的东西,它可以将已收到的数据的信息发送给发送方,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据。Linux 2.4 后默认打开这个功能。
第四种:Duplicate SACK
Duplicate SACK
又称 D-SACK
,其主要使用了 SACK
来告诉发送方有哪些数据被重复接收了。
窗口大小指无需等待确认应答,而可以继续发送数据的最大值。
假设窗口大小为3个TCP段,那么发送方就可以连续发送3个TCP段,并且中途若有ACK丢失,可以通过下一个确认应答进行确认,这个模式就叫累计确认或者累计应答。
TCP 头里有一个字段叫 Window
,也就是窗口大小。这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据,所以,通常窗口的大小是由接收方的窗口大小来决定的。
接收窗口和发送窗口并不是一定完全相等,接收窗口的大小是约等于发送窗口大小的。因为新的发送窗口大小是通过TCP报文中的Windows字段来告诉发送方,这个传输过程是存在时延的。
TCP 提供一种机制可以让发送方根据接收方的实际接收能力控制发送的数据量,这就是所谓的流量控制。
实际上,发送窗口和接收窗口中所存放的字节数,都是放在操作系统内存缓冲区中的,而操作系统的缓冲区,会被操作系统调整。
TCP 规定是不允许同时减少缓存又收缩窗口的,而是采用先收缩窗口,过段时间再减少缓存,这样就可以避免了丢包情况。
如果窗口大小为 0 时,就会阻止发送方给接收方传递数据,直到窗口变为非 0 为止,这就是窗口关闭。
接收方向发送方通告窗口大小时,是通过 ACK 报文来通告的。那么,当发生窗口关闭时,接收方处理完数据后,会向发送方通告一个窗口非 0 的 ACK 报文,如果这个通告窗口的 ACK 报文在网络中丢失了,这会导致发送方一直等待接收方的非 0 窗口通知,接收方也一直等待发送方的数据,如不采取措施,这种相互等待的过程,会造成了死锁的现象。
为了解决这个死锁问题,TCP 为每个连接设有一个持续定时器,只要 TCP 连接一方收到对方的零窗口通知,就启动持续计时器。如果持续计时器超时,就会发送窗口探测 ( Window probe ) 报文,而对方在确认这个探测报文时,给出自己现在的接收窗口大小。
拥塞控制,控制的目的就是避免发送方的数据填满整个网络。
拥塞窗口 cwnd是发送方维护的一个的状态变量,它会根据网络的拥塞程度动态变化的。
发送窗口的值是swnd = min(cwnd, rwnd)
,也就是拥塞窗口和接收窗口中的最小值。
其实只要发送方没有在规定时间内接收到 ACK 应答报文,也就是发生了超时重传,就会认为网络出现了拥塞。
拥塞控制主要是四个算法:慢启动、拥塞避免、拥塞发生、快速恢复。
(1)慢启动
当发送方每收到一个 ACK
,拥塞窗口 cwnd
的大小就会加 1。可以发现慢启动算法,发包的个数是指数性的增长。
当拥塞窗口大小大于等于慢启动门限 ssthresh
(slow start threshold)时,就会使用拥塞避免算法。
(2)拥塞避免算法
进入拥塞避免算法后,每当收到一个 ACK
时,cwnd
增加 1/cwnd
。
拥塞避免算法就是将原本慢启动算法的指数增长变成了线性增长,还是增长阶段,但是增长速度缓慢了一些。
当拥塞窗口增大到一定程度时,网络慢慢进入拥塞的状况,出现丢包的现象,这时就会触发重传机制,也就进入了拥塞发生算法。
(3)拥塞发生算法
ssthresh
设为 cwnd/2
,cwnd
重置为初始化值。cwnd = cwnd/2
,也就是设置为原来的一半,ssthresh = cwnd
,进入快速恢复算法。(4)快速恢复算法
快速重传和快速恢复算法一般同时使用,进入快速恢复算法如下:
cwnd = ssthresh + 3
( 3 的意思是确认有 3 个数据包被收到了);在 TCP
三次握手的时候,Linux
内核会维护两个队列,分别是:半连接队列,也称 SYN
队列;全连接队列,也称 accept
队列。
服务端收到客户端发起的 SYN
请求后,内核会把该连接存储到半连接队列,并向客户端响应 SYN+ACK
,接着客户端会返回 ACK
,服务端收到第三次握手的 ACK
后,内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其添加到 accept
队列,等待进程调用 accept
函数时把连接取出来。
当用户消息通过 TCP 协议传输时,消息可能会被操作系统分组成多个的 TCP 报文,因此,我们不能认为一个用户消息对应一个 TCP 报文,正因为这样,所以 TCP 是面向字节流的协议。
一般有三种方式分包的方式来解决TCP粘包,分别是:固定长度的消息、特殊字符作为边界、自定义消息结构。
其中对于自定义一个消息结构,由包头和数据组成,其中包头包是固定大小的,而且包头里有一个字段来说明紧随其后的数据有多大。
tcp_tw_recycle
参数,并且在 NAT
环境下,造成 SYN 报文被丢弃;syn
攻击,就有可能导致 TCP 半连接队列满了,这时后面来的syn
包都会被丢弃。但是,如果开启了syncookies
功能,即使半连接队列满了,也不会丢弃syn
包。增大半连接队列、开启 tcp_syncookies
功能、减少 SYN+ACK
重传次数。
这个问题关键要看客户端发送的 SYN 报文中的源端口是否和上一次连接的源端口相同。
1.客户端的 SYN 报文里的端口号与历史连接不相同
如果客户端恢复后发送的 SYN 报文中的源端口号跟上一次连接的源端口号不一样,此时服务端会认为是新的连接要建立,于是就会通过三次握手来建立新的连接。
如果服务端发送了数据包给客户端,由于客户端的连接已经被关闭了,此时客户的内核就会回 RST
报文,服务端收到后就会释放连接。
如果服务端一直没有发送数据包给客户端,在超过一段时间后,TCP 保活机制就会启动,检测到客户端没有存活后,接着服务端就会释放掉该连接。
2. 客户端的 SYN 报文里的端口号与历史连接相同
处于 Established
状态的服务端,如果收到了客户端的 SYN
报文(注意此时的 SYN
报文其实是乱序的,因为 SYN
报文的初始化序列号其实是一个随机数),会回复一个携带了正确序列号和确认号的 ACK
报文,这个 ACK
被称之为 Challenge ACK
。
接着,客户端收到这个 Challenge ACK
,发现确认号(ack num
)并不是自己期望收到的,于是就会回 RST
报文,服务端收到后,就会释放掉该连接。
通过伪造一个能关闭 TCP 连接的 RST
报文,来关闭一个TCP连接。
而且RSA
报文必须同时满足四元组相同和序列号是对方期望的这两个条件。
要想获取到对方期望的序列号,可以借助tcpkill
和killcx
工具。
tcpkill
工具只能用来关闭活跃的 TCP 连接,无法关闭非活跃的 TCP 连接,因为 tcpkill 工具是等双方进行 TCP 通信后,才去获取正确的序列号,如果这条 TCP 连接一直没有任何数据传输,则就永远获取不到正确的序列号。killcx
工具可以用来关闭活跃和非活跃的 TCP 连接,因为 killcx 工具是主动发送 SYN 报文,这时对方就会回复 Challenge ACK ,然后 killcx 工具就能从这个 ACK 获取到正确的序列号。当服务器发送的数据包被网络延迟,客户端处于FIN_WAIT_2
状态,如果提前收到了第三次挥手FIN
报文,那么就被会加入到乱序队列,并不会进入到 TIME_WAIT 状态。
等再次收到前面被网络延迟的数据包时,会判断乱序队列有没有数据,然后会检测乱序队列中是否有可用的数据,如果能在乱序队列中找到与当前报文的序列号保持的顺序的报文,就会看该报文是否有 FIN
标志,如果发现有 FIN
标志,这时才会进入 TIME_WAIT
状态。
针对这个问题,关键是要看SYN的序列号和时间戳是否合法,然后根据收到的SYN是否合法做不同的处理。
TIME_WAIT
状态的连接收到合法的 SYN后,就会重用此四元组连接,跳过 2MSL 而转变为 SYN_RECV 状态,接着就能进行建立连接过程。TIME_WAIT
状态的连接收到非法的 SYN后,就会再回复一个第四次挥手的 ACK 报文,客户端收到后,发现并不是自己期望收到确认号(ack num),就回 RST 报文给服务端。如果客户端进程崩溃,客户端的进程在发生崩溃的时候,内核会发送FIN
报文,与服务器进行四次挥手。
如果客户端主机宕机,根据服务器是否发送数据,分情况讨论:
TCP keepalive
机制:
TCP keepalive
机制,探测对方是否存在,如果探测到对方已经消亡,则会断开自身的 TCP 连接;ESTABLISHED
状态。客户端拔掉网线后,并不会直接影响 TCP 连接状态。所以,拔掉网线后,TCP 连接是否还会存在,关键要看拔掉网线之后,有没有进行数据传输。
有数据传输的情况:
RST
报文,客户端收到后就会断开 TCP 连接。至此, 双方的 TCP 连接都断开了。没有数据传输的情况:
TCP keepalive
机制,那么在客户端拔掉网线后,如果客户端一直不插回网线,那么客户端和服务端的 TCP 连接状态将会一直保持存在。TCP keepalive
机制,那么在客户端拔掉网线后,如果客户端一直不插回网线,TCP keepalive
机制会探测到对方的 TCP 连接没有存活,于是就会断开 TCP 连接。而如果在 TCP 探测期间,客户端插回了网线,那么双方原本的 TCP 连接还是能正常存在。在同时满足以下两个条件的情况下,HTTPS 中的 TLS 握手过程可以同时进行TCP三次握手:
TCP Keepalive 和 HTTP Keep-Alive是两个完全不同的机制。
如果两个 TCP 服务进程同时绑定的 IP 地址和端口都相同,那么执行 bind() 时候就会出错,错误是“Address already in use”。
如果两个 TCP 服务进程绑定的端口都相同,而 IP 地址不同,那么执行 bind() 不会出错。
我们可以对 socket
设置 SO_REUSEADDR
属性,这样即使存在一个和绑定 IP+PORT 一样的 TIME_WAIT 状态的连接,依然可以正常绑定成功,因此可以正常重启成功。
在客户端执行 connect
函数的时候,只要客户端连接的服务器不是同一个,内核允许端口重复使用。
TCP 连接是由四元组(源IP地址,源端口,目的IP地址,目的端口)唯一确认的,那么只要四元组中其中一个元素发生了变化,那么就表示不同的 TCP 连接的。
要看客户端是否都是与同一个服务器(目标地址和目标端口一样)建立连接。
如果客户端都是与同一个服务器(目标地址和目标端口一样)建立连接,那么如果客户端 TIME_WAIT 状态的连接过多,当端口资源被耗尽,就无法与这个服务器再建立连接了。即使在这种状态下,还是可以与其他服务器建立连接的,只要客户端连接的服务器不是同一个,那么端口是重复使用的。
建立连接的过程中根本不需要accept()参与, 执行accept()只是为了从全连接队列里取出一条连接,因此可以建立TCP连接。
虽然都叫队列,但其实全连接队列(icsk_accept_queue)是个链表,而半连接队列(syn_table)是个哈希表。
先对比下全连接里队列,他本质是个链表,因为也是线性结构,说它是个队列也没毛病。它里面放的都是已经建立完成的连接,这些连接正等待被取走。而服务端取走连接的过程中,并不关心具体是哪个连接,只要是个连接就行,所以直接从队列头取就行了。这个过程算法复杂度为O(1)。
而半连接队列却不太一样,因为队列里的都是不完整的连接,嗷嗷等待着第三次握手的到来。那么现在有一个第三次握手来了,则需要从队列里把相应IP端口的连接取出,如果半连接队列还是个链表,那我们就需要依次遍历,才能拿到我们想要的那个连接,算法复杂度就是O(n)。
而如果将半连接队列设计成哈希表,那么查找半连接的算法复杂度就回到O(1)了。
半连接队列满了,可能是因为受到了SYN Flood
攻击,可以设置tcp_syncookies
,绕开半连接队列。
当tcp_syncookies
被设置为1的时候,客户端发来第一次握手SYN时,服务端不会将其放入半连接队列中,而是直接生成一个cookies
,这个cookies
会跟着第二次握手,发回客户端。客户端在发第三次握手的时候带上这个cookies
,服务端验证到它就是当初发出去的那个,就会建立连接并放入到全连接队列中。可以看出整个过程不再需要半连接队列的参与。
内核有个全局hash
表,可以用于存放sock
连接的信息。在TCP自连接的情况中,客户端在connect方法时,最后会将自己的连接信息放入到这个全局hash表中,然后将信息发出,消息在经过回环地址重新回到TCP传输层的时候,就会根据IP端口信息,再一次从这个全局hash中取出信息。于是握手包一来一回,最后成功建立连接。
注意⚠️:shutdown 函数也可以指定只关闭读取方向,而不关闭发送方向,但是这时候内核是不会发送 FIN 报文的,因为发送 FIN 报文是意味着我方将不再发送任何数据,而 shutdown 如果指定不关闭发送方向,就意味着 socket 还有发送数据的能力,所以内核就不会发送 FIN。
TCP 延迟确认的策略:
当被动关闭方在 TCP 挥手过程中,如果没有数据要发送,同时没有开启 TCP_QUICKACK(默认情况就是没有开启,没有开启 TCP_QUICKACK,等于就是在使用 TCP 延迟确认机制),那么第二和第三次挥手就会合并传输,这样就出现了三次挥手。
所以,出现三次挥手现象,是因为 TCP 延迟确认机制导致的。