一溪风月
一溪风月
Published on 2022-12-26 / 44 Visits
0
0

计算机网络协议之TCP

计算机网络协议之TCP

TCP协议比UDP协议要复杂的多,它需要处理各种丢包乱序重传拥塞的场景。但这并不意味着他能让网络情况变好,如果网络的确很差,是无法在软件层面上避免的。TCP协议能做的就是不断的重传重试,通过各种算法保证。

TCP包头格式

Snipaste_2022-12-26_09-38-46.png

  • 源端口号和目的端口号是指明这个包需要发送给哪个应用

  • 包的序号是为了解决乱序问题

  • 确认序号是确认某个包对方是否收到,解决丢包问题

  • 状态位:SYN 是发起一个连接;ACK 是回复;RST 是重新连接;FIN 是结束连接。TCP协议是面向连接的,这些状态位包的发送,会引起双方状态变更。

  • 窗口大小:TCP 要做流量控制,通信双方各声明一个窗口,标识当前自己能够处理的能力。如果发送的太快,超出处理能力范围,就降低发送速度;如果发送太慢,性能就不能很好发挥,需要快一点发送。根据双方的能力范围动态调整发送速度。

TCP三次握手

TCP 建立连接需要进行三次握手

  1. A 机器向 B 机器发送请求:B 你好,我是 A

  2. B 机器收到 A 机器的请求后,做出回应:A 你好,我是 B

  3. A 机器收到 B 机器的回应后,针对这个回应再做回应:你好 B

这个过程通常称之为“请求 -> 应答 -> 应答之应答”。下面来仔细剖析一下这个过程。

A 要发起一个连接,请求发出后,网络世界是未知的,这个包可能会丢,也可能没有找出到 B 机器的最短路劲,绕了弯路,亦或者 B 收到了请求,但是目的端口号没有程序监听或待确认队列数已满时,B 没有做出回应。总之这个包发出后就杳无音信了。

A 不知道到底发生了什么情况,它会不断的重新发送,假设有一个包正常到达 B 了,如果 B 发现目的端口号无监听或者待确认队列已满,它不会做出回应,那么 A 在重试一段时间后就会放弃,这个连接建立失败。如果 B 没有上述情况,那它就会发送应答包给 A。

对于 B 来说,应答包进入网络世界后就充满位置,可能会丢,可能绕弯路,可能 A 已经挂了。所以 B 的应答包可能会发送多次,只要有一个应答包能够到达 A,A 就认为连接已经建立了,因为对于 A 来讲,它的消息有去有回。A 会给 B 发送应答之应答,如果这个消息也到达了 B ,这个连接就建立了。

过程剖析完了,思考一个问题,TCP 建立连接为什么需要三次握手?两次不行吗?四次不行吗?

如果两次握手可以建立连接的话,看这种情况,A 和 B 两次握手建立了连接,简单的通信后,连接结束。但是 A 在建立连接的时候,请求包会发送多次,有的包饶了一点路,但最终还是找到了 B,B 收到这个请求后会以为 A 要重新建立连接,于是会发送应答包,好了,现在进行了两次握手,一个连接又建立了,当 A 收到 B 的应答包后,A 不会做任何事情,因为 A 没有要求建立连接,自然也不会发送结束连接的消息。因此两次握手不行。

A 和 B 建立连接时,A 给 B 发送的应答之应答可能也会丢。按理来说,B 应该有一个应答之应答之应答,所以四次是可以的,四十次也是可以的,关键是就算四百次也不能保证就真的可靠。只要双方的消息有去有回,就基本可以了。

好在大部分情况,A 和 B 通过三次握手建立连接后,A 会马上发送数据,一旦 A 发送数据,即使 A 发送给 B 的应答之应答丢了,当 A 发送的后续数据到达时,B 认为这个连接也就建立了。或者 B 挂掉了,A 发送的数据,会报错,说 B 不可达,那 A 就会停止发送。

如果建立连接后 A 不发送数据,我们在程序设计时,可以要求开启 keepalive 机制,定时发送探活包,保证连接不断开。

另外,作为服务端 B 的程序设计者,对于建立连接之后长时间不发包的情况,可以主动关闭,从而空出资源来给其他客户端使用。

三次握手除了双方建立连接外,还会沟通 TCP 包的序号问题

连接建立后,双方会确认序号是从哪个号开始的。因为如果从 1 开始,很大概率会出现冲突。

例如,A 和 B 建立连接后,发送了 1,2,3 三个包,但是发送 3 的时候,3 中间绕路了,后来 A 掉线了,重新和 B 建立连接后,序号又从 1 开始,然后发送 2 ,还没发送 3 时,上次绕路的 3 找打了 B 。B 自然认为这是下一个包,于是发生了错误。因而,每个连接都要有不同的序号,这个序号的起始序号是随着时间变化的,可以看成一个 32 位的计数器,每 4 微秒加一。序号不从 1 开始而是要从时间计数器开始的原因是,防止有效时间内包的序号重复,发生拼接包的错误。

