随机数是骗人的,.Net、Java、C为我做证

几乎全部编程语言中都提供了"生成一个随机数"的方法,也就是调用这个方法会生成一个数,咱们事先也不知道它生成什么数。好比在.Net中编写下面的代码:html

Random rand = newRandom(); 
Console.WriteLine(rand.Next()); 

运行后结果以下:java

    Next()方法用来返回一个随机数。一样的代码你执行和个人结果极可能不同,并且我屡次运行的结果也极可能不同,这就是随机数。程序员

1、陷阱 算法

    看似很简单的东西,使用的时候有陷阱。我编写下面的代码想生成100个随机数:数据库

for(int i=0;i<100;i++) 
{ 
    Random rand = new Random(); 
    Console.WriteLine(rand.Next()); 
}

 

    太奇怪了,居然生成的"随机数"有好多连续同样的,这算什么"随机数"呀。有人指点"把new Random()"放到for循环外面就能够了:编程

Random rand = newRandom(); 
for(int i=0;i<100;i++) 
{             
    Console.WriteLine(rand.Next()); 
} 

     运行结果:缓存

    确实能够了! 安全

2、这是为何呢? 服务器

    这要从计算机中"随机数"产生的原理提及了。咱们知道,计算机是很严格的,在肯定的输入条件下,产生的结果是惟一肯定的,不会每次执行的结果不同。那么怎么样用软件实现产生看似不肯定的随机数呢?网络

    生成随机数的算法有不少种,最简单也是最经常使用的就是 "线性同余法":  第n+1个数=(第n个数*29+37) % 1000,其中%是"求余数"运算符。不少像我同样的人见了公式都头疼,我用代码解释一下吧,MyRand是一个自定义的生成随机数的类:

class MyRand
 { 
    private int seed; 
    public MyRand(int seed) 
   { 
    this.seed = seed; 
   } 

  public int Next() 
   { 
     int next = (seed * 29 + 37) % 1000; 
     seed = next; 
     return next; 
  } 

} 

 以下调用:

MyRand rand = newMyRand(51); 
for (int i = 0; i < 10; i++) 
 { 
    Console.WriteLine(rand.Next()); 
 } 

执行结果以下:

生成的数据是否是看起来"随机"了。简单解释一下这个代码:咱们建立MyRand的一个对象,而后构造函数传递一个数51,这个数被赋值给seed,每次调用Next方法的时候根据(seed * 29 + 37) % 1000计算获得一个随机数,把这个随机数赋值给seed,而后把生成的随机数返回。这样下次再调用Next()的时候seed就再也不是51,而是上次生成的随机数了,这样就看起来好像每一次生成的内容都很"随机"了。注意"%1000"取余预算的目的是保证生成的随机数不超过1000。 

固然不管是你运行仍是我每次运行,输出结果都是同样的随机数,由于根据给定的初始数据51,咱们就能够依次推断下来下面生成的全部"随机数"是什么均可以算出来了。这个初始的数据51就被称为"随机数种子",这一系列的51六、一、6六、95一、616……数字被称为"随机数序列"。咱们把51改为52,就会有这样的结果:

3、楼主好人,跪求种子

    那么怎么可使得每次运行程序的时候都生成不一样的"随机数序列"呢?由于咱们每次执行程序时候的时间极可能不同,所以咱们能够用当前时间作"随机数种子"

MyRand rand = newMyRand(Environment.TickCount); 
for (int i = 0; i < 10; i++) 
 { 
    Console.WriteLine(rand.Next()); 
 } 

     Environment.TickCount为"系统启动后通过的微秒数"。这样每次程序运行的时候Environment.TickCount都不大可能同样(靠手动谁能一微秒内启动两次程序呢),因此每次生成的随机数就不同了。

    固然若是咱们把new MyRand(Environment.TickCount)放到for循环中: 

