1 什么是GUID
在百度百科里解释如下:
全局唯一标识符(GUID,Globally Unique Identifier)是一种由算法生成的二进制长度为128位的数字标识符。GUID主要用于在拥有多个节点、多台计算机的网络或系统中。在理想情况下,任何计算机和计算机集群都不会生成两个相同的GUID。GUID 的总数达到了2^128(3.4×10^38)个,所以随机生成两个相同GUID的可能性非常小,但并不为0。所以,用于生成GUID的算法通常都加入了非随机的参数(如时间),以保证这种重复的情况不会发生。
GUID的格式为“xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx”(即8-4-4-4-12的结构);其中每个x是0-9或a-f范围内的一个十六进制数。例如:6F9619FF-8B86-D011-B42D-00C04FC964FF 即为有效的 GUID 值。
2 GUID的特点
1、需要GUID的时候,可以完全由算法自动生成,不需要一个权威机构来管理。
2、GUID理论上能产生全宇宙唯一的值,对于以后的数据导入很方便。
3 GUID在BIM中的价值
对与BIM(Building Information Modeling,建筑信息模型化)来说,IFC更有其特殊意义。在IFC(Industry Foundation Classes,工业基础类)中作为对象的唯一标志值,无论是核心层、核心层、共享层还是专业领域层,都被广泛使用。在我们国家标准《工业基础类平台规范》(GB/T 25507——2010)附录A中专门提供了《全球唯一标识C语言代码》。
但遗憾的是,该附录并未给出编程的逻辑思路,给出的代码其实也并不清晰(个人认为),可读性不高。另外,里边生成GUID的核心函数是Windows系统的CoCreateGuid函数(在系统DLL库ole32中),如果用这个代码生成,就直接限制了程序跨平台的能力。
因此,我尝试用Racket语言来实现GUID的程序,同时介绍实现思路,希望能达到抛砖引玉的效果。
4 简单生成GUID
这里我们将GUID的每一段都通过随机生成来实现,由于生成的随机性很大,总数达到了3.4×10^38个,相同的几率非常小。听说有人查看Win的源代码,那个神奇的函数的每段都是随机函数生成的(^-^)。
Racket里生成随机数的常见函数为"random",它返回一个范围为0~k-1的随机精确整数。
不过还有一个函数——"crypto-random-bytes",它在这里更适合使用。它返回n个随机字节。在Unix系统上,字节是从“/dev/urandom”获得,而在Windows上则使用RtlGenRand系统函数获得。取得随机数后,通过"bytes->hex-string"(来自于库“file/sha1”,因此需要加上需求(require file/sha1))直接转换成16进制字符串。
这个思路很直接,实现起来简单。我们直接上代码:
(define (create-guid-string-by-random) (string-append (bytes->hex-string (crypto-random-bytes 4)) "-" (bytes->hex-string (crypto-random-bytes 2)) "-" (bytes->hex-string (crypto-random-bytes 2)) "-" (bytes->hex-string (crypto-random-bytes 2)) "-" (bytes->hex-string (crypto-random-bytes 6))))
前面说过,这种方式随机生成两个相同GUID的可能性非常小,但并不为0。但并不为0!(估计遇到相同的时候一定是前无古人的巨额大奖)。但是为防万一,这个也是要避免的,毕竟在很多场景下,即使出现一次,造成的问题也是无法想象的。
5 更可靠的生成方案
为了进一步减少重复的可能性,我们将随机的范围缩小。这里从两方面来实现这一点:
1、限定特定时间:在特定时间里去生成随机数,即使生成了相同的随机数,但加上时间限制,也是不同的。
特定时间有日期(date)和时间(time)两个方面。我们可以将其分别嵌入到GUID中。
2、限定程序运行的主机相关信息。在不同的机器上即使生成了相同随机数,也是不同的。主机相关信息有:主板标记信息、CMOS标记信息、芯片标记信息、IP地址、MAC地址、主机名称标记。另外还有一种,就是程序的进程标志。我们这里就用这个来生成。
由此,我们将日期的年月日、时间、程序的进程标志号分别转换成16进制字符串嵌入到GUID里,剩下的通过随机数生成。具体为:将年转换为第一段8位,月日转换为第二段4位,时、分、秒转换为第三段4位,进程标志转换为第四段4位,第五段12位全部由随机生成。
5.1 用日期时间生成数据(第一、二、三段)
Racket的"current-date"函数取得当前系统时间(时间分分秒秒向前走,不停地创造新数据),它返回一个时间结构值。
时间结构的定义如下:
(struct date (second minute hour day month year week-day year-day dst? time-zone-offset))
由此可以看出,我们可以直接从取得的时间结构值里取得我们想要的数据,根据上边的思路转换成第一至三段的内容。如下:
(define (guid-data-by-date) (let* ([dt (current-date)] [y (date-year dt)] [ys (~a y #:width 4)] [m (date-month dt)] [d (date-day dt)] [h (date-hour dt)] [min (date-minute dt)] [sec (date-second dt)]) (string-append (bytes->hex-string (string->bytes/locale ys)) "-" (bytes->hex-string (bytes m d)) "-" (bytes->hex-string (bytes (+ min h) (+ sec h))))))
以上我们用Racket函数"bytes->hex-string"将字节转换为16进制字符串(每个字节转换为2位)。"bytes"函数用于返回新的可变字节字符串,"string-append"将给定的字符串连接起来。
这里有两个地方需要说明:
-
一是对年的处理。由于年的十进制位数长度不是定长的,这里统一转换(限定)为4位(在9999年之后会出现与之前重复的年数值)。
-
二是对时间的时的处理。为了将时、分、秒限定在4位16进制数里,这里在分、秒的数值上加上时的数值,这样生成2个数,转换出来则为4位16进制数。
5.2 用进程标志生成数据(第四段)
Racket的"getpid"函数取得一个标识系统当前进程的整数值。我们将这个整数值分成两部分,每一部分转换成2位16进制数的字符串即可。如下:
(define (guid-data-by-process) (let ([pid (getpid)]) (bytes->hex-string (bytes (quotient pid 100) (remainder pid 100)))))
5.3 用随机数生成数据(第五段)
同上。如下:
(define (guid-data-by-random) (bytes->hex-string (crypto-random-bytes 6)))
最后,将以上生成的各段16进制字符串连起来就生成了GUID的字符串码:
(define (create-guid-string) (string-append (guid-data-by-date) "-" (guid-data-by-process) "-" (guid-data-by-random)))
在Windows中,GUID的字符串表示形式实际上还包括两边的大括号(“{}”)。我们可以通过"string->guid"函数将GUID字符串转换成GUID值,也可以通过"guid->string"函数将GUID值转换为GUID字符串。这两个函数需要"(require ffi/com)"支持。
(以上内容由Racket语言的Scribble编辑生成)