TCP 连接状态的维护是在内核协议栈上的,它保存了一个 TCP 中的序列号、四元组、绑定进程等信息,并且负责维护进程的收发缓冲区,TCP 异常也是根据这些信息来进行处理的
报文处理原则
任何非 RST 报文都是按照次序进行处理的,若出现包延迟或丢包,seq 不连续的报文将会被放入等待队列中等待处理。
异常原则
在 TCP 状态机中,只要有任何一个位置出现异常,都会以一个 RST 包来结束本次 TCP 连接来标识本次连接发生了异常。当内核中的 TCP 协议栈收到 RST 包后,会检查 RST 包的序列号,如果序列号正确,将会立刻丢弃掉缓冲区内的所有数据并且告知本端进程错误,即 Connection Reset By Peer。
本端进程状态
内核协议栈会检查 Socket 所绑定进程的状态,以及该进程对 Socket 的读写权限;并根据这些信息来处理相关异常情况。
Socket 直接由协议栈回收时,将会释放协议栈相关资源,并且向对方发送 RST 包关闭连接;
若进程关闭了读权限,对端继续写入将会收到 RST 包,因为进程永远不会读到新发送的数据;
若进程在有读缓冲区的情况下关闭 Socket,将会给对方发送 RST 包;
情况一对应了一个进程崩溃,或者进程没有正确关闭 Socket 的情况。在这种情况下,Socket 并不是由进程主动释放的,而是由协议栈来进行回收的,这时协议栈会判断为进程因为某些原因而异常退出,协议栈所能采取的更安全的做法就是立刻丢弃掉缓冲区内的数据,并且向对端发送 RST。因此,只有在进程退出时主动释放 Socket 相关资源才是安全的,否则会导致写入缓冲区的数据并不会完全被对方收到。
情况二对应了 TCP 四次挥手时进程不正常关闭而导致出现孤儿 Socket。当 TCP 主动关闭的端直接使用 close 来关闭 Socket 的读写时,对端继续写入数据是无法被对端收到的。但协议栈并不能判断对方是否仍然有数据来写入,因此协议栈中仍然会保留该 Socket,如果对端在四次挥手中没有需要写入的数据,那么就会被视为一次正常关闭,否则将会被视为异常关闭。
情况三则代表了虽然协议栈中收到了 TCP 数据包,但是在进程层面来看则并没有收到,因此内核协议栈是有责任来告知这一次连接并不是被安全关闭的。
TCP 状态机
内核协议栈还会根据当前维护的 TCP 状态机来决定如何对收到的 TCP 报文做出应答。虽然 TCP 已经采取了多种机制来避免收到非本次连接中的 TCP 包,但是仍有一定概率会收到异常包。根据收到的报文种类,可以分为以下几个异常情景:
- 收到非窗口内的非 SYN 报文,不回复;
- 已建立连接收到了一个 SYN 包,这时会回复 Challenge ACK;
- 收到一个指向不存在的或不存活的 Socket 的请求,回复 RST;
- Time Wait 状态收到 SYN 包。
情况一比较容易理解,即收到了延迟较高的数据包,或者收到了非本次连接的包。
情况二区别于情况一,收到一个乱序的 SYN 包并不会丢弃,而是要采取更为复杂的处理措施。因为该乱序的 SYN 包可能是本次连接的超时包,也可能是来自一个新的三次握手的请求包。即对端崩溃后,本端并没有及时感知到,对端又再一次发送了连接请求。显然,后一种情况下,TCP 连接已经不安全了,因此内核会发送一个带有当前 TCP 序列号的 ACK 包,称为 Challenge ACK,用于确认对端的状态。如果这是一个过期的 SYN 包,那么对端 TCP 仍然是存活的,这时该 ACK 包会在窗口内,对端不会进行回复。如果对端是崩溃后重连,则该 ACK 序列号并不符合期望,对端协议栈将会发送一个 RST 包来重置连接。
情况三也比较容易理解,即当前 host 没有绑定进程,或者 host 正处于 timewait 等不可用状态。
而第四种情况较为特殊,当 Time Wait 状态下收到一个序列号大于当前序列号的 SYN 包,并且四元组相同,该 socket 将会被复用。