NS3学习-Packet数据包结构
目录
- [1. 数据包结构Packet](#1. 数据包结构Packet)
-
- [1.1. Packet内存布局](#1.1. Packet内存布局)
- [1.2. 数据包的操作](#1.2. 数据包的操作)
- [1.3. 示例代码](#1.3. 示例代码)
- [2. Buffer字节缓冲区](#2. Buffer字节缓冲区)
- [3. PacketMetadata元数据](#3. PacketMetadata元数据)
-
- [3.1. PacketMetadata结构](#3.1. PacketMetadata结构)
- [3.2. PacketMatedata核心设计:](#3.2. PacketMatedata核心设计:)
-
- [3.2.1. 数据结构设计](#3.2.1. 数据结构设计)
- [3.2.2. 链表的 "扁平化存储"](#3.2.2. 链表的 “扁平化存储”)
- [3.2.3. ULEB128 编码优化](#3.2.3. ULEB128 编码优化)
- [4. 标签Tag](#4. 标签Tag)
- [5. Packet主要方法接口](#5. Packet主要方法接口)
-
- [5.1. 创建和基本操作](#5.1. 创建和基本操作)
- [5.2. 头部/尾部操作](#5.2. 头部/尾部操作)
- [5.3. 标签操作](#5.3. 标签操作)
- [5.4. 数据访问](#5.4. 数据访问)
- [6. Packet使用示例](#6. Packet使用示例)
- [7. 重要特性说明](#7. 重要特性说明)
- [8. 数据包收发过程](#8. 数据包收发过程)
1. 数据包结构Packet
Packet类在NS-3中用于表示网络中的数据包。它包含了数据包的字节内容以及一些用于模拟的元数据,如标签(Tags)和字节标签(ByteTags)。此外,Packet类还支持数据包的分片和重组。
主要组成部分:
- 字节缓冲区(Buffer):存储了数据包的头部和尾部内容,这些内容以序列化的形式存在。序列化是将 C++ 数据结构转化为二进制格式的过程,以便与真实网络数据包的格式匹配。反之,去序列化则是将二进制数据还原为数据结构。。
- 标签(Tags):用于在数据包上附加模拟信息,这些信息与数据包一起传递,但不作为实际传输的数据。标签分为两种:PacketTag和ByteTag。
- PacketTag:与整个数据包关联,适用于不随分片改变的信息。
- ByteTag:与数据包中的特定字节范围关联,当数据包分片时,这些标签会相应地分配到分片中。
- 元数据(Metadata):用于记录数据包在模拟过程中的一些信息,如数据包的大小、Uid等。
1.1. Packet内存布局
Packet 对象:
┌─────────────────────────────┐
│ Packet │
│ ┌─────────────────────┐ │
│ │ Buffer │ │
│ │ ┌──────────────┐ │ │
│ │ │ 实际数据字节 │ │ │
│ │ └──────────────┘ │ │
│ └─────────────────────┘ │
│ │
│ ┌─────────────────────┐ │
│ │ PacketMetadata │ │
│ │ ┌──────────────┐ │ │
│ │ │ EthernetHeader│ │ │
│ │ │ IP Header │ │ │
│ │ │ TCP Header │ │ │
│ │ │ Payload │ │ │
│ │ └──────────────┘ │ │
│ └─────────────────────┘ │
│ │
│ ┌─────────────────────┐ │
│ │ ByteTagList │ │
│ │ PacketTagList │ │
│ └─────────────────────┘ │
└─────────────────────────────┘
有以下主要类:
- Packet:核心类,继承自SimpleRefCount。
- Buffer:数据缓冲区,Packet包含一个Buffer对象。
- PacketMetadata:元数据管理,Packet包含一个PacketMetadata对象。
- ByteTagList:字节标签列表,Packet包含一个ByteTagList对象。
- PacketTagList:包标签列表,Packet包含一个PacketTagList对象。
- Header:协议头部基类,Packet可以添加多个Header。
- Trailer:协议尾部基类,Packet可以添加多个Trailer。
- Tag:标签基类,分为PacketTag和ByteTag。
类图如下:
包含
包含
包含
包含
添加/移除
添加/移除
管理
管理
创建
返回
使用
管理
管理
管理
序列化/反序列化
序列化/反序列化
序列化/反序列化
1 1 1 1 1 1 1 1 1 1 1 * * * Packet
-Ptr<Buffer> m_buffer
-PacketMetadata m_metadata
-ByteTagList m_byteTagList
-PacketTagList m_packetTagList
-uint32_t m_size
-uint64_t m_uid
-static uint64_t m_globalUid
+Packet()
+Packet(uint32_t size)
+Packet(uint8_t* buffer, uint32_t size)
+~Packet()
+GetSize() : uint32_t
+GetUid() : uint64_t
+Copy() : Ptr<Packet>
+AddHeader(Header& header) : void
+RemoveHeader(Header& header) : uint32_t
+PeekHeader(Header& header) : uint32_t
+AddTrailer(Trailer& trailer) : void
+RemoveTrailer(Trailer& trailer) : uint32_t
+AddPacketTag(Tag& tag) : void
+RemovePacketTag(Tag& tag) : bool
+PeekPacketTag(Tag& tag) : bool
+AddByteTag(Tag& tag) : void
+FindFirstMatchingByteTag(Tag& tag) : bool
+Begin() : BufferIterator
+End() : BufferIterator
+Serialize(uint8_t* buffer, uint32_t size) : void
+Deserialize(uint8_t* buffer, uint32_t size) : void
<<模板类>>
SimpleRefCount<Packet>
-int m_count
+Ref() : void
+Unref() : void
Buffer
-uint8_t* m_data
-uint32_t m_start
-uint32_t m_maxSize
-uint32_t m_size
+Buffer()
+Buffer(uint32_t size)
+~Buffer()
+GetSize() : uint32_t
+AddAtStart(uint32_t size) : void
+AddAtEnd(uint32_t size) : void
+RemoveAtStart(uint32_t size) : void
+RemoveAtEnd(uint32_t size) : void
+CreateFragment(uint32_t start, uint32_t length) : Buffer
+GetIterator() : Iterator
PacketMetadata
-struct Item* m_head
-struct Item* m_tail
-uint32_t m_used
-Item* m_chunks
+PacketMetadata()
+~PacketMetadata()
+AddHeader(uint32_t size, uint32_t chunkUid) : void
+RemoveHeader(uint32_t size) : void
+AddTrailer(uint32_t size, uint32_t chunkUid) : void
+RemoveTrailer(uint32_t size) : void
+GetSerializedSize() : uint32_t
+Deserialize(uint8_t* buffer, uint32_t size) : void
<<抽象类>>
Tag
#uint32_t m_tid
+GetInstanceTypeId() : TypeId
+GetSerializedSize() : uint32_t
+Serialize(TagBuffer buffer) : void
+Deserialize(TagBuffer buffer) : void
+Print(ostream& os) : void
<<接口>>
PacketTag
+仅用于包级标签的标识接口
<<接口>>
ByteTag
+仅用于字节级标签的标识接口
<<抽象类>>
Header
+GetSerializedSize() : uint32_t
+Serialize(BufferIterator start) : void
+Deserialize(BufferIterator start) : uint32_t
+Print(ostream& os) : void
<<抽象类>>
Trailer
+GetSerializedSize() : uint32_t
+Serialize(BufferIterator start) : void
+Deserialize(BufferIterator start) : uint32_t
+Print(ostream& os) : void
BufferIterator
-uint8_t* m_current
-uint32_t m_offset
+Next() : uint8_t
+Prev() : uint8_t
+WriteU8(uint8_t value) : void
+ReadU8() : uint8_t
+WriteU16(uint16_t value) : void
+ReadU16() : uint16_t
+WriteU32(uint32_t value) : void
+ReadU32() : uint32_t
ByteTagList
-struct TagData* m_head
+ByteTagList()
+~ByteTagList()
+Add(Tag tag, uint32_t start, uint32_t end) : void
+RemoveAll() : void
+Begin() : Iterator
PacketTagList
-struct TagData* m_head
+PacketTagList()
+~PacketTagList()
+Add(Tag tag) : void
+Remove(Tag tag) : bool
+Peek(Tag tag) : bool
+RemoveAll() : void
<<辅助类>>
TypeId
+uint32_t m_tid
+LookupByName(string name) : TypeId
+GetName() : string
MetadataItem
TagBuffer
Ipv4Header
+Serialize(BufferIterator start) : void
+Deserialize(BufferIterator start) : uint32_t
+GetSerializedSize() : uint32_t
+Print(ostream& os) : void
TcpHeader
+Serialize(BufferIterator start) : void
+Deserialize(BufferIterator start) : uint32_t
+GetSerializedSize() : uint32_t
+Print(ostream& os) : void
FlowIdTag
-uint32_t m_flowId
+GetSerializedSize() : uint32_t
+Serialize(TagBuffer buffer) : void
+Deserialize(TagBuffer buffer) : void
+GetInstanceTypeId() : TypeId
1.2. 数据包的操作
NS3 提供了丰富的成员函数来操作数据包,包括但不限于以下功能:
- 添加头部和尾部 :通过 Packet::AddHeader() 和 Packet::AddTrailer() 方法,可以将序列化的头部或尾部添加到数据包中。
- 连接数据包 :可以通过 Packet::AddAtEnd() 方法将两个数据包连接起来。
- 添加标签 :使用 Packet::AddByteTag() 或 Packet::AddPacketTag() 方法为数据包添加标签。
- 打印和检查元数据 :通过 Packet::Print() 方法可以打印数据包的详细信息。
1.3. 示例代码
以下是一个简单的示例,展示如何在 NS3 中创建和操作数据包:
c++
#include "ns3/packet.h"
#include "ns3/ipv4-address.h"
#include "ns3/aodv-packet.h"
using namespace ns3;
int main() {
// 创建一个空数据包
Ptr<Packet> packet = Create<Packet>();
// 添加头部 (以 AODV 的 RREQ 消息为例)
RreqHeader rreqHeader(0, 0, 1, 12345, Ipv4Address("192.168.1.1"), 1, Ipv4Address("192.168.1.2"), 2);
packet->AddHeader(rreqHeader);
// 打印数据包内容
packet->Print(std::cout);
return 0;
}
2. Buffer字节缓冲区
Buffer类表示一个字节缓冲区。其大小会自动调整为 保留用户前置或添加的任何数据。其实现得到了优化 通过创建新 来确保缓冲区调整大小的次数最小化 缓冲区大小为有史以来最大大小。正确的最大尺寸可学习于 通过记录每个数据包的最大大小来实现运行时间。
字节缓冲区的实现方式如下:
struct BufferData {
uint32_t m_count;
uint32_t m_size;
uint32_t m_initialStart;
uint32_t m_dirtyStart;
uint32_t m_dirtySize;
uint8_t m_data[1];
};
struct BufferData *m_data;
uint32_t m_zeroAreaSize;
uint32_t m_start;
uint32_t m_size;
BufferData::m_count:结构的参考计数BufferDataBufferData::m_size:结构中存储的数据缓冲区大小BufferDataBufferData::m_initialStart:偏移量为数据缓冲区起始点,其中数据 首次插入BufferData::m_dirtyStart: 偏移量,从缓冲区起始点,所有持有该实例引用的 都已写入数据Buffer``BufferDataBufferData::m_dirtySize:目前数据写入面积的大小BufferData::m_data:指向数据缓冲区的指针Buffer::m_zeroAreaSize:在 之前延伸的零面积大小m_initialStartBuffer::m_start:从缓冲区起始到该缓冲区使用的区域的偏移量Buffer::m_size:其结构中所使用的面积大小Buffer``BufferData
3. PacketMetadata元数据
PacketMetadata类:用于处理与数据包协议头、协议尾相关的数据包元数据。同时,它也为 Packet::Print 方法提供了实现逻辑 ------ 该方法会借助元数据来解析数据包缓冲区中的内容。
3.1. PacketMetadata结构
PacketMetadata类维护着一个被称为 "项"(item) 的链表,链表中的每一项都对应一个协议头、一个协议尾、一段负载,或者是它们中任意一种的分片。每个项都包含 next 和 prev 两个字段,分别指向链表中的下一项和前一项。PacketMetadata 类则通过一对指针,分别指向该链表的头节点和尾节点。
链表中的每一项还会维护以下信息:
- 原始大小(native size):该项首次被添加到数据包时的字节长度
- 类型:标识该项对应的具体协议头、协议尾类型,或判断该项是否为负载
- 归属数据包 UID:该项首次被添加时对应的数据包唯一标识
- 分片区间:若该项是一个分片,则记录其对应的原始数据起始和结束位置
这个链表会被扁平化 存储在 Data 结构体的字节缓冲区中。链表中的每一项都通过一个偏移量来标识 ------ 该偏移量代表该项的首个字节相对于数据缓冲区起始位置的距离。此数据缓冲区的最大容量为 (2^{16}-1) 字节,这在一定程度上限制了链表可存储的项数,但在实际的仿真场景中,几乎不会触及这个上限。
C++
/**
* 元数据核心存储结构体
*/
struct Data
{
/** 指向该 Data 实例的引用计数 */
uint32_t m_count;
/** 下方 m_data 缓冲区的字节长度 */
uint16_t m_size;
/** 所有引用该 Data 实例的对象中,m_used 字段的最大值 */
uint16_t m_dirtyEnd;
/** 可变长度字节缓冲区 */
uint8_t m_data[PACKET_METADATA_DATA_M_DATA_SIZE];
};
/* 注意:由于 next 和 prev 字段是16位整数,且 0xffff 被保留用于标识链表的首尾,因此 m_data 字节缓冲区中可存储的元素数量存在上限。
*/
/**
* \brief 管理元数据缓冲区内存的自由链表类
*/
class DataFreeList : public std::vector<struct Data*>
{
public:
~DataFreeList();
};
链表中的每一项都是一个可变长度的字节缓冲区,由多个字段构成。其中部分字段以固定长度的 32 位整数存储,另一些以固定长度的 16 位整数存储,还有一部分则以可变长度的 32 位整数存储。
类图:
包含元数据项
持有核心存储
存储链表节点
可选扩展信息
遍历目标
关联数据包缓冲区
类型枚举
内存池管理
1 1 1 0..1 1 1 1 1 * 1 * 0..1 1 1 1 * PacketMetadata
- struct Data* m_data
- uint16_t m_head
- uint16_t m_tail
- uint16_t m_used
- uint64_t m_packetUid
- static DataFreeList m_freeList
- static bool m_enable
- static bool m_enableChecking
- static bool m_metadataSkipped
- static uint32_t m_maxSize
- static uint16_t m_chunkUid
-- 友元声明 --
friend class ItemIterator - static void Enable()
- static void EnableChecking()
- PacketMetadata(uint64_t uid, uint32_t size)
- PacketMetadata(const PacketMetadata& o)
- PacketMetadata& operator=(const PacketMetadata& o)
- ~PacketMetadata()
- void AddHeader(const Header& header, uint32_t size)
- void RemoveHeader(const Header& header, uint32_t size)
- void AddTrailer(const Trailer& trailer, uint32_t size)
- void RemoveTrailer(const Trailer& trailer, uint32_t size)
- PacketMetadata CreateFragment(uint32_t start, uint32_t end) : const
- void AddAtEnd(const PacketMetadata& o)
- void AddPaddingAtEnd(uint32_t end)
- void RemoveAtStart(uint32_t start)
- void RemoveAtEnd(uint32_t end)
- uint64_t GetUid() : const
- uint32_t GetSerializedSize() : const
- ItemIterator BeginItem(Buffer buffer) : const
- uint32_t Serialize(uint8_t* buffer, uint32_t maxSize) : const
- uint32_t Deserialize(const uint8_t* buffer, uint32_t size)
- static uint8_t* AddToRawU8(const uint8_t& data, uint8_t* start, uint8_t* current, uint32_t maxSize)
- static uint8_t* AddToRawU16(const uint16_t& data, uint8_t* start, uint8_t* current, uint32_t maxSize)
- static uint8_t* AddToRawU32(const uint32_t& data, uint8_t* start, uint8_t* current, uint32_t maxSize)
- static uint8_t* AddToRawU64(const uint64_t& data, uint8_t* start, uint8_t* current, uint32_t maxSize)
- static uint8_t* AddToRaw(const uint8_t* data, uint32_t dataSize, uint8_t* start, uint8_t* current, uint32_t maxSize)
- static uint8_t* ReadFromRawU8(uint8_t& data, const uint8_t* start, const uint8_t* current, uint32_t maxSize)
- static uint8_t* ReadFromRawU16(uint16_t& data, const uint8_t* start, const uint8_t* current, uint32_t maxSize)
- static uint8_t* ReadFromRawU32(uint32_t& data, const uint8_t* start, const uint8_t* current, uint32_t maxSize)
- static uint8_t* ReadFromRawU64(uint64_t& data, const uint8_t* start, const uint8_t* current, uint32_t maxSize)
- uint16_t AddSmall(const SmallItem* item)
- uint16_t AddBig(uint32_t head, uint32_t tail, const SmallItem* item, const ExtraItem* extraItem)
- void ReplaceTail(SmallItem* item, ExtraItem* extraItem, uint32_t available)
- void UpdateHead(uint16_t written)
- void UpdateTail(uint16_t written)
- uint32_t GetUleb128Size(uint32_t value) : const
- uint32_t ReadUleb128(const uint8_t** pBuffer) : const
- void Append16(uint16_t value, uint8_t* buffer)
- void Append32(uint32_t value, uint8_t* buffer)
- void AppendValue(uint32_t value, uint8_t* buffer)
- void AppendValueExtra(uint32_t value, uint8_t* buffer)
- void Reserve(uint32_t n)
- void ReserveCopy(uint32_t n)
- uint32_t GetTotalSize() : const
- uint32_t ReadItems(uint16_t current, SmallItem* item, ExtraItem* extraItem) : const
- void DoAddHeader(uint32_t uid, uint32_t size)
- bool IsStateOk() : const
- bool IsPointerOk(uint16_t pointer) : const
- bool IsSharedPointerOk(uint16_t pointer) : const
- static void Recycle(Data* data)
- static Data* Create(uint32_t size)
- static Data* Allocate(uint32_t n)
- static void Deallocate(Data* data)
friend DataFreeList::~DataFreeList()
<<enumeration>>
ItemType
PAYLOAD
HEADER
TRAILER
Item
- ItemType type
- bool isFragment
- TypeId tid
- uint32_t currentSize
- uint32_t currentTrimedFromStart
- uint32_t currentTrimedFromEnd
- Buffer::Iterator current
ItemIterator
- const PacketMetadata* m_metadata
- Buffer m_buffer
- uint16_t m_current
- uint32_t m_offset
- bool m_hasReadTail
- ItemIterator(const PacketMetadata* metadata, Buffer buffer)
- bool HasNext() : const
- Item Next()
Data
- uint32_t m_count
- uint16_t m_size
- uint16_t m_dirtyEnd
- uint8_t m_data[PACKET_METADATA_DATA_M_DATA_SIZE]
SmallItem - uint16_t next
- uint16_t prev
- uint32_t typeUid
- uint32_t size
- uint16_t chunkUid
ExtraItem - uint32_t fragmentStart
- uint32_t fragmentEnd
- uint64_t packetUid
DataFreeList
<> vector
- ~DataFreeList()
Buffer
3.2. PacketMatedata核心设计:
- 低内存开销(适配仿真中大量数据包的场景);
- 兼容 Packet 的 COW 机制(元数据拷贝仅复制引用,修改时才拷贝);
- 支持协议头 / 尾 / 负载的精细化追踪(包括分片)。
3.2.1. 数据结构设计
分层存储模型
| 层级 | 结构体 / 类 | 作用 |
|---|---|---|
| 对外抽象层 | Item | 封装元数据项的对外访问接口(类型、长度、分片状态、迭代器等); |
| 链表节点层 | SmallItem + ExtraItem | 链表的底层存储单元: - SmallItem:基础信息(链表指针、类型、长度); - ExtraItem:扩展信息(分片区间、归属 UID); |
| 内存管理层 | Data + DataFreeList | 元数据的物理存储: - Data:带引用计数的字节缓冲区(COW 核心); - DataFreeList:内存池(回收 / 复用 Data 实例); |
3.2.2. 链表的 "扁平化存储"
- 传统链表的 next/prev 指针被替换为偏移量(16 位整数),指向 Data::m_data 缓冲区中的字节位置;
- 优势:避免指针直接指向内存地址,使元数据可序列化 / 反序列化,且内存布局更紧凑;
- 限制:偏移量为 16 位,最大支持 (2^{16}-1) 字节的缓冲区(实际足够,因元数据项体积小)。
3.2.3. ULEB128 编码优化
- 对可变长度的 32 位整数(如size、fragmentStart)采用ULEB128(无符号小端基 128)编码:
- 原理:小数值用 1 字节存储,大数值用多字节存储(如数值 < 128 仅占 1 字节);
- 优势:大幅降低元数据存储体积(仿真中协议头 / 尾长度多为小数值)。
4. 标签Tag
标签由一个指向 数据结构链表。每个结构指向 到列表中的下一个TagData(其下一个指针包含零到 表示链表的结尾)。每个标签数据包含一个整数 唯一标识,用于识别存储在 TagData 中标签的类型。TagDataTagData
cpp
struct TagData {
struct TagData *m_next;
uint32_t m_id;
uint32_t m_count;
uint8_t m_data[Tags::SIZE];
};
class Tags {
struct TagData *m_next;
};
添加标签只需在链表列表的开头插入一个新的 TagData。查看标签时,你需要在链表中找到相关的 TagData。并将其数据复制到用户数据结构中。移除标签并更新标签内容需要在执行此作之前,对链表进行深度复制。而复制数据包及其标签则是复制 TagData 头指针并增加其引用计数。
标签通过标签类型与其底层 ID 之间的唯一映射来找到。这就是为什么最多只能存储一个标签实例存储在一个数据包中。标签类型与底层 ID 之间的映射通过注册执行如下:
cpp
/* A sample Tag implementation
*/
struct MyTag {
uint16_t m_streamId;
};
5. Packet主要方法接口
5.1. 创建和基本操作
cpp
// 创建数据包
Packet(); // 空包
Packet(uint32_t size); // 指定大小的包
Packet(const uint8_t *buffer, uint32_t size); // 从缓冲区创建
// 基本属性
uint32_t GetSize() const; // 获取包大小
uint64_t GetUid() const; // 获取唯一ID
Ptr<Packet> Copy() const; // 深拷贝
5.2. 头部/尾部操作
cpp
// 添加/移除协议头部
template <typename T>
void AddHeader(const T &header);
template <typename T>
uint32_t RemoveHeader(T &header);
// 添加/移除协议尾部
template <typename T>
void AddTrailer(const T &trailer);
template <typename T>
uint32_t RemoveTrailer(T &trailer);
// 查看头部(不移除)
template <typename T>
uint32_t PeekHeader(T &header) const;
5.3. 标签操作
cpp
// 包标签
void AddPacketTag(const Tag &tag);
bool RemovePacketTag(Tag &tag);
bool PeekPacketTag(Tag &tag) const;
// 字节标签
void AddByteTag(const Tag &tag) const;
bool FindFirstMatchingByteTag(Tag &tag) const;
5.4. 数据访问
cpp
// 通过迭代器访问
Buffer::Iterator Begin() const;
Buffer::Iterator End() const;
// 直接数据访问
uint32_t CopyData(uint8_t *buffer, uint32_t size) const;
void CopyData(std::ostream &os, uint32_t size) const;
6. Packet使用示例
cpp
// 创建数据包
Ptr<Packet> packet = Create<Packet>(100); // 100字节负载
// 添加协议头部
EthernetHeader ethHeader;
ipv4Header.SetPayloadSize(packet->GetSize());
packet->AddHeader(ipv4Header);
// 添加标签
FlowIdTag flowTag;
flowTag.SetFlowId(123);
packet->AddPacketTag(flowTag);
// 移除头部
Ipv4Header ipHeader;
packet->RemoveHeader(ipHeader);
// 序列化/反序列化
uint8_t buffer[1024];
uint32_t size = packet->CopyData(buffer, 1024);
7. 重要特性说明
-
零拷贝设计:Packet 使用引用计数和缓冲区共享,避免不必要的内存拷贝。
-
元数据跟踪:自动跟踪协议头部的添加/移除顺序,便于序列化和反序列化。
-
标签系统:
- PacketTag:适用于整个包的元数据(如流ID、时间戳)
- ByteTag:适用于包中特定字节范围的元数据(如TTL)
-
协议独立性:通过模板方法支持任意协议头部类型。
-
序列化支持:Header/Trailer 类必须实现 Serialize() 和 Deserialize() 方法。
8. 数据包收发过程
这两张图展示了数据包如何通过 互联网节点对象。
发送过程:

接收过程:
