# 传输层协议

常见的传输层协议有:TCP(传输控制)协议、UDP(用户数据报)协议。

首先明确,TCP 是一种面向连接的可靠的基于字节流传输层通信协议。

而另外一个常说的 UDP 是一种无连接的不可靠的基于数据报传输层通信协议。


# TCP 连接

TCP 连接可谓计算机网络的重要知识之一,单独解析这方面的内容都可以出一个章节。

这里只挑出常问的三次握手、四次挥手过程进行讲解。


# 三次握手

TCP 连接在构建时,需要先进行三次握手,以保证双方确实处于可通信的状态,使数据包的发送与接收同步,从而进一步保证每一个数据包的送达以及先后顺序等功能。

下面假设有主机 A 与主机 B。

其中,大写的英文为标志位,仅取0或1;小写的英文占4个字节,取具有具体意义的值。


🤝第一次握手

A 通过向 B 发送一个含有同步标志位(SYN)以及序列号(seq)的数据段给 B,向 B 请求建立连接。

通过这个数据段,A 告诉 B 两件事:

  • 我想要和你通信
  • 你可以用哪个序列号(假设为x)作为起始数据段来回应我

🤝第二次握手

B 收到 A 的请求后,用一个带有确认应答标志位(ACK)和同步标志位(SYN)的数据段响应 A,然后确认收到序列号 ack=x+1。

这就告诉了 A 两件事:

  • 我同意你的通信请求
  • 你要用哪个序列号(假设为y)作为起始数据段来回应我

可以看到 A 的序列号(seq=x)与 B 的序列号(seq=y)是不同的。

我们沟通的时候都是确认(ack=x+1)对方的序列号,然后发送自己的序列号(seq=y)。


🤝第三次握手

A 收到这个数据段后,再发送一个确认应答(ACK),确认已收到 B 的数据包。

这是最后的确认:

  • 我已收到回复,我现在要开始传输实际数据了。

以上,3次握手就完成了,A 和 B 就可以开始传输数据了。


关键的问题就是,为什么是“三次握手”而不是“二次握手”

其实我们用“反证法”就能够很好地理解:

我们知道 TCP 是一个可靠的传输层协议,为了足够的可靠,我们必须考虑到网络不好时会出现的“丢包”问题。

如果在构建 TCP 连接的过程中:

  • 第一次握手出现了丢包,我们可以通过超时重传的方式再次请求建立 TCP 连接,来确认到底是丢包了还是主机 B 并不在线。
  • 而第二次握手如果出现了丢包,假设我们仅使用二次握手建立 TCP 连接,则对于主机 A 来说,它并不知道主机 B 到底是否在线,也不知道主机 B 会以什么样的序列号(seq=y)来向它传输信息,所以它会拒绝主机 B 的所有其他包并不做反应,只等待第二次握手的 SYN-ACK 包。
  • 这个时候,对于主机 B 而言,它以为 A 已经和它建立了连接,于是只会发送它想传的后续数据,这个时候它收不到 A 返回的确认(ACK)包,就会以为是后续的数据包丢失了,从而不断地重传这些数据包,变成了一个死锁的情况。

于是我们需要进行第三次握手,表示我们的主机 A 确认收到了“B 确认收到了 A 的连接请求”。

  • 如果第二次握手的包丢失了,B 就收不到第三次握手的 ACK 包,从而会重新进行第二次握手。
  • 如果第三次握手的包丢失了,由于已经建立过两次握手,AB 彼此之间都知道对方的序列号(seq),即便 A 向未准备好的 B 发送后续数据,B 也能进行反馈,告知 B 还没有收到第三次握手的 ACK 包,从而令 A 重新进行第三次握手。

就这样,在三次握手后,TCP 连接完成建立,后续就可以在该连接上进行应用层的服务(如 HTTP 请求)了。


# 四次挥手

同样的,为了保证可靠,我们的 TCP 连接也不能想断开连接就断开连接,需要双方都同意结束 TCP 连接后,才断开连接。

其过程比较类似,只是请求同步的标志位 SYN 变成了请求断开的标志位 FIN,这里不再重讲一遍流程。


