拆解 UDP 协议:从协议格式理解UDP

...过云雨-CSDN博客主页

目录

    • [1. 再谈端口号](#1. 再谈端口号)
      • [1.1 端口号范围划分](#1.1 端口号范围划分)
      • [1.2 认识知名端口号(Well-Know Port Number)](#1.2 认识知名端口号(Well-Know Port Number))
      • [1.3 一个进程是否可以bind多个端口号?](#1.3 一个进程是否可以bind多个端口号?)
      • [1.4 一个端口号是否可以被多个进程bind?](#1.4 一个端口号是否可以被多个进程bind?)
    • [2. UDP协议](#2. UDP协议)
      • [2.1 UDP 协议首部格式](#2.1 UDP 协议首部格式)
      • [2.2 UDP 核心特点](#2.2 UDP 核心特点)
      • [2.3 UDP 的缓冲区](#2.3 UDP 的缓冲区)
      • 补:sk_buff简介
        • [1. sk_buff 核心指针(基础定义)](#1. sk_buff 核心指针(基础定义))
        • [2. 报文封装(发送端,从上到下)](#2. 报文封装(发送端,从上到下))
        • [3. 报文解包(接收端,从下到上)](#3. 报文解包(接收端,从下到上))
        • [4. 核心总结](#4. 核心总结)
      • [2.4 UDP 传输限制与分包处理](#2.4 UDP 传输限制与分包处理)
      • [2.5 基于 UDP 的典型应用层协议](#2.5 基于 UDP 的典型应用层协议)

1. 再谈端口号

  • 端口号(Port)标识了一个主机上进行通信的不同的应用程序

在TCP/IP协议中, 用 "源IP", "源端口号", "目的IP", "目的端口号", "协议号" 这样一个五元组来标识一个通信(可以通过netstat -n查看);

1.1 端口号范围划分

  • 0 - 1023:知名端口号,HTTP,FTP,SSH等这些广为使用的应用层协议,他们的端口号都是固定的。

  • 1024 - 65535:操作系统动态分配的端口号。客户端程序的端口号,就是由操作系统从这个范围分配的。

1.2 认识知名端口号(Well-Know Port Number)

有些服务器是非常常用的,为了使用方便,人们约定一些常用的服务器,都是用以下这些固定的端口号:

  • ssh服务器,使用22端口

  • ftp服务器,使用21端口

  • telnet服务器,使用23端口

  • http服务器,使用80端口

  • https服务器,使用443

执行下面的命令,可以看到知名端口号

bash 复制代码
cat /etc/services

我们自己写一个程序使用端口号时,要避开这些知名端口号。

1.3 一个进程是否可以bind多个端口号?

答案是:可以。

一个进程完全可以绑定多个不同的端口号,实现这一操作的核心方式是:在进程内创建多个套接字(socket),并分别对每个套接字调用 bind() 系统调用,将其绑定到不同的、未被占用的端口号上

1.4 一个端口号是否可以被多个进程bind?

一般情况下,不可以。

2. UDP协议

2.1 UDP 协议首部格式

UDP 首部共 8 字节,包含以下字段:

字段(16 位) 说明
源端口号 发送端的端口,可选字段,若不需要可设为 0
目的端口号 接收端的端口,用于交付给上层应用
UDP 长度 整个 UDP 数据报(首部 + 数据)的字节数,最小值为 8(仅含首部),最大值为 65535(2¹⁶-1)
UDP 检验和 用于检测首部和数据在传输中是否出错,若校验失败则直接丢弃报文;该字段为可选,但在 IPv4 中推荐启用,IPv6 中必须启用

2.2 UDP 核心特点

  1. 无连接

    通信前不需要建立连接,只需知道对方的 IP 和端口号就可以直接发送数据,减少了连接建立和释放的开销。

  2. 不可靠

    没有确认、重传、排序等机制。如果报文丢失或乱序,UDP 协议层不会向应用层返回错误信息,也不会尝试恢复。

  3. 面向数据报

    应用层交给 UDP 多长的报文,UDP 就原样发送,既不会拆分,也不会合并。

    • 发送端调用 1 次 sendto 发送 100 字节,接收端必须也调用 1 次 recvfrom 完整接收 100 字节,不能分多次接收。
  4. 全双工

    UDP 的 socket 支持同时读写,同一连接可以双向传输数据。


2.3 UDP 的缓冲区

  • 发送缓冲区 :UDP 没有真正的发送缓冲区。调用 sendto 后,数据会直接交给内核,由内核传递给网络层,应用层无法控制数据在发送缓冲区的停留。
  • 接收缓冲区:UDP 有接收缓冲区,但不能保证收到的报文顺序与发送顺序一致;如果缓冲区已满,后续到达的 UDP 报文会被直接丢弃。

我们梳理一下UDP的完整传输流程:

发送端:应用层序列化数据 → 调用sendto拷贝到内核 → UDP层封装首部 → IP层封装首部 → 数据链路层封装以太网帧头 → 网卡发送;

接收端:网卡接收报文 → 数据链路层解包 → IP层解包 → UDP层验校并解包 → 应用层调用recvfrom拷贝到用户空间。

在这个流程中,有两个关键问题需要内核解决:

  1. 如何在不同协议层之间传递UDP报文,同时完整保留各层首部和应用数据?
  2. 如何实现高效的封装和解包,避免频繁的内存拷贝(毕竟UDP追求轻量化、低开销)?

这两个问题的答案,都指向了Linux内核中的sk_buff结构。它不仅是UDP报文的"载体",更是内核网络协议栈的"血脉"------从UDP数据进入内核的那一刻起,就被封装在sk_buff中,后续所有协议层的处理(封装、解包、验校)都基于这个结构完成,甚至UDP接收缓冲区的实现,也是通过sk_buff链表来管理待读取的报文。

可以说,不理解sk_buff,就无法真正理解UDP缓冲区的底层逻辑,也无法明白UDP为何能实现"无真正发送缓冲区、快速交付"的特性。

补:sk_buff简介

struct sk_buff定义

cpp 复制代码
struct sk_buff {
    // 1. 链表管理:用于将多个 sk_buff 组织成队列/链表(如 UDP 接收缓冲区)
    struct sk_buff      *next;
    struct sk_buff      *prev;
    struct sock         *sk;       // 关联的 socket

    // 2. 缓冲区指针:核心定位字段,实现无拷贝封装/解包
    unsigned char       *head;     // 缓冲区起始地址(固定不变)
    unsigned char       *data;     // 当前层有效数据起始地址(核心,可移动)
    unsigned char       *tail;     // 当前层有效数据结束地址(可移动)
    unsigned char       *end;      // 缓冲区结束地址(固定不变)

    // 3. 长度信息
    unsigned int        len;       // 当前层有效数据长度(tail - data)
    unsigned int        truesize;  // 整个缓冲区的实际大小(end - head)

    // 4. 协议头指针:快速访问各层协议头部(通过 union 节省空间)
    union {
        struct tcphdr   *th;
        struct udphdr   *uh;
        struct icmphdr  *icmph;
        struct igmphdr  *igmph;
        struct iphdr    *iph;
        struct ipv6hdr  *ipv6h;
        unsigned char   *raw;
    } h;  // 指向传输层/网络层头部

    union {
        struct iphdr    *iph;
        struct ipv6hdr  *ipv6h;
        struct arphdr   *arph;
        unsigned char   *raw;
    } nh; // 指向网络层头部

    union {
        unsigned char   *raw;
    } mac; // 指向数据链路层头部

    // 5. 其他控制字段(协议类型、校验和、设备信息等)
    __be16              protocol;  // 报文所属的协议(如 ETH_P_IP、ETH_P_IPV6)
    unsigned int        pkt_type;  // 报文类型(如广播、多播、单播)
    struct net_device   *dev;      // 接收/发送该报文的网络设备
};
1. sk_buff 核心指针(基础定义)
指针名称 核心含义 是否随协议层变化 关键备注
head 指向整个缓冲区的起始地址(固定不变) 缓冲区的 "头部边界",分配后直到释放都不会移动
data 指向当前协议层有效数据的起始地址 封装 / 解包的核心操作指针,用于跳过各层协议头
tail 指向当前协议层有效数据的结束地址 用于标记当前层数据尾部,添加数据时会向后移动
end 指向整个缓冲区的结束地址(固定不变) 缓冲区的 "尾部边界",限制 tail 移动的最大范围

补充说明

  • 缓冲区可用总空间:end - head
  • 当前层有效数据长度:tail - data
  • 头预留空闲空间:data - head(用于封装时向前添加协议头)
  • 尾预留空闲空间:end - tail(用于封装时向后添加数据)
2. 报文封装(发送端,从上到下)

封装流程:应用层 → 传输层(UDP/TCP) → 网络层(IP) → 数据链路层(以太网)

核心逻辑:先填充数据,再向前(head 方向)移动 data 指针,添加各层协议头(避免内存拷贝,高效封装)

处理阶段 指针操作步骤 对应 sk_buff 变化
1. 应用层数据写入 1. 应用层调用 sendto,将数据拷贝到 sk_buff 中2. 内核将 data 指向数据起始位置,tail 移动到数据结束位置 data 初始定位,tail = data + 应用层数据长度
2. 传输层封装(UDP) 1. 向前移动 data 指针,预留 UDP 首部空间(8 字节)2. 在 data 指向的新位置填充 UDP 首部(源端口、目的端口等)3. 不修改 tail(数据长度不变,仅添加头部) data = data - sizeof(udphdr)``tail 保持不变
3. 网络层封装(IP) 1. 继续向前移动 data 指针,预留 IP 首部空间(通常 20 字节)2. 在 data 指向的新位置填充 IP 首部(源 IP、目的 IP 等)3. 不修改 tail data = data - sizeof(iphdr)``tail 保持不变
4. 数据链路层封装(以太网) 1. 继续向前移动 data 指针,预留以太网帧头空间(14 字节)2. 在 data 指向的新位置填充以太网帧头(源 MAC、目的 MAC 等)3. 不修改 tail data = data - sizeof(ethhdr)``tail 保持不变
5. 发送报文 此时 sk_buff 中包含完整报文(以太网头 + IP 头 + UDP 头 + 应用数据),内核将其传递给网卡发送 最终有效数据长度:tail - data(包含所有层头部 + 应用数据)
3. 报文解包(接收端,从下到上)

解包流程:数据链路层(以太网) → 网络层(IP) → 传输层(UDP/TCP) → 应用层

核心逻辑:向后(tail 方向)移动 data 指针,跳过各层协议头,最终定位到应用层数据(无内存拷贝,高效解包)

处理阶段 指针操作步骤 对应 sk_buff 变化
1. 接收完整报文 1. 网卡接收报文,拷贝到 sk_buff 中2. 内核将 data 指向以太网帧头起始位置,tail 指向报文结束位置 data 初始定位到帧头,tail 定位到报文尾部
2. 数据链路层解包(以太网) 1. 向后移动 data 指针,跳过以太网帧头(14 字节)2. 不修改 tail(仅跳过头部,数据主体不变)3. 验证帧尾校验和(可选),确认报文完整性 data = data + sizeof(ethhdr)``tail 保持不变
3. 网络层解包(IP) 1. 向后移动 data 指针,跳过 IP 首部(20 字节,按需读取首部长度字段)2. 不修改 tail3. 验证 IP 校验和,提取源 / 目的 IP 地址 data = data + sizeof(iphdr)``tail 保持不变
4. 传输层解包(UDP) 1. 向后移动 data 指针,跳过 UDP 首部(8 字节)2. 不修改 tail3. 验证 UDP 校验和,提取源 / 目的端口号 data = data + sizeof(udphdr)``tail 保持不变
5. 应用层读取数据 1. 此时 data 指向应用层数据起始位置,tail 指向应用层数据结束位置2. 应用层调用 recvfrom,将 [data, tail) 区间的数据拷贝到用户空间 最终获取:tail - data 长度的应用层原始数据
4. 核心总结
  1. 封装 / 解包的本质:仅移动 data 指针(核心),不移动 head/end,极少修改 tail,避免频繁内存拷贝,提升内核处理效率。
  2. 封装是 "向前预留空间加头部 "(datahead 靠近),解包是 "向后跳过头部找数据 "(datatail 靠近)。
  3. 所有协议层共享同一个 sk_buff 缓冲区,通过指针定位区分各层数据,这是 sk_buff 设计的核心优势。

2.4 UDP 传输限制与分包处理

UDP 首部的 16 位长度字段,决定了单个 UDP 数据报的最大长度为 64KB(包含首部)

  • 在现代网络环境中,64KB 是一个很小的数值,若需传输超过 64KB 的数据,必须在应用层手动分包,多次发送,并在接收端手动拼装。

2.5 基于 UDP 的典型应用层协议

  • DNS:域名解析协议,通过 UDP 快速完成域名与 IP 的映射。
  • DHCP:动态主机配置协议,为设备自动分配 IP 地址等网络参数。
  • TFTP:简单文件传输协议,用于小文件的简单传输。
  • NFS:网络文件系统,实现远程文件共享。
  • BOOTP:启动协议,为无盘设备提供启动所需的网络参数。

也包括自定义的 UDP 应用层协议。

相关推荐
Loo国昌2 小时前
【LangChain1.0】第二篇 快速上手实战
网络·人工智能·后端·算法·microsoft·语言模型
无忧智库2 小时前
一网统飞:城市级低空空域精细化管理与服务平台建设方案深度解析(WORD)
大数据·网络·人工智能
橘颂TA2 小时前
【剑斩OFFER】算法的暴力美学——力扣 692 题:前 K 个高频单词
网络·算法·leetcode·哈希算法·结构与算法
世人万千丶2 小时前
鸿蒙跨端框架 Flutter 学习 Day 4:网络交互——HTTP 请求基础与数据反序列化实战
网络·学习·flutter·ui·交互·harmonyos·鸿蒙
乾元2 小时前
当奥本海默遇到图灵:AI 开启的网络安全新纪元
服务器·网络·人工智能·网络协议·安全·web安全
零基础的修炼2 小时前
Linux网络---UDP原理
linux·网络·udp
科技块儿2 小时前
如何编程调用IP查询API?(PythonJava等示例)
网络协议·tcp/ip·lua
鄭在秀11 小时前
【SD-WAN介绍】
网络·网络安全·sd-wan
Gofarlic_oms111 小时前
Windchill用户登录与模块访问失败问题排查与许可证诊断
大数据·运维·网络·数据库·人工智能