2 使用数据包
文章目录
2.1 概述
INET Packet API 提供了许多有用的 C++ 组件,旨在简化通信协议和应用程序的实现。在下面的章节中,我们将详细介绍 Packet API,并通过示例阐明许多常见的 API 使用方法。
注:
为简洁起见,本章中的代码片段有所简化。例如,省略了一些const修饰符和const传递,省略了设置字段,并简化了一些算法以方便理解。
数据包的表示对于通信网络模拟至关重要。应用程序和通信协议以多种方式构建、解构、封装、分片、聚合和处理数据包。为了简化这些行为模式的实现,INET 提供了一个功能丰富的通用数据结构–Packet类。
Packet数据结构能够表示应用程序数据包、TCP片段、IP 数据报、以太网帧、IEEE 802.11帧以及各种数字数据。它旨在提供高效的存储、复制、共享、封装、聚合、分片、序列化和数据表示选择。其他功能,如支持排队发送数据和缓冲接收到的数据以进行重新组装和/或重新排序,都是在 Packet 的基础上以单独的 C++ 数据结构提供的。
2.2 表示数据
Packet数据结构建立在另一套名为chunks的数据结构之上。chunks为表示数据提供了多种选择。
INET 提供以下内置chunk C++ 类:
- Chunk,所有chunk 类的基类
- 重复的字节或位块(ByteCountChunk, BitCountChunk)
- 原始字节或比特块(BytesChunk、BitsChunk)
- 有序块序列(SequenceChunk)
- 由偏移量和长度指定的另一个块的片段(SliceChunk)
- 许多基于字段的特定协议块(如 FieldsChunk 的 Ipv4Header 子类)
此外,通信协议和应用程序通常会定义自己的块类型。用户定义的数据块通常在 msg 文件中定义为 FieldsChunk 的子类,OMNeT++ MSG 编译器会将其转化为 C++ 代码。也可以从头开始编写用户定义的块。
数据块通常代表应用数据和协议头。以下示例演示了各种数据块的构建。
auto bitCountData = makeShared<BitCountChunk>(b(3), 0); // 3 zero bits
auto byteCountData = makeShared<ByteCountChunk>(B(10), '?'); // 10 '?' bytes
auto rawBitsData = makeShared<BitsChunk>();
rawBitsData->setBits({1, 0, 1}); // 3 raw bits
auto rawBytesData = makeShared<BytesChunk>(); // 10 raw bytes
rawBytesData->setBytes({243, 74, 19, 84, 81, 134, 216, 61, 4, 8});
auto fieldBasedHeader = makeShared<UdpHeader>(); // create new UDP header
fieldBasedHeader->setSrcPort(1000); // set some fields
一般来说,必须调用 makeShared 函数而不是标准的 C++ new 操作符来构建数据块,因为数据块是使用 C++ 共享指针在数据包之间共享的。
数据包在通过协议层时,通常包含由不同协议插入的多个数据块。表示数据包内容的最常用方法是通过连接形成复合块。
auto sequence = makeShared<SequenceChunk>(); // create empty sequence
sequence->insertAtBack(makeShared<UdpHeader>()); // append UDP header
sequence->insertAtBack(makeShared<ByteCountChunk>(B(10), 0)); // 10 bytes
协议通常需要对数据进行切分,例如提供分片,这也直接由块应用程序接口(chunk API)提供支持。
auto udpHeader = makeShared<UdpHeader>(); // create 8 bytes UDP header
auto firstHalf = udpHeader->peek(B(0), B(4)); // first 4 bytes of header
auto secondHalf = udpHeader->peek(B(4), B(4)); // second 4 bytes of header
为了避免分片造成数据表示混乱,分块应用程序接口会自动合并连续的分块。
auto sequence = makeShared<SequenceChunk>(); // create empty sequence
sequence->insertAtBack(firstHalf); // append first half
sequence->insertAtBack(secondHalf); // append second half
auto merged = sequence->peek(B(0), B(8)); // automatically merge slices
使用自动序列化作为共同基础,可以很容易地将其他表示法相互转换。
auto raw = merged->peek<BytesChunk>(B(0), B(8)); // auto serialization
auto original = raw->peek<UdpHeader>(B(0), B(8)); // auto deserialization
下面的 MSG 片段是一个更完整的示例,显示了如何定义 UDP 报头:
enum CrcMode
{
CRC_DISABLED = 0; // CRC is not set, serializable
CRC_DECLARED = 1; // CRC is correct without the value, not serializable
CRC_COMPUTED = 2; // CRC is potentially incorrect, serializable
}
class UdpHeader extends FieldsChunk
{
chunkLength = B(8); // UDP header length is always 8 bytes
int sourcePort = -1; // source port field is undefined by default
int destinationPort = -1; // destination port field is undefined by default
B lengthField = B(-1); // length field is undefined by default
uint16_t crc = 0; // checksum field is 0 by default
CrcMode crcMode = CRC_DISABLED; // checksum mode is disabled by default
}
区分 UdpHeader 块中与长度相关的两个字段非常重要。一个是分块本身的长度(chunkLength),另一个是报头长度字段的值(lengthField)。
2.3 表示数据包
Packet数据结构使用单个块数据结构来表示其内容。内容可能是简单的原始字节(BytesChunk),但最有可能的是各种协议特定头(如 FieldsChunk 子类)和应用数据(如 ByteCountChunk)的连接(SequenceChunk)。
Packet可由应用程序和通信协议创建。Packet在发送节点的协议层向下传递时,会在处理过程中插入新的协议特定报头和挂载字段。
auto emptyPacket = new Packet("ACK"); // create empty packet
auto data = makeShared<ByteCountChunk>(B(1000));
auto dataPacket = new Packet("DATA", data); // create new packet with data
auto moreData = makeShared<ByteCountChunk>(B(1000));
dataPacket->insertAtBack(moreData); // insert more data at the end
auto udpHeader = makeShared<UdpHeader>(); // create new UDP header
dataPacket->insertAtFront(udpHeader); // insert header into packet
为了便于接收节点的通信协议处理数据包,Packet在数据包数据中保留了两个偏移量,将数据分为三个区域:前端弹出部分、数据部分和后端弹出部分。在数据包处理过程中,当数据包通过协议层时,包头和挂载字段会从数据包的开头和结尾弹出,并移动相应的偏移量。这将有效减少剩余的未处理部分,即数据部分,但不会影响数据包中存储的数据。
packet->popAtFront<MacHeader>(); // pop specific header from packet
packet->popAtBack<MacTrailer>(); // pop specific trailer from packet
auto data = packet->peekData(); // peek remaining data in packet
2.4 表示信号
协议和应用程序在网络节点内处理过程中使用Packet数据结构来表示数字数据。相比之下,无线传输介质使用另一种名为Signal的数据结构来表示用于传输数据包的物理现象。
auto signal = new Signal(transmission);
signal->setDuration(duration);
signal->encapsulate(packet);
信号总是封装一个数据包,并包含模拟域表示的描述。信号最重要的物理特性是信号持续时间和信号功率。
2.5 表示传输错误
通信网络仿真的一个重要部分是了解出现错误时的协议行为。Packet API 为表示错误提供了多种选择。这些方案既有简单但计算成本低廉的,也有精确但计算成本高昂的。
- 标记错误数据包(简单)
- 标记错误块(良好的妥协)
- 改变raw chunks中的bits(准确)
第一个示例展示了如何在数据包级别表示传输错误。数据包根据其长度和相关的比特错误率被标记为错误。除了丢弃错误数据包外,这种表示方法不会给协议带来太多机会。
Packet *ErrorModel::corruptPacket(Packet *packet, double ber)
{
auto length = packet->getDataLength();
auto hasErrors = hasProbabilisticError(length, ber); // decide randomly
auto corruptedPacket = packet->dup(); // cheap operation
corruptedPacket->setBitError(hasErrors); // set bit error flag
return corruptedPacket;
}
第二个示例展示了如何在数据块级别表示传输错误。与上一示例类似,一个数据块也会根据其长度和相关的比特错误率被标记为错误。这种表示方法允许协议只丢弃数据包的某些部分。例如,聚合数据包可以部分丢弃,部分处理。
Packet *ErrorModel::corruptChunks(Packet *packet, double ber)
{
b offset = b(0); // start from the beginning
auto corruptedPacket = new Packet("Corrupt"); // create new packet
while (auto chunk = packet->peekAt(offset)->dupShared()) { // for each chunk
auto length = chunk->getChunkLength();
auto hasErrors = hasProbabilisticError(length, ber); // decide randomly
if (hasErrors) // if erroneous
chunk->markIncorrect(); // set incorrect bit
corruptedPacket->insertAtBack(chunk); // append chunk to corrupt packet
offset += chunk->getChunkLength(); // increment offset with chunk length
}
return corruptedPacket;
}
最后一个示例展示了如何在字节级别上实际表示传输错误。与前几个例子不同的是,这次数据包的实际数据被修改了。这样,协议就可以根据校验和来丢弃或更正任何部分。
Packet *ErrorModel::corruptBytes(Packet *packet, double ber)
{
vector<uint8_t> corruptedBytes; // bytes of corrupted packet
auto data = packet->peekAllAsBytes(); // data of original packet
for (auto byte : data->getBytes()) { // for each original byte do
if (hasProbabilisticError(B(1), ber)) // if erroneous
byte = ~byte; // invert byte (simplified corruption)
corruptedBytes.push_back(byte); // store byte in corrupted data
}
auto corruptedData = makeShared<BytesChunk>(); // create new data
corruptedData->setBytes(corruptedBytes); // store corrupted bits
return new Packet("Corrupt", corruptedData); // create new packet
}
物理层模型通过可配置参数支持上述不同的错误表示。高层协议通过检查数据包和数据块上的错误位以及标准 CRC 机制来检测错误。
2.6 数据包标记(Packet Tagging)
在网络节点内,补充数据往往需要与数据包一起传输。例如,当应用层模块打算使用 TCP 传输数据时,它必须为 TCP 指定一个连接标识符。同样,当 TCP 通过 IP 传输数据段时,IP 需要一个目标地址;当 IP 将数据报发送到以太网接口进行传输时,必须指定一个目标 MAC 地址。这些附加细节作为标记附在数据包上。
下面的代码片段演示了如何在 IPv4 协议模块中设置数据包标记:
void Ipv4::sendDown(Packet *packet, Ipv4Address nextHopAddr, int interfaceId)
{
auto macAddressReq = packet->addTag<MacAddressReq>(); // add new tag for MAC
macAddressReq->setSrcAddress(selfAddress); // source is our MAC address
auto nextHopMacAddress = resolveMacAddress(nextHopAddr); // simplified ARP
macAddressReq->setDestAddress(nextHopMacAddress); // destination is next hop
auto interfaceReq = packet->addTag<InterfaceReq>(); // add tag for dispatch
interfaceReq->setInterfaceId(interfaceId); // set designated interface
auto packetProtocolTag = packet->addTagIfAbsent<PacketProtocolTag>();
packetProtocolTag->setProtocol(&Protocol::ipv4); // set protocol of packet
send(packet, "out"); // send to MAC protocol module of designated interface
}
数据包标记不会从一个网络节点传输到另一个网络节点。所有物理层协议在将数据包发送到所连接的对等节点或传输介质之前,都会删除数据包中的所有数据包标记。
2.7 本地标记(Region Tagging)
要收集某些统计数据,可能需要在数据包数据的不同区域添加元数据。例如,要确定 TCP 数据流的端到端延迟,就必须在源端用创建时间戳标记数据区域。随后,当数据到达时,接收方会计算每个区域的端到端延迟。
void ClientApp::send()
{
auto data = makeShared<ByteCountChunk>(); // create new data chunk
auto creationTimeTag = data->addTag<CreationTimeTag>(); // add new tag
creationTimeTag->setCreationTime(simTime()); // store current time
auto packet = new Packet("Data", data); // create new packet
socket.send(packet); // send packet using TCP socket
}
在 TCP 数据流中,数据可能会被底层网络以各种方式分割、重新排列和组合。数据包数据表示负责保留相关的区域标签,就像它们单独附在每个比特上一样。为防止因上述特性而导致数据表示混乱,标签 API 可自动合并连续、等效的标签区域。
void ServerApp::receive(Packet *packet)
{
auto data = packet->peekData(); // get all data from the packet
auto regions = data->getAllTags<CreationTimeTag>(); // get all tag regions
for (auto& region : regions) { // for each region do
auto creationTime = region.getTag()->getCreationTime(); // original time
auto delay = simTime() - creationTime; // compute delay
cout << region.getOffset() << region.getLength() << delay; // use data
}
}
上述循环可以对整个数据执行一次,也可以执行多次,这取决于数据在发送方的创建情况和底层网络的运行情况。
2.8 解析数据包(Dissecting Packets)
了解数据包内部的内容是一项非常重要且经常使用的功能。仅仅使用表示法可能是不够的,因为Packet可以用字节块(BytesChunk)来表示。数据包 API 提供了一个 PacketDissector 类,该类仅根据指定的数据包协议和数据包包含的实际数据分析数据包。
分析是根据协议逻辑而不是数据的实际表示来进行的。PacketDissector 的工作原理与解析器类似。基本上,它会按顺序浏览数据包的每个部分(如协议头)。对于每个部分,它都会确定相应的协议以及该协议最具体的表示法。
PacketDissector 类依赖于对所需 ProtocolDissector 基类进行子类化的小型注册特定协议解析器类(如 Ipv4ProtocolDissector)。实施者应使用 PacketDissector::ICallback 接口通知解析器有关数据包结构的信息。
void startProtocolDataUnit(Protocol *protocol);
void endProtocolDataUnit(Protocol *protocol);
void markIncorrect();
void visitChunk(Ptr<Chunk>& chunk, Protocol *protocol);
void dissectPacket(Packet *packet, Protocol *protocol);
要使用 PacketDissector,用户需要实现 PacketDissector::ICallback 接口。当 PacketDissector 处理数据包的每一部分时,回调接口都会收到通知。
auto& registry = ProtocolDissectorRegistry::getInstance();
PacketDissector dissector(registry, callback);
auto packetProtocolTag = packet->findTag<PacketProtocolTag>();
auto protocol = packetProtocolTag->getProtocol();
dissector.dissectPacket(packet, protocol);
2.9 过滤数据包
根据数据包包含的实际数据过滤数据包是另一项广泛使用且非常重要的功能。在数据包解析器的帮助下,创建任意定制的数据包过滤器非常简单。数据包过滤器通常用于记录数据包和可视化各种数据包相关信息。
为了简化过滤,Packet API 提供了基于表达式的通用数据包过滤器,该过滤器在 PacketFilter 类中实现。表达式语法与其他 OMNeT++ 表达式相同,数据过滤器与数据包剖析器发现的数据包单个块相匹配。
例如,数据包过滤表达式 "ping*"匹配所有名称前缀为 "ping "的数据包,而数据包块过滤表达式 "inet::Ipv4Header and srcAddress(10.0.0.*) "匹配所有包含 IPv4 报头且前缀为 "10.0.0 "源地址的数据包。
PacketFilter filter; // patterns for the whole packet and for the data
filter.setPattern("ping*", "Ipv4Header and sourceAddress(10.0.0.*)");
filter.matches(packet); // returns boolean value
2.10 打印数据包
在模型开发过程中,数据包通常需要以人类可读的形式显示。Packet API 提供了一个 PacketPrinter 类,该类能以人类可读的字符串形式显示数据包。PacketPrinter 类依赖于已注册的小型特定协议PacketPrinter类(例如,Ipv4ProtocolPrinter 子类化了所需的 ProtocolPrinter 基类。
OMNeT++ 运行时用户界面会自动使用数据包打印机在数据包日志窗口中显示数据包。数据包打印机为用户界面提供了多个日志窗口列:“源”、“目的地”、“协议”、"长度 "和 “信息”。这些列显示数据包数据的方式与著名的 Wireshark 协议分析器类似。
PacketPrinter printer; // turns packets into human readable strings
printer.printPacket(std::cout, packet); // print to standard output
PacketPrinter 还提供了一些其他函数,这些函数具有额外的选项,可以控制生成的人类可读表单的细节。
2.11 记录 PCAP
将模拟的数据包导出到 PCAP 文件中,可以使用第三方工具进行进一步处理。数据包 API 提供了一个 PcapDump 类,用于创建 PCAP 文件。数据包过滤可用于减小文件大小和提高性能。
PcapDump dump;
dump.openPcap("out.pcap", 65535, 0); // maximum length and PCAP type
dump.writePacket(simTime(), packet); // record with current time
2.12封装数据包
许多通信协议都采用简单的数据包封装。它们在发送节点用自己的协议特定报头和挂载字段对数据包进行封装,并在接收节点对数据包进行解封装。报头和挂载字段包含提供协议特定服务所需的信息。
例如,以太网 MAC 协议在封装 IP 数据报时,会在数据包前加上以太网 MAC 报头,并在数据包后加上可选的填充和以太网 FCS。下面的示例显示了 MAC 协议如何封装数据包:
void Mac::encapsulate(Packet *packet)
{
auto header = makeShared<MacHeader>(); // create new header
header->setChunkLength(B(8)); // set chunk length to 8 bytes
header->setLengthField(packet->getDataLength()); // set length field
header->setTransmitterAddress(selfAddress); // set other header fields
packet->insertAtFront(header); // insert header into packet
auto trailer = makeShared<MacTrailer>(); // create new trailer
trailer->setChunkLength(B(4)); // set chunk length to 4 bytes
trailer->setFcsMode(FCS_MODE_DECLARED); // set trailer fields
packet->insertAtBack(trailer); // insert trailer into packet
}
接收数据包时,以太网 MAC 协议会从数据包中移除以太网 MAC 标头和以太网 FCS,并将生成的 IP 数据报传递给对方。下面的示例展示了 MAC 协议如何解封装数据包:
void Mac::decapsulate(Packet *packet)
{
auto header = packet->popAtFront<MacHeader>(); // pop header from packet
auto lengthField = header->getLengthField();
cout << header->getChunkLength() << endl; // print chunk length
cout << lengthField << endl; // print header length field
cout << header->getReceiverAddress() << endl; // print other header fields
auto trailer = packet->popAtBack<MacTrailer>(); // pop trailer from packet
cout << trailer->getFcsMode() << endl; // print trailer fields
assert(packet->getDataLength() == lengthField); // if the packet is correct
}
虽然 popAtFront 和 popAtBack 函数会改变数据包中剩余的未处理部分,但它们不会对数据包的实际数据产生影响。也就是说,当数据包到达高层协议时,它仍然包含所有接收到的数据,但剩余的未处理部分会变小。
2.13 数据包切片
通信协议通常提供分片功能,以克服各种物理限制(如长度限制、错误率)。它们在发送节点将数据包分割成更小的片段,然后逐个发送。它们在接收节点通过合并接收到的碎片形成原始数据包。
例如,IEEE 802.11 协议会对数据包进行分片,以克服大数据包丢失概率增加的问题。下面的示例展示了 MAC 协议如何对数据包进行分片:
vector<Packet *> *Mac::fragment(Packet *packet, vector<b>& sizes)
{
auto offset = b(0); // start from the packet's beginning
auto fragments = new vector<Packet *>(); // result collection
for (auto size : sizes) { // for each received size do
auto fragment = new Packet("Fragment"); // header + data part + trailer
auto header = makeShared<MacHeader>(); // create new header
header->setFragmentOffset(offset); // set fragment offset for reassembly
fragment->insertAtFront(header); // insert header into fragment
auto data = packet->peekAt(offset, size); // get data part from packet
fragment->insertAtBack(data); // insert data part into fragment
auto trailer = makeShared<MacTrailer>(); // create new trailer
fragment->insertAtBack(trailer); // insert trailer into fragment
fragments->push_back(fragment); // collect fragment into result
offset += size; // increment offset with size of data part
}
return fragments;
}
接收片段时,协议需要收集同一数据包的相干片段,直到所有片段都可用为止。下面的示例展示了 MAC 协议如何从一组相干片段中生成原始数据包:
Packet *Mac::defragment(vector<Packet *>& fragments)
{
auto packet = new Packet("Original"); // create new concatenated packet
for (auto fragment : fragments) {
fragment->popAtFront<MacHeader>(); // pop header from fragment
fragment->popAtBack<MacTrailer>(); // pop trailer from fragment
packet->insertAtBack(fragment->peekData()); // concatenate fragment data
}
return packet;
}
2.14 数据包聚合
通信协议通常提供聚合功能,通过减少协议开销来更好地利用通信信道。它们会等待多个数据包到达发送节点,然后形成一个大的聚合数据包,再一次性发送出去。在接收节点,聚合数据包会被拆分成原始数据包,然后继续传递。
例如,IEEE 802.11 协议在 MSDU 和 MPDU 层面聚合数据包,以提高信道利用率。下面的示例展示了 MAC 协议如何创建聚合数据包:
Packet *Mac::aggregate(vector<Packet *>& packets)
{
auto aggregate = new Packet("Aggregate"); // create concatenated packet
for (auto packet : packets) { // for each received packet do
auto header = makeShared<SubHeader>(); // create new subheader
header->setLengthField(packet->getDataLength()); // set subframe length
aggregate->insertAtBack(header); // insert subheader into aggregate
auto data = packet->peekData(); // get packet data
aggregate->insertAtBack(data); // insert data into aggregate
}
auto header = makeShared<MacHeader>(); // create new header
header->setAggregate(true); // set aggregate flag
aggregate->insertAtFront(header); // insert header into aggregate
auto trailer = makeShared<MacTrailer>(); // create new trailer
aggregate->insertAtBack(trailer); // insert trailer into aggregate
return aggregate;
}
下面的示例展示了一个 MAC 协议如何分解数据包的版本:
vector<Packet *> *Mac::disaggregate(Packet *aggregate)
{
aggregate->popAtFront<MacHeader>(); // pop header from packet
aggregate->popAtBack<MacTrailer>(); // pop trailer from packet
vector<Packet *> *packets = new vector<Packet *>(); // result collection
b offset = aggregate->getFrontOffset(); // start after header
while (offset != aggregate->getBackOffset()) { // up to trailer
auto header = aggregate->peekAt<SubHeader>(offset); // peek sub header
offset += header->getChunkLength(); // increment with header length
auto size = header->getLengthField(); // get length field from header
auto data = aggregate->peekAt(offset, size); // peek following data part
auto packet = new Packet("Original"); // create new packet
packet->insertAtBack(data); // insert data into packet
packets->push_back(packet); // collect packet into result
offset += size; // increment offset with data size
}
return packets;
}
2.15 序列化数据包
在实际通信系统中,数据包通常直接按照网络字节顺序存储为字节序列。相比之下,INET 通常将数据包存储在基于小字段的 C++ 类中(由 OMNeT++ MSG 编译器生成),以方便调试。为了计算校验和或与实际硬件通信,所有协议特定部分都必须可序列化为字节序列。
协议头序列化器是独立于实际协议头的类。它们必须在 ChunkSerializerRegistry 中注册才能使用。下面的示例展示了如何将 MAC 协议头序列化为字节序列:
void MacHeaderSerializer::serialize
(MemoryOutputStream& stream, Ptr<Chunk>& chunk)
{
auto header = staticPtrCast<MacHeader>(chunk);
stream.writeUint16Be(header->getType()); // unsigned 16 bits, big endian
stream.writeMacAddress(header->getTransmitterAddress());
stream.writeMacAddress(header->getReceiverAddress());
}
反序列化要比序列化复杂一些,因为它必须准备好处理因网络引入的错误而导致的不完整甚至不正确的数据。下面的示例展示了如何从字节序列反序列化 MAC 协议报头:
Ptr<Chunk> MacHeaderSerializer::deserialize(MemoryInputStream& stream)
{
auto header = makeShared<MacHeader>(); // create new header
header->setType(stream.readUint16Be()); // unsigned 16 bits, big endian
header->setTransmitterAddress(stream.readMacAddress());
header->setReceiverAddress(stream.readMacAddress());
return header;
}
2.16 仿真支持
为了能够与真实硬件通信,数据包必须转换成字节序列或从字节序列转换成数据包。这是因为操作系统和外部库的编程接口是通过发送和接收原始数据来工作的。
数据包中的所有协议头和数据块都必须有一个已注册的序列化器,才能创建字节的原始序列。协议模块也必须配置为禁用或计算校验和,因为序列化器无法进行校验和计算。
下面的示例展示了如何将数据包转换成字节序列,以便通过外部接口发送:
vector<uint8_t>& ExternalInterface::prepareToSend(Packet *packet)
{
auto data = packet->peekAllAsBytes(); // convert to a sequence of bytes
return data->getBytes(); // actual bytes to send
}
下面的示例显示了从外部接口接收数据包时如何将其转换成字节序列:
Packet *ExternalInterface::prepareToReceive(vector<uint8_t>& bytes)
{
auto data = makeShared<BytesChunk>(bytes); // create chunk with bytes
return new Packet("Emulation", data); // create packet with data
}
在 INET 中,由于数据包的双重表示,所有协议都自动支持硬件模拟。上面的示例创建了一个数据包,其中包含一个字节序列的单块。当数据包通过协议时,它们可以根据自己的需要解释数据(例如调用 peekAtFront)。数据包应用程序接口(Packet API)总是能提供所需的表示形式,要么是因为数据包中已经有了这种表示形式,要么是因为数据包被自动反序列化了。
2.17 数据包排队
有些协议在实际处理之前,会在发送节点临时存储数据包数据。例如,TCP 协议必须存储从应用程序接收到的传出数据,以便提供传输流控制。
下面的示例展示了传输协议如何临时存储接收到的数据,直到数据被实际使用:
class TransportSendQueue
{
ChunkQueue queue; // stores application data
B sequenceNumber; // position in stream
void enqueueApplicationData(Packet *packet);
Packet *createSegment(b length);
};
void TransportSendQueue::enqueueApplicationData(Packet *packet)
{
queue.push(packet->peekData()); // store received data
}
Packet *TransportSendQueue::createSegment(b maxLength)
{
auto packet = new Packet("Segment"); // create new segment
auto header = makeShared<TransportHeader>(); // create new header
header->setSequenceNumber(sequenceNumber); // store sequence number for reordering
packet->insertAtFront(header); // insert header into segment
if (queue.getLength() < maxLength)
maxLength = queue.getLength(); // reduce length if necessary
auto data = queue.pop(maxLength); // pop requested amount of data
packet->insertAtBack(data); // insert data into segment
sequenceNumber += data->getChunkLength(); // increase sequence number
return packet;
}
ChunkQueue 类的作用与二进制 FIFO 队列类似,只是它使用的是块。与 Packet 类似,它也会自动合并连续数据并选择最合适的表示方式。
2.18 缓存数据包
接收节点上的协议通常需要缓冲接收到的数据包数据,直到可以进行实际处理。例如,数据包到达时可能没有按顺序排列,其中包含的数据必须重新组合或重新排序后才能传递。
INET 提供了一些特殊用途的 C++ 类来支持数据缓冲:
- ChunkBuffer 可自动合并大数据块和不按顺序排列的小数据块。
- ReassemblyBuffer 可根据预期长度重新组装乱序数据。
- ReorderBuffer 可将失序数据从预期偏移量重新排序到连续数据流中。
所有缓冲器都只处理以块为单位的数据,而不是数据包。它们会自动合并连续数据,并选择最合适的表示方式。使用这些缓冲区的协议可自动支持 INET 提供的所有数据表示方式及其任意组合。例如,字节数块(ByteCountChunk)、字节块(BytesChunk)、字段块(FieldsChunk)和片块(SliceChunk)可以在同一个缓冲区中自由混合。
2.19 重组数据包
有些协议可能会使用不可靠的服务在网络上传输大量数据。这种不可靠的服务要求接收节点做好准备,以接收不按顺序和可能重复的部分。
例如,IP 协议必须在接收节点存储接收到的片段,因为它必须等到数据报变得完整后才能传递。此外,IP 协议还必须做好准备,以接收不按顺序排列且可能重复的单个片段。
下面的示例展示了网络协议如何将接收到的数据包中的数据存储并重新组合成一个完整的数据包:
class NetworkProtocolDefragmentation
{
ReassemblyBuffer buffer; // stores received data
void processDatagram(Packet *packet); // processes incoming packes
Packet *getReassembledDatagram(); // reassembles the original packet
};
void NetworkProtocolDefragmentation::processDatagram(Packet *packet)
{
auto header = packet->popAtFront<NetworkProtocolHeader>(); // remove header
auto fragmentOffset = header->getFragmentOffset(); // determine offset
auto data = packet->peekData(); // get data from packet
buffer.replace(fragmentOffset, data); // overwrite data in buffer
}
Packet *NetworkProtocolDefragmentation::getReassembledDatagram()
{
if (!buffer.isComplete()) // if reassembly isn't complete
return nullptr; // there's nothing to return
auto data = buffer.getReassembledData(); // complete reassembly
return new Packet("Datagram", data); // create new packet
}
ReassemblyBuffer 支持在给定偏移量处替换存储的数据,如果有的话,它还能提供完整的重新组装数据和预期长度。
2.20 对数据包重新排序
有些协议可能会使用不可靠的服务在网络上传输长数据流。这种不可靠的服务要求发送节点重新发送未确认的部分,还要求接收节点做好接收不按顺序和可能重复的部分的准备。
例如,TCP 协议必须在接收节点对接收到的数据进行缓冲,因为 TCP 网段可能会不按顺序到达,而且可能会重复或重叠,TCP 必须以正确的顺序向应用程序提供数据,而且只能提供一次。
下面的示例展示了传输协议如何存储和重新排列传入数据包的数据,因为这些数据包可能是无序到达的,同时也展示了这种协议如何只按正确的顺序传递可用数据:
class TransportReceiveQueue
{
ReorderBuffer buffer; // stores receive data
B sequenceNumber;
void processSegment(Packet *packet);
Packet *getAvailableData();
};
void TransportReceiveQueue::processSegment(Packet *packet)
{
auto header = packet->popAtFront<TransportHeader>(); // pop transport header
auto sequenceNumber = header->getSequenceNumber();
auto data = packet->peekData(); // get all packet data
buffer.replace(sequenceNumber, data); // overwrite data in buffer
}
Packet *TransportReceiveQueue::getAvailableData()
{
if (buffer.getAvailableDataLength() == b(0)) // if no data available
return nullptr;
auto data = buffer.popAvailableData(); // remove all available data
return new Packet("Data", data);
}
ReorderBuffer 支持在给定偏移量处替换已存储的数据,并提供预期偏移量处的可用数据(如果有的话)。