IP 包头里面有个 TTL,即生存时间。这个生存时间是指 IP 包最大跳转次数,TTL 字段由 IP 数据包的发送者设置,在 IP 数据包从源到目的地的整个转发路劲上,每经过一个路由器,路由器都会修改这个 TTL 字段值,具体做法是将 TTL 的值减 1。然后再将这 IP 包转发出去,如果 IP 包在到达目的地之前,TTL 减少为 0。路由器会丢弃收到的 TTL=0 的 IP 包并向 IP 包的发送者发送 ICMP TIME EXCEEDED消息。序列号每 4 微秒加一,32位,如果到重复,大概需要 4 个多小时的时间。没有一个 IP 包会生存 4 个小时这么久,IP 包有一个最大生存时间 MSL,协议规定为 2 分钟,所以序列号加到重复的时候,原来发送过的 IP 包都已经被丢弃了,避免了包序号重复的问题。

总结一下,TCP 三次握手确认了两件事情。

  1. 客户端和服务端各自确认了对方的存在

  2. 约定各自发送初始数据包的序列号

至此,双方终于建立了连接,为了维护这个连接,双反需要维护一个状态机。如下图所示。

Snipaste_2022-12-26_10-58-08.png

一开始,客户端和服务端都处于 CLOSED 状态。先是服务端主动监听某个端口,处于 LISTEN 状态。然后客户端主动发起连接 SYN,之后处于 SYN_SENT 状态。服务端收到客户端发起的连接,返回 SYN,并且 ACK 客户端的 SYN,之后处于 SYN_RCVD 状态。客户端收到服务端发送的 SYN 和 ACK 之后,发送 ACK 的 ACK ,之后处于 ESTABLISHED 状态,因为它一发一收成功了。服务端收到了 ACK 的 ACK 之后,处于 ESTABLISHED 状态,因为它也一发一收了。

TCP四次挥手

三次握手建立了连接,那如何关闭连接呢?这常被称为四次挥手。

  1. A:B,我要关闭连接了

  2. B:好的

  3. B:A,我也要关闭连接了

  4. A:好的,再见

通过上面四次请求,连接就会关闭了。注意:在第二步的时候,只是 A 想要关闭请求,即 A 不会再发送数据,但是 B 不能在接受到 A 的关闭请求后直接关闭。因为很可能是 A 发完了最后的数据准备关闭连接,但是 B 还没有做完自己的事情,还是可以发送数据的,所以称为半关闭状态。这个时候 A 可以选择不再接收数据,也可以选择最后再接收一段数据,等待 B 主动关闭。

第一步的时候,A 发送关闭连接的请求,然后直接关闭连接,是有问题的,因为 B 还没有发起结束,如果 A 已经关闭了连接,B 就算发起结束,也不会收到回答,B 就不知道该怎么办了;另外一种情况是,A 发送关闭连接的请求,B 直接关闭连接,也是有问题的,因为 A 不知道 B 是还有事情要处理,还是过一会会发送结束。

为了解决上面的问题,TCP 协议设计了几个状态来处理。

Snipaste_2022-12-26_11-59-50.png

A 和 B 要断开连接的时候,A 发送断开连接的请求,就进入了 FIN_WATI_1 的状态。B 收到 A 断开连接的请求,发送应答 ACK,进入 CLOSE_WAIT 的状态。

A 收到 B 的 ACK 响应,进入 FIN_WAIT_2 的状态,如果这个时候 B 直接断开连接,A 将永远在这个状态。TCP 协议里面并没有对这个状态的处理,但是 Liunx 有,可以调整 tcp_fin_timeout 这个参数,设置一个超时时间。

如果 B 没有断开连接,并且处理好自己的事情后,发送 B 断开连接的请求。A 接收到 B 断开连接的请求时,结束 FIN_WAIT_2 状态,并发送应答 ACK。按理说这个时候 A 可以关闭连接了,但是万一最后这个 ACK B 没有收到,B 会重新发送要求断开连接的请求,如果 A 已经断开连接,B 就永远收不到应答的 ACK 了。所以 TCP 协议要求 A 最后等待一段时间 TIME_WAIT。这个时间足够长,长到如果 B 没收到 ACK 的话,B 会重新发送断开连接的请求,A 会重新发送一个 ACK 并且足够时间到达 B。

A 直接断开连接的另一个问题是,A 的端口就直接空出来,但是 B 不知道,B 原来发过的很多包很可能还在路上,如果 A 的端口被一个新的应用占用,这个新的应用会收到上个连接中 B 发过来的包,虽然序列号是重新生成的,为了产生混乱,因而需要等待足够长的时间,等到原来 B 发送的所有包都已经被路由器丢弃,再空出端口来。

等待时间设为 2MSL,MSL 是 Maximum Segment Lifetime报文最大生存时间。它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。TCP 协议规定 MSL 为 2 分钟,实际应用中常用的是 30 秒,1 分钟和 2 分钟等。

另外一种情况是,因为主动关闭连接的是 A,因此 A 在给 B 发完 ACK 后会进入 TIME_WAIT 状态,并保持 2 MSL,以便等待 B 重新发送 FIN。但是 B 超过了 2MSL 后才发 FIN,其实 A 这个时候已经把连接中断了,此时 B 再发任何消息到 A 都会得到 RST 表示连接有问题。


Comment