# 传输层之UDP协议

&#x20;`UDP` &#x662F;**`User Datagram Protocol`**&#x7684;简称， 中文名是**用户数据报协**议，是`OSI`（`Open System Interconnection`，开放式系统互联） 参考模型中一种**无连接的传输层协议**，**传输可靠性没有保证**。`UDP`只是在`IP`的数据报服务之上增加了最基本的服务：复用和分用以及差错检测。

## :pencil2: 1、`UDP`的特点

`UDP`提供不可靠服务，具有`TCP`所没有的优势：

* `UDP`**无连接**，时间上不存在建立连接需要的时延。空间上，`TCP`需要在端系统中维护连接状态，需要一定的开销。此连接装入包括接收和发送缓存，拥塞控制参数和序号与确认号的参数。`UCP`不维护连接状态，也不跟踪这些参数，开销小。空间和时间上都具有优势。

  > `DNS`如果运行在`TCP`之上而不是`UDP`，那么`DNS`的速度将会慢很多。\
  > `HTTP`使用`TCP`而不是`UDP`，是因为对于基于文本数据的Web网页来说，可靠性很重要。\
  > 同一种专用应用服务器在支持`UDP`时，一定能支持更多的活动客户机。
* **分组首部开销小**，`TCP`首部20字节，`UDP`首部8字节。
* `UDP`**没有拥塞控制**，应用层能够更好的控制要发送的数据和发送时间，网络中的拥塞控制也不会影响主机的发送速率。某些实时应用要求以稳定的速度发送，能容忍一些数据的丢失，但是不能允许有较大的时延（比如实时视频，直播等）。
* `UDP`提供尽**最大努力的交付，不保证可靠交付**。所有维护传输可靠性的工作需要用户在应用层来完成。没有`TCP`的确认机制、重传机制。如果因为网络原因没有传送到对端，`UDP`也不会给应用层返回错误信息。
* `UDP`是**面向报文的**，对应用层交下来的报文，添加首部后直接乡下交付为IP层，既不合并，也不拆分，保留这些报文的边界。对IP层交上来`UDP`用户数据报，在去除首部后就原封不动地交付给上层应用进程，报文不可分割，是`UDP`数据报处理的最小单位。
* > 正是因为这样，`UDP`显得不够灵活，不能控制读写数据的次数和数量。比如我们要发送100个字节的报文，我们调用一次`sendto`函数就会发送100字节，对端也需要用`recvfrom`函数一次性接收100字节，不能使用循环每次获取10个字节，获取十次这样的做法。
* `UDP`常用一次性传输比较少量数据的网络应用，如`DNS`，`SNMP`等，因为对于这些应用，若是采用`TCP`，连接的创建，维护和拆除带来不小的开销。`UDP`也常用于多媒体应用（如IP电话，实时视频会议，流媒体等）数据的可靠传输对他们而言并不重要，`TCP`的拥塞控制会使他们有较大的延迟，也是不可容忍的。
* UDP支持一对一、一对多、多对一和多对多。

## :pencil2: **2**、`UDP`报文头部

