最近石家庄桥西区 yq 比较严重,上周我封在了公司三天,洗漱吃饭都成问题,好不容易从公司申请回家到现在又封控一周了,每天游离在各种 app 、微信群的抢菜环节,真的是抢而不是买,家乐福超市商品上线两分钟瞬间秒没,我都不知道这群人是怎么抢菜的,难道都是圈里人?还有每天无穷无尽的核酸检测,让人无奈且头疼,每天两位数的阳性新增让人麻木且看不到希望。今天刚接到通知,说是你可以出单元门活动活动筋骨了 ......

说点好的:?

写了很多篇关于 TCP 和 UDP 的文章,还没有好好聊过这两个协议的区别,这篇我们就来谈一谈这个问题。

关于 TCP 和 UDP ,想必大家都看过一张这样的图。

有一个小姑娘在对着瓶口慢慢喝水,下面写着可靠的传输,少女的衣服没有被水浸湿,这张图被称为 TCP 。

然后又有一个小姑娘在举着水瓶以很快的速度向下倒水,少女的头发凌乱,脸色泛红,衣服也被水浸湿,这张图被称为 UDP 。

这两张图我认为是个程序员都能大致总结出来这两个传输协议的不同点(毕竟图上都写的很清楚了)甚至不少同学对 UDP 产生了邪恶的念想,你说作者好好的画个图不行吗,非要在脸上挂个红,把衣服弄湿了才行。。。。。。。

咳,咱们言归正传,TCP 和 UDP 的区别一直是面试的重点,也是经常被用来拿来各种比较的两个协议。

建立连接的差异

TCP 建立连接需要经过三次握手,同时 TCP 断开连接需要经过四次挥手,这也表示 TCP 是一种面向连接的协议,这个连接不是用一条网线或者一个管道把两个通信双方绑在一起,而是建立一条虚拟通信管道。

TCP 的三次握手流程(客户端向服务器发送建立连接请求):

  • 服务端进程准备好接收来自外部的 TCP 连接,一般情况下是调用 bind、listen、socket 三个函数完成。这种打开方式被认为是被动打开(passive open)​。然后服务端进程处于LISTEN 状态,等待客户端连接请求。
  • 客户端通过connect​ 发起主动打开(active open)​,向服务器发出连接请求,请求中首部同步位 SYN = 1,同时选择一个初始序号 sequence ,简写 seq = x。SYN 报文段不允许携带数据,只消耗一个序号。此时,客户端进入SYN-SEND 状态。
  • 服务器收到客户端连接后,需要确认客户端的报文段。在确认报文段中,把 SYN 和 ACK 位都置为 1 。确认号是 ack = x 1,同时也为自己选择一个初始序号 seq = y。请注意,这个报文段也不能携带数据,但同样要消耗掉一个序号。此时,TCP 服务器进入SYN-RECEIVED(同步收到) 状态。
  • 客户端在收到服务器发出的响应后,需要给出确认连接。确认连接中的 ACK 置为 1 ,序号为 seq = x 1,确认号为 ack = y 1。TCP 规定,这个报文段可以携带数据也可以不携带数据,如果不携带数据,那么下一个数据报文段的序号仍是 seq = x 1。这时,客户端进入ESTABLISHED (已连接) 状态。
  • 服务器收到客户的确认后,也进入ESTABLISHED 状态。

而 UDP 是面向数据报的协议,所以 UDP 压根不会有连接的概念,也就不会有三次握手建立连接的过程。

数据传输结束后,通信双方可以释放连接。数据传输结束后的客户端主机和服务端主机都处于 ESTABLISHED 状态,然后进入释放连接的过程。

(客户端主机主动关闭连接)

TCP 断开连接需要历经的过程如下:

  • 客户端应用程序发出释放连接的报文段,并停止发送数据,主动关闭 TCP 连接。客户端主机发送释放连接的报文段,报文段中首部 FIN 位置为 1 ,不包含数据,序列号位 seq = u,此时客户端主机进入 FIN-WAIT-1(终止等待 1) 阶段。
  • 服务器主机接收到客户端发出的报文段后,即发出确认应答报文,确认应答报文中 ACK = 1,生成自己的序号位 seq = v,ack = u 1,然后服务器主机就进入 CLOSE-WAIT(关闭等待) 状态,这个时候客户端主机 -> 服务器主机这条方向的连接就释放了,客户端主机没有数据需要发送,此时服务器主机是一种半连接的状态,但是服务器主机仍然可以发送数据。
  • 客户端主机收到服务端主机的确认应答后,即进入 FIN-WAIT-2(终止等待2) 的状态。等待客户端发出连接释放的报文段。
  • 当服务器主机没有数据发送后,应用进程就会通知 TCP 释放连接。这时服务端主机会发出断开连接的报文段,报文段中 ACK = 1,序列号 seq = w,因为在这之间可能已经发送了一些数据,所以 seq 不一定等于 v 1。ack = u 1,在发送完断开请求的报文后,服务端主机就进入了 LAST-ACK(最后确认)的阶段。
  • 客户端收到服务端的断开连接请求后,客户端需要作出响应,客户端发出断开连接的报文段,在报文段中,ACK = 1, 序列号 seq = u 1,因为客户端从连接开始断开后就没有再发送数据,ack = w 1,然后进入到 TIME-WAIT(时间等待)​ 状态,请注意,这个时候 TCP 连接还没有释放。必须经过时间等待的设置,也就是 2MSL​ 后,客户端才会进入 CLOSED​ 状态,时间 MSL 叫做最长报文段寿命(Maximum Segment Lifetime)。
  • 服务端主要收到了客户端的断开连接确认后,就会进入 CLOSED 状态。因为服务端结束 TCP 连接时间要比客户端早,而整个连接断开过程需要发送四个报文段,因此释放连接的过程也被称为四次挥手。

