传输层协议UDP

发布于:2025-05-19 ⋅ 阅读:(23) ⋅ 点赞:(0)

UDP是在网络协议中传输层的一种协议。它位于操作系统内部。我们在网络基础那里了解过,UDP是一种无连接,不可靠,面向数据报的协议,其设计目标是高效传输数据,而非保证可靠性。它直接将应用层数据封装成UDP数据报发送,不建立连接,不确认数据,不重传丢失报文,适用于对实时性要求高(如直播)或数据量小(如DNS查询)的场景。

一.通信五元组

端口号是用来标识一台主机上唯一的一个进程的。当主机收到网络来的报文,通过解包和分用到了传输层之后,要明确这个报文是发送给那个进程的。

而我们使用源ip和目的ip,就可以知道全网中通信的两台主机。再通过源端口和目的端口,就可以得知这两台主机上要进行通信的两个进程。

但在通信过程中,传输层协议有不止一种,如果通信双方使用不同的传输层协议,就会导致报文到了传输层后,双发无法识别报头信息,导致网络通信失败。所以,在网络通信过程中,除了借助源ip,源端口,目的ip和目的端口号外,还有增加协议号,来表示通信双方使用的是什么传输层协议。

一般TCP协议号为6,UDP协议号为17.

综上,源ip地址,源端口号,协议号,目的ip地址,目的端口号就可以唯一确定一条通信链路。这五部分称为“五元组”。 

二.UDP协议

应用层要发送数据到网络中,要贯穿整个网络协议栈,且需要进行封装和解包、分用操作。所以,在传输层,从应用层下来的数据需要在传输层添加对应的协议报头,才能传入下一层中。

1.UDP报头 

我们可以看到,udp报头是定长的,占8个字节。而报头又包含16位udp长度字段,该字段表明在传输层UDP报头+有效载荷的总长度。

我们通过一下几个问题来了解UDP报头:

0x1.UDP如何做到将报头和有效载荷分离?

UDP报头中的16位UDP长度表明udp报头+有效载荷的总长度,而udp报头又是定长的。所以,我们使用16位udp长度,减去8字节,就可以得知有效载荷大小。

所以,在接收端传输层每一个报文的长度都是固定的,这也就意味着报文之间是有边界的。

0x2.UDP如果做到分用?将有效载荷传递给那个进程?

UDP报头中包含16位目的端口号,该端口号映射的就是该主机上唯一的一个进程。 

16位UDP校验和:可选字段(可设为 0),用于检测数据在传输中是否出错(非可靠校验,出错时直接丢弃)。

而udp在操作系统内核中,其实就是一个结构体变量:

struct udphdr {
    __u16   source;     // 源端口号
    __u16   dest;       // 目的端口号
    __u16   len;        // UDP 数据报总长度(首部 + 数据)
    __u16   check;      // 校验和(用于错误检测)
};

因为UDP协议在内核中,而操作系统都是c语言写的,且网络部分代码都是一样的,所以在操作系统内部,传递UDP时直接传递结构体变量。

2.UDP的特点

UDP协议是无连接的,不可靠,面向数据报的。这些都是他的特点,并不是说不可靠就是它的缺点。

无连接:

使用UDP进行通信时,只需要对方的ip地址和端口号就可以直接进行通信。不像TCP,通信前还需要进行connect。节省了连接建立和释放的开销。

适合对实时性要求高、数据量小的场景(如视频流、实时游戏、DNS 查询等)。

不可靠:

在使用UDP在进行通信时,不关系对方是否收到了数据报。并且数据报在通信中丢失,UDP对丢失的数据报不进行任何操作(重传)。

UDP不保证数据的顺序性与完整性,数据报可能乱序、重复或丢失,需要应用层自行处理(如有需要)。

面向数据报:

UDP 传输的基本单位,它是一个独立、完整的信息单元,包含UDP报头和有效载荷。

我们可以将数据报理解为一个一个的信件,信件之间相互独立,彼此没有关联。我们sendto发送几次,对端recvform就收到几次。就像我发送3个信件给你,你就会收到3个信件。

数据报之间存在明显的边界,就好比信件一样。每个数据报在网络中独立路由,可能因路径不同导致到达顺序与发送顺序不一致。

接收端只要收到报文,那么就一定是完整的。

UDP报头包含16位UDP长度,也就意味着UDP报文的最大长度就为16位全1,即64kb。然而在当今的互联网,64kb是一个非常小的数字。如果需要传输的数据大小超过了64kb,则需要我们在应用层手动进行分包,多次发送,并在接收端进行手动拼接。

 3.UDP缓冲区

