TCP 基本认识
什么是 TCP ?
TCP 是一种 面向连接、可靠、基于字节流的协议
什么是 TCP 连接?
Socket + Seq + WindowSize
- Socket:IP + Port
- Seq:序列号
- WindowSize:窗口大小
如何唯一确定一个 TCP 连接
源 IP + 源 Port + 目的 IP + 目的 Port
TCP 连接数的上限?
理论上限:32 位 IP + 16 位 Port,即 2^48
实际远远达不到,取决于:
- 最大文件描述符数量(三个等级:系统级、用户级、进程级)
- 系统内存大小
TCP 与 UDP 区别?使用场景有什么不同?
- TCP 有连接,UDP 无连接
- TCP 可靠,UDP 不保证可靠(尽最大努力交付,可以在应用层实现可靠,如 QUIC)
- TCP 一对一,UDP 可以一对一、一对多、多对多
- TCP 有流量控制、拥塞窗口等,UDP 没有
- TCP 面向字节流,UDP 面向报文
- 分片机制:TCP 在传输层就可以分片,UDP 仅可能在 IP(网络层)分片
- 首部开销:TCP 的首部较大,UDP 较小
使用场景:
TCP 适用于需要保证绝对可靠的场景,如下载、HTTP 等
UDP 适用于允许一定丢包的场景,如视频通话、直播等
TCP、UDP 可以用同一个端口吗?
可以
> 图片来自小林 coding![]()
TCP 连接建立
三次握手过程?
第三次握手可以携带应用层数据
为什么是三次?不是两次?四次?
常见的说法:三次握手可以保证通信双方都具有发送和接收的能力
更详细的说,分为三点:
避免历史连接
如果是两次握手,那么 Server 先收到历史的 SYN 报文,就会 立即分配资源,建立 TCP 连接(历史连接)
然后,Client 收到历史报文的 ACK 后,发现 ack 与预期不符,就会发送 RST 报文给 Server
Server 收到 RST 报文,终止建立的 TCP 连接
可以发现,对于 Server 来说,造成了资源浪费,不应该为历史连接分配资源
而采取三次握手,Server 在第二次握手不会立即分配资源,就避免了资源浪费
因此,三次握手最主要的原因就是 避免历史连接
同步双方序列号
序列号在 TCP 中非常重要:
- 去除重复数据
- 实现按序接收
- 实现重传
采用两次握手,只能同步一方的序列号
避免资源浪费
与 避免历史连接 的情景是一样的
此外,为什么不是四次,这是因为,三次握手已经可以同步双方序列号,并且,可以确定信道是「可用」的,四次、五次… 只不过是增强了可信程度
为啥每次的初始 seq 不同?
主要是为了避免「过期」的报文对现有连接的混淆
如果每次的初始 seq 不同,就能 很大程度上 避免这个问题(不是完全避免,因为序列号是一个循环,下面会讲)
初始 seq 的产生算法?
ISN = M + F
- M 是一个计时器,4us + 1
- F 是一个哈希算法,取四元组的哈希值
为什么有了 MTU,TCP 还需要 MSS 来分片?
主要是为了减少重传的数据
假设 TCP 没有 MSS 来分片,如果一个 TCP 报文过长,在 IP 层也会被分片
正常情况,没有丢包,那当然没啥问题
但是,如果丢包,由于 TCP 的重传机制,会重传丢失的报文
由于 TCP 对 IP 分片(网络层)是无感的,因此,TCP 会 重传整个报文
如果 TCP 基于 MSS(MTU - IP 首部 - TCP 首部)分片,就可以保证在 IP 层不被分片,即使出现重传,也只需要重传丢失的片段,提高效率
第一、二、三次握手丢失,分别发生什么?
这里假设客户端是连接请求发起方
第一次握手丢失
客户端会重传 SYN 报文(Linux 默认重传 5 次),且每次的间隔时间指数递增(1、2、4…)
超过重传次数,客户端就会终止 TCP 连接
第二次握手丢失
对于客户端来说,与第一次握手丢失的情况是一样的
对于 Server 来说,超时未收到客户端的第三次握手,也会重传 ACK 报文
第三次握手丢失
对于 Server 来说,与第二次握手丢失情况是一样的
对于 Client 来说,第三次握手请求发出后,就进入了 ESTABLISHED 状态
Client 若想终止 TCP 连接,就只能依靠 TCP 的保活机制了
SYN 攻击是什么?如何减少 SYN 攻击带来的影响?
SYN 攻击是指:大量客户端向 Server 发起 TCP 连接请求,收到 Server 的 ACK 以后,始终不回答,导致 Server 的 半连接队列被打满,使 Server 无法处理正常客户端的连接请求
SYN 攻击也是一种 Dos 攻击
如何防御 SYN 攻击?
思路是保证半连接队列不被打满
- 防火墙:限制一个 IP 可以请求的客户端的数量
- 增加半连接队列的大小(调整 net.ipv4.tcp_max_syn_backlog、listen() 函数中的 backlog、net.core.somaxconn)
- 减少 SYN-ACK 的重传次数以及重传时间,使「半连接」快速释放
- 启用 SYN-Cookies
启用 SYN-Cookies 后,三次握手的过程:
- 第二次握手,即使半连接队列满了,也不会丢弃连接,Server 计算出一个 cookie 值,放到 seq 中,发给 Client
- 如果第三次握手,Client 的 ack 值合法,就加到 accept 的全连接队列中
![]()
TCP 连接断开
四次挥手过程
为什么是四次?可不可以是三次?
分析上图,可以发现:被动关闭连接方通常需要处理数据,看看有没有数据还要发送,ACK 和 FIN 是分开发的
因此,需要四次挥手
但是,在特定情况下,四次是可以变成三次的,即将 ACK 和 FIN 一起发送,但需要一定的条件:
- 被动断开方需要开启 ACK 延迟确认机制(默认开启)
- 被动断开方没有数据要传输
第一、二、三、四次挥手丢失,分别发生什么?
假设客户端主动终止连接
第一次挥手丢失
与 SYN 报文的重传一样,超时未收到 Server 的 ACK 后,客户端会重传 FIN 报文,最终强制关闭
第二次挥手丢失
客户端与第一次挥手丢失一样
Server 的 CLOSE_WAIT 结束后,发送 FIN 报文给 Client,但此时 Client 已经关闭,因此,Server 收不到 ACK,继续重传 FIN 直到重传上限
第三次挥手丢失
Server 会一直重传 FIN 报文直到重传上限
但 Client 就要分情况讨论了
如果 Client 是调用的 close 关闭的 TCP 连接:
如果 Client 是调用的 shutdown 关闭的 TCP 连接,由于 tcp_fin_timeout 无法控制 shutdown 关闭的连接,因此,Client 会一直处于 FIN_WAIT2 状态
第四次挥手丢失
为什么要有 TIME_WAIT?
保证被动关闭方能正确关闭
假设 Client 为主动关闭方
当 Client 发出对 Server 的 FIN 报文的 ACK(也就是第四次挥手)后,如果没有 TIME_WAIT,Client 就直接进入 CLOSED 状态
如果 ACK 丢失,Server 重传 FIN 给 Client,但 Client 已经关闭,因此会返回一个 RST 报文给 Server,导致 Server 的 TCP 连接不正常关闭
防止历史数据被下一次连接(同样四元组)接收
个人感觉这个的可能性比较小
要出现这种情况,至少满足两个条件的其中之一:
- 历史数据传输时间很长很长,以至于初始 Seq 已经跑了一个循环
- 传输速度很快,Seq 用得很快,以至于用完整个循环的 Seq
第一个情况可以说不可能发生(MSL 的概念)
第二个情况发生的可能性也很小
因此,TIME_WAIT 最主要的原因还是保证被动关闭方能正确关闭
为啥 TIME_WAIT 的时间为 2MSL?
MSL:最大报文生存时间,超过 MSL,可以认为一个彻底在网络中消失
如果 ACK 丢失了,被动关闭连接的一方会超时重传 FIN 报文
如果 TIME_WAIT 的时间是 2MSL,可以保证主动关闭连接的一方可以收到重传的 FIN 报文(ACK 过去消耗一个 MSL,重传的 FIN 回来,消耗一个 MSL),然后再次发送 ACK,让对方正确关闭
也就是说,2MSL 的时间至少允许 ACK 报文丢失一次
超过 2MSL 的时间也不是不可以,不过丢包率这么高的网络出现的概率太小了,忽略它比解决它更有性价比
TIME_WAIT 过多会怎么样?
无论对于客户端还是服务端来说,TIME_WAIT 过多,都会 占用资源
对于客户端来说,如果将所有可用的端口都用完了(都处于 TIME_WAIT 状态)就无法对相同的「目的 IP + 目的 Port」发起连接了
如何优化 TIME_WAIT?
TIME_WAIT 被设计出来,就不是用来优化掉的
相反,应该利用 TIME_WAIT 来保护我们的系统
服务器出现大量 TIME_WAIT 的原因?
出现 TIME_WAIT 的本质原因,还是因为 Server 主动关闭了 TCP 连接
- 未启用 HTTP 长连接(或服务器禁用 keep-alive):服务器在发送完资源以后,会主动断开 TCP 连接
- 客户端禁用 keep-alive
- 启用了 HTTP 长连接,但是单个连接的请求数太多(nginx 超过 100 次,会断开该连接)
为啥客户端禁用 keep-alive,还是 Server 主动关闭连接呢?
这个问题要从系统调用来谈
如果是 Server 主动关闭,只需要一次系统调用(close)
如果是 Client 主动关闭
- Server 在写完最后一个 response 后,还是要调用 epoll/select 来监听 socket,
- 当 Client 调用 close 后,Server 这边产生 read 事件,发起 read 系统调用
- 发现连接被对方关闭,于是调用 close
一共是 3 次系统调用
因此,考虑到这一点,还是应该让 Server 主动关闭连接
服务器出现大量 CLOSE_WAIT 的原因?
这个主要是因为程序的 bug 问题(没有调用 close)
- 没有注册 server socket 到 epoll,产生 close 事件,server 也不知道
- 没有及时 accept 客户端请求,导致大量 Client 主动关闭连接
- accept 后,没有注册 clnt_sock 到 epoll 读事件
- 忘记调用 close
如果已经建立了连接,但是客户端突然出现故障了怎么办?
分两种情况:
- 二者之间有数据传输
- 二者之间没有数据传输
对于第一种情况,客户端挂了,server 由于收不到 ack,会持续重传直到最大重传次数,关闭连接
对于第二种情况,就只能依靠 TCP 的 keep-alive 机制了
默认保活时间是 2h
超过 2h,双方都没有通信过,就会启动 keep-alive 机制
keep-alive 机制会持续向对方发起一个探测报文,直到对方响应,或者达到最大重传次数
- 默认间隔时间:75s
- 默认最大重传次数:9
可以发现,TCP 的保活机制需要很长的时间才能断开一个「死亡」连接
因此,一般需要应用层的协议手动实现 heart-beat 机制
Socket 编程
针对 TCP,Server 的流程
- 初始化 socket
- bind
- listen
- accept
- read/write
- close
listen 的 backlog 的意义?
在 早期 Linux 内核 backlog 是 SYN 队列的容量,也就是「半连接队列」容量。
但在 Linux 内核 2.2 之后,backlog 变成 accept 队列
也就是说,对于 2.2 之后的 Linux,backlog 的大小,决定了「全连接队列」的容量
但实际上,全连接队列的容量还取决于内核的 somaxconn 参数
因此,全连接队列实际容量是 min(somaxconn, backlog)
accept 发生在三次握手的哪一步?
发生在 server 收到 client 第三次握手时,即三次握手成功后
没有 accept,可以建立 TCP 连接吗?
可以
accept 发生在三次握手结束后,在 accept 前,TCP 连接就已经建立好了
没有 listen,可以建立 TCP 连接吗?
可以
存在两种情况:
- TCP 自连接(自己连接自己)
- 双方同时向对方发起连接
两种情况的共同点:没有服务端的参与,也就是没有 listen