UDP 不存在这条连接,所以它也不需要四次挥手操作。

所以总结一点:TCP 是面向连接的,它的数据传输前需要维护一条虚拟连接,数据传输需要在这条虚拟连接上进行,数据传输完毕后需要断开这条连接,而 UDP 传输不是面向连接的,UDP 发送数据不会建立连接,也不会关心接收端的状态。

可靠性的差异

TCP 和 UDP 一个主要拿来作对比的就是可靠性,TCP 是一种可靠性的传输层协议,UDP 是一种不可靠的传输层协议。TCP 的这种可靠性主要由下面这些特征来保证:

通过序列号和应答号实现可靠性

计算机网络主机之间的相互通信非常类似于我们日常生活中两个人之间打电话,这种对话通常是一问一答形式,如果你讲了一句话并没有收到任何回应,你通常需要再说一次来确保对方是否听到,如果对方给你回应了一句话,就说明他已经听到你的讲话了,这就是一个完整的通话流程(抛开建立连接不谈,我们着重点放在建立连接之后)。

"对方给你的响应" 在计算机网络中被称为确认应答(ACK)​,TCP 就是通过 ACK 来实现可靠的数据传输,也就是说,发送方在发出请求之后会等待目标主机的响应,如果没有收到响应,发送方在经过一段时间后就会重传请求。所以,即使在发送过程中产生丢包,TCP 仍然能够通过重传来实现可靠性。

上面描述的情况属于发送方请求丢失,还有一种情况属于响应丢失,也就是说请求发送到目标主机后,目标主机会发 ACK 给请求方,这个 ACK 也有可能丢失,如果 ACK 在链路中丢失,一段时间后请求方没有收到目标主机的 ACK ,仍然会选择重传未收到 ACK 的这个请求。

除了消息丢失之外,还存在一种延迟到达的现象,延迟到达指的是发送方发送一个报文段之后,这个报文也许是由于网络抖动或者网络拥堵导致一个报文段迟迟没有到达目标主机,或者目标主机的响应 ACK 迟迟没有到达发送方的现象。这个一段时间判断的标准就是重传时间,一旦过了重传时间发送方会重传报文段,很可能存在重传报文段到达之后,第一次发送的报文段才刚到的情况,这就存在一个问题:目标主机收到了两个相同的报文段。必须选择一个报文段进行丢弃,但是应该选择哪个报文段呢?

可以通过序列号(seq)来实现,序列号是按照顺序给发送数据的每一个字节都标上号码的编号。接收端通过查询 TCP 首部中的序列号和数据的长度,将自己下一步应该接收的序列号作为确认应答返送回去。通过序列号和确认应答号,TCP 能够识别是否已经接收数据,又能够判断是否需要接收,从而实现可靠传输。

如上图所示,请求按照顺序发送的话是 seq = 1 ,这个请求会把第 1 字节到第 n 字节的数据一起发送过去,等待目标主机一次确认每个字节后,再发送 seq = n 1 的请求,确认完成后再发送 seq = m 1 的请求,这样能够保证序列号不会重复。

UDP 没有所谓的序列号和确认号,所以不会对数据进行确认,数据丢失后也不会进行重传,所以 UDP 是一种不可靠的协议。

如果使用 TCP 和 UDP 来比喻开发人员:TCP 就是那种凡事都要设计好,没设计不会进行开发的工程师,需要把一切因素考虑在内后再开干!所以非常靠谱​;而 UDP 就是那种上来直接干干干,接到项目需求马上就开干,也不管设计,也不管技术选型,就是干,这种开发人员非常不靠谱,但是适合快速迭代开发,因为可以马上上手!

有序性差异

我们上面说到,TCP 会对请求分开发送,每次请求所携带的数据都会被目标主机进行确认,目标主机依次确认每个请求后,就会对请求中的数据进行重组,由于请求是由 seq 的,所以 TCP 在重组这些数据时,也会按照顺序进行重组,而 UDP 没有有序性的这种保证。

报文段的差异

TCP 和 UDP 同属于传输层协议,传输层协议传输的数据统称为报文段,TCP 和 UDP 的报文段的主要差异如下。