UDP并不存在真正意义上的发送缓冲区。调用sendto会直接将报文交给内核,由内核将报文交由网络层进行下一层封装。而且UDP其实并没有必要存在发送缓冲区。

TCP之所以有发送缓冲区,是因为TCP是可靠的,数据在发送过程中,如果出现了丢包,TCP是要进行重传的。所以,TCP通过write将数据拷贝到发送缓冲区后,tcp将数据发送到网络之后,不会立即将缓冲区清空,而是等到数据被对方收到后在进行清空。这样就能保证在传递过程中对丢包数据进行重传。

而UDP是不可靠的,不关系数据是否丢包,也就不需要重传,数据直接发出去即可,不需要发送缓冲区。

UDP具有接收缓冲区。但是这个接受缓冲区不能保证收到的UDP报文顺序和发送UDP报文的顺序一致。且如果发送缓冲区剩余空间不足报文长度,就会将UDP数据丢弃。

UDP具有接收缓冲区是合理的,收数据的时候,应用层、网络层、传输层就好比一个生产者消费者模型。当缓冲区满的时候,此时就不能再生产了,应由消费者进行消费。并且,数据拿到之后可能会进行数据路由,或者交给后端的线程池去执行,还有很长的路程,如果没有接收缓冲区,网络层每收到一个数据报,必须立即触发应用层处理(类似同步阻塞模式),应用层若正处理其他任务(如复杂业务逻辑),内核被迫等待,期间到达的新数据报只能丢弃。

应用层读取数据报后,可能需经历:

数据报 → 协议解析(如解析为JSON) → 路由决策(判断转发至哪个后端服务) → 线程池调度(分配工作线程处理) → 业务逻辑执行

若没有接收缓冲区,整个处理链的任何延迟(如线程池满、数据库写入阻塞)都会直接导致内核无法接收新数据报。 

 三.sk_buff

对于一台服务器来说,它在任意一个时刻内可能受到来自多个客户端发来的报文。也就是说,操作系统内部可能会同时存在多个报文。那么操作系统就要对这些报文进行管理——先描述再组织。

struct sk_buff {
    struct sk_buff  *next;          // 链表指针,用于批量处理
    struct sock     *sk;           // 关联的Socket(若有)
    struct net_device *dev;        // 接收/发送的网络设备
    unsigned int    len;           // 数据长度
    unsigned int    data_len;      // 数据缓冲区长度
    __be16          protocol;      // 上层协议类型(如ETH_P_IP)
    unsigned char   *head;         // 数据缓冲区起始地址
    unsigned char   *data;         // 当前数据区起始地址
    unsigned char   *tail;         // 当前数据区结束地址
    unsigned char   *end;          // 数据缓冲区结束地址
    unsigned int    truesize;      // 总分配大小(含sk_buff本身)
    atomic_t        users;         // 引用计数(用于零拷贝)
    // 网络层和传输层头部指针
    struct ethhdr   *ethhdr;       // Ethernet头部
    struct iphdr    *iph;          // IP头部
    struct tcphdr   *th;           // TCP头部
    struct udphdr   *uh;           // UDP头部
    // 路由和QoS相关信息
    __u32           priority;      // 数据包优先级
    __u32           mark;          // 用于防火墙标记
    // 时间戳和统计信息
    ktime_t         tstamp;        // 接收时间戳
};

每当接收一个报文,就会在操作系统内部创建一个sk_buff结构,用来描述一个报文。报文要在内存中存储,肯定就得有物理内存,也就是缓冲区。sk_buff就指向该缓冲区,且sk_buff中有特殊的字段指向报文,描述报文的报头与有效载荷。 sk_buff中head指针和end指针分别指向一个报文的头部空间和尾部空间,来表示一个报文。tail指针指向应用层数据结尾,即有效载荷的结尾。

data指针刚开始指向应用层数据的开始,接着发送数据要进行封装,即添加报文。此时将传输层协议报头添加在有效载荷头部,此时data指针移动,执行TCP报头。向下封装的过程中,data指着一直在移动。

所以,封装和解包的本质,就是移动data指针在缓冲区的指向。

因为有sk_buff的存在,就算应用层正在对报文进行解析、处理,操作系统依旧可以从网络总读取报文,链入sk_buff中,由内核管理,与应用层处理完全解耦。即使应用层处理缓慢,内核仍可继续从网卡读取数据并暂存到sk_buff队列中。


网站公告

今日签到

点亮在社区的每一天
去签到