IP 协议在发送数据包时,途中会遇到各种事情。例如,可能路由器突然崩溃,使包丢失;或者一个包可能沿低速链路移动,而另一个包可能沿高速链路移动而超过前面的包,最后使得包的顺序搞乱。TCP 协议使两台主机上的进程顺利通信,不必担心包丢失或包顺序搞乱。TCP 跟踪包顺序并在包顺序搞乱时按正确顺序重组包。如果包丢失,则 TCP 会请求源主机重发包。
进行通信的两台主机上会有很多进程。当主机 A 上的进程 A1 向主机 B 上的进程 B1 发送数据时,IP 协议根据主机 B 的 IP 地址,把进程 A1 发送的数据送达主机 B。接下来 TCP 需要决定把数据发送到主机 B 中的哪个进程。TCP 采用端口来区分进程,端口不是物理设备,而是用于标识进程的逻辑地址,更确切地说,是用于标识 TCP 连接的端点的逻辑地址。当两个进程进行一次通信,就意味着建立了一个 TCP 连接,TCP 连接的两个端点用端口来标识。
端口号的范围为 0 到 65535,其中 0 到 1023 的端口号一般固定分配给一些服务。例如,21 端口分配给 FTP 服务,80 端口分配给 HTTP 服务等。从 1024 到 65535 的端口号供用户自定义的服务使用。
客户进程的端口一般由所在主机的操作系统动态分配,当客户进程要求与一个服务器进程进行 TCP 连接时,操作系统为客户进程随机分配一个还未占用的端口,当客户进程与服务器进程断开连接时,这个端口就被释放。此外,TCP 和 UDP 都用端口来标识进程。在一个主机中,TCP 端口与 UDP 端口的取值范围都是各自独立的,允许存在取值相同的 TCP 端口与 UDP 端口。
TCP 报文段格式
TCP 通过报文段的交互来建立连接、传输数据、发出确认、进行流量控制及关闭连接。整个 TCP 报文段也被分为 TCP 首部和数据两部分,TCP 首部就是 TCP 为了实现端到端的可靠传输所加上的 TCP 协议控制信息,而数据部分则是指由应用层传来的用户数据。每个 TCP 数据包是封装在 IP 包中的,每个 IP 头的后面紧接着的就是 TCP 头,TCP 报文格式如下图所示:
源端口和目的端口
分别代表呼叫方和被叫方的 TCP 端口号。一个端口与其主机的 IP 地址就可以完整地标识一个端点了,也就是 Socket(套接字)。
序列号
指 TCP 报文段中的数据部分的第一个字节的序号。在一个 TCP 连接中,传送的数据字节流中的每一个数据字节都要按顺序进行编号,整个要传送的字节流的起始序号必须在连接建立时设置。例如一个报文段的序号为 101,而该报文段中的数据部分共有 100 个字节,则表明数据部分的最后一个字节的编号是 200。这样下一个报文段的序号则为 201。这样可以去掉重复序列号的数据。
确认序号
指期望接收到对方下一个报文段中数据部分的第一个字节序号。确认号仅表示在此数值之前的所有数据对方已连续正确接收了,因为在两个已正确接收的报文段之间可能会有一个或多个数据还没有正确接收。序号和确认号这两个字段共同用于 TCP 服务中的差错控制,确保了 TCP 数据传输的可靠性。
例如,主机 A 向主机 B 发送的一个报文段,其序号为 101,假设该报文段数据部分的长度是 100 个字节。当主机 B 正确接收后可能会立即向主机 A 返回一个 ACK 确认报文段,该 ACK 报文段中的确认号为 201。
头部长度
用于确定 TCP 报文首部的长度,占 4 位。因为 TCP 报文首部中有一些长度不确定的可选项字段,所以头部长度,即数据偏移字段是很必要的,这样接收方在向上层传输数据时就知道所去掉的 TCP 协议头有多少字节。
ACK
确认控制位,指示 TCP 报文段中的确认号字段是否有效。
SYN
同步控制位,用来在传输连接建立时同步传输连接序号。比如当 SYN=1、ACK=0 时,表明这是一个连接请求报文段,如果对方同意建立连接则会返回一个 SYN=1、ACK=1 的确认。
FIN
最后控制位,用于释放一个传输连接。当 FIN=1 时,表示数据已全部传输完成。当发送端没有数据要传输时可要求释放当前连接,但接收端仍然可以继续接收还没有接收完的数据。正常传输时该位置为 0。
窗口大小
指发送者当前还可以发送的最大字节数。窗口大小的值用于告诉接收本报文段的主机,从本报文段中所设置的确认号值算起,本端目前允许对端发送的字节数。
校验和
对 TCP 首部、数据部分进行校验,目的是检测数据在传输过程中是否发生变化。如果接收方比对校验和与发送方不一致,那么数据一定传输有误,TCP 将丢弃这个报文段并不确认收到此报文段。
TCP 三次握手
TCP 连接建立通常是由一方主动发起的。此时 TCP 协议使用三次握手机制来建立传输连接。
首先是服务器初始化的过程,服务器端通过 socket,bind 和 listen 完成了被动套接字的准备工作,被动的意思就是等着别人来连接,然后调用 accept 就会阻塞在这里,等待客户端的连接来临。客户端通过调用 socket 和 connect 函数后也会阻塞。接下来的事情就是由操作系统内核网络协议栈完成的了。
- 客户端的协议栈向服务器端发送 SYN 包,并告诉服务器端当前发送序号为 x(假设报文的初始序号为 x 且没有确认号,因为它仅是 SYN 数据段,请求与 TCP 服务器建立连接的),客户端主动打开端口,进入到 SYNC_SENT(已发送连接请求,等待对方确认)状态。
- 服务器端的协议栈收到这个包之后,和客户端进行 ACK 应答,应答的值为 x+1,表示对客户端发送的 SYN 包 x 的确认(因为 TCP 连接建立时报文的数据部分只有 1 字节)。服务器也发送一个 SYN 包,告诉客户端当前我的发送序号为 y,服务器端进入 SYNC_RCVD(已收到连接请求但未进行确认)状态。
- 客户端协议栈收到 ACK 之后,使得应用程序从 connect 调用返回,表示客户端到服务器端的单向连接建立成功,客户端进入 ESTABLISHED(连接建立)状态,同时客户端协议栈也会对服务器端的 SYN 包进行应答,应答数据为 y+1。
- 服务器端收到客户端的 ACK 报文段后,服务器端协议栈使得 accept 阻塞调用返回,这个时候服务器端到客户端的单向连接也建立成功,服务器端也进入 ESTABLISHED(连接建立)状态。
可以看到,这样的应答过程总共进行了三次,这就是 TCP 连接建立之所以被叫为三次握手的原因了。三次握手是连接两端正确同步的充要条件,具体原因如下:
因为 TCP 建立在不可靠的分组传输服务之上,报文可能丢失、延迟、重复和乱序,因此协议必须使用超时和重传机制。如果重传的连接请求和原先的连接请求在连接正在建立时到达,或者当一个连接已经建立、使用和结束之后,某个延迟的连接请求才到达,就会出现问题。
而采用三次握手协议就可以解决这些问题。如客户端发送的 ACK 数据段就是为了避免因网络延迟而导致的重复连接,因为这时客户端通过检查 ACK 报文段中的确认号就可以得知该连接请求是否已失效。
TCP 在协议层面支持 Keep Alive 功能,即隔段时间通过向对方发送数据表示连接处于健康状态。不少服务将确保连接健康的行为放到了应用层,通过定期发送心跳包检查连接的健康度。一旦心跳包出现异常不仅会主动关闭连接,还会回收与连接相关的其他用于提供服务的资源,确保系统资源最大限度地被有效利用。
TCP 四次挥手
TCP 是全双工通信,双方都能作为数据的发送方和接收方,但 TCP 连接也会有断开的时候。TCP 传输连接的释放需要经过四次挥手过程。
TCP 四次挥手的具体过程如下:
TCP 连接终止时,主机 1 先发送 FIN 报文(假设此报文段的序号为 m)然后进入 FIN_WAIT 状态,等待主机 2 的确认。
主机 2 在收到主机 1 发来的 FIN 报文段后会发送一个 ACK 应答表示前面的数据已经全部收到了,然后进入 CLOSE_WAIT 状态。同时主机 2 通过 read 调用获得 EOF,并将此结果通知应用程序进行主动关闭连接的操作。
当主机 1 收到主机 2 应答的 ACK 报文段后会进入 FIN_WAIT_2 状态,进一步等待主机 2 发出连接释放的报文段。
当主机 2 接收完主机 1 的全部数据或发送完全部向主机 1 传输的数据后,会向主机 1 发送 FIN 报文(假设此时的报文段序号已变为 n)并进入 LAST_ACK 状态,等待主机 1 的确认。
主机 1 在接收到 FIN 报文后发送 ACK 应答,然后进入 TIME_WAIT 状态。但此时 TCP 连接还没有释放,必须要等待 2MSL 时间后主机 1 才进入到 CLOSED 状态,彻底释放了 TCP 连接。
主机 2 在收到主机 1 发来的 ACK 报文段后也进入 CLOSED 状态,彻底释放 TCP 连接。
为什么不直接进入 CLOSED 状态,而要停留在 TIME_WAIT 这个状态?
这要从两个方面来说。
首先,这样做是为了确保最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭。TCP 在设计的时候,做了充分的容错性设计,比如,TCP 假设报文会出错,需要重传。在这里,如果图中主机 1 的 ACK 报文没有传输成功,那么主机 2 就会重新发送 FIN 报文。
如果主机 1 没有维护 TIME_WAIT 状态,而直接进入 CLOSED 状态,它就失去了当前状态的上下文,只能回复一个 RST 操作,从而导致被动关闭方出现错误。现在主机 1 知道自己处于 TIME_WAIT 的状态,就可以在接收到 FIN 报文之后,重新发出一个 ACK 报文,使得主机 2 可以进入正常的 CLOSED 状态。
第二个理由和连接“化身”和报文迷走有关系,为了让旧连接的重复分节在网络中自然消失。
我们知道,在网络中,经常会发生报文经过一段时间才能到达目的地的情况,产生的原因是多种多样的,如路由器重启,链路突然出现故障等。如果迷走报文到达时,发现 TCP 连接四元组(源 IP,源端口,目的 IP,目的端口)所代表的连接不复存在,那么很简单,这个报文自然丢弃。
我们考虑这样一个场景,在原连接中断后,又重新创建了一个原连接的“化身”,说是化身其实是因为这个连接和原先的连接四元组完全相同,如果迷失报文经过一段时间也到达,那么这个报文会被误认为是连接“化身”的一个 TCP 分节,这样就会对 TCP 通信产生影响。
所以,TCP 就设计出了这么一个机制,经过 2MSL 这个时间,足以让两个方向上的分组都被丢弃,使得原来连接的分组在网络中都自然消失,再出现的分组一定都是新化身所产生的。
划重点,2MSL 的时间是从主机 1 接收到 FIN 后发送 ACK 开始计时的。如果在 TIME_WAIT 时间内,因为主机 1 的 ACK 没有传输到主机 2,导致主机 1 又接收到了主机 2 重发的 FIN 报文,那么 2MSL 时间将重新计时。因为 2MSL 的目的是为了让旧连接的所有报文都能自然消亡,现在主机 1 重新发送了 ACK 报文,自然需要重新计时,以便防止这个 ACK 报文对新可能的连接化身造成干扰。
为什么需要进行四次挥手?
TCP 连接是双向的,服务器在收到客户端的 FIN 报文段后一般不会同时接收完数据,或者还有数据要向客户端发送,所以不会马上发送 FIN 数据段,只是先向客户端发送 ACK 对所接收的 FIN 报文段进行一个确认(此时客户端关闭了与服务器的传输连接),等服务端的数据全部接收完后再发送 FIN 报文段关闭与客户端的连接。
最大分组 MSL 是 TCP 分组在网络中存活的最长时间吗?
MSL 是任何 IP 数据报能够在因特网中存活的最长时间。其实它的实现不是靠计时器来完成的,在每个数据报里都包含有一个被称为 TTL(time to live)的 8 位字段,它的最大值为 255。TTL 可译为“生存时间”,这个生存时间由源主机设置初始值,它表示的是一个 IP 数据报可以经过的最大跳跃数,每经过一个路由器,就相当于经过了一跳,它的值就减 1,当此值减为 0 时,则所在的路由器会将其丢弃,同时发送 ICMP 报文通知源主机。
在 RFC793 中规定了 MSL 的时间为 2 分钟,但是在当前的高速网络中,2 分钟的等待时间又会造成资源的极大浪费,因此在 Linux 中的实际设置为 30 秒。