4.1 TCP 三次握手与四次挥手面试题
TCP 基本认识
什么是 TCP?
一种传输层通信协议。
- 面向连接:一对一连接
- 可靠的:通过序列号和确认应答号保证报文无差错、不重复、有序到达接收端;
- 字节流:消息被编码成字节报文帧,一个用户消息会被拆分成多个报文或者多个消息一个报文,流式传输
- 可能出现粘包:多个消息的某个部分在同一个TCP报文中,消息间无明显边界
- 可以通过以下方式避免
- 1、特殊字符作为边界:如回车或换行,会导致消息中无法使用这类特殊字符
- 2、自定义消息结构,如json结构
- 通过以下四元组确定一个 TCP 连接
- 源地址、目的地址(IP头部中,确定主机)
- 源端口、目的端口(TCP头部中,确定进程)
- 最大连接数就是 IP 数 ✖️ 端口数,但是每个连接都要占用内存,且系统有最大文件打开数量,所以很难达到理论值
- 适用于文件传输、HTTP等
UDP
- 无连接:支持多对多通信
- 不可靠传输:尽最大可能交付数据,无序,可能丢包
- 面向报文:每个 UDP 报文就是一个用户完整消息,由IP层进行分片,TCP在传输层分片
- 只需要确定目标端口和源端口(确定对方的进程即可)
- 适用于DNS、视频、广播等
TCP 和 UDP 允许使用同一个端口,因为同一个进程可以同时有可靠和不可靠传输,直播软件等直播是UDP、用户信息、聊天时TCP这种,在内核中时完全独立的,其实是在内核中两个进程
一般因为 UDP 不需要保证可靠性,所以 UDP 更快,但是对于一些需要可靠的,比如语音电话(需要保证有序),那么丢包也需要重传 但是 UDP 本身不会分片,依赖 IP 层的分片,所以如果丢包后,会重传一个大的数据包,而 TCP 本身会分片,丢包后,会重传对应的分段就行,这个时候 UDP 可能就会更慢了,如果这个时候 UDP 也实现了分段,那其实就差不多的。
TCP 连接建立
建立过程
- 1、服务端主动监听某个端口,处于
LISTEN状态 - 2、客户端:发送给服务端请求连接,之后客户端处于
SYN-SENT状态。- 随机初始化序号(
client_isn),避免上一次连接的数据被下一次连接接收(序列号在下一次的有效范围内),造成混乱 - 如果报文丢失,会触发超时重传,一般五次,每次等待时间翻倍(1s 开始),一直收不到断开本次连接
- 随机初始化序号(
- 3、服务端收到
SYN报文后,发给客户端响应报文,之后服务端处于SYN-RCVD状态- 随机初始化序号(
server_isn) - 确认应答号:
client_isn + 1 - 如果报文丢失,客户端没有收到 ACK 报文,会重传,客户端没有收到 ACK 报文,也会重传
- 这个是响应和请求报文合并了,所以少一次握手
- 随机初始化序号(
- 4、客户端收到服务端报文后,发送给服务端响应报文,之后客户端处于
ESTABLISHED状态- 确认应答号:
server_isn + 1 - 可以携带客户到服务端的数据,因为大概率能成功
- 如果报文丢失,服务器会一直收不到 ACK 报文,会重传 SKY-ACK 报文
- ACK 报文是不会有重传的,只能等对方重传
- 确认应答号:
- 5、服务端收到客户端的应答报文后,也进入
ESTABLISHED状态,可以正常响应数据 - 之后相互发送数据,前面因为还没建立,发送数据容易出问题
- 不需要 accept 进行建立,accept 只是负责从 TCP 全连接队列取出一个已经建立连接的 socket 给应用程序使用
- 没有 listen 可以最按揭,但是没啥用
为什么三次握手:
- 1、确保双方具备接受和发送的能力
- 2、组织重复历史连接的初始化
- 如果客户端发送 SYN 报文后宕机,重启后重新发送 SYN 报文
- 两次报文都到达服务器,服务器回复两个 SYN 报文,但是序列号不一样
- 客户端对于旧的报文的回复会因为匹配不上自己预期的序列号,回复 RST,服务器释放旧的连接,避免了重复历史连接的初始化
- 如果只有两次握手,服务器回复 SYN 报文就已经 established 了,可能会直接发送数据,浪费资源,如果一次握手,服务器可能都收不到 RST 报文
- 3、同步双方的初始序列化
- 双方初始化序列号后都需要得到对方的应答,保证双方可靠传输
- 4、避免资源浪费
- 如果少于三次握手:如果服务器没有收到 AKC 报文,会重复发送,服务器会在 ACK 发送之后就建立连接,造成资源浪费
- 三次握手已经建立可靠连接,多于三次握手就没必要了
既然 IP 层会分片,为什么 TCP 层还需要 MSS 呢?
MTU(Maximum Transmit Unit):一个网络包的最大长度,以太网中一般为1500字节(这个是权衡得到的值);(ifconfig 可以进行查看)- 由数据链路层提供
MSS:除去 IP 和 TCP 头部之后,一个网络包所能容纳的 TCP 数据的最大长度;- 如果 TCP 不进行分段,只有 IP 层分段,那么如果一个 IP 分片丢失,整个 TCP 报文都要重传,效率很低。
- TCP 协议在建立连接的时候要协商双方的 MSS 值,以双方允许的最小值作为发送的 MSS,每次发送的时候都会重新计算
- 一般 TCP 分段后,IP 就不会分段了,但是如果链路上有设备有更小的 MTU,那么 IP 层可能还是会进行分段,当然如何可以预先知道链路上最小的 MTU,就可以避免 IP 层的二次分片
TCP 连接断开
TCP 四次挥手过程是怎样的?
双方都可以主动断开连接,如果客户端主动断开:
- 1、客户端发送一个
FIN报文,之后客户端进入FIN_WAIT_1状态。- 如果报文丢失,会触发重传
- 2、服务端收到后
- 为 FIN 包插入一个文件结束符
EOF到接收缓冲区中,程序读到这个数据后,向客户端发送ACK应答报文,接着服务端进入CLOSE_WAIT状态。 - 如果报文丢失,客户端没收到 ACK 报文,就会触发重传
- 服务端还可以单向发送数据,客户端可以接收数据不能发送
- 为 FIN 包插入一个文件结束符
- 3、客户端收到服务端的
ACK应答报文后,之后进入FIN_WAIT_2状态- 服务端发送完数据后,向客户端发送
FIN报文,之后服务端进入LAST_ACK状态。- 这个阶段不会持续太久,一般超过 60s 没收到服务端的 FIN 报文,客户端直接关闭
- 如果报文丢失,服务端会触发超时重传,一直没收到,服务端主动关闭(客户端保证了自己一段时间后自动关闭)
- 如果服务端没有数据发送,那么 FIN 报文和 ACK 报文可以合并发送,成为三次挥手,或者将数据一次整合,和 FIN-ACK 报文一起发送,也是三次挥手
- 服务端发送完数据后,向客户端发送
- 4、客户端收到服务端的
FIN报文后,回一个ACK应答报文,之后进入TIME_WAIT状态- 如果报文丢失,服务端会触发 FIN 包重传
- 服务端收到了
ACK应答报文后,就进入了CLOSE状态,至此服务端已经完成连接的关闭。 - 客户端在经过
2MSL一段时间后,自动进入CLOSE状态,至此客户端也完成连接的关闭。 - FIN 报文是需要进程主动发起的,ACK 报文是内核自动响应的
- 挥手四次是因为被动断开方可能还需要发送数据
time_wait
- 主动关闭方,才会有该状态
- 为什么等待时间是 2MSL(Maximum Segment Lifetime,报文最大生存时间)
- 至少允许 ACK 报文丢失一次,当 ACK 丢失,服务端会重传 FIN 报文
- 而如果网络不好,多次丢失,那么也没必要等待了,并且长时间 time_wait 会一直占用资源
- 每次收到服务端的 FIN 报文,这个时间会重置
- 为什么需要 time_wait 状态
- 防止历史连接中的数据,被后面相同四元组的连接错误接受
- 本次连接中数据,会在这段时间里面被丢弃或者被接收,不会保留到下一次连接中(避免有数据包在下一次连接的接受窗口内造成数据混乱)
- 保证被动关闭连接以防,能被正确的关闭
- 如果客户端的 ACK 报文丢失,服务端会重传 FIN 报文,如果没有 time_wait,客户端直接关闭连接,收到重传的 FIN 报文,会回 RST 报文,服务端收到后认为是一个错误,结束的不够优雅
- 防止历史连接中的数据,被后面相同四元组的连接错误接受
- time_wait 过多会占用系统资源,造成一些不可用的情况
- 优化:由客户端持有 time_wait
- 使用长连接,如果使用短连接,由于需要服务端主动关闭连接,服务端会出现大量的 TIME_WAIT
- 客户端长时间不发送请求,主动断开连接,否则nginx 会在超时后调用服务端主动断开连接,导致服务端持有大理胖 time_wait,客户端需要发送时,重新建立连接
如果已经建立了连接,出现故障了怎么办?
- 客户端:如宕机、断电
- 服务端无法感知,连接会一直处于
ESTABLISH状态 - 服务端会触发保活机制,发送探测报文,如果多次没有收到响应,认为当前 TCP 连接 死亡,断开连接,如果得到响应,重置保活时间
- 如果客户端没有启动,那么服务端收不到任何响应
- 如果客户端重启了,那么因为本地找不到对应的 TCP 连接,会发送 RST 报文,服务端直接关闭
- 由于时间比较长,可以在客户端增加探活
- 如果是进程崩溃,操作系统在挥手资源时会发送 FIN 报文,主动断开连接
- 服务端无法感知,连接会一直处于
- 服务端:
