本篇描述QualNet中离散事件仿真的实现细节:事件类型、实现事件的数据结构和类以及用于事件操作的API函数。
1. 事件和消息
在QualNet中,用于表示事件的类称为消息。消息保存有关事件的信息,例如事件的类型和相关的数据。在QualNet的文本中,术语事件和消息经常被交替使用。QualNet中有两种类型的事件:数据包事件和定时器事件。数据包事件被用来仿真层间或节点之间数据包的交换,也用于对同一层不同实体之间的通信进行建模。定时器事件用于发起或取消某项工作,使整个过程在时间线上完美呈现。在本节中,我们将描述Message类和其API函数。
1.1 消息类
~/include/message.h中定义的消息类用于实现事件,数据包和定时器事件都使用消息类。
class Message
{
private:
static const UInt8 SENT = 0x01; // Message is being sent
static const UInt8 FREED = 0x02; // MESSAGE_Free has been called
static const UInt8 DELETED = 0x04; // Deleted using "delete"
UInt8 m_flags;
public:
Message();
Message(const Message& m);
Message(PartitionData *partition,
int layerType,
int protocol,
int eventType,
bool isMT = false);
virtual ~Message();
void operator = (const Message &p);
// 用默认值初始化消息
void initialize(PartitionData* partition);
Message* next;
PartitionData* m_partitionData;
// 以下为仿真相关信息
short layerType; /// Layer which will receive the message
short protocolType; /// Protocol which will receive the message in the layer.
short instanceId; /// Which instance to give message to (for multiple
/// copies of a protocol or application).
short m_radioId; /// which radio this belongs to (if any)
short eventType; /// Message's Event type.
unsigned int naturalOrder; /// used to maintain natural ordering
/// for events at same time & node
char error; /// Does the packet contain errors?
bool mtWasMT; // Messages handed to the worker thread
// can't participate in the message recycling.
// As the partitionData->msgFreeList isn't
// locked.
bool getSent() { return (m_flags & SENT) != 0; }
void setSent(bool v);
bool getFreed() { return (m_flags & FREED) != 0; }
void setFreed(bool v);
bool getDeleted() { return (m_flags & DELETED) != 0; }
void setDeleted(bool v);
bool allowLoose; // used only by the parallel code
NodeId nodeId; // used only by the parallel code
clocktype eventTime; // used only by the parallel code
clocktype eot; // used only by the parallel code
int sourcePartitionId; // used only by the parallel code
// 消息的信息字段
double smallInfoSpace[SMALL_INFO_SPACE_SIZE / sizeof(double)];
// 消息的数据包
int packetSize;
char *packet;
char *payload;
int payloadSize;
int virtualPayloadSize;
// 消息的创建和网络发送时间.
clocktype packetCreationTime;
clocktype pktNetworkSendTime;
bool cancelled;
// 包追踪相关信息.
NodeAddress originatingNodeId;
int sequenceNumber;
int originatingProtocol;
int numberOfHeaders;
int headerProtocols[MAX_HEADERS];
int headerSizes[MAX_HEADERS];
NodeAddress relayNodeAddr;
std::vector<MessageInfoHeader> infoArray;
std::vector<MessageInfoBookKeeping> infoBookKeeping;
// 常用类方法
// 添加和删除虚拟负载
void addVirtualPayload(int size) { virtualPayloadSize += size; }
void removeVirtualPayload(int size)
{
virtualPayloadSize -= size;
ERROR_Assert(virtualPayloadSize >= 0, "invalid virtual payload size");
}
// 返回消息的数据包指针.
char* returnPacket() const { return packet; }
int returnPacketSize() const { return packetSize + virtualPayloadSize; }
int returnActualSize() const { return (isPacked)? actualPktSize : packetSize;}
int returnVirtualSize() const { return virtualPayloadSize; }
void setLayer(int layer, int protocol) {
layerType = layer;
protocolType = protocol;
}
int getLayer() const { return layerType; }
int getProtocol() const { return protocolType; }
void setEvent(int event) { eventType = event; }
int getEvent() const { return eventType; }
void setInstanceId(int instance) { instanceId = instance; }
int getInstanceId() const { return instanceId; }
void radioId(int p_radioId) { m_radioId = (short)p_radioId; }
int radioId() const { return (int)m_radioId; }
bool hasRadioId() const { return m_radioId >= 0; }
clocktype getPacketCreationTime() { return packetCreationTime; }
};
消息类的主要成员:
- layerType:与事件关联的层类型,在~/include/main.h中列出。
- protocolType:与事件关联的协议类型,在某个层的头文件中列出,如网络层协议类型在network.h中列出。
- instanceId: 如果有多个协议实例,则此字段表示与事件关联的协议实例号。
- eventType:事件类型。事件类型在~/include/api.h中列出。
- packet:如果使用类实例来仿真网络中的实际数据包,则此字段存储数据包。在此字段中包含由不同层添加的报头。
- packetSize:数据包字段的大小。
- virtualPayLoadSize:用户数据中内容不重要的部分的大小,没有分配任何内存,但影响传输时间和缓冲区大小的计算。
- packetCreationTime:如果类实例用于仿真网络中的实际数据包,则此字段存储数据包的创建时间。
- infoArray:一个数组,用于存储事件处理的附加信息以及需要在层或节点之间传输的信息。
1.2 消息的infoArray成员
infoArray不影响传输延迟计算,因为它不对正在传输的实际数据建模。infoArray的每个成员都是MessageInfoHeader类型的结构,在~/include/messages.h中声明。
struct MessageInfoHeader
{
unsigned short infoType; // type of the info field
unsigned int infoSize; // size of buffer pointed to by "info" variable
char* info; // pointer to buffer for holding info
};
MessageInfoHeader数据结构的字段:
- infoType:表示包含在此结构中的信息的类型。该字段的值可以是MessageInfoType枚举中的任何一个,它在~/include/message.h中声明,如下所示。用户可以向此枚举中添加其他成员以供使用。
- infoSize:结构的info字段的大小。
- info:指向存储信息的缓冲区的指针。
enum MessageInfoType
{
INFO_TYPE_UNDEFINED = 0, // an empty info field.
INFO_TYPE_DEFAULT = 1, // default info type used in situations where
// specific type is given to the info field.
INFO_TYPE_AbstractCFPropagation,
INFO_TYPE_AppName,
INFO_TYPE_StatCategoryName,
INFO_TYPE_DscpName,
INFO_TYPE_SourceAddr,
INFO_TYPE_SourcePort,
INFO_TYPE_DestAddr,
INFO_TYPE_DestPort,
INFO_TYPE_DeliveredPacketTtlTotal, // Pass from IP to APP for session-based hop counts
INFO_TYPE_IpTimeStamp,
INFO_TYPE_DataSize,
INFO_TYPE_AbstractPhy,
……
};
infoArray字段的不同元素可用于不同目的,既可用于定时器事件,也可用于数据包事件。例如,数组的一个元素可以用来存储额外的与定时器相关联的信息,例如,对于指示路由已过期的定时器,过期路由的目标地址可以存储在infoArray的一个元素中。这个可以帮助定时器事件处理程序定位和删除路由表中的正确条目。比较常见到的类型有1(默认类型),52(INFO_TYPE_StatsTiming)、77(INFO_TYPE_IPPacketSentTime)等。QualNet提供了几个API来操作infoArray字段,主要有:
- MESSAGE_AddInfo:此函数添加指定消息的infoArray字段的一个元素。信息的infoType和infoSize字段作为参数传递。
- MESSAGE_EmplaceInfo:此函数创建一个指定infoType的info字段,infoType作为参数传递。此API用于创建动态注册声明类型的info字段。
- MESSAGE_ReturnInfo:此函数接受infoType作为参数,并返回指向指定消息的info字段的指针。
- MESSAGE_ReturInfoSize:此函数接受infoType作为参数,并返回指定消息的infoSize字段。
1.3 消息的packet字段
消息packet字段仿真正在传输的实际数据。与infoArray字段不同,此字段的大小(由消息类的packetSize成员指示)确实会影响传输延迟的计算。在QualNet中提供了一些API函数,用于消息操作。可以从任何层调用消息API。函数的源码在~/include/message.h中,实现代码在~/main/message.cpp中。查看消息API及其参数的完整列表可参见API参考指南或message.h。以下是部分常用API。
- MESSAGE_Send:此函数在指定延迟后发生指定事件(消息)。注意:在调用消息MESSAGE_Send来调度事件之后,不要改变消息类实例的任何字段值。
- MESSAGE_Alloc:此函数分配一个新的消息结构并用函数参数来设置layerType, protocolType 和 eventType字段。
- MESSAGE_Free:此函数释放指定的消息。消息的数据包和信息阵列字段被释放,然后消息本身被释放。
- MESSAGE_PacketAlloc:此函数分配指定消息的packet字段。packet字段的大小和创建数据包的协议的名称作为参数传递。
- MESSAGE_ReturnPacket:此函数返回指定消息的packet字段。
- MESSAGE_ReturActualPacketSize:此函数返回指定消息的packetSize字段。
- MESSAGE_AddHeader:此函数向包含在指定消息中的数据包添加一个报头。消息的packetSize字段增加报头大小值,packet字段指向新分配的报头。添加报头的协议的报头大小和名称被传递为参数。
- MESSAGE_RemoveHeader:此函数从指定消息的数据包中删除一个报头,消息的packetSize字段减小报头大小值,packet字段指向删除报头后的空间。删除报头的协议的报头大小和名称被传递为参数。
- MESSAGE_GetLayer:此函数返回指定消息的layerType字段。
- MESSAGE_GetProtocol:此函数返回指定消息的protocolType字段。
- MESSAGE_GetEvent:该函数返回指定消息的eventType字段。
2. 事件类型
2.1 数据包事件
数据包事件用于仿真数据包在网络中的传输。数据包定义为协议栈的任何层上的虚拟或真实数据单元。当节点需要发送数据包至QualNet协议栈中的一个相邻层时,它在相邻层调度数据包事件,仿真数据包的到达。
当驻留在一个节点的特定层的协议向另一个节点的同一层相关协议发送数据包时,在发送节点,该数据包沿着协议栈向下传输,通过网络,然后在接收节点上沿着协议栈向上传送。在发送节点,协议栈每层将报头信息添加到数据包中,并发送到下面的层。每个层负责将包发送到其相邻层。在接收节点,每个层剥离其报头并将数据包发送到上面的层。直到原始数据包最终可用于接收协议为止。当始发协议驻留在应用层时,整个传输过程步骤如下:
- 协议使用MESSAGE_Alloc创建一个新消息,使用MESSAGE_PacketAlloc创建此消息的packet字段。
- 协议将要发送的数据放置到消息的packet字段中,适当地设置消息的其他字段,使用MESSAGE_Send将消息发送给下一层(在本例中为传输层)。函数MESSAGE_Send调度数据包事件,以便在指定为参数的延迟之后发生下一层。
- 在传输层协议接收数据包时,传输层协议通过使用MESSAGE_AddHeader将其报头附加到数据包,并适当地设置报头字段。然后,传输层协议使用MESSAGE_Send将结果数据包发送到堆栈中的下一层。
- 前一步在协议栈的每一层重复:每层将其报头添加到数据包中,并将结果数据包发送到下一层。
- 当数据包到达源节点的物理层时,为接收节点的物理层调度数据包接收事件。
- 当目标节点上的层接收到数据包时,它使用MESSAGE_RemoveHeader删除相应的标头,并发送生成的数据包。使用MESSAGE_Send到协议栈中的上一层。
- 前一步在协议栈的每一层重复:每层移除其报头并发送结果发送到上一层。当数据包到达目的地节点的应用层时,接收协议处理数据包并使用MESSAGE_Free释放消息。
在QualNet中,相邻层之间的通信可以通过使用消息API进行,如图所示,或者通过使用层特定API来进行。消息API是通用的并且可以在任何层使用, 而特定层的API是基于特定层的。第3.3.2.1.1节概述了特定层API在层间数据包交换中的使用。第3.3.2.1.2节概述了使用消息API在层间数据包交换中的使用。
2.1.1 使用特定层的API发送数据包
为简化协议开发,QualNet提供了特定层API函数来发送数据包,而不是使用原始消息API,协议开发人员可以简单地使用特定层API函数从特定层发送数据包。特定层API函数负责调度相邻层事件在协议栈中传输数据包。每个层提供的API封装了消息API,并隐藏了相邻层调度事件的详细信息,因此提供了易于使用的功能,用于从协议栈的特定层发送数据包。
特定层API功能因层而异,每个层上可用的API调用另文讨论。要了解每个层上可用的API,请参阅QualNet协议实现源代码中用于发送数据包的API函数。作为一个例子,本节概述了应用层的特定层数据包交换API。
应用层数据包交换分为两类:在传输层用UDP协议交换数据包;在传输层用TCP协议交换数据包。
表 1列出了在应用层使用传输层的UDP发送数据包可用的API调用。表 2列出了在应用层使用传输层的TCP发送数据包可用的API调用。这些函数在~/main/app_util.cpp中定义,底层代码使用消息API创建和发送消息。
2.1.2 用消息API发送数据包
特定层API为通过协议栈发送数据包提供了一种方便的方法,但有时可能需要绕过特定层API,这可能是由于某些特定协议设计的特殊性。本节介绍如何使用消息API发送数据包。
要理解消息API的使用,请看从应用层往传输层UDP协议发送数据包的特定层API:APP_UdpSendNewDataWithPriority的实现。
void
APP_UdpSendNewHeaderDataWithPriority(
Node *node,
AppType appType,
NodeAddress sourceAddr,
short sourcePort,
NodeAddress destAddr,
int outgoingInterface,
char *header,
int headerSize,
char *payload,
int payloadSize,
TosType priority,
clocktype delay,
TraceProtocolType traceProtocol
)
{
Message *msg;
AppToUdpSend *info;
ActionData acnData;
msg = MESSAGE_Alloc(
node,
TRANSPORT_LAYER,
TransportProtocol_UDP,
MSG_TRANSPORT_FromAppSend);
MESSAGE_PacketAlloc(node, msg, payloadSize, traceProtocol);
memcpy(MESSAGE_ReturnPacket(msg), payload, payloadSize);
MESSAGE_AddHeader(node, msg, headerSize, traceProtocol);
memcpy(MESSAGE_ReturnPacket(msg), header, headerSize);
MESSAGE_InfoAlloc(node, msg, sizeof(AppToUdpSend));
info = (AppToUdpSend *) MESSAGE_ReturnInfo(msg);
SetIPv4AddressInfo(&info->sourceAddr, sourceAddr);
info->sourcePort = sourcePort;
SetIPv4AddressInfo(&info->destAddr, destAddr);
info->destPort = (short) appType;
info->priority = priority;
info->outgoingInterface = outgoingInterface;
info->ttl = IPDEFTTL;
//Trace Information
acnData.actionType = SEND;
acnData.actionComment = NO_COMMENT;
TRACE_PrintTrace(node, msg, TRACE_APPLICATION_LAYER,
PACKET_OUT, &acnData);
MESSAGE_Send(node, msg, delay);
}
函数APP_UdpSendNewDataWithPriority使用MESSAGE_Alloc分配消息变量msg。然后调用MESSAGE_PacketAlloc来分配消息的数据包字段。MESSAGE_PacketAlloc的第三个参数payloadSize用于设置数据包字段的大小。调用MESSAGE_PacketAlloc后,可以使用消息结构中的数据包字段进入这个空间。API函数MESSAGE_ReturnPacket用于访问消息的数据包字段。使用memcpy函数将用户数据复制到数据包字段中,其他信息可以存储在消息的infoArray字段。info字段使用MESSAGE_Alloc分配(MESSAGE_Alloc相当于使用MESSAGE_AddInfo将INFO_TYPE_DEFAULT作为info字段类型,并分配info Array的第0元素)。MESSAGE_ReturnInfo用于访问消息的infoArray[0].info字段。在消息的infoArray[0].info字段存储信息后,使用MESSAGE_Send函数将数据包发送到下一层。(当消息在第一步中使用MESSAGE_Alloc分配时,layerType、protocolType、eventType字段分别设置为TRANSPORT_LAYER、TransportProtocol_Udp和MSG_TRANSPORT_FromAppSend。在APP_UdpSendNewDataWithPriority的最后一步,调用MESSAGE_Send的结果是在传输层UDP协议上延迟指定时间后调度一个MSG_TRANSPORT_FromAppSend事件,延迟时间由MESSAGE_Send的第三个参数指定。)
当来自应用层的数据包到达传输层处的UDP协议时,UDP在分组中附加报头并将其发送到下一层(网络层)。这是在UDP函数TransportUdpSendToNetwork中完成的,它在~/libraries/developer/src/transport_udp.cpp中实现,主要代码如下。
void TransportUdpSendToNetwork(Node *node, Message *msg)
{
TransportDataUdp *udp = (TransportDataUdp *) node->transportData.udp;
TransportUdpHeader *udpHdr;
AppToUdpSend *info;
unsigned char protocol = IPPROTO_UDP;
info = (AppToUdpSend *) MESSAGE_ReturnInfo(msg);
MESSAGE_AddHeader(node, msg, sizeof(TransportUdpHeader), TRACE_UDP);
udpHdr = (TransportUdpHeader *) msg->packet;
udpHdr->sourcePort = info->sourcePort;
udpHdr->destPort = info->destPort;
udpHdr->length = (unsigned short) MESSAGE_ReturnPacketSize(msg);
udpHdr->checksum = 0;
// udp统计信息
if (udp->udpStatsEnabled)
{
int type = STAT_AddressToDestAddressType(node, info->destAddr);
udp->newStats->AddReceiveFromUpperLayerDataPoints(node, msg);
udp->newStats->AddSegmentSentDataPoints(
node,
msg,
0,
MESSAGE_ReturnPacketSize(msg) - sizeof(TransportUdpHeader),
sizeof(TransportUdpHeader),
info->sourceAddr,
info->destAddr,
info->sourcePort,
info->destPort);
}
// 调用网络层函数接收传输层数据包
NetworkIpReceivePacketFromTransportLayer(
node,
msg,
info->sourceAddr,
info->destAddr,
info->outgoingInterface,
info->priority,
protocol,
FALSE,
info->ttl);
}
在函数TransportUdpSendToNetwork中,API函数MESSAGE_AddHeader用于在数据包之前添加报头。此函数在数据包中为报头保留额外的空间。报头大小由函数的第三个参数指定。MESSAGE_AddHeader还相应地更新消息结构中的packetSize字段。调用此函数后,消息的packet字段指向这个新报头占用的空间。TransportUdpSendToNetwork接下来更新报头字段并调用特定于传输层的API函数NetworkIpReceivePacketFromTransportLayer将数据包发送到下一层(网络层)。通过这种方式,数据包沿着协议栈向下移动,每一层都添加自己的报头。
2.2 定时器事件
定时器事件用于执行警报功能。它们基本上允许应用程序在未来某个时间为自己安排事件。定期警报由在定时器事件发生后重新设置它实现。定时器事件在协议中被设置和接收,它们不会通过协议栈传输。
定时器事件的例子有:
- 定时器警报,如每5秒发送一次路由更新。
- 定时器警报,在路由表安装3秒后,删除过期路由。
2.2.1设置定时器
定时器事件也可以使用message类来实现。要设置定时器事件,可使用函数MESSAGE_Alloc分配新消息,将节点指针、层、协议和事件类型参数传递到函数。事件类型在~/include/api.h中定义。
例如,以下代码为从当前仿真时间延迟5秒后,应用层RIP协议调度MSG_APP_RIP_RegularUpdateAlarm类型的事件。
Message* newMsg;
clocktype delay;
newMsg = MESSAGE_Alloc(node,
APP_LAYER,
APP_ROUTING_RIP,
MSG_APP_RIP_RegularUpdateAlarm);
delay = 5 * SECOND;
MESSAGE_Send(node,newMsg,delay);
注意,如果延迟设置为0,则事件发生在当前函数完成执行之后,但在仿真时钟更新之前。如果需要在定时器中存储一些额外的信息,可用infoArray字段与定时器一起实现。例如,考虑一个超时定时器来接收发送的数据包的确认,在这种情况下, 消息的infoArray[0].info字段可以存储需要确认的数据包的序列号和目标IP地址。
2.2.2 取消定时器
API函数MESSAGE_CancelSelfMsg用于取消QualNet调度程序中的消息。消息必须是self消息,即节点发送给自己的消息。函数接收参数为接受指向节点的指针和指向要取消的消息的指针。
例如,考虑下面的函数调用:
MESSAGE_CancelSelfMsg(node,msgToCancelPtr)
msgToCancelPtr是指向需要取消的原始消息的指针。若要使用此函数,必须保留指向原始消息的指针。