不过值得注意的是,我们在断开连接时需要进行四次挥手而非三次挥手


为什么会产生这样的区别?关键就是存在“某一方提出关闭时,另外一方不一定将所有数据都传输完毕”的情况。

那么就很好理解了:

  • 在 A 提出关闭后,B 就也准备关闭,但出于可靠性,可能需要将其手上准备发出的剩余数据都传完,于是先告知 A 它已经收到了关闭请求,避免 A 反复提出关闭。
  • 前两次握手后,A 就可以进入只接收数据、不再发送数据的状态,而 B 可以进入只发送数据、不再接收数据的状态,从而节约资源。
  • 等待 B 必要的数据都发送完后,就可以发送 FIN-ACK 包来进行下一步的连接关闭。于是又是两次握手,确认双方都收到了关闭结果,就可以安全地关闭 TCP 连接了。

这里的一个细节就是,第四次握手中,A 会等待 2MSL(Maximum Segment Lifetime,即两倍的报文最大生存时间)。

我们应该有感觉了,这肯定也是为了保证可靠性而存在的。

因为 MSL 代表了保存的最大生存时间,那么假设最后两次握手的数据包传输都很慢,恰好赶上 1MSL,则“主机 B 从发出 FIN-ACK 包”到“B 收到 A 的 ACK 包”,最多只需 2MSL,如果超过这个时间长度还没有收到最后的 ACK 包,则说明 A 可能没有收到 FIN-ACK 包,于是 B 会进行重传。

对于主机 A 也是如此,如果主机 B 没有收到 A 发出的最后一个 ACK 包,则在 2MSL 内 B 会重发 FIN-ACK 包,告知 A 需要重发 ACK 包。


不过,这个流程算是比较理想的完整 TCP 连接,如果客户端出现了故障导致没来得及四次挥手,直接单方面关闭了,这个情况也是 TCP 所要考虑到的。

TCP 连接存在一个保活计时器,服务端每次收到客户端发来的请求都会重置这个计时器。

如果客户端连续一段时间没有发送请求,则计时器不断累加达到阈值(如2小时),这个时候服务器就会向客户端发送一个心跳包,来检测客户端是否还活着。

一般情况下,服务端每个75秒发送次心跳包,连续10次都没有收到客户端的回应,则说明客户端应该是出现了故障,这时服务端就会自己关闭这个连接。


# UDP 连接

我们前面说到,UDP 是一种无连接的不可靠的基于数据报传输层通信协议。

它没有 TCP 用的那么广泛,但在不少的场景下,仍然是非常好用的协议。这是因为以下的一些特点:

  1. UDP 在传输数据之前发送端和接收端不建立连接
    • 当它想传送时去抓取来自应用程序的数据,并尽可能快地把它扔到网络上
    • 在发送端,UDP 传送数据的速度仅受应用程序生成数据的速度、计算机的能力和传输带宽的限制
    • 在接收端,UDP 把每个消息段放在队列中,应用程序每次从队列中读一个消息段
  2. 由于不建立连接,因此也无需维护连接状态,因此一台服务器可同时向多个客户端传输相同消息(即广播)
  3. UDP 的包头很短(8个字节),相对于 TCP 的20字节包头的额外开销很小
  4. UDP 仅尽最大努力交付,即不保证可靠交付,因此主机无需维持复杂的链接状态表
  5. UDP 是面向报文的
    • 发送方的 UDP 对应用程序交下来的报文,既不拆分,也不合并,而是保留这些报文的边界
    • 因此,应用程序需要选择合适的报文大小
    • 而 TCP 则会帮忙分片,无需应用程序操心

总结 TCP 与 UDP 连接的区别:

  1. 基于连接与无连接
  2. 对系统资源的要求 TCP 相对多,UDP 相对少
  3. UDP 程序结构较简单
  4. 流模式与数据报模式
  5. TCP 具有流量控制、拥塞控制、差错控制,保证数据正确性,UDP 可能丢包
  6. TCP 保证数据顺序,UDP 不保证

上次更新: 2020/4/15 16:14:30