关于UUID的简单研究
1.基本介绍
UUID(Universally Unique Identifier),中文名为“通用唯一识别码”,目的是让分布式系统中的所有元素都能有唯一的辨识信息,而不需要通过中央控制端来做辨识信息的指定,这样每个人都可以创建不与其它人冲突的UUID。RFC 4122文档规定了规范版本的UUID,另外还有微软使用的GUID等。本文将研究RFC 4122文档规定的UUID。
2.RFC文档规定的UUID格式以及版本
RFC文档规定,UUID是一个32位的16进制数,长度为16字节(128 bit),一般表示为{hhhhhhhh-hhhh-Mhhh-Nhhh-hhhhhhhhhhhh}即{8-4-4-4-12}的标准格式。十六进制位M表示UUID版本,在RFC规范下M可选值为1,2,3,45;十六进制N表示UUID变体,该位固定为10xx,因此可选值为8,9,a,b。RFC 4122规定了5个版本的UUID实现算法,即V1,V2,V3,V4,V5。它们各自的特点如下:
UUID V1:基于时间的UUID
基于时间的UUID通过计算当前时间戳、随机数和机器MAC地址得到。由于在算法中使用了MAC地址,这个版本的UUID可以保证在全球范围的唯一性。但与此同时,使用MAC地址会带来安全性问题,这就是这个版本UUID受到批评的地方。如果应用只是在局域网中使用,也可以使用退化的算法,以IP地址来代替MAC地址。
UUID V2:DCE安全的UUID
DCE(Distributed Computing Environment)安全的UUID和基于时间的UUID V1算法相同,但会把时间戳的前4位置换为POSIX的UID或GID。这个版本的UUID在实际中较少用到。
UUID V3:基于名字空间与给定字符串构造的UUID(MD5)
通过计算名字空间与给定字符串的MD5散列值得到,其中名字空间是由标准规定的一个16字节值。这个版本的UUID保证了:相同名字空间中不同字符串生成的UUID的唯一性;不同名字空间中的UUID的唯一性;相同名字空间中相同字符串的UUID重复生成是相同的。
UUID V4:随机UUID
根据随机数,或者伪随机数生成UUID。
UUID V5:基于名字空间与给定字符串构造的UUID(SHA1)
和UUID V3算法类似,只是散列值计算使用SHA1算法。
除此之外,还有一个特殊种类的UUID:Nil UUID,各位均为0,即{00000000-0000-0000-0000000000000000}。
3.MD5和SHA1
3.1 MD5
MD5信息摘要算法(英语:MD5 Message-Digest Algorithm),一种被广泛使用的密码散列函数,用于确保信息传输完整一致。这套算法的程序在 RFC 1321 标准中被加以规范。MD5算法将输入的信息每512位(64字节)分为一组,特别地,最后一组为448位数据内容+64位数据长度信息,不足的部分按一定规则填充;经过一系列的处理,算法的输出由四个32位分组组成,将这四个32位分组级联后将生成一个128位(16字节)的MD5散列值,这个散列值也称为摘要(digest)。
在代码实现层面,MD5算法实现为LMd5类,有以下数据成员:
1 | /** |
需要两个辅助函数:
1 | /** |
算法具体的功能实现分为三个步骤:
第一步是初始化(Init),按照标准规定对散列值的状态进行初始化:
1 | void LMd5::init() |
第二步是算法运行(Update),处理输入数据并运行MD5算法,需要注意的是,该步骤会以输入数据流的形式处理所有的输入数据:在Update步骤中可能会多次接收输入数据,而该对象会将这些数据作为同一组数据流来看待。例如,在该步骤中先后输入了长度分别为524bit,1036bit的数据,则它们等同于一组长度为1560bit的数据,其内容按照不同输入数据的前后次序无缝拼接。在同一个对象中,上述Update步骤会一直到后面的Final步骤才会结束:
1 | void LMd5::update(const unsigned char *data, std::size_t size) |
第三步是得到最终结果(Final),获取最终的MD5散列结果,并重置本对象。此步骤同时标志着Update步骤中输入数据流的终止,若再输入数据时将会重新开始输入数据流与算法运行过程。
1 | void LMd5::final(unsigned char *result) |
在具体算法上,MD5标准首先规定了以下几种形式的位运算:
1 | /** |
transform函数运算获得每个数据块block的散列值,block的长度按照标准须为64字节(512位)。
1 | void LMd5::transform(const unsigned char block[64]) |
3.2 SHA1
SHA1(英语:Secure Hash Algorithm 1,中文名:安全散列算法1)是一种密码散列函数,美国国家安全局设计,并由美国国家标准技术研究所(NIST)发布为联邦数据处理标准(FIPS)。SHA-1可以生成一个的160位(20字节)散列值,散列值通常的呈现形式为40个十六进制数,这个散列值也称为摘要(digest)。SHA1算法对输入数据采用与MD5相同的方式进行分组,不过分组之后SHA1算法会按照一定规则将每组数据扩展为80个32字节的小分组数据。
在代码实现层面,SHA1算法实现为LSha1类,有以下数据成员:
1 | /** |
同时需要RotateLeft和encode辅助函数,与LMd5当中的对应函数相同。
算法具体的功能实现分为三个步骤:
第一步是初始化(Init),按照标准规定对散列值的状态进行初始化:
1 | void LSha1::init() |
第二步是算法运行(Update),处理输入数据并运行SHA1算法,需要注意的是,该步骤会以输入数据流的形式处理所有的输入数据:在Update步骤中可能会多次接收输入数据,而该对象会将这些数据作为同一组数据流来看待。例如,在该步骤中先后输入了长度分别为524bit,1036bit的数据,则它们等同于一组长度为1560bit的数据,其内容按照不同输入数据的前后次序无缝拼接,这与MD5中的做法一致。在同一个对象中,上述Update步骤会一直到后面的Final步骤才会结束:
1 | void LSha1::update(const unsigned char *data, std::size_t size) |
第三步是得到最终结果(Final),获取最终的SHA1散列结果,并重置本对象。此步骤同时标志着Update步骤中输入数据流的终止,若再输入数据时将会重新开始输入数据流与算法运行过程。
1 | void LSha1::final(unsigned char *result) |
transform函数运算获得每个数据块block的散列值,block的长度按照标准须为64字节(512位)。
1 | void LSha1::transform(const unsigned char block[64]) |
4.UUID模块的简单实现
下文将重点讲述LUuid类对V3、V4、V5版本的UUID生成的方式,以及UUID模块的其他重要功能。
4.1 LUuid类
LUuid类具有以下数据成员和枚举:
1 | /** |
V3/V5版本的UUID实现如下。其中,V5版本使用的SHA1算法会生成20字节的散列值,但是UUID只取前16个字节。
1 | void uuidV3(unsigned char result[16], unsigned char nsid[16], const LString &name) |
V4版本的UUID实现如下:
1 | void uuidV4(unsigned char result[16]) |
V4版本的UUID不使用经典C的rand函数,而使用了C++的random标准库中的方法进行实现。
4.2 重要功能——fromString
1 | bool LUuid::fromString(const LString &str) |
该函数将符合特定格式的字符串转换为Uuid,并将其保存在当前的LUuid对象中。
该函数的str参数输入的字符串格式应当能够匹配下列正则表达式:
^({)?([0-9a-fA-F]{8})(?-)?([0-9a-fA-F]{4})(?(DASH)-)([0-9a-fA-F]{4})(?(DASH)-)([0-9a-fA-F]{4})(?(DASH)-)([0-9a-fA-F]{12})(?(1)})$
若非法输入会返回false,同时本对象中保存的UUID不会被改变。
具体来说,该方法在字符串转换为Uuid的过程中为以下技术问题提供了一个方案:
(1)如何验证输入的字符串的长度与有效的十六进制位数合法?输入字符串的长度可能因为大括号、破折号等出现不确定,但是转换成的UUID长度总是确定的,即16个字节。所以,在读取字符串内容时,设置两个计数器i和j,其中i用于读取字符串的内容,而j则用于转换后的UUID当中对所在十六进制位的计数,在验证输入字符串的长度与有效位数时,我们不关心i如何,而是对j进行验证,因为j最后所达到的值是确定的。
(2)如何验证输入的字符串的格式合法?这里的问题与解决方案的思想都与(1)类似:输入字符串中的大括号、破折号的位置可能因为具体格式的不同出现不确定,但是UUID当中在哪些十六进制位的旁边出现的大括号和破折号可以视为合法,这是确定的!涉及到UUID当中十六进制位的位置计数,同样可以采用(1)中的两个计数器的方式,i读取字符串的内容,验证时不去关心,而是对j进行验证——例如,假设该字符串中有破折号,那么在读取到时,便设置一个布尔值“hasDashes”为真;每当j累加到“允许出现”破折号的位置,同时“hasDashes”为真,那么便会检查字符串中当前对应的位置是否为破折号,当然,这里就用到i来获取字符串中的对应位置字符了。