浅谈TCP连接的创建和销毁过程

1. 概述

本文主要讲述的内容主要包含以下部分

  • TCP的概念
  • 连接的建立过程
  • 连接的拆除过程

2. TCP协议

我们首先要明确一点,这里讨论的TCP协议并不是指TCP/IP协议簇,而是单指TCP协议,即Transmission Control Protocol,指传输控制协议,位于传输层(OSI七层模型的第四层,或是TCP/IP四层协议的第三层)

都应该清楚,传输层负责报文从进程到进程的传递,也可以说是从端口到端口的协议,所以TCP协议也属于一种端对端的协议。常用的端口号有:

  • 20:FTP(文件传输协议)的数据传输
  • 25:SMTP(简单邮件传输协议)
  • 53:DNS(域名系统)
  • 80:HTTP(超文本传输协议)
  • 111:RPC(远程过程调用)

TCP是一个面向流的协议,允许发送进程以字节流的方式传递数据,同样地,接收方也以字节流的方式接收数据,这就存在了一个抽象的“管道”,管道的建立与拆除,就对应着TCP连接的创建和销毁,在接下来我们就来详细讲解这一过程

3. TCP分组

在讲解具体的过程之前,先来做一些准备工作。首先,TCP是一个全双工的通信协议,也就是连接的双方都能够发送和接收数据,这些数据,也就是字节,TCP会对其进行编号,这是因为TCP需要保证接收数据的顺序。编好号之后,TCP会为发送的每一个段分配一个序号,实际就是段中第一个字节的编号

发送的段,在TCP中也叫做分组,分组首部(首部不包含数据)的格式是如下这样的:浅谈TCP连接的创建和销毁过程
源端口、目的端口、序号和校验和的概念相信应该大家都能理解,这里就不再展开了,数据偏移、保留部分、紧急指针、选项,以及填充部分相对来说不太重要,感兴趣的可以自行了解。剩余相对重要的确认号、控制位(URG/ACK/PSH…)和窗口中,我们这里指讲和本文相关的前两种

如果了解过数据链路控制的相关知识,应该会知道ack的概念,就相当于我们这里的确认号,指的是我们期待接收到的分组的序号,因为我们发送的每一个分组都是为了期待对方给我们回应,为了避免接收到不正确的数据,就使用确认号机制,来明确我们需要的是什么数据

接着是控制位,一共有6种,其各自含义如下:

  • URG:紧急指针有效
  • ACK:确认有效
  • PSH:请求急迫
  • RST:连接复位
  • SYN:同步***
  • FIN:终止连接

在这6种中,我们用到的至少有3种:ACK、SYN,还有FIN,ACK表示分组是一个ACK响应分组,SYN表示分组是一个请求连接分组,FIN表示分组是一个终止连接分组。我们要注意的是,这些控制位之间并不一定是互斥的,比如ACK可以和SYN同时有效,一个分组中并不是只能存在一个控制位

4. 连接的建立过程

即使完全不了解TCP协议,也应该听说过“三次握手与四次挥手”的概念,三次握手指的就是连接创建的过程,四次挥手就是连接拆除的过程

我们先来看“三次握手”的过程,首先来看图示:浅谈TCP连接的创建和销毁过程
我们把主动建立连接的一方叫做客户端,另一方叫做服务端,整个过程可以简要表述为以下这样:

  • 背景:客户端和服务端均处于CLOSED状态,等待进一步操作
  • 准备:服务端进入LISITEN状态,监听客户端建立连接的请求
  • 第一次握手:客户端服务端发起建立连接的请求(SYN分组),自身进入SYN-SENT状态
  • 第二次握手:服务端接收到客户端的请求,给客户端发送确认消息(SYN + ACK分组),自身进入SYN-RCVD状态
  • 第三次握手:客户端服务端发送最终确认消息(ACK分组),自身进入ESTABLISHED状态,可以进行数据传送
  • 连接建立:服务端接受到客户端的确认消息,自身也进入ESTABLISHED状态,可以进行数据传送

我们可以在图中看到,每一次握手消息的发送都携带了seq,即***,这里需要明确一点,前两次握手消息均不携带任何数据,但是仍占用一个***,最终传送数据的***就是基于握手消息的***(连接的一方发送的分组***是连续的,每次加1)

我们刚才指说道前两次握手消息不携带数据,并占用一个***,但是我们并没有提到第三次握手,是因为第三次握手是一个纯ACK消息,也就是单纯地进行响应(对服务端响应的消息进行响应),所以这个报文如果不携带数据,则不占用***(不占用***的意思是不使用新的***,而不是不携带***)

这里还有一种很少见的情况,就是双方同时建立连接,则彼此都发送SYN+ACK段,直接建立连接,但是这种情况毕竟很少,所以不再进行进一步的讨论

虽然连接创建的过程已经讲完了,但是可能很多人疑惑为什么创建的过程要设计的这么麻烦,这里我们从正反两方面来解释:

为什么不使用一次握手或者两次握手?

首先我们要知道,定义TCP协议的人肯定希望连接的创建过程尽量简化,现在三次握手已经能解决问题了,所以我们这里就不讨论为什么不使用四次握手或者五次握手

那为什么不使用一次握手?其实一次握手和两次握手遇到的问题都是一样的,所以我们放在一起说。一次握手和两次握手都有一个共同的特点,那就是接收到第一次握手的一方会立刻建立一条TCP连接通路,这就带来了一个问题。因为数据通信的理论之一是网络链路的不可靠性,所以我们不能以发送的数据包都能被接收作为协议的前提。TCP协议制订了一个超时重传的机制,如果发送的分组一定时间内没有响应,就会触发超时重传,进行分组的再次发送

