…过云雨-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 核心特点
无连接
通信前不需要建立连接,只需知道对方的 IP 和端口号就可以直接发送数据,减少了连接建立和释放的开销。
不可靠
没有确认、重传、排序等机制。如果报文丢失或乱序,UDP 协议层不会向应用层返回错误信息,也不会尝试恢复。
面向数据报
应用层交给 UDP 多长的报文,UDP 就原样发送,既不会拆分,也不会合并。
- 发送端调用 1 次
sendto发送 100 字节,接收端必须也调用 1 次recvfrom完整接收 100 字节,不能分多次接收。
- 发送端调用 1 次
全双工
UDP 的 socket 支持同时读写,同一连接可以双向传输数据。
2.3 UDP 的缓冲区
- 发送缓冲区:UDP 没有真正的发送缓冲区。调用
sendto后,数据会直接交给内核,由内核传递给网络层,应用层无法控制数据在发送缓冲区的停留。 - 接收缓冲区:UDP 有接收缓冲区,但不能保证收到的报文顺序与发送顺序一致;如果缓冲区已满,后续到达的 UDP 报文会被直接丢弃。
我们梳理一下UDP的完整传输流程:
发送端:应用层序列化数据 → 调用sendto拷贝到内核 → UDP层封装首部 → IP层封装首部 → 数据链路层封装以太网帧头 → 网卡发送;
接收端:网卡接收报文 → 数据链路层解包 → IP层解包 → UDP层验校并解包 → 应用层调用recvfrom拷贝到用户空间。
在这个流程中,有两个关键问题需要内核解决:
- 如何在不同协议层之间传递UDP报文,同时完整保留各层首部和应用数据?
- 如何实现高效的封装和解包,避免频繁的内存拷贝(毕竟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. 不修改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. 核心总结
- 封装 / 解包的本质:仅移动
data指针(核心),不移动head/end,极少修改tail,避免频繁内存拷贝,提升内核处理效率。 - 封装是 “向前预留空间加头部”(
data向head靠近),解包是 “向后跳过头部找数据”(data向tail靠近)。 - 所有协议层共享同一个
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 应用层协议。