大家好,我是三叔,很高兴这期又和大家见面了,一个奋斗在互联网的打工人。
笔者在TCP 机制一文中有说到 TCP 是面向字节流的,这篇博客给大家介绍一下:为什么 TCP 是面向字节流协议的。
首先说一下 UDP ,是一个面向报文的协议,当用户消息通过 UDP 协议传输时,操作系统不会对消息进行拆分,在组装好 UDP 头部后就交给网络层来处理,所以发出去的 UDP 报文中的数据部分就是完整的用户消息,也就是每个 UDP 报文就是一个用户消息的边界,这样接收方在接收到 UDP 报文后,读一个 UDP 报文就能读取到完整的用户消息。
如果收到了两个 UDP 报文,那么 UDP 是如何将这些消息进行区分的?
如图所示:UDP 底层维护了一个队列,通过对队列进行消费
那么,为什么 TCP 是面向字节流
当用户消息通过 TCP 协议传输时,消息可能会被操作系统分组成多个的 TCP 报文,也就是一个完整的用户消息被拆分成多个 TCP 报文进行传输。这时,接收方的程序如果不知道发送方发送的消息的长度,也就是不知道消息的边界时,是无法读出一个有效的用户消息的,因为用户消息被拆分成多个 TCP 报文后,并不能像 UDP 那样,一个 UDP 报文就能代表一个完整的用户消息。
例如:发送方准备发送 hello 和 world 这两个消息,在发送端,当我们调用 send 函数完成数据 “发送” 以后,数据并没有被真正从网络上发送出去,只是从应用程序拷贝到了操作系统内核协议栈中。至于什么时候真正被发送,取决于发送窗口、拥塞窗口以及当前发送缓冲区的大小等条件。也就是说,我们不能认为每次 send 调用发送的数据,都会作为一个整体完整地消息被发送出去。所以发送 hello + world 信息,如果没有分界,会出现 hel + loworld,又或者 hell + oword等。
我们不知道 hello 和 world 这两个用户消息是如何进行 TCP 分组传输的。
因此,我们不能认为一个用户消息对应一个 TCP 报文,正因为这样,所以 TCP 是面向字节流的协议。
当两个消息的某个部分内容被分到同一个 TCP 报文时,就是我们常说的 TCP 粘包问题,这时接收方不知道消息的边界的话,是无法读出有效的消息。
要解决这个问题,要交给应用程序。
如何解决粘包
一般有三种方式分包的方式:
-
固定长度的消息:
比如规定一个消息的长度是 64 个字节,当接收方接满 64 个字节,就认为这个内容是一个完整且有效的消息。但是这种方式灵活性不高,实际中很少用 -
特殊字符作为边界:
如果报文中就有特殊字符,那么系统很有可能将消息中的特殊字符当成分界点,刚好消息内容里有这个特殊字符,我们要对这个字符转义,避免被接收方当作消息的边界点而解析到无效的数据。
-
自定义消息结构:
可以自定义一个消息结构,由包头和数据组成,其中包头包是固定大小的,而且包头里有一个字段来说明紧随其后的数据有多大。当接收方接收到包头的大小(比如 4 个字节)后,就解析包头的内容,于是就可以知道数据的长度,然后接下来就继续读取数据,直到读满数据的长度,就可以组装成一个完整到用户消息来处理了。
总结
所以 TCP 是面向字节流的协议,在消息体传输的过程中,是分段进行的,而 UDP 则是面向报文的传输协议,一次发送一个报文,用一个队列来维护。