我们再谈回一/两次握手,如果发送的第一次握手消息没有在指定时间内响应,客户端就会重新发送一份握手消息,现在我们想一下,如果连接被拆除后,因为网络原因第一次发送的分组才慢悠悠地传到服务端,则会导致创建一个新的TCP通路,显然这种情况会导致资源被浪费

如果用更正式的说法就是,为了防止已失效的连接请求报文段重新突然发送到服务端,导致建立不必要的连接,所以一次握手和两次握手均是不可行的,为了解决这种情况要么使用三次握手,要么就只能消耗更多的资源来进行额外的判断

为什么要使用三次握手

三次握手协议是有其本身的意义的,每一次握手消息都不是多余的。TCP连接创建的前提就是确认连接双方都能够正常的接收和发送消息,因为不存在第三方确认机构,所以只能由连接双方自行判断,每一次握手消息都是在进行询问与判断:

  • 服务端接收到第一次握手消息:服务端确认了客户端可以正常发送消息(发送了握手消息)
  • 客户端接收到第二次握手消息:客户端确认了服务端可以正常接收消息(接收了自己发送的握手消息),也可以正常发送消息(发送了握手消息)
  • 服务端接收到第三次握手消息:服务端确认了客户端可以正常接收消息(接受了自己发送的握手消息)

这样,经过至少三次握手之后,连接的一方才能够对另一方接收和发送消息的能力有一个准确的判断,我个人认为这种解释相比于反证的方式要更为清晰

5. 连接的拆除

连接的拆除,或者叫连接的释放,是一个四次挥手的过程,我们同样来看图:浅谈TCP连接的创建和销毁过程
我们把主动提出关闭请求的一方叫做客户端,另一方叫做服务端(实际上一般情况下都是由客户端发起主动关闭),整个过程可以分为以下步骤:

  • 背景:客户端和服务端都处于ESTABISHED状态,等待进一步操作
  • 第一次挥手:客户端服务端发送连接拆除的请求(FIN分组),自身进入FIN-WAIT-1状态
  • 第二次挥手:服务端收到客户端的请求,单向关闭客户端的连接,向客户端发送响应消息(ACK分组),自身进入CLOSE-WAIT状态
  • 单向数据传送:客户端接收到服务端的响应,不再发送数据(但是可以进行响应),仅由服务端发送一些剩余的数据,进入FIN-WAIT-2状态
  • 第三次挥手:服务端的剩余消息发送完毕,向客户端发送连接拆除请求(FIN + ACK分组),自身进入LAST-ACK状态
  • 第四次挥手:客户端收到服务端的请求,向服务端发送响应消息(ACK分组),自身进入TIME-WAIT状态
  • 服务端连接断开:服务端收到客户端的响应消息,断开连接,自身进入CLOSED状态
  • 客户端连接断开:客户端等待一段时间后,自行断开连接,自身进入CLOSED状态

有了三次握手的基础,相信四次挥手的过程理解起来会容易很多。挥手信息中发送的***和确认号我就不再进一步讲解了,图中已经很详细了,这里就提两点:

  • 即使FIN分组中不携带任何数据,也要占用一个***
  • 如果FIN + ACK分组中不携带任何数据,则仅占用一个***
  • 第四次挥手的ACK分组不携带数据,也不占用***

四次挥手的过程与三次握手大同小异,我们这里只针对最重要的两个点进行讨论

为什么要使用四次挥手?

讲三次握手的地方已经证明了一次和两次握手的不可行性,但是我们很容易就能想到,为什么不采用三次挥手?其实三次挥手是可行的,但是在TCP中,一端停止发送数据后,仍可以继续接收数据,也就是半关闭的特性。为了这一特性,所以在挥手过程中插入了一个单向数据传送的过程,使得服务端可以在客户端主动关闭后继续发送数据

TIME-WAIT状态是什么

TIME-WAIT状态的字面意义是等待时间,和三次握手不一样,客户端发送最后的ACK段之后并没有直接进入CLOSED状态,而是等待一段时间后再自行关闭,这一段时间默认为2MSL(Maximum Segment Lifetime,即分组最长存活时间),可以在系统配置文件中手动更改。如果在TIME-WAIT期间收到了服务端的消息,依然可以进行响应

我们以客户端发送的最后一次ACK段为起点分析,如果这个ACK由于网络问题暂时没有被服务端接收到,服务端的定时器到期,会进行FIN段的重传,假设我们没有TIME-WAIT状态,而是在发送ACK后立刻进入CLOSED状态,资源都已经被销毁了,客户端无法对服务端的消息作出响应,这就导致服务端一方得不到对FIN的响应消息,造成了连接的异常

TIME-WAIT的存在理由知道了,那么为什么要设定为2MSL呢?刚才我们提到了,MSL指的就是一个分组在网络中存活的最大时间,换句话来说,如果一个分组经过了MSL还没有传送到目的方,那么完全就能够认为这个分组在网络中丢失,不可能再次被目的方接收。一个分组一来一回最多为2MSL,也就是说如果2MSL之后没有任何消息发送过来,就能够确信对方已经对自己的消息做出了回应

在实际生产中,如果发现系统存在大量的TIME-WAIT状态,可以适当调小这个值

补充

最后再提一个保活时间,并不是很重要的一个点,感兴趣的可以自行了解,其意义是当客户端故障后,服务端在保活时间后会发送探测消息,来决定是否释放连接