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