☰
11.【网络】传输层协议UDP:从协议格式理解UDP
2026/10/10 8:38:31 网站建设 项目流程

…过云雨-CSDN博客主页

目录

    • 1. 再谈端口号
      • 1.1 端口号范围划分
      • 1.2 认识知名端口号(Well-Know Port Number)
      • 1.3 一个进程是否可以bind多个端口号?
      • 1.4 一个端口号是否可以被多个进程bind?
    • 2. UDP协议
      • 2.1 UDP 协议首部格式
      • 2.2 UDP 核心特点
      • 2.3 UDP 的缓冲区
      • 补:sk_buff简介
        • 1. sk_buff简介
        • 2. sk_buff详解
      • 2.4 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 端口):提供网页访问服务,负责接收浏览器的 HTTP 请求并返回网页、图片等资源,默认不加密。
  • HTTPS 服务器(443 端口):提供加密的网页访问服务,在 HTTP 基础上使用 TLS 加密,保护客户端与服务器之间传输的数据。

📌 大白话记忆:

  • SSH:安全地远程操作服务器。
  • FTP:上传、下载文件。
  • Telnet:不加密地远程操作服务器。
  • HTTP:普通网页访问。
  • HTTPS:加密、安全的网页访问。

注意: 以上端口均为常用的默认 TCP 端口,并非只能使用这些端口。

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

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简介

1. sk_buff简介

1. 什么是struct sk_buff?

struct sk_buff(Socket Buffer,简称 SKB) 是 Linux 内核网络协议栈中用于描述和管理网络数据包的核心数据结构。

它不仅用于管理网络数据,还保存数据包的相关信息,例如协议类型、数据长度、网络设备等。

主要作用:

  • 管理网络数据包:记录数据包的位置、长度等信息。
  • 保存数据包信息:例如协议类型、网络设备、协议头位置等。
  • 支持分层处理:方便 TCP/IP 协议栈各层对数据进行封装和解封装。
  • 支持队列管理:方便内核对网络数据包进行排队、发送和接收。

2. 大白话理解

可以把sk_buff理解成一个快递包裹的管理单。

  • 网络数据包:快递包裹,装着真正要传输的数据。
  • sk_buff:快递管理单,记录包裹的大小、位置、运输信息等。
  • 网络协议栈:快递运输系统,按照管理单的信息处理包裹。

需要注意:sk_buff并不等于网络数据本身,它是描述和管理网络数据的结构体,通过指针关联实际的数据缓冲区。

3.sk_buff的重要成员

以下是部分常见成员(并非完整结构体,不同内核版本可能有所差异):

struct sk_buff { unsigned int len; // 数据包总长度 unsigned int data_len; // 非线性数据部分的长度 unsigned char *head; // 缓冲区起始位置 unsigned char *data; // 当前有效线性数据起始位置 unsigned char *tail; // 线性数据末尾位置 unsigned char *end; // 缓冲区末尾位置 // 还有协议类型、网络设备、协议头偏移等信息 };

其中,tail和end在内核实现中通常是偏移量类型,而不是真正的指针。上面只是为了方便理解的示意。

4.sk_buff在 TCP/IP 协议栈中的作用

发送数据时:

应用程序发送数据 ↓ Socket 接口 ↓ 传输层(TCP/UDP) ↓ 网络层(IP) ↓ 数据链路层 ↓ 网卡发送

在这个过程中,Linux 内核使用sk_buff管理网络数据,并在不同协议层之间传递。各层可以利用它记录的缓冲区位置、协议头等信息处理数据。

📌 面试总结

struct sk_buff是 Linux 内核网络协议栈中用于描述和管理网络数据包的核心数据结构。它通过关联数据缓冲区并保存数据包的长度、协议类型、协议头位置等信息,实现网络数据在协议栈中的高效处理和传递。

记住一个区别:

  • struct socket:主要表示 Socket 接口对应的内核对象。
  • struct sock:主要管理内核中的网络通信状态,如 TCP 连接状态、收发缓冲区等。
  • struct sk_buff:主要描述和管理网络数据包。

对于普通 C++ / Linux 软件开发岗位,理解这三个结构体的区别通常就足够了;如果面试涉及 Linux 内核网络协议栈,则需要进一步掌握sk_buff的内存布局和操作函数。

2. sk_buff详解

struct sk_buff定义

structsk_buff{// 1. 链表管理:用于将多个 sk_buff 组织成队列/链表(如 UDP 接收缓冲区)structsk_buff*next;structsk_buff*prev;structsock*sk;// 关联的 socket// 2. 缓冲区指针:核心定位字段,实现无拷贝封装/解包unsignedchar*head;// 缓冲区起始地址(固定不变)unsignedchar*data;// 当前层有效数据起始地址(核心,可移动)unsignedchar*tail;// 当前层有效数据结束地址(可移动)unsignedchar*end;// 缓冲区结束地址(固定不变)// 3. 长度信息unsignedintlen;// 当前层有效数据长度(tail - data)unsignedinttruesize;// 整个缓冲区的实际大小(end - head)// 4. 协议头指针:快速访问各层协议头部(通过 union 节省空间)union{structtcphdr*th;structudphdr*uh;structicmphdr*icmph;structigmphdr*igmph;structiphdr*iph;structipv6hdr*ipv6h;unsignedchar*raw;}h;// 指向传输层/网络层头部union{structiphdr*iph;structipv6hdr*ipv6h;structarphdr*arph;unsignedchar*raw;}nh;// 指向网络层头部union{unsignedchar*raw;}mac;// 指向数据链路层头部// 5. 其他控制字段(协议类型、校验和、设备信息等)__be16 protocol;// 报文所属的协议(如 ETH_P_IP、ETH_P_IPV6)unsignedintpkt_type;// 报文类型(如广播、多播、单播)structnet_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. 不修改taildata = data - sizeof(iphdr)``tail保持不变
4. 数据链路层封装(以太网)1. 继续向前移动data指针,预留以太网帧头空间(14 字节)2. 在data指向的新位置填充以太网帧头(源 MAC、目的 MAC 等)3. 不修改taildata = 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. 封装是 “向前预留空间加头部”(data向head靠近),解包是 “向后跳过头部找数据”(data向tail靠近)。
  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 应用层协议。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询