Linux 47 UDP协议:数据报传输的核心机制
传输层负责数据在发送端和接收端之间的传输,其中端口号用于标识不同应用程序。知名端口号(0-1023)用于常用服务如HTTP(80)、SSH(22),动态端口号(1024-65535)由系统分配。UDP协议具有无连接、不可靠和面向数据报的特点,其16位长度限制单个数据报最大为64K。UDP通过结构体sk_buff管理报文,使用链表组织多个报文。虽然UDP简单高效,但大文件传输需应用层手动分包。基于U
目录
一.传输层
二.再谈端⼝号
端⼝号(Port)标识了⼀个主机上进⾏通信的不同的应⽤程序;
• 0 - 1023: 知名端⼝号, HTTP, FTP, SSH等这些⼴为使⽤的应⽤层协议, 他们的端⼝号都是固定的.(我们无法绑定)• 1024 - 65535: 操作系统动态分配的端⼝号. 客⼾端程序的端⼝号, 就是由操作系统从这个范围分配的.
• ssh服务器, 使⽤22端⼝• ftp服务器, 使⽤21端⼝• telnet服务器, 使⽤23端⼝• http服务器, 使⽤80端⼝• https服务器, 使⽤443
cat /etc/services
三.UDP协议

• 如果校验和出错, 就会直接丢弃;
3.1 UDP特点
3.2 面向数据报
应⽤层交给UDP多⻓的报⽂, UDP原样发送, 既不会拆分, 也不会合并; ⽤UDP传输100个字节的数据:
• 如果发送端调⽤⼀次sendto, 发送100个字节, 那么接收端也必须调⽤对应的⼀次recvfrom, 接收
3.3UDP的缓存区
UDP的socket既能读, 也能写, 这个概念叫做 全双⼯

根据前面的知识,我们知道UDP是没有写入缓存区的,而UDP也因为它的8字节固定报头及报头带有报文的长度,所以可以数据报读取
3.4管理报文
UDP协议,一个OS内部一定有大量的报文,那么如何管理呢?先描述,再组织
UDP的内部结构体代码
#include <linux/types.h>
#include <linux/socket.h>
#include <linux/netdevice.h>
#include <linux/skbuff.h>
#include <linux/atomic.h>
#include <net/ip.h>
#include <net/udp.h>
#include <net/tcp.h>
#include <net/icmp.h>
#include <net/igmp.h>
#include <net/ipv6.h>
#include <net/arp.h>
#include <net/dst.h>
#include <net/secpath.h>
struct sk_buff {
/* 链表节点:用于将多个sk_buff链接成队列 */
struct sk_buff *next;
struct sk_buff *prev;
/* 关联的套接字(如udp_sock/tcp_sock) */
struct sock *sk;
/* 时间戳(数据包接收/发送时间) */
struct skb_timestamp tstamp;
/* 数据包的输出/输入网卡设备 */
struct net_device *dev;
struct net_device *input_dev;
/* 传输层及以上报头(TCP/UDP/ICMP等) */
union {
struct tcphdr *th; // TCP 报头指针
struct udphdr *uh; // UDP 报头指针
struct icmphdr *icmph; // ICMP 报头指针
struct igmphdr *igmph; // IGMP 报头指针
struct iphdr *iph; // IPv4 报头指针(也可能放在nh)
struct ipv6hdr *ipv6h; // IPv6 报头指针(也可能放在nh)
unsigned char *raw; // 原始字节指针(通用访问)
} h;
/* 网络层报头(IP/ARP等) */
union {
struct iphdr *iph; // IPv4 报头指针
struct ipv6hdr *ipv6h; // IPv6 报头指针
struct arphdr *arph; // ARP 报头指针
unsigned char *raw; // 原始字节指针(通用访问)
} nh;
/* 链路层报头(MAC地址等) */
union {
unsigned char *raw; // 原始字节指针(通用访问)
} mac;
/* 路由相关信息(目的缓存) */
struct dst_entry *dst;
/* 安全路径(用于IPsec等) */
struct sec_path *sp;
/* 以下字段必须在结构体末尾(内存分配相关) */
unsigned int truesize; // sk_buff实际占用的内存大小
atomic_t users; // 引用计数(多模块共享时使用)
unsigned char *head; // 缓冲区起始地址(整个内存空间的头)
unsigned char *data; // 当前数据起始地址(报头/有效载荷的起始)
unsigned char *tail; // 当前数据结束地址(有效载荷的末尾)
unsigned char *end; // 缓冲区结束地址(整个内存空间的尾)
};
3.4.1 描述
描述UDP报头的那些属性跳过,我们直接看关于内存描述方面
/* 以下字段必须在结构体末尾(内存分配相关) */
unsigned int truesize; // sk_buff实际占用的内存大小
atomic_t users; // 引用计数(多模块共享时使用)
unsigned char *head; // 缓冲区起始地址(整个内存空间的头)
unsigned char *data; // 当前数据起始地址(报头/有效载荷的起始)
unsigned char *tail; // 当前数据结束地址(有效载荷的末尾)
unsigned char *end; // 缓冲区结束地址(整个内存空间的尾
这里的head等四个指针就是关于内存的描述

我们可以发现UDP的描述与内存与PCD和进程内存一样
解析head data tail end
head是数据区的起始地址,end是数据区的结束地址,两者之间的区间就是sk_buff分配的整块线性缓冲区的总大小(包含了头空间、各层协议头、应用层数据、尾空间等所有部分)。- 而实际被使用的 “有效数据” 范围,是由
data(有效数据的起始)和tail(有效数据的结束)指针来确定的。简单总结:
head+end→ 缓冲区总容量的边界;data+tail→ 缓冲区中有效数据的边界。

因此当你打包数据,从顶往下封装时,就是data指针在不断移动,封装协议,而接收方解包则是相反的过程
将设我们上面data移动,再强转读取
这里
data强转是为了把缓冲区地址映射成 UDP 协议头的结构体类型,方便直接操作协议头字段。具体原因:
data本身是sk_buff里的字符指针(char *),它指向的是缓冲区的地址,但字符指针只能按字节访问数据;- 而 UDP 协议头是一个结构化的数据(
struct udphdr),包含源端口、目的端口、长度、校验和等字段;- 把
data强转成(struct udphdr *)后,就能以 “结构体成员” 的方式直接读写 UDP 协议头的字段(比如((struct udphdr *)data)->source获取源端口),而不用手动按字节偏移计算位置。
3.4.2 组织
通过next prev两个1指针,将多个sk_buff形成队列,进行管理
/* 链表节点:用于将多个sk_buff链接成队列 */
struct sk_buff *next;
struct sk_buff *prev;
3.5 UDP使⽤注意事项
3.6 基于UDP的应⽤层协议
🔥个人主页:Milestone-里程碑
❄️个人专栏: <<力扣hot100>> <<C++>><<Linux>>
🌟心向往之行必能至
更多推荐

所有评论(0)