for (int i = 0; i < 100; i++) 
 { 
    MyRand rand = newMyRand(Environment.TickCount); 
    Console.WriteLine(rand.Next()); 
 } 

 

    运行结果又变成"不少是连续"的了,原理很简单:因为for循环体执行很快,因此每次循环的时候Environment.TickCount极可能还和上次同样(两行简单的代码运行用不了一毫秒那么长事件),因为此次的"随机数种子"和上次的"随机数种子"同样,这样Next()生成的第一个"随机数"就同样了。从"-320"变成"-856"是由于运行到"-856"的时候时间过了一毫秒。 

4、各语言的实现

    咱们看到.Net的Random类有一个int类型参数的构造函数:

public Random(int Seed)

就是和咱们写的MyRand同样接受一个"随机数种子"。而咱们以前调用的无参构造函数就是给Random(int Seed)传递Environment.TickCount类进行构造的,代码以下:

        public Random() : this(Environment.TickCount)
        {
        }

    这下咱们终于明白最开始的疑惑了。  

一样道理,在C/C++中生成10个随机数不该该以下调用:

    int i; 
    for(i=0;i<10;i++) 
    { 
        srand( (unsigned)time( NULL ) ); 
        printf("%d\n",rand()); 
    } 

 而应该:

    srand( (unsigned)time( NULL ) ); //把当前时间设置为"随机数种子" 
    int i; 
    for(i=0;i<10;i++) 
    {          
        printf("%d\n",rand()); 
    } 

 5、"奇葩"的Java

Java学习者可能会提出问题了,在Java低版本中,以下使用会像.Net、C/C++中同样产生相同的随机数: 

        for(int i=0;i<100;i++) 
        { 
            Random rand = new Random(); 
            System.out.println(rand.nextInt()); 
        } 

 由于低版本Java中Rand类的无参构造函数的实现一样是用当前时间作种子:

public Random() { this(System.currentTimeMillis()); } 

可是在高版本的Java中,好比Java1.8中,上面的"错误"代码执行倒是没问题的:

    为何呢?咱们来看一下这个Random无参构造函数的实现代码:

public Random() 
{ 
this(seedUniquifier() ^ System.nanoTime()); 
} 
private static long seedUniquifier() { for (;;) { long current = seedUniquifier.get(); long next = current * 181783497276652981L; if (seedUniquifier.compareAndSet(current, next)) return next; } } privatestaticfinal AtomicLong seedUniquifier = new AtomicLong(8682522807148012L);

     这里再也不是使用当前时间来作"随机数种子",而是使用System.nanoTime()这个纳秒级的时间量而且和采用原子量AtomicLong根据上次调用构造函数算出来的一个数作异或运算。关于这段代码的解释详细参考这篇文章《解密随机数生成器(2)——从java源码看线性同余算法

最核心的地方就在于使用static变量AtomicLong来记录每次调用Random构造函数时使用的种子,下次再调用Random构造函数的时候避免和上次同样。

6、高并发系统中的问题

    前面咱们分析了,对于使用系统时间作"随机数种子"的随机数生成器,若是要产生多个随机数,那么必定要共享一个"随机数种子"才会避免生成的随机数短期以内生成重复的随机数。可是在一些高并发的系统中一个不注意还会产生问题,好比一个网站在服务器端经过下面的方法生成验证码:

Random rand = new Random();

