ndnSIM学习(五)——data包和interest包的tlv编码、解码过程以及如何判断包的类型

前言

前几天,我们组打算加一种新的数据包类型,这就需要我们了解data包和interest包的编解码过程,从而仿照这一过程,加入我们的新的包的类型。

具体师姐给我的任务要求是:

节点收到一个包,一般是根据第一个Type字段判断包的类型,Type=5是兴趣包,Type=6是数据包,然后执行对应的TLV解码和转发操作。请找一下,这段“判断包类型”的代码在那个文件里?

注意:本文的结论并不完整,只是整个编解码的一部分。不过最近我有其他优先级更高的事情要做,所以暂时没有时间来修改本文,所以暂时让本文就这样。等我有空了再来重新理清这部分内容。

一些前置知识

为了让读者能够读懂本文,先介绍一些ndn里用的编码的一些前置知识。

非负整数编码规则

首先,ndn对于非负整数的编码规则是按照官网上的Non-Negative Integer Encoding规则进行编码1的。(推荐大家到官网去看,这里讲得并不详细

具体来说,对于ndn中的一个非负整数(Non-Negative Integer),其按照非负整数编码(Non-Negative Integer Encoding)规则执行编码。详细的规则为:

  • 如果这个数 ⩽ 252 \leqslant 252 252 ,那么就用1字节表示它本身
  • 如果这个数 ⩽ 2 16 \leqslant 2^{16} 216 ,那么用3字节表示它。其中首字节为 253 253 253 ,用于标志长度为 2 2 2 ,代表真正表示这个数的是后面 2 2 2 字节
  • 如果这个数 ⩽ 2 32 \leqslant 2^{32} 232 ,那么用3字节表示它。其中首字节为 254 254 254 ,用于标志长度为 4 4 4 ,代表真正表示这个数的是后面 4 4 4 字节
  • 如果这个数 ⩽ 2 64 \leqslant 2^{64} 264 ,那么用3字节表示它。其中首字节为 255 255 255 ,用于标志长度为 8 8 8 ,代表真正表示这个数的是后面 8 8 8 字节

举几个具体的例子,大家就懂了(如果还不懂,请怒骂博主sb,然后去看官网!

0      ⇒  0x00
127    ⇒  0x7F
252    ⇒  0xFC
253    ⇒  0xFD00FD
256    ⇒  0xFD0100
65535  ⇒  0xFDFFFF
65536  ⇒  0xFE00010000

其实这种编码也挺好理解的,作为一种变长码,既保证了短码字的利用率,也保证了长码字的可扩展性。

TLV编码

所谓的TLV编码,全名就是 Type(T)-Length(L)-Value(V) 编码。说白了,每个包都被切成 TLV 的格式,前面 Type 代表类型,给大家摘录一段 tlv.hpp 的代码就懂了

namespace tlv {
enum : uint32_t {
  Invalid                         = 0,
  Interest                        = 5,
  Data                            = 6,
  Name                            = 7,
  GenericNameComponent            = 8,
  ImplicitSha256DigestComponent   = 1,
  ParametersSha256DigestComponent = 2,
  CanBePrefix                     = 33,
  MustBeFresh                     = 18,
  ...  // 以下太长了,不再全部摘录,自己去 <ns-3 folder>/build/ns3/ndnSIM/ndn-cxx/encoding/tlv.hpp 看吧
};
}

比如 tlv::Interest = 5 的意思就是:你看到 Type 是5,那么就代表这个包是一个interest包。那么这个interest包有多长呢?这就得要看 Length(L) 了。其中 TypeLength(L) 都是按照非负整数编码规则编码的,解码环节的代码会按照 Length(L) 取出对应长度的 Value

注意:T和L都是按照非负整数编码规则编码的

举个例子:假设 Type=256, Length=65535 ,按照非负整数编码规则,这个TLV就应当是 (0xFD0100)(0xFDFFFF)(xxxxxxxxxxxxxx) ,其中加括号只是为了方便读者理解, (xxxxxxxxxxxxxx) 代表65535字节长的包。

data包和interest包的编码规则

上面介绍了TLV编码规则,我们知道 tlv::data=5 是不是说一个长度为 256 256 256 的data包的内容就是 (0x05)(0xFD0100)(xxxxxxx) ,其中 xxxxxxx 只有数据?并不是,其实是 xxxxxxx 中包含了数据、签名等各种信息,每种信息都用 tlv 编码进行。或者跟具体地说,最外层的 tlv=(Type=Name)(Length)(V)V 里面

咱们还是拿一具体的例子来说吧。比如data包,假设其中含有 Name+Content+SignatureInfo+SignatureValue 的信息。所以,实际上,这个data包里面真正包含的内容如下图所示:

在这里插入图片描述

因此,我们一定要注意一件事情——TLV编码的过程是反向进行的,TLV的解码是正向进行的,因为越里面的越应该先塞进去。

正片:data包和interest包是如何执行tlv编码、解码过程

有了前面的前置知识后,我们再来理解整个编解码过程就轻松多了。

编码过程

以下代码为在 ~/ndnSIM/ns-3/src/ndnSIM/ndn-cxx/ndn-cxx/data.cpp 的第46-84行。

template<encoding::Tag TAG>
size_t
Data::wireEncode(EncodingImpl<TAG>& encoder, bool wantUnsignedPortionOnly) const
{
  // Data ::= DATA-TLV TLV-LENGTH
  //            Name
  //            MetaInfo?
  //            Content?
  //            SignatureInfo
  //            SignatureValue

  size_t totalLength = 0;

  // SignatureValue
  if (!wantUnsignedPortionOnly) {
    if (!m_signature) {
      NDN_THROW(Error("Requested wire format, but Data has not been signed"));
    }
    totalLength += encoder.prependBlock(m_signature.getValue());
  }

  // SignatureInfo
  totalLength += encoder.prependBlock(m_signature.getInfo());

  // Content
  totalLength += encoder.prependBlock(getContent());

  // MetaInfo
  totalLength += getMetaInfo().wireEncode(encoder);

  // Name
  totalLength += getName().wireEncode(encoder);

  if (!wantUnsignedPortionOnly) {
    totalLength += encoder.prependVarNumber(totalLength);
    totalLength += encoder.prependVarNumber(tlv::Data);
  }
  return totalLength;
}

解码函数、判断包的类型

首先,回顾我们的问题

节点收到一个包,一般是根据第一个Type字段判断包的类型,Type=5是兴趣包,Type=6是数据包,然后执行对应的TLV解码和转发操作。请找一下,这段“判断包类型”的代码在那个文件里?

详见ndnSIM学习(九)——从consumer发兴趣包到producer返回data包的全过程,收到包判断是在 GenericLinkService::decodeNetPacket 函数。物理层收到包后

答案是:见 ~/ndnSIM/ns-3/src/ndnSIM/ndn-cxx/ndn-cxx/face.cpp 第273~307行的函数 Face::onReceiveElement ,其中第282行的 switch (netPacket.type()) 就是“判断包类型”的代码——

void
Face::onReceiveElement(const Block& blockFromDaemon)
{
  lp::Packet lpPacket(blockFromDaemon); // bare Interest/Data is a valid lp::Packet,
                                        // no need to distinguish

  Buffer::const_iterator begin, end;
  std::tie(begin, end) = lpPacket.get<lp::FragmentField>();
  Block netPacket(&*begin, std::distance(begin, end));
  switch (netPacket.type()) {
    case tlv::Interest: {
      auto interest = make_shared<Interest>(netPacket);
      if (lpPacket.has<lp::NackField>()) {
        auto nack = make_shared<lp::Nack>(std::move(*interest));
        nack->setHeader(lpPacket.get<lp::NackField>());
        extractLpLocalFields(*nack, lpPacket);
        NDN_LOG_DEBUG(">N " << nack->getInterest() << '~' << nack->getHeader().getReason());
        m_impl->nackPendingInterests(*nack);
      }
      else {
        extractLpLocalFields(*interest, lpPacket);
        NDN_LOG_DEBUG(">I " << *interest);
        m_impl->processIncomingInterest(std::move(interest));
      }
      break;
    }
    case tlv::Data: {
      auto data = make_shared<Data>(netPacket);
      extractLpLocalFields(*data, lpPacket);
      NDN_LOG_DEBUG(">D " << data->getName());
      m_impl->satisfyPendingInterests(*data);
      break;
    }
  }
}

具体来说,以一个interest包为例。假设我们现在收到了一个 Block 类型的包,此时触发了函数 Face::onReceiveElement(const Block& blockFromDaemon) 。该函数会看这个 Blocktype ,即 switch (netPacket.type())

如果 case tlv::Interest: ,那么就会执行 auto interest = make_shared<Interest>(netPacket) ,也就是说将这个包强制转化为 Interest 类型,这其实是调用了 Interest 的构造函数

Interest::Interest(const Block& wire)
{
  wireDecode(wire);
}

同样的道理, case tlv::Data: 也为转化为 Data 类型。具体调用关系如下图所示——

文件路径'~/ndnSIM/ns-3/build/ns3/ndnSIM/ndn-cxx/'
第185~320行
第121~192行
第192行
第132行
./ndn-cxx/encoding/block.cpp第324~350行
第336行
./encoding/tlv.hpp第437~447行
./ndn-cxx/interest.cpp
./ndn-cxx/data.cpp
Interest::wireDecode()
Data::wireDecode()
m_wire.parse()
Block::parse()
type = tlv::readType(pos, end)
readType函数,用于读取数据包的Type

这里由于时间关系,不跟大家细讲代码了,这里主要捋一捋整体框架性的思路。

  • Face::onReceiveElementBlock 类型的内容,通过其 type 解码判别出它是 Interest 类还是 Data 类,从而将 Block 转化为对应的类型
  • 转化为对应类型后,由对应的构造函数执行 wireDecode 函数。
  • 每个类的 wireDecode 函数都配备有 m_wire.parse() 函数,对 Block 类的 m_wire 进行参数解析
  • 参数解析方式基于前面的非负整数编码规则TLV编码data包和interest包的编码规则的前置知识,对于 Block 类型执行对应的解码。具体不想细讲了,前面已经讲得很细了。

这里的 readType 函数其实就是按照非负整数编码规则进行解码,这里我对于 readType 函数的一些坑点提示一下,以免大家重复踩坑。

其中 readType 函数调用了 readVarNumber

template<typename Iterator>
uint32_t
readType(Iterator& begin, Iterator end)
{
  uint64_t type = readVarNumber(begin, end);
  if (type == Invalid || type > std::numeric_limits<uint32_t>::max()) {
    NDN_THROW(Error("Illegal TLV-TYPE " + to_string(type)));
  }

  return static_cast<uint32_t>(type);
}

我们以 ReadNumberSlow 函数为例,讲解一下大致流程。注意begin参数传的是引用,执行完后,begin会指向下一个字节,也就是说这个函数是有副作用的,一定要注意

template<typename Iterator>
class ReadNumberSlow
{
public:
  constexpr bool
  operator()(size_t size, Iterator& begin, Iterator end, uint64_t& number) const noexcept
  {
    number = 0;
    size_t count = 0;
    for (; begin != end && count < size; ++begin, ++count) {
      number = (number << 8) | *begin;
    }
    return count == size;
  }
};

??

完蛋,忘了记录了,两天后全忘了,先把残存的遗骸保留下来吧

  • ~/ndnSIM/ns-3/src/ndnSIM/NFD/daemon/fw/forwarder.cpp 的479-521行 Forwarder::onOutgoingNack 函数的第519行调用了 Face::sendNack 函数
  • ???
  • ~/ndnSIM/ns-3/src/ndnSIM/NFD/daemon/face/link-service.cpp 的103-111行 LinkService::receiveData
  • ???

  1. https://named-data.net/doc/NDN-packet-spec/current/tlv.html#non-negative-integer-encoding ↩︎

  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 华为公司是一家全球知名的通信技术解决方案供应商,也是世界领先的设备制造商之一。在华为的通信设备中,使用了一种称为TLV(Type-Length-Value)的编码格式来表示各种信息。TLV编码是一种二进制编码格式,通常用于在通信协议中传输和存储结构化数据。 在Java中,我们可以使用以下步骤来解析和解码TLV编码。 首先,我们需要定义TLV的数据结构。每个TLV项由三个部分组成:Type(标识符)、Length(长度)和Value(值)。这些部分在TLV编码中是按照一定的规则依次排列的。 接下来,我们需要读取二进制TLV数据,并按照TLV格式解析。可以使用Java的字节流和位操作来实现这一步骤。首先,我们读取TLV的Type字段,确定接下来需要读取的数值类型。然后,根据Type字段的值,读取Length字段,确定Value字段的长度。最后,根据Length字段的值,读取对应长度的Value数据。 最后,我们将解码后的TLV数据进行处理和使用。根据业务需求,我们可以根据Type字段的值来判断TLV项的含义,并对Value字段进行相应的处理和解析。 在TLV编码解码过程中,我们需要注意一些细节。例如,长度字段可能是固定长度的,也可能是可变长度的。在解码过程中,我们需要根据实际情况对长度字段进行解析。此外,在处理Value字段时,我们也需要注意不同类型数据的编码解码规则。 总之,使用Java解码华为设备中的TLV编码需要读取二进制数据、按照Type-Length-Value的格式进行解析,并根据实际需求进行相应的处理。这样,我们就能够有效地解码和处理华为设备中的TLV编码了。 ### 回答2: TLVTag-Length-Value)编码是一种数据编码格式,常用于在通信协议中传输结构化数据。在使用Java解TLV编码解码时,我们可以借助Java的位操作来实现。 首先,我们需要了解TLV的基本结构。TLV由三部分组成: 1. Tag:标识数据的类型,用于区分不同的数据项。 2. Length:表示Value字段的长度,以字节为单位。 3. Value:实际的数据。 为了解码编码TLV数据,我们可以按照以下步骤进行: TLV编码: 1. 定义数据结构,Tag、Length和Value字段。 2. 将数据结构中的Tag、Length和Value字段依次写入字节数组中。 TLV解码: 1. 从字节数组中读取Tag字段,并解析得到标识类型。 2. 从字节数组中读取Length字段,并解析得到Value长度。 3. 从字节数组中读取Value字段,并解析得到实际数据。 在具体的TLV编码解码中,我们可以使用Java的ByteArrayInputStream和ByteArrayOutputStream等类来进行字节操作,根据具体的协议规范和TLV的结构进行解析和构造。 以华为为例,可以参考华为的通信协议文档,了解具体的TLV编码解码规则,并根据解码的需求,使用Java中的相关类库和算法来实现。可以使用Java的位操作类来快速读取和写入字节,通过循环和条件判断等控制结构来处理TLV编码解码的逻辑。 总之,使用Java解TLV编码解码能够有效地处理结构化数据,提高数据传输的效率和可靠性。 ### 回答3: TLVTag-Length-Value)是一种常见的数据编码格式,常用于在通信协议中传输结构化数据。使用Java语言解析TLV编码,可以通过以下步骤进行: 1. 定义TLV结构体:TLV编码含标签(Tag)、长度(Length)和值(Value)三个部分。可以定义一个TLV类,含这三个属性,并提供对应的读取和设置方法。 2. 解码TLV编码:将收到的TLV编码字节数组进行解析。首先读取字节数组的第一个字节,该字节表示Tag的值。接着读取接下来的1~4个字节,表示Length的值。最后按照Length的值读取对应长度的字节,表示Value的值。将这些值赋给对应的TLV对象的属性。 3. 编码TLV数据:将TLV对象转成TLV编码字节数组。首先将Tag的值转成一个字节,接着根据Value的长度计算出Length的字节数组,然后将Tag字节、Length字节数组和Value字节数组按顺序合并,即得到TLV编码字节数组。 对于解码华为的TLV编码,需要根据具体需求和协议定义Tag的含义,以及对应的Value值的解释。然后根据Tag值进行相应的解析和处理,将Value值转换为对应的数据类型编码时,根据要发送的数据类型,将数据转换为对应的字节数组,并使用TLV编码格式进行封装。 使用Java语言解析TLV编码解码华为的TLV编码需要注意字节序(Little Endian或Big Endian)等相关细节,请根据具体需求进行相应的处理。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值