大家可能都用过CHINAREN的校友录,不久前它的留言簿上加了一个防止灌水的方法,就是系统每次产生一个由随机的数字和字母组成的图片,每次留言必须正确地输入这些随机产生的字符,否则不能添加留言。这是一个很好的防止恶意攻击的方法,其核心的技术就是如何产生随机数。CHINAREN网站是使用PHP实现的,而我们可以充分利用ASP.NET的强大功能很轻易地实现。
在.NET FRAMEWORK中提供了一个专门用来产生随机数的类SYSTEM.RANDOM,使用这个类时必须导入SYSTEM命名空间。当然,命名空间SYSTEM在每个ASP.NET页面中都是自动导入的,所以我们可以直接使用这个类。
对于随机数,大家都知道,计算机不可能产生完全随机的数字,所谓的随机数发生器都是通过一定的算法对事先选定的随机种子做复杂的运算,用产生的结果来近似的模拟完全随机数,这种随机数被称作伪随机数。伪随机数是以相同的概率从一组有限的数字中选取的。所选数字并不具有完全的随机性,但是从实用的角度而言,其随机程度已足够了。伪随机数的选择是从随机种子开始的,所以为了保证每次得到的伪随机数都足够地“随机”,随机种子的选择就显得非常重要。如果随机种子一样,那么同一个随机数发生器产生的随机数也会一样。一般地,我们使用同系统时间有关的参数作为随机种子,这也是.NET FRAMEWORK中的随机数发生器默认采用的方法。
我们可以使用两种方式初始化一个随机数发生器:
第一种方法不指定随机种子,系统自动选取当前时间作为随机种子:
RANDOM RO = NEW RANDOM();
第二种方法可以指定一个INT型参数作为随机种子:
INT ISEED=10;
RANDOM RO = NEW RANDOM(10);
之后,我们就可以使用这个RANDOM类的对象来产生随机数,这时候要用到RANDOM.NEXT()方法。这个方法使用相当灵活,你甚至可以指定产生的随机数的上下限。
不指定上下限的使用如下:
INT IRESULT;
IRESULT=RO.NEXT();
下面的代码指定返回小于100的随机数:
INT IRESULT;
INT IUP=100;
IRESULT=RO.NEXT(IUP);
而下面这段代码则指定返回值必须在50-100的范围之内:
INT IRESULT;
INT IUP=100;
INT IDOWN=50;
IRESULT=RO.NEXT(IDOWN,IUP);
除了RANDOM.NEXT()方法之外,RANDOM类还提供了RANDOM.NEXTDOUBLE()方法产生一个范围在0.0-1.0之间的随机的双精度浮点数:
DOUBLE DRESULT;
DRESULT=RO.NEXTDOUBLE();
另外一个与RANDOM.NEXTDOUBLE()方法相似的方法是RANDOM.SAMPLE(),它跟RANDOM.NEXTDOUBLE()方法唯一的区别在于访问级别,我们可以看看它们的原始声明:
PROTECTED VIRTUAL DOUBLE SAMPLE();
PUBLIC VIRTUAL DOUBLE NEXTDOUBLE();
RANDOM.SAMPLE()方法是保护方法,只允许子类的对象访问,而RANDOM.SAMPLE()方法则可以看作是RANDOM.SAMPLE()的公开版本。一般地,用户在RANDOM的子类中重写SAMPLE()方法来得到更一般的分布。
这个例子中,我们使用RANDOM.NEXT()方法来产生随机数。
下面这个函数是这个例子的核心,我们利用他来产生一个随机的INT数组:
PRIVATE INT []GETRANDOMARRAY(INT LENGTH,INT UP,INT DOWN){ INT IFIRST=0; INT []RTARRAY=NEW INT32[LENGTH]; RANDOM RO=NEW RANDOM(LENGTH*UNCHECKED((INT)DATETIME.NOW.TICKS)); IFIRST=RO.NEXT(UP,DOWN); RTARRAY[0]=IFIRST; FOR(INT I=1;I
读者或许都注意到了,我们采用了一种相当麻烦的方式来产生这个随机数组,为什么不简单地使用如下代码呢?请先看下面代码,这里我们使用了系统时间作为随机种子,连续获取两个随机数,并且将其输出:
< %@ PAGE LANGUAGE="C#" DEBUG="TRUE" TRACE="FALSE" TRACEMODE="SORTBYCATEGORY"% >< % @IMPORT NAMESPACE="SYSTEM" % >
< SCRIPT LANGUAGE=C# RUNAT=SERVER >
PUBLIC VOID PAGE_LOAD(OBJECT SENDER,EVENTARGS E){ INT RE=0; INT RE1=0; GETRANDOMDEFAULT(REF RE); GETRANDOMDEFAULT(REF RE1); RANDOMNUM.TEXT=RE.TOSTRING(); RANDOMNUM.TEXT+=" "+RE1.TOSTRING();}PRIVATE VOID GETRANDOMDEFAULT(REF INT RE){ RANDOM RO=NEW RANDOM(UNCHECKED((INT)DATETIME.NOW.TICKS)); RE=RO.NEXT(10,20);}PRIVATE VOID GETRANDOMBYINT(REF BYTE []RE){ RANDOM RO=NEW RANDOM(); RO.NEXTBYTES(RE);}
< /SCRIPT >
< HTML >
< HEAD >
< TITLE >随机数测试< /TITLE >
< META HTTP-EQUIV="CONTENT-TYPE" CONTENT="TEXT/HTML; CHARSET=GB2312" >
< /HEAD >
< BODY BGCOLOR="#FFFFFF" TEXT="#000000" >
< FORM RUNAT=SERVER >
< ASP:LABEL ID="RANDOMNUM" RUNAT=SERVER / >
< /FORM >
< /BODY >
< /HTML >
下面是笔者机器上产生的结果的截图:

是的,如你所见,产生了一样的两个随机数,无论重复多少次,都是一样的。原因在哪里呢?
不要以为使用系统时间作为随机种子就万无一失了如果应用程序在一个较快的计算机上运行,则该计算机的系统时钟可能没有时间在此构造函数的调用之间进行更改,RANDOM 的不同实例的种子值可能相同。这种情况下,我们就需要另外的算法来保证产生的数字的随机性。所以为了保证产生的随机数足够“随机”,我们不得不使用复杂一点的方法来获得随机种子。
在上面的这段程序中,我们首先使用系统时间作为随机种子,然后将上一次产生的随机数跟循环变量和一个与系统时间有关的整型参数相乘,以之作为随机种子,从而得到了每次都不同的随机种子,保证了产生足够“随机”的随机数。
得到整型的随机数组以后,我们将它变成字符串,然后使用SYSTEM.DRAWING中与GDI+相关的类生成一个图片并且在网页上显示出来。
生成图片的ASP.NET页面全部代码如下:
< %@ PAGE LANGUAGE="C#" DEBUG="TRUE" TRACE="FALSE" TRACEMODE="SORTBYCATEGORY"% >< % @IMPORT NAMESPACE="SYSTEM.DRAWING" % >< % @IMPORT NAMESPACE="SYSTEM.DRAWING.IMAGING" % >< % @IMPORT NAMESPACE="SYSTEM.DRAWING.TEXT" % >< % @IMPORT NAMESPACE="SYSTEM.IO" % >< SCRIPT LANGUAGE=C# RUNAT=SERVER >
PUBLIC VOID PAGE_LOAD(OBJECT SENDER,EVENTARGS E){ STRING STRNUM=GETRANDOMSTRING();
STRING STRFONTNAME;
INT IFONTSIZE;
INT IWIDTH;
INT IHEIGHT;
STRFONTNAME="宋体";
IFONTSIZE=12;
IWIDTH=10*STRNUM.LENGTH;
IHEIGHT=25;
COLOR BGCOLOR=COLOR.YELLOW;
COLOR FORECOLOR=COLOR.RED;
FONT FOREFONT=NEW FONT(STRFONTNAME,IFONTSIZE,FONTSTYLE.BOLD);
BITMAP PIC=NEW BITMAP(IWIDTH,IHEIGHT,PIXELFORMAT.FORMAT32BPPARGB);
GRAPHICS G=GRAPHICS.FROMIMAGE(PIC);
RECTANGLE R=NEW RECTANGLE(0,0,IWIDTH,IHEIGHT);
G.FILLRECTANGLE(NEW SOLIDBRUSH(BGCOLOR),R);
G.DRAWSTRING(STRNUM,FOREFONT,NEW SOLIDBRUSH(FORECOLOR),2,2);
MEMORYSTREAM MSTREAM=NEW MEMORYSTREAM();
PIC.SAVE(MSTREAM,IMAGEFORMAT.GIF);
G.DISPOSE();
PIC.DISPOSE();
RESPONSE.CLEARCONTENT();
RESPONSE.CONTENTTYPE="IMAGE/GIF";
RESPONSE.BINARYWRITE(MSTREAM.TOARRAY());
RESPONSE.END();
}
PRIVATE INT []GETRANDOMARRAY(INT LENGTH,INT UP,INT DOWN)
{
INT IFIRST=0;
INT []RTARRAY=NEW INT32[LENGTH];
RANDOM RO=NEW RANDOM(LENGTH*UNCHECKED((INT)DATETIME.NOW.TICKS));
IFIRST=RO.NEXT(UP,DOWN);
RTARRAY[0]=IFIRST;
FOR(INT I=1;I< LENGTH;I++)
{
RANDOM RI=NEW RANDOM(I*IFIRST*UNCHECKED((INT)DATETIME.NOW.TICKS));
RTARRAY[I]=RI.NEXT(UP,DOWN);
IFIRST=RTARRAY[I];
}
RETURN RTARRAY;
}
其中生成图片的部分相对复杂,但由于不是本文的主题所在,所以本文不对之做详细说明,有兴趣的读者可以参考杜亮编写的《亲密接触ASP.NET》一书中的相关内容。
最后我们可以编写一个普通的HTML页面来查看效果,只要把图片的SRC属性指向这个页面就行了(这里我们假设上面那个ASP.NET文件的名字是“RANDOMPIC.ASPX”):
< !DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 TRANSITIONAL//EN" >
< HTML >
< HEAD >
< TITLE > NEW DOCUMENT < /TITLE >
< META NAME="GENERATOR" CONTENT="EDITPLUS" >
< META NAME="AUTHOR" CONTENT="" >
< META NAME="KEYWORDS" CONTENT="" >
< META NAME="DESCRIPTION" CONTENT="" >
< /HEAD >
< BODY >
< IMG SRC="RANDOMPIC.ASPX" >
< /BODY >
< /HTML >
在笔者的机器上成功地看到了如下结果:

要实现像CHINAREN网站那样的防恶意攻击的效果,只需要在留言簿的页面里产生随机数并且编写相应的JAVASCRIPT验证代码(事实上这个工作可以交给ASP.NET的验证控件很容易地完成),然后传递到生成图片的页面里生成图片提示用户就可以了。
除此以外,随机数还有其它很多用途,特别是开发游戏的时候更是必不可少。到此,读者应该完全掌握在ASP.NET中随机数的产生方法,如此,本文的目的也就达到了。
最后,有兴趣的读者可以试着解决这个问题:
在桥牌游戏中,发牌可以视作一个随机过程,但是后续过程受到前面的影响,即已经发出去的牌不可能再次发出。试编写一个程序模拟发牌过程。