本章主要介绍NS-3当中的哈希函数的使用。NS-3提供了一个使用哈希函数的通用的接口。可以方便地调用内置的哈希函数来将一个字节数组或者字符串转换成一个哈希值。根据底层算法的不同,哈希值可以是32位或者是64位的整数。NS-3默认使用的哈希算法是murmur3,它可以返回32和64位的值。可以轻易地使用其他哈希算法实现。如果NS-3所提供的哈希实现都无法达到要求,也可以方便地添加自己的实现。
1. 哈希函数简介
Hash,一般翻译做“散列”,也有直接音译为“哈希”的,就是把任意长度的输入(又叫做预映射pre-image)通过散列算法变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来确定唯一的输入值。
所有哈希函数都有如下一个基本特性:如果两个同一哈希算法生成的值是不相同的,那么这两个散列值的原始输入也是不相同的。这个特性是散列函数具有确定性的结果。但另一方面,散列函数的输入和输出不是一一对应的,如果两个散列值相同,两个输入值很可能是相同的,但不绝对肯定二者一定相等(即可能出现哈希碰撞)。输入一些数据计算出散列值,然后部分改变输入值,一个具有强混淆特性的散列函数会产生一个完全不同的散列值。
哈希函数在不同的领域具有广泛的应用:
- 数据校验 (例如网络数据包当中常用的CRC检验,文件的MD5或SHA-1校验)
- 数字签名 (为了提高认证速度,RSA当中只对摘要部分签名)
- 高速数据查询 (例如Openflow当中的流表查询,Java当中的HashMap查询)
- 网络负载均衡 (例如ECMP协议实现的负载均衡)
- 数据缓存 (例如Redis分布式缓存系统)
- ……
值得注意的是,由于哈希函数的应用的多样性,它们经常是专为某一应用而设计的。例如,加密散列函数假设存在一个要找到具有相同散列值的原始输入的敌人。一个设计优秀的加密散列函数是一个“单向”操作:对于给定的散列值,没有实用的方法可以计算出一个原始输入,也就是说很难伪造。为加密散列为目的设计的函数,如MD5,被广泛的用作检验散列函数。这样软件下载的时候,就会对照验证代码之后才下载正确的文件部分。此代码有可能因为环境因素的变化,如机器配置或者IP地址的改变而有变动。以保证源文件的安全性。对于用于负载均衡的哈希应用,设计的目标就是希望在大量的数据出现的情况下,哈希值尽量呈现均匀分布的特性。
2. NS-3哈希算法的使用
2.1. 简单的用法
在NS-3当中,如果只是简单的想使用哈希算法得到哈希值,那么最简单的方法就是直接调用封装好的哈希函数即可。首先我们自己要明白需要32位哈希值还是64位哈希值。确定好之后才能选择调用不同的版本。NS-3当中为32位版本和64位版本分别提供了两个重载:
1 | uint32_t Hash32 (const char * buffer, const std::size_t size); |
一个版本接受一个字节数组(以及需要编码的部分的长度),另外一个版本接受一个字符串作为参数。最终返回32位或者64位整数。下面通过一个简单的例子来演示最简单的哈希函数的使用:
1 |
|
运行程序,得到如下结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
程序最后输出了32位和64位版本的哈希值,可见,在NS-3当中如果只是想简单地获取哈希值,用法是非常方便。
继续查看代码:
1 | Hasher::Hasher () |
从代码中可以看出,默认的哈希实现算法是murmur3。这个算法运算速度非常快,但是碰撞率比较低,其最新版本是3,是作者加入Google之后对murmur2的改进。其实现原理在这里。
2.2. 进阶版用法
哈希函数的简单用法的问题在于只能使用默认的murmur3算法,虽然这种算法已经很好了,但是正如前面所说的,不同的应用领域对哈希算法的需求是不一样的。因此,我们更希望能够自己指定哈希算法。
实际上,从刚刚的源代码分析也不难看出,其最终还是依赖于一个名为Hasher的类。这个类当中提供了两个构造方法:
1 | Hasher::Hasher () |
第一个构造方法默认创建了Murmur3对象。第二个构造函数可以让用户自己传入需要的哈希算法实现类。目前NS-3当中只默认提供了两种实现:一个就是刚刚用过的murmur3算法,另外一个就是FNV-1a算法,其实现原理可以看这里。下面我们通过一个简单的例子来演示如何指定哈希算法:
1 |
|
程序关键点在于创建Hasher的时候通过构造函数参数制定了底层使用的哈希算法。另外一个值得注意的点在于使用这种方法,每次计算全新的数据的时候必须先清空(调用clear()方法),否则会变成和前面的数据累加,结果会不正确。运行程序,得到如下结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
2.3. 分段(增量)哈希
通过上面的例子,我们可以看到,每次要计算新的哈希值时,必须先调用clear()方法,把之前的数据清空。而如果没有清空数据,将会和上次的数据进行累加。这可以实现一种称为增量哈希的方法。你通过一个简单的例子来解释。
1 |
|
程序中,我们将原来要计算哈希值的字符串分成了两段,每次只往hasher里面添加一段,然后再添加另一段。而在计算32位和64位的哈希值时,中间做了清空。运行程序之后,得到如下结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
对比前面的运行结果,我们发现,分段计算的结果和完整计算的结果完全相同,说明他们具有一样的效果。
总结:如果不希望使用增量哈希,那么请习惯每次计算哈希值的时候都清空,养成如下调用哈希算法的习惯:
1 | hasher.clear ().GetHash32 (key) |
3. 添加自定义的哈希算法
不同的应用对哈希算法性能的诉求是不一样的,因此,NS-3的内置哈希算法多半都不能满足需求。我们可以使用满足NS-3哈希算法接口的方式实现自己的哈希算法来满足自己的需求。如果哈希算法比较简单,可以使用一个函数表示,那么可以使用添加哈希函数的方式来实现。如果哈希算法比较复杂,内部必须有复杂的数据传递和方法调用,那么更建议使用添加类的方法来实现。
3.1. 添加哈希函数
如果你自己实现的哈希算法比较简单,完全可以使用一个函数来实现,那么推荐使用添加哈希函数的方法来实现。只需添加一组符合NS-3哈希算法接口的函数即可。下面通过一个简单的例子来演示如何通过函数的方式来添加哈希算法实现。
例如,NS-3当中我们想在NS-3当中添加ELF哈希算法。这是一个Unix和Linux系统内核当中使用的一个哈希算法,由于它主要在Linux的可执行文件格式ELF(Executable and Linking Format)中使用,所以取名为ELF哈希。这个算法中,字符串中每个字符都有同样的作用,它巧妙地对字符的ASCII编码值进行计算,ELF哈希函数对于能够比较均匀地把字符串分布在散列表中。因此,任意修改一个字符对哈希值都有很大影响,符合哈希函数的需求。
实现的算法函数必须符合如下的文件签名:
1 | uint32_t hash_name32(const char * data, const std::size_t size); |
随后可以通过如下方法来创建哈希函数类:
1 | Hasher hasher = Hasher ( Create<Hash::Function::Hash32> (&hash_name32) ); |
然后即可通过hasher来计算哈希值,过程和之前的例子一致。下面我们就开始实现ELFHash算法。
1 |
|
运行程序得到如下结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
3.2. 添加哈希类
如果算法比较复杂,则可以通过类的方式来实现。这个类必须继承ns3::Hash::Implementation类。其中ns3、Hash是名称空间,而Implementation才是类名。然后实现其中的GetHash32()和clear()方法,而GetHash64()方法可以不实现,也可以实现,如果不实现,那么自动返回32位的结果值。
例如,我们需要实现CRC(Cyclic Redundant Checksum)这个算法。可以实现CRC类,然后实现32位或者64位算法。然后即可按照之前的方法创建Hasher类的实例即可。当然这个例子选得不好,即无需增量计算,也没有复杂的内部调用或者中间变量需要在类的内部传递。就凑合着看看吧。按照NS-3的管理,我们还是将哈希算法的类放到ns3::Hash::Function::名称空间下,这样可以避免和其他用途的类重名的风险。代码如下:
1 |
|
运行得到如下结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.29/ns-3.29/build' |
可见,轻松的实现自己的哈希算法。
到本章为止,NS-3网络仿真的基础部分的知识都已经完全介绍完了。这部分的内容都主要集中在core这个模块中,这个模块是所有NS-3功能的基础。从下一章开始主要介绍NS-3网络建模仿真的基本概念,主要涉及core以外的其他模块,包括network、internet、traffic-control、point-to-point、point-to-point-layout、csma、application、internet-apps等模块,当然也可能涉及到其他的模块。