UDP 报文段结构

  • 源端口号(Source Port) :这个字段占据 UDP 报文头的前 16 位,通常包含发送数据报的应用程序所使用的 UDP 端口。接收端的应用程序利用这个字段的值作为发送响应的目的地址。这个字段是可选项,有时不会设置源端口号。没有源端口号就默认为 0 ,通常用于不需要返回消息的通信中。
  • 目标端口号(Destination Port): 表示接收端端口,字段长为 16 位。
  • 长度(Length): 该字段占据 16 位,表示 UDP 数据报长度,包含 UDP 报文头和 UDP 数据长度。因为 UDP 报文头长度是 8 个字节,所以这个值最小为 8,最大长度为 2 ^ 16 = 65535 字节。
  • 校验和(Checksum):UDP 使用校验和来保证数据安全性,UDP 的校验和也提供了差错检测功能,差错检测用于校验报文段从源到目标主机的过程中,数据的完整性是否发生了改变。

TCP 报文段结构

TCP 报文段结构相比 UDP 报文结构多了很多内容。但是前两个 32 比特的字段是一样的。它们是 源端口号​ 和 目标端口号​。另外,和 UDP 一样,TCP 也包含校验和(checksum field) ,除此之外,TCP 报文段首部还有下面这些

  • 32 比特的序号字段(sequence number field)​ 和 32 比特的确认号字段(acknowledgment number field) 。这些字段被 TCP 发送方和接收方用来实现可靠的数据传输。
  • 4 比特的首部字段长度字段(header length field),这个字段指示了以 32 比特的字为单位的 TCP 首部长度。TCP 首部的长度是可变的,但是通常情况下,选项字段为空,所以 TCP 首部字段的长度是 20 字节。
  • 16 比特的 接受窗口字段(receive window field) ,这个字段用于流量控制。它用于指示接收方能够/愿意接受的字节数量
  • 可变的选项字段(options field),这个字段用于发送方和接收方协商最大报文长度,也就是 MSS 时使用
  • 6 比特的 标志字段(flag field)​, ACK​ 标志用于指示确认字段中的值是有效的,这个报文段包括一个对已被成功接收报文段的确认;RST、SYN、FIN​ 标志用于连接的建立和关闭;CWR​ 和 ECE​ 用于拥塞控制;PSH​ 标志用于表示立刻将数据交给上层处理;URG​ 标志用来表示数据中存在需要被上层处理的 紧急 数据。紧急数据最后一个字节由 16 比特的紧急数据指针字段(urgeent data pointer field) 指出。一般情况下,PSH 和 URG 并没有使用。

所以从报文段结构的对比可以看出,TCP 相比 UDP 多了许多 Flags、序号和确认号,这些都属于 TCP 的连接控制。除此之外还有接收窗口,这些属于拥塞控制和流量控制的内容。TCP 的首部开销要比 UDP 大,因为 TCP 首部固定有 20 字节,UDP 首部固定才 8 字节。TCP 和 UDP 都提供了数据校验功能。

效率的差异

TCP 报文段的发送采用的是"一问一答"形式的,每个请求都会被目标主机确认后再发送下一条报文,效率很慢,后来为了解决这个问题,TCP 引入了 窗口 这个概念,即使在往返时间较长、频次很多的情况下,它也能控制网络性能的下降。

我们之前每次请求发送都是以报文段的形式进行的,引入窗口后,每次请求都可以发送多个报文段,也就是说一个窗口可以发送多个报文段。窗口大小就是指无需等待确认应答就可以继续发送报文段的最大值。

在这个窗口机制中,大量使用了 缓冲区 ,通过对多个段同时进行确认应答的功能。

如下图所示,发送报文段中高亮部分即是我们提到的窗口,在窗口内,即使没有收到确认应答也可以把请求发送出去。不过,在整个窗口的确认应答没有到达之前,如果部分报文段丢失,那么发送方将仍会重传。为此,发送方需要设置缓存来保留这些需要重传的报文段,直到收到他们的确认应答。

在滑动窗口以外的部分是尚未发送的报文段和已经接收到的报文段,如果报文段已经收到确认则不可进行重发,此时报文段就可以从缓冲区中清除。

在收到确认的情况下,会将窗口滑动到确认应答中确认号的位置,如上图所示,这样可以顺序将多个段同时发送,用以提高通信性能,这种窗口也叫做 滑动窗口(Sliding window)。

UDP 发送的报文段不需要确认,也就没有窗口的概念,所以 UDP 传输效率比较高。

使用场景的差异

TCP 和 UDP 在效率、报文段、流量控制、连接管理上均存在差异,由于这些差异导致了应用场景要有不同的选择,由于 TCP 每个包都需要进行确认,因此 TCP 不适合告诉传输数据的场景,像是这种场景使用 UDP 就好了;像是 Ping 和 DNS Lookup,这类型的操作只需要一次简单的请求/返回,不需要建立连接,用 UDP 就足够了。比如 HTTP 协议需要考虑请求响应的可靠性,这种场景应该使用 TCP 协议,但是像 HTTP 3.0 这类应用层协议,从功能性上思考,暂时没有找到太多的优化点,但是想要把网络优化到极致,就会用 UDP 作为底层技术,然后在 UDP 基础上解决可靠性。