TCP连接建立与断开
在TCP概述与报文格式中介绍了OSI/RM网络模型与TCP报文格式,这篇文章紧接前篇介绍TCP连接的建立与断开过程以及其中可能的异常场景。
TCP连接状态机
TCP连接状态的转移过程一般使用如下图片进行表述:
图中带箭头的粗实线表示客户端状态转移,带箭头的粗虚线表示服务端状态转移,带箭头的细线表示不常见事件,如复位、同时打开、同时关闭等。各状态含义如下:
- CLOSED,关闭状态,表示主机当前没有活动或进行传输的连接
- LISTEN,监听状态,表示服务器正在等待新的传输连接进入
- SYN_RCVD,表示主机已经收到一个传输连接请求,但尚未确认
- SYN_SENT,表示主机已经发出一个传输连接请求,等待对方确认
- EASTABLISHED,传输连接已建立,通信双方正常数据传输状态
- FIN_WAIT_1,主动关闭方已经发送关闭连接请求,等待对方确认
- FIN_WAIT_2,主动关闭方已经收到对方对关闭连接请求的确认,等待对方发送关闭连接请求
- TIMED_WAIT,完成双向连接关闭,等待所有分组消失
- CLOSING,双方同时关闭传输连接,等待对方确认
- CLOSE_WAIT,被动关闭方收到对方的关闭连接请求,并已确认
- LAST_ACK,被动关闭方等待最后一个关闭传输连接请求确认,并等待所有分组消失
TCP连接建立
TCP是面向连接的,每次数据通信前需要进行连接建立,一般称为三次握手:
- Client Say: Hi
- Server Say: Hi, I see you
- Client Say: I see you too
笔者曾认为学习TCP三次握手知道上述程度就可以了,但现在改变了观点:仅知道上述过程不足以在实际工作中解决具体问题或是困难问题,作为技术从业者不能仅局限于上述程度的了解,还要更深入掌握具体细节。
建立连接过程
TCP连接建立过程是TCP连接状态机状态变化的一部分,正常如下:
- Server正常启动,状态从CLOSED=>LISTEN,涉及操作系统原语:SOCKET、BIND、LISTEN、ACCEPT
- Client请求连接,向Server发送一个SYN包(SYN=1, seq=i),状态从CLOSED=>SYN_SENT,涉及操作系统原语:SOCKET、CONNECT
- Sever收到SYN包,向Client发送一个SYN+ACK包(SYN=1, ACK=1, seq=j, ack=i+1),状态从LISTEN=>SYN_RCVD
- Client收到SYN+ACK包,状态从SYN_SENT=>EASTABLISHED,向Server发送一个ACK包(ACK=1, seq=i+1, ack=j+1)
- Server收到ACK包,状态从SYN_RCVD=>EASTABLISHED,连接建立完成
可能出现的异常
在连接建立过程的每一步,都有可能发生异常,知道这些异常的处理才是真的理解三次握手,就像平时代码量主要花在处理异常流程而不是正常流程。
Client SYN包丢包
- Client,触发超时重传机制,尝试三次,间隔时间分别是5.8s、24s、48s(《TCP/IP详解卷Ⅰ:协议》),三次时间大约是 76s 左右
- Server,不感知
Server SYN+ACK包丢包
- Client,Server SYN+ACK包丢包Client不感知,因此还是按照SYN包丢包进行超时重传
- Server,超时内没有收到Client发来的ACK包则会触发重传,时间间隔3s、6s、12s,且这个过程中Client也在进行SYN重传,Server端收到时会立即回复SYN+ACK,SYN+ACK重传次数达到上限(Linux可配置)后Server会关闭连接
Client ACK包丢包
- Client,不感知丢包,连接状态正常改变为EASTABLISHED
- Server,未及时收到ACK包,触发超时重传机制继续发送SYN+ACK包,Client收到时回复ACK包
Client与Server同时主动发起连接
- 第一回合:Client发送SYN包(SYN=1, seq=i)、Server发送SYN包(SYN=1,seq=j),双方状态由CLOSED=>SYN_SENT
- 第二回合:Client与Server互相收到对方的SYN包,双方状态改变为SYN_RCVD,Client发送包(SYN=1, ACK=1, seq=i+1, ack=j+1)、Server发送包(SYN=1, ACK=1, seq=j+1, ack=i+1)
- 第三回合:Client与Server互相收到对方的SYN+ACK包,双方状态改变为EASTABLISHED
Client发送SYN包但故意不回复SYN+ACK包
Server将处于一种半连接的状态,此时Server会认为自己的SYN+ACK包出现丢包并触发重传,但如果大量的连接需要重传SYN+ACK包则会消耗Server资源,因此存在被攻击的可能性(SYN FLOOD)
Client向Server发送SYN包后Server拒绝连接
- Server向Client发送FIN包然后进入FIN_WAIT_1状态
- Client收到FIN包后发送ACK包,连接状态从SYN_SENT=>CLOSING
- Server收到ACK包,连接状态从FIN_WAIT_1=>TIME_WAIT
Server未收到SYN+ACK包所对应的Client ACK包,但收到Client数据包
此时连接可正常进入EASTABLISHED
TCP连接断开
与三次握手对应,TCP连接断开被称为四次挥手:
- Client Say: I’m done
- Server Say: Ok
- Server Say: I’m done
- Client Say: Ok
建立连接只需要三次交互但断开连接需要四次,这是因为TCP连接内数据传输是全双工的,断开连接时每个方向必须单独关闭,这称为TCP的半关闭特性。相互打完招呼开始沟通后,你不说话了,我还可以说即使如此。
连接断开的过程
TCP连接断开过程是TCP连接状态机中的一个子过程,假设Client作为主动方进行连接断开:
- Client发送FIN包(FIN=1, seq=m),状态从EASTABLISHED=>FIN_WAIT_1
- Server收到FIN包,发送ACK包(ACK=1, seq=n, ack=m+1),状态从EASTABLISHED=>CLOSE_WAIT
- Client收到ACK包,状态从FIN_WAIT_1=>FIN_WAIT_2
- Server发送FIN+ACK包(FIN=1, ACK=1, seq=w, ack=m+1),状态从CLOSE_WAIT=>LAST_ACK
- Client收到FIN+ACK包,发送ACK包(ACK=1, seq=m+1, ack=w+1),状态从FIN_WAIT_2=>TIME_WAIT,等待2MSL后状态从TIME_WAIT=>CLOSED
- Server收到ACK包,状态从LAST_ACK=>CLOSED
可能出现的异常
主动方FIN丢包
- 主动方,超时重传FIN包
- 被动方,不感知
被动方ACK包丢包
- 主动方,不感知被动方ACK包丢包,感知自己FIN包丢包,超时重传FIN包
- 被动方,不感知自己丢包,收到FIN包时继续回ACK,同时被动方处于CLOSE_WAIT状态,继而也会断开连接主动发送FIN+ACK包
被动方FIN+ACK包丢包
- 主动方,不感知
- 被动方,超时重传FIN+ACK包
主动方ACK包丢包
- 主动方,不感知自己ACK包丢包
- 被动方,没有收到FIN+ACK包的ACK包,超时重传FIN+ACK包
双方同时断开连接
- 第一回合,Client发送FIN包(FIN=1, seq=i),Server发送FIN包(FIN=1, seq=j),双方均进入FIN_WAIT_1状态
- 第二回合,互相收到对方的FIN包,状态由FIN_WAIT_1=>CLOSING,Client发送ACK包(ACK=1, seq=i+1, ack=j+1),Server发送ACK包(ACK=1, seq=j+1, ack=i+1)
- 第三回后,互相收到对方的ACK包,状态由CLOSING=>TIME_WAIT,等待2MSL后变为CLOSED
主动方FIN包后未收到ACK包,但收到了FIN+ACK包
状态从FIN_WAIT_1=>TIME_WAIT,主动方回复ACK包,跳过FIN_WAIT_2状态
上述可能异常只是普通异常,更多异常可能发生在所有超时重传的位置,每个超时重传位置都有可能无法符合预期行为。另外这些有超时重试的位置都有可能被攻击,攻击方不按约定行为交互而故意使对方做无效超时重试消耗资源。
TIME_WAIT与2MSL
MSL是Maximum Segment Lifetime英文的缩写,中文可以译为“报文最大生存时间”,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。
在连接断开的最后一个阶段,主动方从TIME_WAIT=>CLOSED需要等待2MSL时间,这个2MSL是一个超时重传窗口时间用来保证被动方连接正确进入CLOSED状态,因为断开连接主动方最后一个ACK包无法知晓是否达到对方,因而需要留下一个时间窗口让对方重发FIN+ACK包。
2MSL除了留给对方超时重试FIN+ACK以保证连接正常关闭,同时也可以避免网络中此连接遗留的在途数据包导致连接状态异常,如果双方商议连接断开不等待2MSL就完成,部分因为网络延迟导致的重传包(可能是FIN+ACK或其他)可能继续达到,这样将干扰新连接的可靠传输。
建立连接的目的
一般而言建立连接的目的是为了可靠传输,但为了可靠传输是否一定要先建立连接呢,答案是未必,可以这么论证:“在建立连接之前不存在连接,而建立连接过程中的通信同样需要可靠性,因此连接并不是可靠传输的必要条件”。
大家知道UDP协议是面向无连接的,假设我们使用UDP协议做内容传输,并在其之上设计一种类似TCP握手阶段数据传输的方式进行数据传输,那么这种数据传输过程同样可靠。TCP建立连接的目的并非仅是为了可靠传输中的“可靠”,而是为达到可靠传输目的采用建立连接这种方式最高效,这个高效即是理论成果也与实践相符。