注:该篇文章已与我的个人博客同步更新。欢迎移步https://cqh-i.github.io/体验更好的阅读效果。
TCP简介
-
TCP是面向连接的协议
-
TCP基于运输连接来传送TCP报文段
- TCP运输连接的建立和释放是每一次面向连接的通信中比不可少的过程
-
TCP运输连接有三个阶段:
-
建立连接
TCP通信双方,在数据传送之前, 要通过"三报文握手"来建立连接关系 -
数据传送
基于已建好的连接进行数据传送 -
释放连接
当TCP双方不需要给对方传送数据时, 必须将之前已建立好的逻辑连接关系释放掉, 也就是通过"四报文挥手"来释放连接
- TCP的运输连接管理就是使运输连接的建立和释放都能正常地进行
TCP连接建立过程中要解决的问题
在TCP连接建立过程中要解决以下三个问题:
- 要使TCP双方能够通知对方的存在
- 要允许TCP双方协商一些参数(如最大窗口值, 是否使用窗口扩大选项和时间戳选项以及服务质量等)
- 能够对运输实体资源(如缓存大小, 连接表中的项目等)进行分配
TCP的连接建立过程(“三报文握手”)
下面有两台主机, 他们要基于TCP进行通信, 主机A的某个应用进程主动发起连接建立, 称为TCP客户, 主机B中被动等待建立的连接的应用进程称为TCP服务器。
我们可以将TCP建立连接的过程比喻为"握手", 握手需要在客户和服务器之间交换三个TCP报文段, 下面是"三报文握手"建立TCP连接的过程。
最初两端的TCP进程都处于关闭状态。
一开始主机B中的TCP服务器进程首先创建传输控制块, 用来储存TCP连接中的重要信息, 例如: TCP连接表, 指向发送和接收缓存的指针, 指向重传队列的指针, 当前的发送和接收序号…
之后就准备接受TCP客户进程的连接请求, 此时TCP服务器进程就进入监听状态, 等待TCP客户进程的连接请求。
TCP服务器进程是被动等待TCP客户进程的连接请求, 而不是主动发送, 因此称为被动打开连接。
主机A中的客户进程也是首先创建传输控制块, 然后在打算建立TCP连接时, 向主机B发送TCP连接请求报文段, 并进入同步已发送状态(SYN-SENT)。 TCP连接请求报文段首部中的同步位SYN被设置为1, 表明这是一个TCP连接请求报文段, seq字段被设置为一个初始值x作为TCP客户进程所选择的初始序号, 请注意TCP规定, SYN被设置为1的报文段不能携带数据, 但要消耗一个序号.
TCP报文段格式
由于TCP连接建立是由TCP客户进程主动发起的, 因此称为主动打开连接。
主机B中的TCP服务器进程收到TCP连接请求报文段后, 如果同意建立连接, 则向主机A的客户进程发送TCP连接请求确认报文段, 并进入同步已接收状态(SYN-RCVD)。
TCP连接请求确认报文段首部中的同步位SYN和确认位ACK都设置为1, 表明这是一个TCP连接请求确认报文段, seq字段被设置为一个初始值y,作为TCP服务器进程所选择的初始序号,确认号ack的值被设置为x+1, 这是对TCP客户进程所选择的初始序号的确认, 请注意这个报文段也不能携带数据, 因为它是SYN=1的报文段, 但同样要消耗一个序号。
主机A中的TCP客户进程收到TCP连接请求确认报文段后, 还要向主机B中的TCP服务器进程发送一个普通的TCP确认报文段, 并进入**连接已建立(ESTABLISHED)**状态。
TCP确认报文段首部中的确认位ACK被设置为1, 表明这是一个普通的TCP确认报文段, 序号seq字段被设置为x+1, 确认号字段ack被设置为y+1(这是对TCP服务器进程所选择的初始序号确认)。
注:TCP规定普通的TCP确认报文段可以携带数据, 但如果不携带数据, 则不消耗序号, 在这种情况下, 所发送的下一个数据的报文段序号仍是x+1
主机B中的TCP服务器进程收到该TCP确认报文段后, 也进入连接已建立状态, 现在TCP双方都进入接已建立状态, 它们可以基于已建立好的连接进行数据传输了。
思考:为什么是三次握手
为什么TCP客户进程最后还要发送一次TCP确认报文段呢?这是否多余?换句话说能否两报文握手建立连接?
并不多余, 并且不能简化为两报文握手, 举个粟子说明
如上图这个情况, 主机A的一个客户进程发出一个TPC连接请求报文段(图中的1), 但该报文段在某个节点长时间滞留了, 这必然会造该报文段的超时重传(图中的2).假设超时重传的报文段正常到达主机B, 主机B中的TCP服务器进程收到后, 给TCP客户进程发送一个TCP连接请求确认报文段并进入连接已建立状态。(请注意我们改为两报文握手, 因此TCP服务器进程发送完TCP连接请求确认报文段后, 进入的是连接已建立状态, 而不像三报文握手那样进入同步已接收状态并等待TCP客户进程发来的针对TCP连接请求确认报文段的普通的TCP确认报文段)
TCP客户进程收到TCP连接请求确认报文段后, 进入连接已建立状态, 现在TCP双方都处于连接已建立状态, 他们可以相互传送数据, 之后可以通过四报文挥手来释放连接, TCP双方都进入关闭状态, 之后TCP服务器进程进入监听状态, 准备接受客户进程的连接请求, 而TCP客户进程将一直处于关闭状态, 除非应用进程通知它需要建立新的TCP连接。
然而此时, 之前滞留在网络中失效的TCP连接请求报文段到达主机B, 主机B中的服务器进程会误认为这是主机A中的TCP客户进程又发起了一个新的TCP连接请求, 于是给TCP客户进程发送TCP连接请求确认报文段, 并进入连接已建立状态.
该TCP连接请求确认报文段到达主机A, 由于主机A中的客户进程并没有发起TCP连接, 且处于关闭状态, 因此不会理睬该报文段, 但主机B中的TCP服务器进程已进入连接已建立状态, 它认为新的运输连接已建立好了, 并一直等待主机A中的TCP客户进程发来数据, 主机B中的许多资源就这样白白浪费了。
还可以从另外一个角度理解,3次握手完成两个重要的功能,既要双方做好发送数据的准备工作(双方都知道彼此已准备好),也要允许双方就初始序列号进行协商,这个序列号在握手过程中被发送和确认。
现在把三次握手改成仅需要两次握手,死锁是可能发生的。作为例子,考虑计算机A和B之间的通信,假定A给B发送一个TCP连接请求,B收到了TCP连接请求,并发送了TCP连接请求确认报文段。按照两次握手的协定,B认为连接已经成功地建立了,可以开始发送数据分组。可是,A在B的TCP连接请求确认报文段在传输中被丢失的情况下,将不知道B是否已准备好,不知道B建立什么样的序列号,A甚至怀疑B是否收到自己的TCP连接请求。在这种情况下,A认为连接还未建立成功,将忽略B发来的任何数据,只等待TCP连接请求确认报文段。而B在发出的数据超时后,重复发送同样的数据。这样就形成了死锁。
TCP的连接释放(“四报文挥手”)
数据传输结束后, TCP通信双方都可以释放连接。
现在主机A中TCP客户进程和主机B的TCP服务器进程都处于连接已建立状态。
假设主机A中的应用进程通知其释放TCP连接, 也就是主动关闭TCP连接。
TCP客户进程发送TCP连接释放报文段, 并进入**终止等待1(FIN-WAIT-1)**状态。该报文段中的首部终止控制位FIN和确认位ACK都设置为1, 表明这是一个TCP连接释放报文段, 同时也对之前收到的报文段进行确认, 序号seq的值设置为u, 它等于TCP客户进程之前已传送过的数据的最后一个字节的序号加1, 请注意TCP规定, FIN为1的报文段即使不携带数据, 也要消耗一个序号. 确认号ack的值设置为v, 它等于TCP客户进程之前已收到过的数据的最后一个字节的序号加1
主机B中的服务器进程收到TCP连接释放报文段之后, 会发送一个普通的TCP确认报文段并进入**关闭等待(CLOSE-WAIT)**状态。 该报文段中的首部中的确认位ACK设置为1, 表明这是一个普通的TCP确认报文段, 序号seq设置为v, 它等于TCP服务器进程之前已传送过的数据的最后一个字节的序号加1, 这也与之前收到的TCP连接释放报文段的确认号ack匹配. 确认号ack字段设置为u+1, 这是对TCP连接释放报文段的确认.
TCP服务器进程这时应通知高层应用进程, TCP客户进程要断开与自己的TCP连接, 此时从TCP客户进程到TCP服务器进程这个方向的连接就释放了, 这时的TCP连接处于半关闭状态, 也就是TCP客户进程已经没有数据要发送了, 但TCP服务器进程若有数据要发送, TCP客户进程仍要接收, 也就是说从TCP服务器进程到TCP客户进程这个方向的连接并未关闭, 这个状态可能会持续一段时间。
主机A中的TCP客户进程收到TCP确认报文段后, 就进入**终止等待2(FIN-WAIT-2)**状态, 等待TCP服务器进程发出的TCP连接释放报文段。
若主机B中的应用进程已经没有要向主机A中的应用进程发送的数据, 应用进程就通知其释放TCP连接, 由于其TCP连接释放是由TCP客户进程主动发起的, 因此TCP服务器进程对TCP连接的释放称为被动关闭连接。
主机B中的TCP服务器进程发送TCP连接释放报文段并进入**最后确认(LAST-ACK)**状态。报TCP连接释放报文段中的首部终止控制位FIN和确认位ACK都设置为1, 表明这是一个TCP连接释放报文段, 同时也对之前收到的报文段进行确认, 现在假定序号seq的值为w, 这是因为在半关闭状态下, TCP服务器进程可能又发送了一些数据, 确认号字段ack的值为u+1, 这是对之前收到的TCP连接释放报文段的重复确认。
主机A中的TCP客户进程收到TCP连接释放报文段后, 必须针对该报文, 发送普通的TCP确认报文段, 之后进入时间等待状态(TIME-WAIT)。TCP确认文段中的首部中的确认位ACK设置为1, 表明这是一个普通的TCP确认报文段, seq = u + 1, ack = w + 1(这是对所收到的TCP连接释放报文段的确认)。
主机B中的TCP服务器进程收到TCP确认报文后, 就进入关闭状态(CLOSED), 而主机A中的客户进程还要经过2MSL后, 才能进入关闭状态。
注: MSL(Maximum Segment Lifetime) 最长报文段寿命, RFC793文档建议为2分钟, 对于现在的网络MSL等于两分钟可能太长了, 因此TCP允许不同的实现, 使用更小的MSL值。
思考: 为什么连接的时候是三次握手,关闭的时候却是四次握手?
上述已经表述的很明白了,连接时,因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。
关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,“你发的FIN报文我收到了”。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
思考: 为什么TIME_WAIT状态需要经过2MSL才能进入到CLOSE状态?
在Client发送出最后的ACK报文段回复,但该ACK报文段可能丢失。Server如果没有收到ACK,将不断重复发送FIN片段。所以Client不能立即关闭,它必须确认Server接收到了该ACK。Client会在发送出ACK之后进入到TIME_WAIT状态。Client会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。所谓的2MSL是两倍的MSL(Maximum Segment Lifetime)。MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接。
TCP的保活计时器(keepalive timer)
TCP双方已经建立了连接, 但是TCP客户进程所在的主机突然发生故障, 显然TCP服务器进程以后就不能收到TCP客户进程发来的数据, 因此应当有措施使TCP服务器进程不要白白等地下去, 那么TCP服务器进程如何发现这种情况呢?
方法就是使用保活计时器.
TCP服务器进程每收到一次TCP客户进程的数据, 就重新设置并启动保活计时器(2小时定时)
若保活计时器定时周期内未收到TCP客户进程发来的数据,则当保活计时器到时后, TCP服务器进程就向TCP客户进程发送一个探测报文段, 以后则每隔75秒钟发送一次. 若一连10个探测报文段后仍无TCP客户进程的相应, TCP服务器进程就认为TCP客户进程所在主机出了故障,接着就关闭了这个链接.