![](https://2394460149-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MB7Io-ER4uA8ffGsDSz%2F-MCkdoEJIfXdfV7yXjdM%2F-MClsEPGBK38gfzRzRGo%2F20.jpg?alt=media\&token=53331071-dca3-43de-a638-4dba9c57b3e5)

`UDP`头部的标识如下：

1. **16位源端口号：**&#x6E90;主机的应用程序使用的端口号。
2. **16位目的端口号：**&#x76EE;的主机的应用程序使用的端口号。
3. **16位`UDP`长度：**&#x662F;指`UDP`头部和`UDP`数据的字节长度。因为`UDP`头部长度为8字节，所以该字段的最小值为8。
4. **16位`UDP`校验和：**&#x8BE5;字段提供了与`TCP`校验字段同样的功能；该字段是可选的，当源主机不想计算校验和，则直接令该字段全为0。

### :pen\_fountain: 2.1、包大小

#### :hamster: 2.1.1、 **`UDP` 报文大小的影响因素**

* `UDP`协议本身，`UDP`协议中有16位的`UDP`报文长度，那么`UDP`报文长度不能超过 $$2^{16}-1=65535$$；
* 以太网(Ethernet)数据帧的长度，数据链路层的`MTU`(最大传输单元)；
* socket的`UDP`发送缓存区大小。

#### :hamster: 2.1.2、 **`UDP`数据包最大长度**

根据 `UDP` 协议，从 `UDP` 数据包的包头可以看出，`UDP` 的最大包长度是65535的个字节。由于`UDP`包头占8个字节，而在IP层进行封装后的IP包头占去20字节，所以这个是`UDP`数据包的最大理论长度是65507字节。如果发送的数据包超过65507字节，`send`或`sendto`函数会错误码1(Operation not permitted， Message too long)，当然，一个数据包能否发送65507字节，还和`UDP`发送缓冲区大小（`linux`下`UDP`发送缓冲区大小为：`cat /proc/sys/net/core/wmem_default`）相关，如果发送缓冲区小于65507字节，在发送一个数据包为65507字节的时候，`send`或`sendto`函数会错误码1(Operation not permitted， No buffer space available)。

#### :hamster: 2.1.3、 **`UDP`数据包理想长度**

理论上 `UDP` 报文最大长度是65507字节，实际上发送这么大的数据包效果最好吗？我们知道`UDP`是不可靠的传输协议，为了减少 `UDP` 包丢失的风险，我们最好能控制 `UDP` 包在下层协议的传输过程中不要被切割。

**局域网环境下，建议将`UDP`数据控制在1472字节以下。**

> 以太网(Ethernet)数据帧的长度必须在46-1500字节之间,这是由以太网的物理特性决定的，这个1500字节被称为链路层的`MTU`(最大传输单元)。但这并不是指链路层的长度被限制在1500字节，其实这这个`MTU`指的是链路层的数据区，并不包括链路层的首部和尾部的18个字节。所以，事实上这个1500字节就是网络层IP数据报的长度限制。因为IP数据报的首部为20字节，所以IP数据报的数据区长度最大为1480字节。而这个1480字节就是用来放`TCP`传来的`TCP`报文段或`UDP`传来的`UDP`数据报的。又因为`UDP`数据报的首部8字节，所以`UDP`数据报的数据区最大长度为1472字节。这个1472字节就是我们可以使用的字节数。
>
> **当我们发送的`UDP`数据大于1472的时候会怎样呢？** 这也就是说IP数据报大于1500字节，大于`MTU`，这个时候发送方IP层就需要分片(fragmentation)。把数据报分成若干片，使每一片都小于`MTU`，而接收方IP层则需要进行数据报的重组。这样就会多做许多事情，而更严重的是，由于`UDP`的特性，当某一片数据传送中丢失时，接收方无法重组数据报，将导致丢弃整个`UDP`数据报。因此，在普通的局域网环境下，我建议将`UDP`的数据控制在1472字节以下为好。

**Internet编程时，建议将`UDP`数据控制在548字节以下。**

> 进行Internet编程时则不同，因为Internet上的路由器可能会将`MTU`设为不同的值。如果我们假定`MTU`为1500来发送数据，而途经的某个网络的`MTU`值小于1500字节，那么系统将会使用一系列的机制来调整`MTU`值，使数据报能够顺利到达目的地，这样就会做许多不必要的操作。鉴于Internet上的标准`MTU`值为576字节，所以我建议在进行Internet的`UDP`编程时， 最好将`UDP`的数据长度控件在548字节(576-8-20)以内。  这句话貌似有问题，`unix`网络编程第一卷里说：`ipv4`协议规定`ip`层的最小重组缓冲区大小为576！所以，建议`UDP`包不要超过这个大小，而不是因为internet的标准`MTU`是576！

### :pen\_fountain: 2.2、`UDP`校验

**在计算校验和的时候，需要在`UDP`数据报之前增加12字节的伪首部**，伪首部并不是`UDP`真正的首部。只是在计算校验和，临时添加在`UDP`数据报的前面，得到一个临时的`UDP`数据报。校验和就是按照这个临时的`UDP`数据报计算的。伪首部既不向下传送也不向上递交，而仅仅是为了计算校验和。这样的校验和，既检查了`UDP`数据报，又对IP数据报的源IP地址和目的IP地址进行了检验。

`UDP`校验和的计算方法和IP数据报首部校验和的计算方法相似，都使用二进制反码运算求和再取反。

发送方，首先是把全零放入校验和字段并且添加伪首部，然后把`UDP`数据报看成是由许多16位的子串连接起来，若`UDP`数据报的数据部分不是偶数个字节，则要在数据部分末尾增加一个全零字节（此字节不发送），接下来就按照二进制反码计算出这些16位字的和。将此和的二进制反码写入校验和字段。在接收方，把收到得`UDP`数据报加上伪首部（如果不为偶数个字节，还需要补上全零字节）后，按二进制反码计算出这些16位字的和，当无差错时其结果全为1，否则就表明有差错出现，接收方应该丢弃这个`UDP`数据报。

![](https://2394460149-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MB7Io-ER4uA8ffGsDSz%2F-MCkdoEJIfXdfV7yXjdM%2F-MClvru9896JyImN6hgX%2F84.png?alt=media\&token=ec7c735d-d022-479d-8060-82884416afff)

> 注意：&#x20;
>
> 1.校验时，若`UDP`数据报部分的长度不是偶数个字节，则需要填入一个全0字节，但是次字节和伪首部一样，是不发送的。&#x20;
>
> 2.如果`UDP`校验和校验出`UDP`数据报是错误的，可以丢弃，也可以交付上层，但是要附上错误报告，告诉上层这是错误的数据报。&#x20;
>
> 3.通过伪首部，不仅可以检查源端口号，目的端口号和`UDP`用户数据报的数据部分，还可以检查IP数据报的源IP地址和目的地址。 这种差错检验的检错能力不强，但是简单，速度快。

## :pencil2: 3、发送和接收

### :pen\_fountain: **3.1、`UDP`的通信有界性：**

在阻塞模式下，`UDP`的通信是以数据包作为界限的，即使server端的缓冲区再大也要按照client发包的次数来一次一次地接收，client发送多少次，server就需接收多少次。

### :pen\_fountain: **3.2、`UDP`数据包的无序性和非可靠性：**

client依次发送1、2、3三个`UDP`数据包，server端先后调用3次接收函数，可能会依次收到3、2、1次序的数据包，收包可能是1、2、3的任意排列组合，也可能丢失一个或多个数据包。

### :pen\_fountain: 3.3、**`UDP`数据包的接收：**

client发送两次`UDP`数据，第一次 500字节，第二次300字节，server端阻塞模式下接包，第一次`recvfrom(1000)`，收到是 1000，还是500，还是300，还是其他？

由于`UDP`通信的有界性，接收到只能是500或300，又由于`UDP`的无序性和非可靠性，接收到可能是300，也可能是500，也可能一直阻塞在`recvfrom`调用上，直到超时返回(也就是什么也收不到)。

在假定数据包是不丢失并且是按照发送顺序按序到达的情况下，server端阻塞模式下接包，先后三次调用：`recvfrom(200)`，`recvfrom(1000)`，`recvfrom(1000)`，接收情况如何呢？

由于`UDP`通信的有界性，第一次`recvfrom(200)`将接收第一个500字节的数据包，但是因为用户空间`buf`只有200字节，于是只会返回前面200字节，剩下300字节将丢弃。第二次`recvfrom(1000)`将返回300字节，第三次`recvfrom(1000)`将会阻塞。

### :pen\_fountain: 3.4、**`UDP`包分片问题：**

如果`MTU`是1500，Client发送一个8000字节大小的`UDP`包，那么Server端阻塞模式下接包，在不丢包的情况下，`recvfrom(9000)`是收到1500，还是8000。如果某个IP分片丢失了，`recvfrom(9000)`又返回什么呢？

根据`UDP`通信的有界性，在`buf`足够大的情况下，接收到的一定是一个完整的数据包，`UDP`数据在下层的分片和组片问题由IP层来处理，提交到`UDP`传输层一定是一个完整的`UDP`包，那么`recvfrom(9000)`将返回8000。如果某个IP分片丢失，`UDP`里有个`CRC`检验，如果包不完整就会丢弃，也不会通知是否接收成功，所以`UDP`是不可靠的传输协议，那么`recvfrom(9000)`将阻塞。

### :pen\_fountain: 3.5、`UDP`丢包问题

在不考虑`UDP`下层IP层的分片丢失，`CRC`检验包不完整的情况下，造成`UDP`丢包的因素有哪些呢？

#### **3.5.1、`UDP socket`缓冲区满造成的`UDP`丢包：**

通过 `cat /proc/sys/net/core/rmem_default` 和`cat /proc/sys/net/core/rmem_max`可以查看socket缓冲区的缺省值和最大值。如果socket缓冲区满了，应用程序没来得及处理在缓冲区中的`UDP`包，那么后续来的`UDP`包会被内核丢弃，造成丢包。在socket缓冲区满造成丢包的情况下，可以通过增大缓冲区的方法来缓解`UDP`丢包问题。但是，如果服务已经过载了，简单的增大缓冲区并不能解决问题，反而会造成滚雪球效应，造成请求全部超时，服务不可用。

#### **3.5.2、`UDP socket`缓冲区过小造成的`UDP`丢包：**

如果Client发送的`UDP`报文很大，而socket缓冲区过小无法容下该`UDP`报文，那么该报文就会丢失。

#### **3.5.3、`ARP`缓存过期导致`UDP`丢包：**

`ARP` 的缓存时间约10分钟，APR 缓存列表没有对方的 MAC 地址或缓存过期的时候，会发送 `ARP` 请求获取 MAC 地址，在没有获取到 MAC 地址之前，用户发送出去的 `UDP` 数据包会被内核缓存到 `arp_queue` 这个队列中，默认最多缓存3个包，多余的 `UDP` 包会被丢弃。被丢弃的 `UDP` 包可以从 `/proc/net/stat/arp_cache` 的最后一列的 unresolved\_discards 看到。当然我们可以通过 `echo 30 > /proc/sys/net/ipv4/neigh/eth1/unres_qlen` 来增大可以缓存的 `UDP` 包。

`UDP` 的丢包信息可以从 `cat /proc/net/udp` 的最后一列`drops`中得到，而倒数第四列 `inode` 是丢失 `UDP` 数据包的 socket 的全局唯一的虚拟`i`节点号，可以通过这个 `inode` 号结合 `lsof (lsof -P -n | grep 25445445)`来查到具体的进程。

### :pen\_fountain: 3.6、`UDP`的冗余传输方案

在外网通信链路不稳定的情况下，有什么办法可以降低`UDP`的丢包率呢？一个简单的办法来采用冗余传输的方式。如下图，一般采用较多的是延时双发，双发指的是将原本单发的前后连续的两个包合并成一个大包发送，这样发送的数据量是原来的两倍。这种方式提高丢包率的原理比较简单，例如本例的冗余发包方式，在偶数包全丢的情况下，依然能够还原出完整的数据，也就是在这种情况下，50%的丢包率，依然能够达到100%的数据接收。

## :pencil2: 4、使用`UDP`协议

### :pen\_fountain: 4.1、**为什么需要`UDP`？**

**`UDP(User Datagram Protocol)`传输**与**IP传输**非常类似，它的传输方式也是"**Best Effort**"的，所&#x4EE5;**`UDP`协议**也是**不可靠**的。`TCP`就是为了解决IP层不可靠的传输层协议，既然`UDP`是不可靠的，**为什么不直接使用IP协议而要额外增加一个`UDP`协议呢**？

1、一个重要的原因是**IP协议中并没有端口(port)**&#x7684;概念。IP协议进行的是IP地址到IP地址的传输，这意味者两台计算机之间的对话。但每台计算机中需要有多个通信通道，并将多个通信通道分配给不同的进程使用。**一个端口就代表了这样的一个通信通道**。`UDP`协议实现了端口，从而让数据包可以在送到IP地址的基础上，进一步可以送到某个端口。

2、**对于一些简单的通信，我们只需要“Best Effort”式的IP传输就可以了**，而不需要`TCP`协议复杂的建立连接的方式(特别是在早期网络环境中，如果过多的建立`TCP`连接，会造成很大的网络负担，而`UDP`协议可以相对快速的处理这些简单通信）

3、在使用`TCP`协议传输数据时，如果一个数据段丢失或者接收端对某个数据段没有确认，发送端会重新发送该数据段。`TCP`重新发送数据会带来传输延迟和重复数据，降低了用户的体验。**对于迟延敏感的应用，少量的数据丢失一般可以被忽略，这时使用`UDP`传输将能够提升用户的体验**。

### :pen\_fountain: 4.2、**`UDP`应用场景**

#### **4.2.1、**&#x9AD8;通信实时性要求和低持续性要求的场景

当应用程序**对传输的可靠性要求不高**，但是对**传输速度和延迟要求较高**时，可以用`UDP`协议来替代`TCP`协议在传输层控制数据的转发。

`UDP`适合于**实时数据传输**，如**语音**和**视频通信**，因为它们即使偶尔丢失一两个数据包，也不会对接收结果产生太大影响。

常用的使用`UDP`的协议包括（括号中是默认的端口号）：`TFTP（69）`、`SNMP（161）`、`NFS`、`DNS（53）`、`BOOTP`。

**通信的持续性有两种通信类型：**

* 短连接通信；
* 长连接通信。

对于短连接通信，一方面如果业务只需要发一两个包并且对丢包有一定的容忍度，同时业务自己有简单的轮询或重复机制，那么采用`UDP`会较为好些。在这样的场景下，如果用`TCP`，仅仅握手就需要3个包，这样显然有点不划算，一个典型的例子是`DNS`查询。另一方面，如果业务实时性要求非常高，并且不能忍受重传，那么只能用`UDP`了，例如`NTP`协议，重传`NTP`消息纯属添乱(为什么呢？重传一个过期的时间包过来，还不如发一个新的`UDP`包同步新的时间过来)。如果`NTP`协议采用`TCP`，撇开握手消耗较多数据包交互的问题，由于`TCP`受`Nagel`算法等影响，用户数据会在一定情况下会被内核缓存延后发送出去，这样时间同步就会出现比较大的偏差，协议将不可用。

#### 4.2.2、多点通信的场景

对于一些多点通信的场景，如果采用有连接的`TCP`，那么就需要和多个通信节点建立其双向连接，然后有时在NAT环境下，两个通信节点建立其直接的`TCP`连接不是一个容易的事情，在涉及NAT穿越的时候，`UDP`协议的无连接性使得穿透成功率更高，由于`UDP`的无连接性，**其完全可以向一个组播地址发送数据或者轮转地向多个目的地持续发送相同的数据，从而更为容易实现多点通信。**

### :pen\_fountain: 4.3、**可靠性**

**`UDP`协议不可靠，可靠性由谁保障？**

`UDP`将数据从源端发送到目的端时，无需事先建立连接，没有使用`TCP`中的确认技术或滑动窗口机制，因此`UDP`不能保证数据传输的可靠性，也无法避免接收到重复数据的情况。**`UDP`传输的可靠性由应用层负责，**&#x7531;应用程序根据需要提供报文到达确认、排序、流量控制等功能。

要使用`UDP`来构建可靠的面向连接的数据传输，就要实现类似于`TCP`协议的：

* 超时重传（定时器）【解决报文丢失问题】；
* 有序接受 （添加包序号）【解决包乱序问题】；
* 应答确认 （`Seq/Ack`应答机制）【保证可靠性】；
* 滑动窗口流量控制等机制 （滑动窗口协议）【解决流量控制问题】。

目前已经有一些实现`UDP`可靠传输的机制，比如`UDT`（`UDP-based Data Transfer Protocol`）： 基于`UDP`的数据传输协议，`UDT`是一种互联网数据传输协议。`UDT`的主要目的是支持高速广域网上的海量数据传输，而互联网上的标准数据传输协议`TCP`在高带宽长距离网络上性能很差。 顾名思义，`UDT`建于`UDP`之上，并引入新的拥塞控制和数据可靠性控制机制。`UDT`是面向连接的双向的应用层协议。它同时支持可靠的数据流传输和部分可靠的数据报传输。 由于`UDT`完全在`UDP`上实现，它也可以应用在除了高速数据传输之外的其它应用领域，例如点到点技术（`P2P`），防火墙穿透，多媒体数据传输等等。

### :pen\_fountain: **4.4、工作原理**

主机`A`发送数据包时，这些数据包是以**有序的方式**发送到网络中的，每个数据包独立地在网络中被发送，所以**不同的数据包可能会通过不同的网络路径到达主机`B`**。这样的情况下，先发送的数据包不一定先到达主机`B`。因为`UDP`数据包没有序号，**主机B将无法通过`UDP`协议将数据包按照原来的顺序重新组合**，所以此时**需要应用程序提供报文的到达确认、排序和流量控制**等功能。

通常情况下，`UDP`采用**实时传输机制**和**时间戳**来传输语音和视频数据。

### :pen\_fountain: 4.5、影响`UDP`高效因素

#### **4.5.1、无法智能利用空闲带宽导致资源利用率低：**

一个简单的事实是`UDP`并不会受到`MTU`的影响，`MTU`只会影响下层的IP分片，对此`UDP`一无所知。在极端情况下，`UDP`每次都是发小包，包是`MTU`的几百分之一，这样就造成`UDP`包的有效数据占比较小(`UDP`头的封装成本)；或者，`UDP`每次都是发巨大的`UDP`包，包大小`MTU`的几百倍，这样会造成下层IP层的大量分片，大量分片的情况下，其中某个分片丢失了，就会导致整个`UDP`包的无效。由于网络情况是动态变化的，`UDP`无法根据变化进行调整，发包过大或过小，从而导致带宽利用率低下，有效吞吐量较低。而`TCP`有一套智能算法，当发现数据必须积攒的时候，就说明此时不积攒也不行，`TCP`的复杂算法会在延迟和吞吐量之间达到一个很好的平衡。

#### **4.5.2、无法动态调整发包：**

由于`UDP`没有确认机制，没有流量控制和拥塞控制，这样在网络出现拥塞或通信两端处理能力不匹配的时候，`UDP`并不会进行调整发送速率，从而导致大量丢包。在丢包的时候，不合理的简单重传策略会导致重传风暴，进一步加剧网络的拥塞，从而导致丢包率雪上加霜。更加严重的是，`UDP`的无秩序性和自私性，一个疯狂的`UDP`程序可能会导致这个网络的拥塞，挤压其他程序的流量带宽，导致所有业务质量都下降。

#### **4.5.3、改进`UDP`的成本较高：**

可能有同学想到针对`UDP`的一些缺点，在用户态做些调整改进，添加上简单的重传和动态发包大小优化。然而，这样的改进并比简单的，`UDP`编程可是比`TCP`要难不少的，考虑到改造成本，为什么不直接用`TCP`呢？当然可以拿开源的一些实现来抄一下(例如：`libjingle`)，或者拥抱一下Google的`QUIC`协议，然而，这些都需要不少成本的。

## :pencil2:  &#x35;**、`UDP`和`TCP`的区别**

1. `TCP` 是**面向连接**的传输控制协议，而`UDP` 提供了**无连接**的数据报服务；
2. `TCP` 具有**高可靠性**，确保传输数据的正确性，不出现丢失或乱序；`UDP` 在传输数据前**不建立连接**，不对数据报进行检查与修改，无须等待对方的应答，所以会出现分组丢失、重复、乱序，应用程序需要负责传输可靠性方面的所有工作；
3. `UDP` 具有较好的**实时性**，工作效率较 `TCP` 协议高；
4. `UDP` 首部结构比 `TCP` 的首部结构简单，因此网络**开销也小**。

| UDP(User Datagram Protocol) | TCP(Transmission Control Protocol) |
| :-------------------------: | :--------------------------------: |
|             无连接             |                面向连接                |
|    支持一对一、一对多、多对一和多对多交互通信    |    每一条`TCP`连接只能有两个端点`EP`，只能一对一通信   |
|        对应用层交付的报文直接打包        |                面向字节流               |
| 尽最大努力交付，也就是不可靠；不使用流量控制和拥塞控制 |          可靠传输，使用流量控制和拥塞控制          |
|          首部开销小，仅8字节         |           首部最小20字节，最大60字节          |
