马上注册,结交更多好友,享用更多功能^_^
您需要 登录 才可以下载或查看,没有账号?立即注册
x
本帖最后由 H9enRy 于 2014-8-9 00:43 编辑
三、楼主好人,跪求种子 那么怎么可以使得每次运行程序的时候都生成不同的"随机数序列"呢?因为我们每次执行程序时候的时间很可能不一样,因此我们可以用当前时间做"随机数种子" [backcolor=white !important][size=1em][size=1em]1
[size=1em]2
[size=1em]3
[size=1em]4
[size=1em]5
| [size=1em][size=1em]MyRand rand = newMyRand(Environment.TickCount);
[size=1em]for (int i = 0; i < 10; i++)
[size=1em] {
[size=1em] Console.WriteLine(rand.Next());
[size=1em] }
|
Environment.TickCount为"系统启动后经过的微秒数"。这样每次程序运行的时候Environment.TickCount都不大可能一样(靠手动谁能一微秒内启动两次程序呢),所以每次生成的随机数就不一样了。 当然如果我们把new MyRand(Environment.TickCount)放到for循环中: [backcolor=white !important][size=1em][size=1em]1
[size=1em]2
[size=1em]3
[size=1em]4
[size=1em]5
| [size=1em][size=1em]for (int i = 0; i < 100; i++)
[size=1em] {
[size=1em] MyRand rand = newMyRand(Environment.TickCount);
[size=1em] Console.WriteLine(rand.Next());
[size=1em] }
|
运行结果又变成"很多是连续"的了,原理很简单:由于for循环体执行很快,所以每次循环的时候Environment.TickCount很可能还和上次一样(两行简单的代码运行用不了一毫秒那么长事件),由于这次的"随机数种子"和上次的"随机数种子"一样,这样Next()生成的第一个"随机数"就一样了。从"-320"变成"-856"是因为运行到"-856"的时候时间过了一毫秒。 四、各语言的实现 我们看到.Net的Random类有一个int类型参数的构造函数: public Random(int Seed) 就是和我们写的MyRand一样接受一个"随机数种子"。而我们之前调用的无参构造函数就是给Random(int Seed)传递Environment.TickCount类进行构造的,代码如下: [backcolor=white !important][size=1em][size=1em]1
[size=1em]2
[size=1em]3
| [size=1em][size=1em]public Random() : this(Environment.TickCount)
[size=1em]{
[size=1em]}
|
这下我们终于明白最开始的疑惑了。 同样道理,在C/C++中生成10个随机数不应该如下调用: [backcolor=white !important][size=1em][size=1em]1
[size=1em]2
[size=1em]3
[size=1em]4
[size=1em]5
[size=1em]6
| [size=1em][size=1em]int i;
[size=1em]for(i=0;i<10;i++)
[size=1em]{
[size=1em] srand( (unsigned)time( NULL ) );
[size=1em] printf("%d\n",rand());
[size=1em]}
|
而应该: [backcolor=white !important][size=1em][size=1em]1
[size=1em]2
[size=1em]3
[size=1em]4
[size=1em]5
[size=1em]6
| [size=1em][size=1em]srand( (unsigned)time( NULL ) ); //把当前时间设置为"随机数种子"
[size=1em]int i;
[size=1em]for(i=0;i<10;i++)
[size=1em]{
[size=1em] printf("%d\n",rand());
[size=1em]}
|
五、"奇葩"的Java Java学习者可能会提出问题了,在Java低版本中,如下使用会像.Net、C/C++中一样产生相同的随机数: [backcolor=white !important][size=1em][size=1em]1
[size=1em]2
[size=1em]3
[size=1em]4
[size=1em]5
| [size=1em][size=1em]for(int i=0;i<100;i++)
[size=1em]{
[size=1em] Random rand = new Random();
[size=1em] System.out.println(rand.nextInt());
[size=1em]}
|
因为低版本Java中Rand类的无参构造函数的实现同样是用当前时间做种子: public Random() { this(System.currentTimeMillis()); } 但是在高版本的Java中,比如Java1.8中,上面的"错误"代码执行却是没问题的: 为什么呢?我们来看一下这个Random无参构造函数的实现代码: [backcolor=white !important][size=1em][size=1em]1
[size=1em]2
[size=1em]3
[size=1em]4
[size=1em]5
[size=1em]6
[size=1em]7
[size=1em]8
[size=1em]9
[size=1em]10
[size=1em]11
[size=1em]12
[size=1em]13
[size=1em]14
[size=1em]15
| [size=1em][size=1em]public Random()
[size=1em]{
[size=1em]this(seedUniquifier() ^ System.nanoTime());
[size=1em]} <br>
[size=1em]private static long seedUniquifier() {
[size=1em]for (;;) {
[size=1em]long current = seedUniquifier.get();
[size=1em]long next = current * 181783497276652981L;
[size=1em]if (seedUniquifier.compareAndSet(current, next))
[size=1em]return next;
[size=1em] }
[size=1em]
[size=1em] }
[size=1em]
[size=1em]privatestaticfinal AtomicLong seedUniquifier = new AtomicLong(8682522807148012L);
|
这里不再是使用当前时间来做"随机数种子",而是使用System.nanoTime()这个纳秒级的时间量并且和采用原子量AtomicLong根据上次调用构造函数算出来的一个数做异或运算。关于这段代码的解释详细参考这篇文章《解密随机数生成器(2)——从java源码看线性同余算法》 最核心的地方就在于使用static变量AtomicLong来记录每次调用Random构造函数时使用的种子,下次再调用Random构造函数的时候避免和上次一样。 六、高并发系统中的问题 前面我们分析了,对于使用系统时间做"随机数种子"的随机数生成器,如果要产生多个随机数,那么一定要共享一个"随机数种子"才会避免生成的随机数短时间之内生成重复的随机数。但是在一些高并发的系统中一个不注意还会产生问题,比如一个网站在服务器端通过下面的方法生成验证码: [backcolor=white !important][size=1em][size=1em]1
[size=1em]2
[size=1em]3
| [size=1em][size=1em]Random rand = new Random();
[size=1em]Int code = rand.Next();
|
当网站并发量很大的时候,可能一个毫秒内会有很多个人请求验证码,这就会造成这几个人请求到的验证码是重复的,会给系统带来潜在的漏洞。 再比如我今天看到的一篇文章《当随机不够随机:一个在线扑克游戏的教训》里面就提到了"由于随机数产生器的种子是基于服务器时钟的,Hacker们只要将他们的程序与服务器时钟同步就能够将可能出现的乱序减少到只有 200,000 种。到那个时候一旦Hacker知道 5 张牌,他就可以实时的对 200,000 种可能的乱序进行快速搜索,找到游戏中的那种。所以一旦Hacker知道手中的两张牌和 3 张公用牌,就可以猜出转牌和河牌时会来什么牌,以及其他玩家的牌。" 这种情况有如下几种解决方法: 把Random对象作为一个全局实例(static)来使用。Java中Random是线程安全的(内部进行了加锁处理);.Net中Random不是线程安全的,需要加锁处理。不过加锁会存在会造成处理速度慢的问题。而且由于初始的种子是确定的,所以攻击者存在着根据得到的若干随机数序列推测出"随机数种子"的可能性。 因为每次生成Guid的值都不样,网上有的文章说可以创建一个Guid计算它的HashCode或者MD5值的方式来做种子: new Random(Guid.NewGuid().GetHashCode()) 。但是我认为Guid的生成算法是确定的,在条件充足的情况下也是可以预测的,这样生成的随机数也有可预测的可能性。当然只是我的猜测,没经过理论的证明。 采用"真随机数发生器",快看下一节分解!
七、真随机数发生器 根据我们之前的分析,我们知道这些所谓的随机数不是真的"随机",只是看起来随机,因此被称为"伪随机算法"。在一些对随机要求高的场合会使用一些物理硬件采集物理噪声、宇宙射线、量子衰变等现实生活中的真正随机的物理参数来产生真正的随机数。 当然也有聪明的人想到了不借助增加"随机数发生器"硬件的方法生成随机数。我们操作计算机时候鼠标的移动、敲击键盘的行为都是不可预测的,外界命令计算机什么时候要执行什么进程、处理什么文件、加载什么数据等也是不可预测的,因此导致的CPU运算速度、硬盘读写行为、内存占用情况的变化也是不可预测的。因此如果采集这些信息来作为随机数种子,那么生成的随机数就是不可预测的了。 在Linux/Unix下可以使用"/dev/random"这个真随机数发生器,它的数据主来来自于硬件中断信息,不过产生随机数的速度比较慢。 Windows下可以调用系统的CryptGenRandom()函数,它主要依据当前进程Id、当前线程Id、系统启动后的TickCount、当前时间、QueryPerformanceCounter返回的高性能计数器值、用户名、计算机名、CPU计数器的值等等来计算。和"/dev/random"一样CryptGenRandom()的生成速度也比较慢,而且消耗比较大的系统资源。 当然.Net下也可以使用RNGCryptoServiceProvider 类(System.Security.Cryptography命名空间下)来生成真随机数,根据StackOverflow上一篇帖子介绍RNGCryptoServiceProvider 并不是对CryptGenRandom()函数的封装,但是和CryptGenRandom()原理类似。 八、总结 有人可能会问:既然有"/dev/random" 、CryptGenRandom()这样的"真随机数发生器",为什么还要提供、使用伪随机数这样的"假货"?因为前面提到了"/dev/random" 、CryptGenRandom()生成速度慢而且比较消耗性能。在对随机数的不可预测性要求低的场合,使用伪随机数算法即可,因为性能比较高。对于随机数的不可预测性要求高的场合就要使用真随机数发生器,真随机数发生器硬件设备需要考虑成本问题,而"/dev/random"、CryptGenRandom()则性能较差。 万事万物都没有完美的,没有绝对的好,也没有绝对的坏,这才是多元世界美好的地方。 |