
四、Packets 和 Sockets API
1、Packets
1.1、Packets 介绍
每个网络数据包都包含一个字节缓冲区(byte buffer)、一组字节标记(byte tags)、一组数据包标记(packet tags)和元数据(metadata)。
字节缓冲区存储添加到数据包的报头和尾部的序列化内容。这些标头的序列化表示应与实际网络数据包的序列化表示形式逐位匹配,这意味着数据包缓冲区的内容应是实际数据包的内容。
在此上下文中实现分片和碎片整理是很自然的:因为我们有一个真实字节的缓冲区,我们可以将其拆分为多个片段并重新组合这些片段。我们希望这种选择将使我们的 Packet 数据结构包装在 Linux 风格的 skb 或 BSD 风格的 mbuf 中变得非常容易,以便在模拟器中集成真实世界的内核代码。我们还希望将模拟器实时插入到实际网络将很容易。
数据包是引用计数的对象。它们使用智能指针 (Ptr) 对象进行处理,就像 ns-3 系统中的许多对象一样。您将看到的一个小区别是,类 Packet 不是从类 Object 或类 RefCountBase 继承的,而是直接实现 Ref() 和 Unref() 方法。
Packet 类旨在以较低的成本进行复制;整体设计基于写入时复制 (COW)。当对一个数据包对象的多个引用,并且对其中一个引用执行操作时,只有所谓的 “dirty” 操作才会触发数据包的深层副本:
ns3::Packet::AddHeader()
ns3::Packet::AddTrailer()
Packet::RemovePacketTag()
用于向字节缓冲区添加和删除的基本类是类 Header 和类 Trailer。每个需要从 Packet 实例中插入和删除的协议头都应该从抽象的 Header 基类派生,并实现下面列出的私有纯虚方法:
ns3::Header::SerializeTo()
ns3::Header::DeserializeFrom()
ns3::Header::GetSerializedSize()
ns3::Header::PrintTo()
基本上,前三个函数用于序列化和反序列化信息到 Buffer 或从 Buffer 获取。例如,可以定义 class TCPHeader : public Header。TCPHeader 对象通常由一些私有数据(如序列号)和公共接口访问函数(如检查输入的边界)组成。但是 TCPHeader 在 Packet Buffer 中的底层表示是 20 个序列化字节(加上 TCP 选项)。因此,TCPHeader::SerializeTo() 函数将被设计为按网络字节顺序将这 20 个字节正确写入数据包中。最后一个函数用于定义 Header 对象如何将自身打印到输出流上。
同样,用户定义的 Tag 可以附加到数据包中。与 Headers 不同,Tag 不会序列化到连续的缓冲区中,而是存储在列表中。标记可以灵活地定义为任何类型,但在任何时候,标记缓冲区中都只能有一个特定对象类型的实例。
1.2、使用 Packets 接口
以下命令将创建一个具有唯一 ID 的新数据包:
Ptr<Packet> pkt = Create<Packet>();
什么是 Uid(唯一 ID)?它是系统用于识别数据包的内部 ID。可以通过以下方法获取它:
uint32_t uid = pkt->GetUid();
在初始数据包创建之后,所有后续缓冲区数据都通过添加类 Header 或类 Trailer 的对象来添加。如果添加 Header,则会将其添加到数据包的前面,如果添加 Trailer,则会将其添加到数据包的末尾。如果数据包中没有数据,则添加 Header 还是 Trailer 没有区别。由于 header 和 trailer 的 API 和类几乎相同,因此我们在这里只看类 Header。
第一步是创建新的header类。所有新的 Header 类都必须继承自 Header 类,并实现以下方法:
Serialize ()
Deserialize ()
GetSerializedSize ()
Print ()
例如:udp-header:
class UdpHeader : public Header
{
public:
/**
* \brief Enable checksum calculation for UDP
*/
void EnableChecksums();
/**
* \param port the destination port for this UdpHeader
*/
void SetDestinationPort(uint16_t port);
/**
* \param port The source port for this UdpHeader
*/
void SetSourcePort(uint16_t port);
/**
* \return The source port for this UdpHeader
*/
uint16_t GetSourcePort() const;
/**
* \return the destination port for this UdpHeader
*/
uint16_t GetDestinationPort() const;
/**
* \param source the ip source to use in the underlying
* ip packet.
* \param destination the ip destination to use in the
* underlying ip packet.
* \param protocol the protocol number to use in the underlying
* ip packet.
*
* If you want to use udp checksums, you should call this
* method prior to adding the header to a packet.
*/
void InitializeChecksum(Address source, Address destination, uint8_t protocol);
/**
* \param source the ip source to use in the underlying
* ip packet.
* \param destination the ip destination to use in the
* underlying ip packet.
* \param protocol the protocol number to use in the underlying
* ip packet.
*
* If you want to use udp checksums, you should call this
* method prior to adding the header to a packet.
*/
void InitializeChecksum(Ipv4Address source, Ipv4Address destination, uint8_t protocol);
/**
* \param source the ip source to use in the underlying
* ip packet.
* \param destination the ip destination to use in the
* underlying ip packet.
* \param protocol the protocol number to use in the underlying
* ip packet.
*
* If you want to use udp checksums, you should call this
* method prior to adding the header to a packet.
*/
void InitializeChecksum(Ipv6Address source, Ipv6Address destination, uint8_t protocol);
/**
* \brief Get the type ID.
* \return the object TypeId
*/
static TypeId GetTypeId();
TypeId GetInstanceTypeId() const override;
void Print(std::ostream& os) const override;
uint32_t GetSerializedSize() const override;
void Serialize(Buffer::Iterator start) const override;
uint32_t Deserialize(Buffer::Iterator start) override;
/**
* \brief Is the UDP checksum correct ?
* \returns true if the checksum is correct, false otherwise.
*/
bool IsChecksumOk() const;
/**
* \brief Force the UDP checksum to a given value.
*
* This might be useful for test purposes or to
* restore the UDP checksum when the UDP header
* has been compressed (e.g., in 6LoWPAN).
* Note that, normally, the header checksum is
* calculated on the fly when the packet is
* serialized.
*
* When this option is used, the UDP checksum is written in
* the header, regardless of the global ChecksumEnabled option.
*
* \note The checksum value must be a big endian number.
*
* \param checksum the checksum to use (big endian).
*/
void ForceChecksum(uint16_t checksum);
/**
* \brief Force the UDP payload length to a given value.
*
* This might be useful when forging a packet for test
* purposes.
*
* \param payloadSize the payload length to use.
*/
void ForcePayloadSize(uint16_t payloadSize);
/**
* \brief Return the checksum (only known after a Deserialize)
* \return The checksum for this UdpHeader
*/
uint16_t GetChecksum() const;
private:
/**
* \brief Calculate the header checksum
* \param size packet size
* \returns the checksum
*/
uint16_t CalculateHeaderChecksum(uint16_t size) const;
// The magic values below are used only for debugging.
// They can be used to easily detect memory corruption
// problems so you can see the patterns in memory.
uint16_t m_sourcePort{0xfffd}; //!< Source port
uint16_t m_destinationPort{0xfffd}; //!< Destination port
uint16_t m_payloadSize{0}; //!< Payload size
uint16_t m_forcedPayloadSize{0}; //!< Payload size (forced)
Address m_source; //!< Source IP address
Address m_destination; //!< Destination IP address
uint8_t m_protocol{17}; //!< Protocol number
uint16_t m_checksum{0}; //!< Forced Checksum value
bool m_calcChecksum{false}; //!< Flag to calculate checksum
bool m_goodChecksum{true}; //!< Flag to indicate that checksum is correct
};
void
UdpHeader::Print(std::ostream& os) const
{
os << "length: " << m_payloadSize + GetSerializedSize() << " " << m_sourcePort << " > "
<< m_destinationPort;
}
uint32_t
UdpHeader::GetSerializedSize() const
{
return 8;
}
void
UdpHeader::Serialize(Buffer::Iterator start) const
{
Buffer::Iterator i = start;
i.WriteHtonU16(m_sourcePort);
i.WriteHtonU16(m_destinationPort);
if (m_forcedPayloadSize == 0)
{
i.WriteHtonU16(start.GetSize());
}
else
{
i.WriteHtonU16(m_forcedPayloadSize);
}
if (m_checksum == 0)
{
i.WriteU16(0);
if (m_calcChecksum)
{
uint16_t headerChecksum = CalculateHeaderChecksum(start.GetSize());
i = start;
uint16_t checksum = i.CalculateIpChecksum(start.GetSize(), headerChecksum);
i = start;
i.Next(6);
// RFC 768: If the computed checksum is zero, it is transmitted as all ones
if (checksum == 0)
{
checksum = 0xffff;
}
i.WriteU16(checksum);
}
}
else
{
i.WriteU16(m_checksum);
}
}
uint32_t
UdpHeader::Deserialize(Buffer::Iterator start)
{
Buffer::Iterator i = start;
m_sourcePort = i.ReadNtohU16();
m_destinationPort = i.ReadNtohU16();
m_payloadSize = i.ReadNtohU16() - GetSerializedSize();
m_checksum = i.ReadU16();
if (m_calcChecksum && m_checksum)
{
uint16_t headerChecksum = CalculateHeaderChecksum(start.GetSize());
i = start;
uint16_t checksum = i.CalculateIpChecksum(start.GetSize(), headerChecksum);
m_goodChecksum = (checksum == 0);
}
return GetSerializedSize();
}
一旦你有了 Headers,以下 Packet API 就可以用来添加或删除这样的 Headers:
void AddHeader(const Header & header); // 该函数用于向数据包添加一个头部。
uint32_t RemoveHeader(Header &header); // 该函数用于从数据包中移除头部信息,并可能返回移除的头部的大小
uint32_t PeekHeader(Header &header) const; // 该函数用于查看(而非移除)数据包中的头部信息,并返回头部的大小。这个操作不会修改数据包内容。
例如,以下是添加和删除 UDP 标头的典型操作:
// add header
Ptr<Packet> packet = Create<Packet>();
UdpHeader udpHeader;
// Fill out udpHeader fields appropriately
packet->AddHeader(udpHeader);
...
// remove header
UdpHeader udpHeader;
packet->RemoveHeader(udpHeader);
// Read udpHeader fields as needed
如果标头是可变长度的,则需要 RemoveHeader() 的另一个变体:
uint32_t RemoveHeader(Header &header, uint32_t size);
1.3、Tag
有一个 Tag 基类,所有数据包标记都必须从该基类派生。它们用于数据包中的两个不同的标记列表。
顾名思义,ByteTag 跟随字节,而 PacketTag 跟随数据包。这意味着,当对数据包执行操作(例如分段、串联以及附加或删除标头)时,字节标记会跟踪它们覆盖的数据包字节。例如,如果用户创建了一个 TCP 段,并将 ByteTag 应用于该段,则 TCP 段的每个字节都将被标记。但是,如果下一层向下插入 IPv4 标头,则此 ByteTag 将不会覆盖这些字节。PacketTag 则相反,尽管对数据包进行了操作,但它仍会覆盖一个数据包。
每个类型都必须是 ns3::Tag 的子类,并且每个标签列表中只能有每种标签类型的一个实例。
字节标签 ByteTag 的 Packet API 如下所示:
void AddByteTag(const Tag &tag) const;
ByteTagIterator GetByteTagIterator() const;
bool FindFirstMatchingByteTag(Tag &tag) const;
void RemoveAllByteTags();
void PrintByteTags(std::ostream &os) const;
数据包标签 PacketTag 的 Packet API 如下所示:
void AddPacketTag(const Tag &tag) const;
bool RemovePacketTag(Tag &tag);
bool PeekPacketTag(Tag &tag) const;
void RemoveAllPacketTags();
void PrintPacketTags(std::ostream &os) const;
PacketTagIterator GetPacketTagIterator() const;
1.4、分段 Fragmentation 和串联 Concatenation
数据包可以分段或合并在一起。 例如,要对数据包进行分段 p 的 90 字节分成两个数据包,一个包含前 10 个字节,另一个包含剩余的 80 个字节,可以调用以下代码:
Ptr<Packet> frag0 = p->CreateFragment(0, 10);
Ptr<Packet> frag1 = p->CreateFragment(10, 90);
现在,将它们重新组合在一起:
frag0->AddAtEnd(frag1);
ns-3 提供了两种类型的套接字 API,理解它们之间的区别非常重要。第一种是原生的 ns-3 API,而第二种则使用原生 API 的服务来提供一个类似于 POSIX 的 API。这两种 API 都力求接近 Unix 系统应用程序开发者习惯使用的典型套接字 API,但 POSIX 变体的 API 更接近于真实系统的套接字 API。
2、sockets API
ns-3 的本机套接字 API 为各种类型的传输协议(TCP、UDP)以及数据包套接字以及将来的类似 Netlink 的套接字提供了接口。但是,用户需要注意的是,语义与真实系统中的语义并不完全相同(对于与真实系统非常一致的 API,请参阅下一节)。
ns3::Socket 在 src/network/model/socket.h 中定义。读者会注意到,许多公共成员函数与真正的套接字函数调用保持一致,并且在所有其他条件相同的情况下,我们尝试与 Posix 套接字 API 保持一致。但是,请注意:
ns-3 应用程序处理指向 Socket 对象的智能指针,而不是文件描述符;
没有同步 API 或阻塞 API 的概念;事实上,应用程序和 socket 之间交互的模型是异步 I/O 之一,这在实际系统中通常找不到(更多内容见下文);
不使用 C 样式的套接字地址结构;
2.1、创建套接字
想要使用 sockets 的应用程序必须首先创建一个。在使用基于 C 的 API 的实际系统上,这是通过调用 socket() 来完成的
int socket(int domain, int type, int protocol);
这会在系统中创建套接字并返回一个整数描述符。
在 ns-3 中,我们在较低层没有等效的系统调用,因此我们采用以下模型。有工厂可以创建 sockets 的对象。 每个工厂都能够创建一种类型的套接字,并且如果特定类型的套接字能够在给定节点上创建,然后是可以创建此类套接字的工厂必须聚合到 Node:
static Ptr<Socket> CreateSocket(Ptr<Node> node, TypeId tid);
要传递给此方法的 TypeId 示例包括:ns3::TcpSocketFactory
, ns3::PacketSocketFactory
,ns3::UdpSocketFactory
。
此方法返回指向 Socket 对象的智能指针。下面是一个示例:
Ptr<Node> n0;
Ptr<Socket> localSocket = Socket::CreateSocket(n0, TcpSocketFactory::GetTypeId());
2.2、使用套接字
以下是实际实现中 TCP 客户端的典型套接字调用序列:
sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
bind(sock, ...);
connect(sock, ...);
send(sock, ...);
recv(sock, ...);
close(sock);
ns-3 中都有与所有这些调用类似的调用,
在这个模型中,发送方如果因为缓冲区不足而导致 send() 调用失败,应用程序会暂停发送更多数据,直到调用在 ns3::Socket::SetSendCallback() 回调中注册的函数。应用程序还可以通过调用 ns3::Socket::GetTxAvailable() 来查询套接字当前有多少可用空间。发送数据的典型事件序列(忽略连接设置)可能是:
SetSendCallback(MakeCallback(&HandleSendCallback));
Send();
Send();
...
// Send fails because buffer is full
// Wait until HandleSendCallback is called
// HandleSendCallback is called by socket, since space now available
Send(); // Start sending again
同样,在接收端,套接字用户不会阻止对 recv() 的调用。相反,应用程序会设置一个回调 ns3::Socket::SetRecvCallback() ,套接字会通知应用程序何时(以及有多少)数据需要读取,然后应用程序调用 ns3::Socket::Recv() 来读取数据,直到无法读取更多数据。
有两种用于将数据发送到套接字的方法:
virtual int Send(Ptr<Packet> p, uint32_t flags) = 0;
virtual int SendTo(Ptr<Packet> p, uint32_t flags,
const Address &toAddress) = 0;
如果套接字已经连接 (Socket::Connect()) 到对等地址,则使用第一种方法。对于基于 stream 的套接字(如 TCP),需要 connect 调用将套接字绑定到对等地址,然后通常使用 Send()。
在基于 Datagram 的套接字(如 UDP),则不需要套接字在发送之前连接到对等地址,并且套接字可用于将数据发送到不同的目标地址。在这种情况下, SendTo()方法用于指定数据报的目标地址。
/*
* SPDX-License-Identifier: GPL-2.0-only
*/
#include "ns3/applications-module.h"
#include "ns3/core-module.h"
#include "ns3/internet-module.h"
#include "ns3/network-module.h"
#include "ns3/point-to-point-module.h"
// Default Network Topology
//
// 192.168.1.1 192.168.1.2
// n0 ---------------------- n1
// point-to-point
//
using namespace ns3;
NS_LOG_COMPONENT_DEFINE("MyLog");
void
ReceivePacket(Ptr<Socket> socket)
{
Ptr<Packet> packet = socket->Recv();
NS_LOG_INFO("Received packet with data: " << packet);
}
static void
SendPacket(Ptr<Socket> socket, uint32_t pktSize, uint32_t pktCount, Time pktInterval)
{
if (pktCount > 0)
{
Ptr<Packet> packet = Create<Packet>(pktSize);
socket->Send(packet);
Simulator::Schedule(pktInterval, &SendPacket, socket, pktSize, pktCount - 1, pktInterval);
}
else
{
socket->Close();
}
}
int
main(int argc, char* argv[])
{
CommandLine cmd;
cmd.Parse(argc, argv);
LogComponentEnable("MyLog", LOG_LEVEL_INFO);
NodeContainer nodes;
nodes.Create(2);
PointToPointHelper pointToPoint;
pointToPoint.SetDeviceAttribute("DataRate", StringValue("5Mbps"));
pointToPoint.SetChannelAttribute("Delay", StringValue("1ms"));
NetDeviceContainer devices;
devices = pointToPoint.Install(nodes);
pointToPoint.EnablePcap("OnOffApplication",devices.Get(0));
pointToPoint.EnablePcap("PacketSink",devices.Get(1));
InternetStackHelper stack;
stack.Install(nodes);
Ipv4AddressHelper address;
address.SetBase("192.168.1.0", "255.255.255.0");
Ipv4InterfaceContainer interfaces = address.Assign(devices);
// Receiver socket on node1
TypeId tid = TypeId::LookupByName("ns3::UdpSocketFactory");
Ptr<Socket> recvSocket = Socket::CreateSocket(nodes.Get(1), tid);
InetSocketAddress server = InetSocketAddress(interfaces.GetAddress(1), 9);
recvSocket->Bind(server);
recvSocket->SetRecvCallback(MakeCallback(&ReceivePacket));
// Sender socket on node0
Ptr<Socket> clientSocket = Socket::CreateSocket(nodes.Get(0), tid);
InetSocketAddress client = InetSocketAddress(interfaces.GetAddress(0), 9);
clientSocket->Bind(client);
clientSocket->Connect(server);
Simulator::ScheduleWithContext(clientSocket->GetNode()->GetId(),
Seconds(1.0),
&SendPacket,
clientSocket,
0,
10,
Seconds(1.0));
Simulator::Run();
Simulator::Destroy();
return 0;
}