Int code = rand.Next();

    当网站并发量很大的时候,可能一个毫秒内会有不少我的请求验证码,这就会形成这几我的请求到的验证码是重复的,会给系统带来潜在的漏洞。

     再好比我今天看到的一篇文章《当随机不够随机:一个在线扑克游戏的教训》里面就提到了"因为随机数产生器的种子是基于服务器时钟的,黑客们只要将他们的程序与服务器时钟同步就可以将可能出现的乱序减小到只有 200,000 种。到那个时候一旦黑客知道 5 张牌,他就能够实时的对 200,000 种可能的乱序进行快速搜索,找到游戏中的那种。因此一旦黑客知道手中的两张牌和 3 张公用牌,就能够猜出转牌和河牌时会来什么牌,以及其余玩家的牌。"  

    这种状况有以下几种解决方法:

  1. 把Random对象做为一个全局实例(static)来使用。Java中Random是线程安全的(内部进行了加锁处理);.Net中Random不是线程安全的,须要加锁处理。不过加锁会存在会形成处理速度慢的问题。并且因为初始的种子是肯定的,因此攻击者存在着根据获得的若干随机数序列推测出"随机数种子"的可能性。
  2. 由于每次生成Guid的值都不样,网上有的文章说能够建立一个Guid计算它的HashCode或者MD5值的方式来作种子: new Random(Guid.NewGuid().GetHashCode()) 。可是我认为Guid的生成算法是肯定的,在条件充足的状况下也是能够预测的,这样生成的随机数也有可预测的可能性。固然只是个人猜想,没通过理论的证实。
  3. 采用"真随机数发生器",快看下一节分解!

 7、真随机数发生器

    根据咱们以前的分析,咱们知道这些所谓的随机数不是真的"随机",只是看起来随机,所以被称为"伪随机算法"。在一些对随机要求高的场合会使用一些物理硬件采集物理噪声、宇宙射线、量子衰变等现实生活中的真正随机的物理参数来产生真正的随机数。

固然也有聪明的人想到了不借助增长"随机数发生器"硬件的方法生成随机数。咱们操做计算机时候鼠标的移动、敲击键盘的行为都是不可预测的,外界命令计算机何时要执行什么进程、处理什么文件、加载什么数据等也是不可预测的,所以致使的CPU运算速度、硬盘读写行为、内存占用状况的变化也是不可预测的。所以若是采集这些信息来做为随机数种子,那么生成的随机数就是不可预测的了。

在Linux/Unix下可使用"/dev/random"这个真随机数发生器,它的数据主来来自于硬件中断信息,不过产生随机数的速度比较慢。

Windows下能够调用系统的CryptGenRandom()函数,它主要依据当前进程Id、当前线程Id、系统启动后的TickCount、当前时间、QueryPerformanceCounter返回的高性能计数器值、用户名、计算机名、CPU计数器的值等等来计算。和"/dev/random"同样CryptGenRandom()的生成速度也比较慢,并且消耗比较大的系统资源。

固然.Net下也可使用RNGCryptoServiceProvider 类(System.Security.Cryptography命名空间下)来生成真随机数,根据StackOverflow上一篇帖子介绍RNGCryptoServiceProvider 并非对CryptGenRandom()函数的封装,可是和CryptGenRandom()原理相似。  

8、总结

有人可能会问:既然有"/dev/random" 、CryptGenRandom()这样的"真随机数发生器",为何还要提供、使用伪随机数这样的"假货"?由于前面提到了"/dev/random" 、CryptGenRandom()生成速度慢并且比较消耗性能。在对随机数的不可预测性要求低的场合,使用伪随机数算法便可,由于性能比较高。对于随机数的不可预测性要求高的场合就要使用真随机数发生器,真随机数发生器硬件设备须要考虑成本问题,而"/dev/random"、CryptGenRandom()则性能较差。

万事万物都没有完美的,没有绝对的好,也没有绝对的坏,这才是多元世界美好的地方。

如鹏网.Net培训班正在报名,有网络的地方就能够参加如鹏网的学习,学完就能高薪就业,点击此处了解

 

    三年前只要懂“三层架构”就能够说“精通分层架构”;如今则须要懂IOC(AutoFac等)、CodeFirst、lambda、DTO等才值钱;

    三年前只要会SQLServer就能够说本身“精通数据库开发”;如今则需还须要掌握MySQL等开源数据库才能说是“.Net开源”时代的程序员;

    三年前只要会进行用户上传内容的安全性处理便可;如今则须要熟悉云存储、CDN等才能在云计算时代游刃有余;

    三年前只要掌握Lucene.Net就会说本身“熟悉站内搜索引擎开发”;如今你们都用ElasticSearch了,你还用Lucene.Net就太老土了;

    三年前发邮件仍是用SmtpClient;如今作大型网站发邮件必须用云邮件引擎;

    三年前缓存就是Context.Cache;如今则是Redis、Memcached的天下;

    如鹏网再次引领.Net社区技术潮流!点击此处了解如鹏网.Net最新课程

相关文章
相关标签/搜索