效率极高的分类算法
在网站建设中,分类算法的应用非常的普遍。在设计一个电子商店时,要涉及到商品分类;在设计发布系统时,要涉及到栏目或者频道分类;在设计软件下载这样的程序时,要涉及到软件的分类;如此等等。可以说,分类是一个很普遍的问题。
我常常面试一些程序员,而且我几乎毫无例外地要问他们一些关于分类算法的问题。下面的举几个我常常询问的问题。你认为你可以很轻松地回答么^_^.
1、分类算法常常表现为树的表示和遍历问题。那么,请问:如果用数据库中的一个Table来表达树型分类,应该有几个字段?
2、如何快速地从这个Table恢复出一棵树;
3、如何判断某个分类是否是另一个分类的子类;
4、如何查找某个分类的所有产品;
5、如何生成分类所在的路径。
6、如何新增分类;
在不限制分类的级数和每级分类的个数时,这些问题并不是可以轻松回答的。本文试图解决这些问题。
分类的数据结构
我们知道:分类的数据结构实际上是一棵树。在《数据结构》课程中,大家可能学过Tree的算法。由于在网站建设中我们大量使用数据库,所以我们将从Tree在数据库中的存储谈起。
为简化问题,我们假设每个节点只需要保留Name这一个信息。我们需要为每个节点编号。编号的方法有很多种。在数据库中常用的就是自动编号。这在Access、SQLServer、Oracle中都是这样。假设编号字段为ID。
为了表示某个节点ID1是另外一个节点ID2的父节点,我们需要在数据库中再保留一个字段,说明这个分类是属于哪个节点的儿子。把这个字段取名为FatherID。如这里的ID2,其FatherID就是ID1。
这样,我们就得到了分类Catalog的数据表定义:
CreateTable[Catalog](
[ID][int]NOTNULL,
[Name][nvarchar](50)NOTNULL,
[FatherID][int]NOTNULL
);
约定:我们约定用-1作为最上面一层分类的父亲编码。编号为-1的分类。这是一个虚拟的分类。它在数据库中没有记录。
如何恢复出一棵树
上面的Catalog定义的最大优势,就在于用它可以轻松地恢复出一棵树-分类树。为了更清楚地展示算法,我们先考虑一个简单的问题:怎样显示某个分类的下一级分类。我们知道,要查询某个分类FID的下一级分类,SQL语句非常简单:
selectNamefromcatalogwhereFatherID=FID
显示这些类别时,我们简单地用〈LI〉来做到:
〈%
REMoConn---数据库连接,调用GetChildren时已经打开
REMFID-----当前分类的编号
FunctionGetChildren(oConn,FID)
strSQL=“selectID,NamefromcatalogwhereFatherID=“&FID
setrsCatalog=oConn.Execute(strSQL)
%〉
〈UL〉
〈%
DowhilenotrsCatalog.Eof
%〉
〈LI〉〈%=rsCatalog(“Name“)%〉
〈%
Loop
%〉
〈/UL〉
〈%
rsCatalog.Close
EndFunction
%〉
现在我们来看看如何显示FID下的所有分类。这需要用到递归算法。我们只需要在GetChildren函数中简单地对所有ID进行调用:GetChildren(oConn,Catalog(“ID“))就可以了。
〈%
REMoConn---数据库连接,已经打开
REMFID-----当前分类的编号
FunctionGetChildren(oConn,FID)
strSQL=“selectNamefromcatalogwhereFatherID=“&FID
setrsCatalog=oConn.Execute(strSQL)
%〉
〈UL〉
〈%
DowhilenotrsCatalog.Eof
%〉
〈LI〉〈%=rsCatalog(“Name“)%〉
〈%=GetChildren(oConn,Catalog(“ID“))%〉
〈%
Loop
%〉
〈/UL〉
〈%
rsCatalog.Close
EndFunction
%〉
修改后的GetChildren就可以完成显示FID分类的所有子分类的任务。要显示所有的分类,只需要如此调用就可以了:
〈%
REMstrConn--连接数据库的字符串,请根据情况修改
setoConn=Server.CreateObject(“ADODB.Connection“)
oConn.OpenstrConn
=GetChildren(oConn,-1)
oConn.Close
%〉
如何查找某个分类的所有产品;
现在来解决我们在前面提出的第四个问题。第三个问题留作习题。我们假设产品的数据表如下定义:
CreateTableProduct(
[ID][int]NOTNULL,
[Name][nvchar]NOTNULL,
[FatherID][int]NOTNULL
);
其中,ID是产品的编号,Name是产品的名称,而FatherID是产品所属的分类。
对第四个问题,很容易想到的办法是:先找到这个分类FID的所有子类,然后查询所有子类下的所有产品。实现这个算法实际上很复杂。代码大致如下:
〈%
FunctionGetAllID(oConn,FID)
DimstrTemp
IfFID=-1then
strTemp=““
else
strTemp=“,“
endif
strSQL=“selectNamefromcatalogwhereFatherID=“&FID
setrsCatalog=oConn.Execute(strSQL)
DowhilenotrsCatalog.Eof
strTemp=strTemp&rsCatalog(“ID“)&GetAllID(oConn,Catalog(“ID“))REM递归调用
Loop
rsCatalog.Close
GetAllID=strTemp
EndFunction
REMstrConn--连接数据库的字符串,请根据情况修改
setoConn=Server.CreateObject(“ADODB.Connection“)
oConn.OpenstrConn
FID=Request.QueryString(“FID“)
strSQL=“selecttop100*fromProductwhereFatherIDin(“&GetAllID(oConn,FID)&“)“
setrsProduct=oConn.Execute(strSQL)
%〉
〈UL〉〈%
DowhilenotrsProduct.EOF
%〉
〈LI〉〈%=rsProduct(“Name“)%〉
〈%
Loop
%〉
〈/UL〉
〈%rsProduct.Close
oConn.Close
%〉
这个算法有很多缺点。试列举几个如下:
1、由于我们需要查询FID下的所有分类,当分类非常多时,算法将非常地不经济,而且,由于要构造一个很大的strSQL,试想如果有1000个分类,这个strSQL将很大,能否执行就是一个问题。
2、我们知道,在SQL中使用In子句的效率是非常低的。这个算法不可避免地要使用In子句,效率很低。
我发现80%以上的程序员钟爱这样的算法,并在很多系统中大量地使用。细心的程序员会发现他们写出了很慢的程序,但苦于找不到原因。他们反复地检查SQL的执行效率,提高机器的档次,但效率的增加很少。
最根本的问题就出在这个算法本身。算法定了,能够再优化的机会就不多了。我们下面来介绍一种算法,效率将是上面算法的10倍以上。
分类编码算法
问题就出在前面我们采用了顺序编码,这是一种最简单的编码方法。大家知道,简单并不意味着效率。实际上,编码科学是程序员必修的课程。下面,我们通过设计一种编码算法,使分类的编号ID中同时包含了其父类的信息。一个五级分类的例子如下:
此例中,用32(4+7+7+7+7)位整数来编码,其中,第一级分类有4位,可以表达16种分类。第二级到第五级分类分别有7位,可以表达128个子分类。
显然,如果我们得到一个编码为1092787200的分类,我们就知道:由于其编码为
01000001001000101001110000000000
所以它是第四级分类。其父类的二进制编码是01000001001000101000000000000000,十进制编号为1092780032。依次我们还可以知道,其父类的父类编码是01000001001000000000000000000000,其父类的父类的父类编码是01000000000000000000000000000000。(我是不是太罗嗦了J,但这一点很重要。再回头看看我们前面提到的第五个问题。哈哈,这不就已经得到了分类1092787200所在的分类路径了吗?)。
现在我们在一般的情况下来讨论类别编码问题。设类别的层次为k,第i层的编码位数为Ni,那么总的编码位数为N(N1+N2+..+Nk)。我们就得到任何一个类别的编码形式如下:
2^(N-(N1+N2+…+Ni))*j+父类编码
其中,i表示第i层,j表示当前层的第j个分类。
这样我们就把任何分类的编码分成了两个部分,其中一部分是它的层编码,一部分是它的父类编码。
由下面公式定一的k个编码我们称为特征码:(因为i可以取k个值,所以有k个)
2^N-2^(N-(N1+N2+…+Ni))
对于任何给定的类别ID,如果我们把ID和k个特征码“相与“,得到的非0编码,就是其所有父类的编码!
位编码算法
对任何顺序编码的Catalog表,我们可以设计一个位编码算法,将所有的类别编码规格化为位编码。在具体实现时,我们先创建一个临时表:
CreateTempCatalog(
[OldID][int]NOTNULL,
[NewID][int]NOTNULL,
[OldFatherID][int]NOTNULL,
[NewFatherID][int]NOTNULL
);
在这个表中,我们保留所有原来的类别编号OldID和其父类编号OldFatherID,以及重新计算的满足位编码要求的相应编号NewID、NewFatherID。
程序如下:
〈%
REMoConn---数据库连接,已经打开
REMOldFather---原来的父类编号
REMNewFather---新的父类编号
REMN---编码总位数
REMNi--每一级的编码位数数组
REMLevel--当前的级数
subFormatAllID(oConn,OldFather,NewFather,N,Nm,Nibyref,Level)
strSQL=“selectCatalogID,FatherIDfromCatalogwhereFatherID=“&OldFather
setrsCatalog=oConn.Execute(strSQL)
j=1
dowhilenotrsCatalog.EOF
i=2^(N-Nm)*j
ifLeveltheni=i+NewFather
OldCatalog=rsCatalog(“CatalogID“)
NewCatalog=i
REM写入临时表
strSQL=“InsertintoTempCatalog(OldCatalogID,NewCatalogID,OldFatherID,NewFatherID)“
strSQL=strSQL&“values(“&OldCatalog&“,“&NewCatalog&“,“&OldFather&“,“&NewFather&“)“
Conn.ExecutestrSQL
REM递归调用FormatAllID
Nm=Nm+Ni(Level+1)
FormatAllIDoConn,OldCatalog,NewCatalog,N,Nm,Ni,Level+1
rsCatalog.MoveNext
j=j+1
loop
rsCatalog.Close
endsub
%〉
调用这个算法的一个例子如下:
〈%
REM定义编码参数,其中N为总位数,Ni为每一级的位数。
DimN,Ni(5)
Ni(1)=4
N=Ni(1)
fori=2to5
Ni(i)=7
N=N+Ni(i)
next
REM打开数据库,创建临时表
strSQL=“CreateTempCatalog([OldID][int]NOTNULL,[NewID][int]NOTNULL,[OldFatherID][int]NOTNULL,[NewFatherID][int]NOTNULL);“
SetConn=Server.CreateObject(“ADODB.Connection“)
Conn.OpenApplication(“strConn“)
Conn.ExecutestrSQL
REM调用规格化例程
FormatAllIDConn,-1,-1,N,Ni(1),Ni,0
REM------------------------------------------------------------------------
REM在此处更新所有相关表的类别编码为新的编码即可。
REM------------------------------------------------------------------------
REM关闭数据库
strSQL=“droptableTempCatalog;“
Conn.ExecutestrSQL
Conn.Close
%〉
第四个问题
现在我们回头看看第四个问题:怎样得到某个分类下的所有产品。由于采用了位编码,现在问题变得很简单。我们很容易推算:某个产品属于某个类别的条件是Product.FatherID&(Catalog.ID的特征码)=Catalog.ID。其中“&“代表位与算法。这在SQLServer中是直接支持的。
举例来说:产品所属的类别为:1092787200,而当前类别为1092780032。当前类别对应的特征值为:4294950912,由于1092787200&4294950912=8537400,所以这个产品属于分类8537400。
我们前面已经给出了计算特征码的公式。特征码并不多,而且很容易计算,可以考虑在Global.asa中Application_OnStart时间触发时计算出来,存放在Application(“Mark“)数组中。
当然,有了特征码,我们还可以得到更加有效率的算法。我们知道,虽然我们采用了位编码,实际上还是一种顺序编码的方法。表现出第I级的分类编码肯定比第I+1级分类的编码要小。根据这个特点,我们还可以由FID得到两个特征码,其中一个是本级位特征码FID0,一个是上级位特征码FID1。而产品属于某个分类FID的充分必要条件是:
Product.FatherID〉FID0andProduct.FatherID〈FID1
下面的程序显示分类FID下的所有产品。由于数据表Product已经对FatherID进行索引,故查询速度极快:
〈%
REMoConn---数据库连接,已经打开
REMFID---当前分类
REMFIDMark---特征值数组,典型的情况下为Application(“Mark“)
REMk---数组元素个数,也是分类的级数
SubGetAllProduct(oConn,FID,FIDMarkbyref,k)
REM根据FID计算出特征值FID0,FID1
fori=kto1
if(FIDandFIDMark=FID)thenexit
next
strSQL=“selectNamefromProductwhereFatherID〉“FIDMark(i)&“andFatherID〈“FIDMark(i-1)
setrsProduct=oConn.Execute(strSQL)%〉
〈UL〉〈%
DoWhileNotrsProduct.Eof%〉
〈LI〉〈%=rsProduct(“Name“)
Loop%〉
〈/UL〉〈%
rsProduct.Close
EndSub
%〉
42. Windows内存机制解析
前言
写这篇文章之前相当长的一段时间里,对windows内存机制是有着相当的困惑的。各个进程的内存空间是如何隔离和共享的?GDT(全局描述表)尚在,可分段机制去了那里?既然我们有虚拟的4G空间和结构化异常为何分配内存仍可能失败?在什么时候stack会溢出?―――
当我把这些问题都弄清楚后,我写了这篇文章为自己做了个总结,希望对大家也有帮助。同时由于写Windows内存这块的文章比较多,我将尽力做到与别人的内容不重合。
动笔后不久,我发现imquestion对于Windows内存写了几篇非常不错的文章,总题目叫《JIURL玩玩Win2k内存篇》,推荐阅读。
一、总论
Windows内存管理机制,底层最核心的东西是分页机制。分页机制使每个进程有自己的4G虚拟空间,使我们可以用虚拟线性地址来跑程序。每个进程有自己的工作集,工作集中的数据可以指明虚拟线性地址对应到怎样的物理地址。进程切换的过程也就是工作集切换的过程,如MattPietrek所说如果只给出虚拟地址而不给出工作集,那这个地址是无意义的。
在分页机制所形成的线性地址空间里,我们对内存进行进一步划分涉及的概念有堆、栈、自由存储等。对堆进行操作的API有HeapCreate、HeapAlloc等。操纵自由存储的API有VirtualAlloc等。此外内存映射文件使用的也应该算是自由存储的空间。栈则用来存放函数参数和局部变量,随着stackframe的建立和销毁其自动进行增长和缩减。
说到这里,也许有人会提出疑问:对x86CPU分段机制是必须的,分页机制是可选的。为什么这里只提到了分页机制。那么我告诉你分段机制仍然存在,一是为了兼容以前的16位程序,二是Windows毕竟要区分ring0和ring3两个特权级。用SoftIce看一下GDT(全局描述表)你基本上会看到如下内容:
GDTbase=80036000Limit=03FF
0008Code32Base=00000000Lim=FFFFFFFFDPL=0PRE
//内核态driver代码段
0010Data32Base=00000000Lim=FFFFFFFFDPL=0PRW
//内核态driver的数据段
001BCode32Base=00000000Lim=FFFFFFFFDPL=3PRE
//应用程序的代码段
0023Data32Base=00000000Lim=FFFFFFFFDPL=3PRW
//应用程序的数据段
这意味着什么呢?
我们再看一下线性地址的生成过程。从中我们应该可以得出结论,如果segmengbaseaddress为0的话,那么这个段可以看作不存在,因为偏移地址就是最终的线性地址。
此外还有两个段存在用于KernelProcessorControlRegion和userthreadenvironmentblock。所以如果你在反汇编时看到MOVECX,FS:[2C]就不必惊讶,怎么这里使用逻辑地址而不是线性地址。在以后涉及异常处理的地方会对此再做说明。
二、从Stack说开去
从我个人的经验看,谈到内存时说堆的文章最多,说stack的最少。我这里反其道而行的原因是stack其实要比堆更重要,可以有不使用堆的程序,但你不可能不使用stack,虽然由于对stack的管理是由编译器确定了的,进而他较少出错。
通过链接开关/STACK:reserve[,commit]可以指定进程主线程的stack大小,当你建立其他线程时如果不指定dwStackSize参数,则也将使用/STACK所指定的值。微软说,如果指定较大的commit值将有利于提升程序的速度,我没验证过,但理应如此。通常并不需要对STACK进行什么设定,缺省的情况下将保留1M空间,并提交两个页(8Kforx86)。而1M空间对于大多数程序而言是足够的,但为防止stackoverflow有三点需要指出一是当需要非常大的空间时最好用全局数组或用VirtualAlloc进行分配,二是引用传递或用指针传递尺寸较大的函数参数(这点恐怕地球人都知道),三是进行深度递归时一定要考虑会不会产生stack溢出,如果有可能,可以采用我在《递归与goto》一文中提到的办法来仿真递归,这时候可以使用堆或自由存储来代替stack。同时结构化异常被用来控制是否为stack提交新的页面。(这部分写的比较简略因为很多人都写过,推荐阅读JefferyRitcher《Windows核心编程》第16章)
下面我们来看一下stack的使用。
假设我们有这样一个简单之极的函数:
int__stdcalladd_s(intx,inty)
{
intsum;
sum=x+y;
returnsum;
}
这样在调用函数前,通常我们会看到这样的指令。
moveax,dwordptr[ebp-8]
pusheax
movecx,dwordptr[ebp-4]
pushecx
此时把函数参数压入堆栈,而stack指针ESP递减,stack空间减小。
在进入函数后,你将会看到如下指令:
pushebp
movebp,esp
subesp,44h
这三句建立stack框架,并减小esp为局部变量预留空间。建立stack框架后,[ebp+*]指向函数参数,[ebp-*]指向局部变量。
另外在很多情况下你会看到如下三条指令
pushebx
pushesi
pushedi
这三句把三个通用寄存器压入堆栈,这样这三个寄存器就可以用来存放一些变量,进而提升运行速度。
很奇怪,我这个函数根本用不到这三个寄存器,可编译器也生成了上述三条指令。
对stack中内容的读取,是靠基址指针ebp进行的。所以对应于sum=x+y;一句你会看到
moveax,dwordptr[ebp+8]
addeax,dwordptr[ebp+0Ch]
movdwordptr[ebp-4],eax
其中[ebp+8]是x,[ebp+0Ch]是y,记住压栈方向为从右向左,所以y要在x上边。
我们再看一下函数退出时的情况:
popedi
popesi
popebx
movesp,ebp
popebp
ret8
此时恢复stack框架,使esp与刚进入这个函数时相同,ret8使esp再加8,使esp与没调用这个函数的时候一致。如果使用__cdecl调用规则,则由调用方以类似addesp,8进行清场工作,使stack的大小与未进行函数调用时一致。Stack的使用就这样完全被编译器实现了,只要不溢出就和我们无关,也许也算一种内存的智能管理。最后要补充的两点是:首先stack不像heap会自动扩充,如果你用光了储备,他会准时溢出。其次是不要以为你使用了缺省参数进行链接,你就有1M的stack,看看启动代码你就知道在你拥有stack之前,CRun–Time
Library以用去了一小部分stack的空间。
三、浅谈一下Heap
(鉴于MattPietrek在它的《Windows95系统程式设计大奥秘》对9x系统的heap做了非常详细的讲解,此处涉及的内容将仅限于Win2000)
Heap与Stack正好相反,你需要手动来管理每一块内存的申请和释放(在没有垃圾收集机制的情况下),而对于C/C++程序员来说,操作Heap的方式实在是太多了点。下面是几乎所有可以操作堆内存的方法的列表:
malloc/free
new/delete
GlobalAlloc/GlobalFree
LocalAlloc/LocalFree
HeapAlloc/HeapFree
其中malloc/free由运行时库提供,new/delete为C++内置的操作符。他们都使用运行时库的自己的堆。运行时库的在2000和win9x下都有自己独立的堆。这也就意味着只要你一启动进程,你将至少有两个堆,一个作为进程缺省,一个给C/C++运行时库。
GlobalAlloc/GlobalFree和LocalAlloc/LocalFree现在已失去原有的含义,统统从进程缺省堆中分配内存。
HeapAlloc/HeapFree则从指定的堆中分配内存。
单就分配内存而言(new/delete还要管构造和析构),所有这些方式最终要归结到一点2000和98下都是是HeapAlloc。所以微软才会强调GlobalAlloc/GlobalFree和LocalAlloc/LocalFree会比较慢,推荐使用HeapAlloc,但由于Global**和Local**具有较简单的使用界面,因此即使在微软所提供的源代码中他们仍被大量使用。必须指出的是HeapAlloc并不在kernel32.dll中拥有自己的实现,而是把所有调用转发到ntdll.RtlAllocateHeap。下面这张从msdn中截取的图(图2),应该有助于我们理解同堆相关的API。
堆内部的运作同SGISTL的分配器有些类似,大体上是这样,OS为每个堆维护几个链表,每个链表上存放指定大小范围的区块。当你分配内存时,操作系统根据你所提供的尺寸,先确定从那个链表中进行分配,接下来从那个链表中找到合适的块,并把其线性地址返还给你。如果你所要求的尺寸,在现存的区块中找不到,那么就新分配一块较大的内存(使用VirtualAlloc),再对他进行切割,而后向你返还某一区块的线性地址。这只是一个大致的情形,操作系统总在不停的更新自己的堆算法,以提高堆操作的速度。
堆本身的信息(包括标志位和链表头等)被存放在HeapHeader中,而堆句柄正是指向HeapHeader的指针,HeapHeader的结构没有公开,稍后我们将试着做些分析。非常有趣的是微软一再强调只对toolhelpAPI有效的HeapID其实就是堆句柄。
原来是准备分析一下堆内部的一些结构的,可后来一想这么做实用价值并不是很大,所需力气却不小。因此也就没具体进行操作。但这里把实现监测堆中各种变化的小程序的实现思路公开一下,希望对大家有所帮助。这个小程序非常的简单,主要完成的任务就是枚举进程内所有的堆的变化情况。由于涉及到比较两个链表的不同,这里使用了STL的vector容器和某些算法来减少编码。同时为了使STL的内存使用不对我们要监测的对象产生干扰,我们需要建立自己的分配器,以使用我们单独创建的堆。此外还需要特别注意的一点是由于toolhelpAPIHeap32Next在运行过程中不允许对任何堆进行扰动(否则他总返回TRUE),导致我们只能使用vector,并预先保留足够的空间。(访问堆内部某些信息的另一种方式是使用HeapWalkAPI,看个人喜好了)。
程序的运行过程是这样的,先对当前进程中存在的堆进行枚举,并把结果存入一个set类型的变量heapid1,接下来创建自己的堆给分配器使用,并对进程中存在的堆再次进行枚举并把结果存入另一个set类型的变量heapid2,这样就可以调用set_difference求出我们新建堆的ID,再以后列举队内部的信息时将排除这个ID所表示的堆。接下来就可以在两点之间分别把堆内部的信息存入相应的vector,比较这两个vector,就可以得到对应于分配内存操作,堆内部的变化情况了。
(图2frommsdnbyMuraliR.Krishnan)
下面是一些悬而未决的问题,那位感兴趣可以自己探索。
HeapHeader结构是什么样的?
堆内部对内存块的组织方式?(是链表么)
每一个小块的描述信息在那里?(如果是链表,那就应该有指针使这些小块彼此相连。)
//myallocator.h
#ifndef_MYALLOCATOR_
#define_MYALLOCATOR_
#include〈iostream〉
#include〈windows.h〉
namespaceMyLib{
template〈classT〉
classMyAlloc{
public:
staticHANDLEhHeap;
//typedefinitions
typedefTvalue_type;
typedefT*pointer;
typedefconstT*const_pointer;
typedefT&reference;
typedefconstT&const_reference;
typedefsize_tsize_type;
typedefptrdiff_tdifference_type;
//rebindallocatortotypeU
template〈classU〉
structrebind{
typedefMyAlloc〈U〉other;
};
//returnaddressofvalues
pointeraddress(referencevalue)const{
return&value;
}
const_pointeraddress(const_referencevalue)const{
return&value;
}
/*constructorsanddestructor
*-nothingtodobecausetheallocatorhasnostate
*/
MyAlloc()throw(){
}
MyAlloc(constMyAlloc&)throw(){
}
~MyAlloc()throw(){
}
//returnmaximumnumberofelementsthatcanbeallocated
size_typemax_size()constthrow(){
size_typeN;
N=(size_type)(-1)/sizeof(T);
return(0〈N?N:1);
}
//allocatebutdon’tinitializenumelementsoftypeT
pointerallocate(size_typenum,constvoid*=0){
//printmessageandallocatememorywithglobalnew
/*std::cerr〈〈“allocate“〈〈num〈〈“element(s)“
〈〈“ofsize“〈〈sizeof(T)〈〈std::endl;
*/
pointerret=(pointer)(HeapAlloc(hHeap,0,num*sizeof(T)));
//std::cerr〈〈“allocatedat:“〈〈(void*)ret〈〈std::endl;
returnret;
}
char*_Charalloc(size_typeN)//vc所附带的stl的特色
{
return(char*)HeapAlloc(hHeap,0,N*sizeof(T));
}
//initializeelementsofallocatedstoragepwithvaluevalue
voidconstruct(pointerp,constT&value){
//initializememorywithplacementnew
new((void*)p)T(value);
}
//destroyelementsofinitializedstoragep
voiddestroy(pointerp){
//destroyobjectsbycallingtheirdestructor
p-〉~T();
}
//deallocatestoragepofdeletedelements
//原本应该为pointer
voiddeallocate(void*p,size_typenum){
//printmessageanddeallocatememorywithglobaldelete
/*
std::cerr〈〈“deallocate“〈〈num〈〈“element(s)“
〈〈“ofsize“〈〈sizeof(T)
〈〈“at:“〈〈(void*)p〈〈std::endl;
*/
HeapFree(hHeap,0,(void*)p);
}
};
//returnthatallspecializationsofthisallocatorareinterchangeable
template〈classT1,classT2〉
booloperator==(constMyAlloc〈T1〉&,
constMyAlloc〈T2〉&)throw(){
returntrue;
}
template〈classT1,classT2〉
booloperator!=(constMyAlloc〈T1〉&,
constMyAlloc〈T2〉&)throw(){
returnfalse;
}
}//endnamespaceMyLib
#endif
//teststlmem.cpp
/*
writtenbyleezy_2000
03-9-515:12
*/
#include“stdafx.h“
#pragmawarning(disable:4786)
//#define_STLP_USE_MALLOC
#include“myallocator.h“
#include〈iostream〉
#include〈set〉
#include〈vector〉
#include〈algorithm〉
#include〈windows.h〉
#include〈Tlhelp32.h〉
typedefunsignedlongULONG_PTR,*PULONG_PTR;
usingnamespacestd;
/*
本程序需要注意的几点:
1、在实现自己的分配器,这样可以使stl容器的变化不影响我们要监测的堆
2、容器只能用vector否则任何堆的任何变化将导致Heap32Next始终返回TRUE
这应该是微软的bug
3、分配内存失败的时候应该抛出std::bad_alloc内存,此处考虑不会出现低
内存的情况,没抛出此异常。即认定自编写分配器分配内存时不会失败。
*/
//用于比较堆内存块的仿函数
//以块大小来判定两个HEAPENTRY32的大小
classHeapInfoCompare
{
public:
booloperator()(constHEAPENTRY32&he1,constHEAPENTRY32&he2)const
{
return(he1.dwBlockSize〈he2.dwBlockSize);
}
};
typedefvector〈HEAPENTRY32,MyLib::MyAlloc〈HEAPENTRY32〉〉HEAPENTRYSET;
voidheapinfo(HEAPENTRYSET&hset,ULONG_PTRheapid);
voidgetheapid(set〈ULONG_PTR〉&heapid)
{
HANDLEhSnapShot=CreateToolhelp32Snapshot(TH32CS_SNAPHEAPLIST,GetCurrentProcessId());
HEAPLIST32heaplist32;
heaplist32.dwSize=sizeof(HEAPLIST32);
BOOLbRet=Heap32ListFirst(hSnapShot,&heaplist32);
while(bRet)
{
heapid.insert(heaplist32.th32HeapID);
cout〈〈heaplist32.th32HeapID〈〈endl;
bRet=Heap32ListNext(hSnapShot,&heaplist32);
}
CloseHandle(hSnapShot);
cout〈〈“theend“〈〈endl;
}
HANDLEMyLib::MyAlloc〈HEAPENTRY32〉::hHeap=NULL;
HANDLEhHeap;
intmain(intargc,char*argv[])
{
//枚举此时所有堆并在建立新堆后再次枚举这样从中剔除新建堆
set〈ULONG_PTR〉heapid1,heapid2,heapid3;
getheapid(heapid1);
hHeap=HeapCreate(0,0,0);
getheapid(heapid2);
insert_iterator〈set〈ULONG_PTR〉〉iter(heapid3,heapid3.begin());
set_difference(heapid2.begin(),heapid2.end(),heapid1.begin(),heapid1.end(),
iter);
set〈ULONG_PTR〉::iteratorpos;
ULONG_PTRnewheapid;
for(pos=heapid3.begin();pos!=heapid3.end();++pos)
{
cout〈〈“Thenewheapidis\t“〈〈(*pos)〈〈endl;
newheapid=*pos;
}
MyLib::MyAlloc〈HEAPENTRY32〉::hHeap=hHeap;
//vector〈int,MyLib::MyAlloc〈int〉〉v1;
HEAPENTRYSETheapset1,heapset2,heapset3;
heapset1.reserve(400);//保证vector不自动增长
heapset2.reserve(400);
heapset3.reserve(400);
intsize;
heapinfo(heapset1,newheapid);
sort(heapset1.begin(),heapset1.end(),HeapInfoCompare());
size=heapset1.size();
HANDLEhCurHeap=GetProcessHeap();
//HeapAlloc(hCurHeap,HEAP_ZERO_MEMORY,4*1024);
char*p=newchar[4*1024];
//GlobalAlloc(GHND,4*1024);
char*q=(char*)malloc(4*1024);
cout〈〈“thepis“〈〈(int)p〈〈endl;
heapinfo(heapset2,newheapid);
sort(heapset2.begin(),heapset2.end(),HeapInfoCompare());
size=heapset2.size();
insert_iterator〈HEAPENTRYSET〉miter(heapset3,heapset3.begin());
set_difference(heapset2.begin(),heapset2.end(),heapset1.begin(),heapset1.end(),
miter,HeapInfoCompare());
size=heapset3.size();
HEAPENTRYSET::iteratormpos;
for(mpos=heapset3.begin();mpos!=heapset3.end();++mpos)
{
cout〈〈“Thesizeofthedifferentblockis\t“〈〈(*mpos).dwBlockSize〈〈“\tandtheaddresssis\t“〈〈(*mpos).dwAddress〈〈“\tdwFlagsis\t“〈〈(*mpos).dwFlags〈〈endl;
cout〈〈“Theheapidis:\t“〈〈(*mpos).th32HeapID〈〈endl;
}
return0;
}
voidheapinfo(HEAPENTRYSET&hset,ULONG_PTRhid)
{
HANDLEhSnapShot=CreateToolhelp32Snapshot(TH32CS_SNAPHEAPLIST,GetCurrentProcessId());
HEAPLIST32heaplist32;
heaplist32.dwSize=sizeof(HEAPLIST32);
BOOLbRet=Heap32ListFirst(hSnapShot,&heaplist32);
staticinti=0;
while(bRet)
{
HEAPENTRY32he32;
DWORDtotalsize=0,freesize=0;
if(heaplist32.th32HeapID==hid)
{
bRet=Heap32ListNext(hSnapShot,&heaplist32);
continue;
}
DWORDnumber=10;
HANDLEProcessHeap[10];
DWORDnumget=GetProcessHeaps(number,ProcessHeap);
HANDLEhHeap=GetProcessHeap();
he32.dwSize=sizeof(HEAPENTRY32);
Heap32First(&he32,heaplist32.th32ProcessID,heaplist32.th32HeapID);
if(he32.dwFlags&LF32_FREE)
freesize+=he32.dwBlockSize;
totalsize+=he32.dwBlockSize;
cout〈〈“theheapidis:“〈〈he32.th32HeapID〈〈endl;
cout〈〈“theinformationoffirstblock:“〈〈“Blocksize:“〈〈he32.dwBlockSize〈〈“\tAddress:“〈〈(LONG)he32.dwAddress〈〈endl;
if((he32.dwFlags&LF32_FIXED)||(he32.dwFlags&LF32_MOVEABLE))
hset.push_back(he32);
while(Heap32Next(&he32))
{
cout〈〈“theinformationofblock:“〈〈“Blocksize:“〈〈he32.dwBlockSize〈〈“\tAddress:“〈〈(LONG)he32.dwAddress〈〈endl;
totalsize+=he32.dwBlockSize;
if(he32.dwFlags&LF32_FREE)
freesize+=he32.dwBlockSize;
//cout〈〈++i〈〈endl;
if((he32.dwFlags&LF32_FIXED)||(he32.dwFlags&LF32_MOVEABLE))
hset.push_back(he32);
//char*p=(char*)malloc(300);
}
cout〈〈“thetotalsizeofheapis:“〈〈totalsize〈〈endl;
cout〈〈“thefreesizeofheapis:“〈〈freesize〈〈endl;
cout〈〈“thecommitedsizeofheapis:“〈〈(totalsize-freesize)〈〈endl;
bRet=Heap32ListNext(hSnapShot,&heaplist32);
}
CloseHandle(hSnapShot);
cout〈〈“theend“〈〈endl;
}
Byleezy_200003-9-39:38
43. 一个简单的口令保护程式
本程序可加到用户的源程序之前,起到加锁的作用。本程序的特点是:
口令保密性强(保存于文件KLK.CFG中,此文件名也可由用户重新命名),而且用户可随时修改口令。在程序中当用户输入口令后,系统首先测试口令文件KLK.CFG是否存在(只有用户运行了修改口令子函数后才会生成口令文件,否则系统只认默认口令88888888),若存在则从中取出口令字,否则系统用默认口令与用户输入的口令进行较验。另外,修改口令子函数changkl(),可在用户程序中调用,以修改口令文件中的口令字。
本程序涉及到C语言函数getpass(char*),该函数从键盘读取8位字符并返回,且不在屏幕上显示。我们正好利用这一函数功能进行输入口令。
程序清单如下:
#include
#include
voidmain()
{
char*password;
charkl[9]=“88888888“;
inti=0;
FILE*fpl;
window(1,1,80,25);/*屏幕背景清屏成蓝色*/
textbackground(1);
clrscr();
window(17,10,58,13);/*开阴影窗口*/
textbackground(0);
clrscr();
for(i=0;i<=2;i++)
{
window(16,9,56,12);
textattr(14︳2<<4);
clrscr();
gotoxy(13,2);
password=(char*)getpass(“请输入系统口令:“);
textcolor(4);
/*若口令文件KLK.CFG存在,则从中取出口令字*/
if((fp=fopen(“KLK.CFG“,“rb+“))!=NULL)
{
fseek(fp,O,SEEK_SET);
fgets(kl,9,fp);
fclose(fp);
}
if(i==2&&strcmp(password,kl)!=0)
/*三次口令无效退出*/
{
gotoxy(13,2);
cputs(“口令错误,退出!!“);
getch();
exit(0);
}
if(stcmp(password,kl)!=0)
{
gotoxy(13,2);
cputs(“口令错误,重输!!“);
getch();
}
elsebreak;
}
/*进入主程序体*/
textbackground(1);
window(1,1,80,25);
clrscr();
gotoxy(10,10);
cputs(“执行用户主体程序……按任意键进行更改口令!“);
getch();
changkl();/*用户程序中调用修改口令函数*/
}
changkl()/*修改口令子函数*/
{
char*klk;
charbuf[1];
FILE*fp;
window(17,16,58,19);/*开阴影窗口*/
textbackground(0);
clrscr();
window(16,15,56,18);
textattr(14︳4<<4);
clrscr();
gotoxy(8,4);
cputs(“请修改口令字,必须为八位字符“);
gotoxy(14,2);
klk=(char*)getpass(“请输入新口令:“);
gotoxy(14,2);
textcolor(2);
if(srlen(klk)!=8)
{
cputs(“口令字无效,返回!!“);
getxh();
return;
}
cputs(“口令修改成功!!“);
if((fp=fopen(“KLK,CFG“.“w“))!=null)
/*保存口令到文件KLK.CFG*/
{
fputs(klk,fp);
buf[0]=0xia;
fwrite(&buf[0],1,1,fp);
fclose(fp);
}
getch();
returm;
}
44. COM接口
什么是接口?接口有什么作用?如何用接口?
一系列的问题都会缠绕着你。如果你不想做分布式或是本地的应用程序调用的话,就不用看了;
COM1/COM2等硬件接口,我们都不陌生;但是如果要将接口正真用的软件上,而且用好的话,并不一定很容易;让我们继续吧;
我们所谓的接口其实就是一些过程、函数、属性集;记住,接口不可以有字段的,如果你有这个想法的话,那么从现在开始就要认识是错误的,对接口的访问就是对它提供的方法、事件、属性的访问,而且,接口所提供的方法都是公开的,是全部的公开的,所以就不必要用Public了;
在COM中,接口就是一切,一个组件就是一个接口集,用户只用通过接口才能和COM进行找交道;
最通用的接口
IUnKnownInterFace
如果和Com有关的话,一般用IUnKonow,否则用Interface;
说了这么多,还是看个例程吧;
由于时间关系,我只是匆匆忙忙的写了一下,没有时间整理,如果您觉的有用的话,可以自己整理一下,有什么不明白的地方,可以联系我;
没有加注释;
---------------------
unitMain;
interface
uses
Windows,Messages,SysUtils,Variants,Classes,Graphics,Controls,Forms,
Dialogs,StdCtrls,ComObj,ActiveX,StdVCL;
type
TForm1=class(TForm)
Button1:TButton;
procedureButton1Click(Sender:TObject);
procedureFormCreate(Sender:TObject);
private
{Privatedeclarations}
public
{Publicdeclarations}
end;
ILC=Interface(IUnknown)
[’{4FFE6DDB-80B9-4E2D-A05F-5F3B35311ED7}’]
procedureSetValue(NewValue:String);
functionGetValue:String;
end;
TLC=Class(TInterfacedObject,ILC)
public
Value:String;
procedureSetValue(NewValue:String);
functionGetValue:String;
destructorDestroy;override;
end;
var
Form1:TForm1;
IMyLC:ILC;
implementation
{$R*.dfm}
{TLC}
destructorTLC.Destroy;
begin
Application.MessageBox(’资源已经被完全释放’,’操作提示’,MB_OK+MB_ICONINFORMATION);
inherited;
end;
functionTLC.GetValue:String;
begin
Result:=Value;
ShowMessage(Result);
end;
procedureTLC.SetValue(NewValue:String);
begin
////ShowMessage(NewValue);’
/////NewValue:=’第一个COM例程’;
Value:=NewValue;
end;
procedureTForm1.Button1Click(Sender:TObject);
begin
IMyLC.SetValue(’第一个COM例程’);
IMyLC.GetValue;
end;
procedureTForm1.FormCreate(Sender:TObject);
begin
IMyLC:=TLC.Create;
end;
end.
45. 深入VisualStudio.NET
这个新的开发环境提供了史无前例的灵活性,它预示着.NET美好未来。
在今年于亚特兰大举行的2001年技术论坛会议上,比尔.盖茨向一大群热情的开发人员讲述了Microsoft最新的开发环境。实际上,VisualStudio.NET构成了.NET平台的基石,而不仅仅是一个开发环境。构造VS.NET所使用的技术与开发人员创建普通应用程序的技术是一样的。
当然,更多的新闻是围绕微软公司的.NET平台。.NET架构引入了通用语言运行时(CommonLanguage运行时)这样的新特征,它可以让人们在管理底层代码时对编程和脚本语言进行统一管理。在.NET的框架结构中还加入了一种针对Windows程序员的新的编程模型,增加了对ASP的编译支持并引入了Web服务的概念。
有了通用语言运行时的帮助,VS.NET为C++、C#和VisualBasic的程序员提供了一个通用的开发平台。而且,JScript开发人员在创建ASP.NET和Web服务应用程序时也可以从VS.NET中得到一些有限的支持。XML开发人员则会喜欢VS.NET对XML文档、XML数据结构和XSL变换所提供的强大支持。
在接下去的内容中,我们将带你考察一些VisualStudio.NET提供的特色功能。需要说明的是,下面的内容是针对我考察的VisualStudio.NET专业版的Beta2版本来说的。当你看到本专题的时候,VisualStudio.NET企业版的Beta版应该已经问世了。
理解.NET
.NET架构在某种程度上可以看作是一个多语言应用程序的运行环境,它处理与Windows和Web应用程序开发有关的基本性事务。它所提供的应用程序运行环境可以完成内存管理、地址翻译的功能,并能够改善你的应用程序的可靠性、可升级性和安全性。.NET架构由几个部分组成,包括通用语言运行时、ASP.NET和一套丰富的用于构造XMLWeb服务的类库。快速浏览一下.NET的体系结构有助于理解VisualStudio的功能。
.NET的核心是通用语言运行时,它可以管理代码的运行并提供各种服务。换句话说,C++、VisualBasic、C#和JScript的编译程序都要用到运行时的功能。这些开放的接口允许你书写可在管理运行环境下运行的程序代码。(我们把此环境下运行的代码叫做可管理代码。)运行时所管理的事务还包括意外处理、安全性、版本控制、交叉语言集成、组件交互、调试和数据结构描述,并且它还能处理内存管理的细节问题,如创建对象和管理变量引用。
在运行时之上是包含一系列可重复使用的类的基础类库,它与通用语言运行时紧密接合在一起。该类库里面包括了对ADO.NET、XML、SQL、安全性和多线程处理等等的支持。基础类库也让你能够访问操作系统提供的服务,如网络、多线程、图形和加密机制。在基础类库上面是两个编程模型。第一个模型是ASP.NET,这是用来创建基于Web的应用程序的模型。ASP.NET中引入的功能包括:编译服务器页面、新的服务器控制和Web服务,还提供了把脚本逻辑从外观标记中分离出来的Web表单。如果你想知道更多的细节,可以参看下一节“针对Web开发人员的.NET”。
第二个编程模型是Windows窗体。该模型为所有的Windows程序员提供了基于窗体的VB6模型。正如.NET架构上的一系列类库那样,Windows窗体模型所提供的一整套可重用类型大大简化了WindowsGUI开发。Windows窗体可以让你快速创建一个具有完备功能的Windows应用程序,它包含ActiveX控件、弹出式菜单、.NET安全特性访问功能,还可以包括诸如按钮、复选框之类的GUI组件。此外,通过Windows窗体模型,你的应用将可以利用ADO.NET来进行数据访问,并可以使用最新版本的图形设备接口(GDI+)。
针对Web开发人员的.NET
和编译服务器页面一样,Web表单也是ASP开发者肯定要用到的方法。Web表单实际上可能是一组文件,其中的每一个文件执行特定的功能。不过在最基本的层次上来说,Web表单可以被看作是具有aspx后缀的一个ASP文件。事实上,基本的Web表单看上去就象是老式的动态服务器页面。
.NET架构的另一个重要特点是它的数据访问类库,它使用了最新一代的ActiveX数据对象,因此被称为ADO.NET。ActiveX数据对象(与大多数组件或对象类似)是一个简单的黑盒模型,里面装入了专门用来连接ODBC数据库和OLEDB数据源的知识。无需编写复杂的程序代码,它们就能让你很方便地实现操作记录、执行查询和更新数据源等功能。它们提供的接口可以让你无需学习专门的知识就能够实现所有这些功能。作为一名Web应用程序开发者,你所要做的只是把一个ADO对象放入你的ASP页面、指定数据源,然后就可以开始与数据源进行通信了。数据源可以是关系型数据库、索引顺序访问模式数据库(ISAM)或者分级数据库。实际上,只要你有ODBC兼容的驱动程序,ADO就可以使用任何类型的数据源。
Web服务可能是.NET最吸引人的地方。Web服务是你创建的浓缩了某些行为的组件,它可以完成特定的任务,比如计算保险费用或者管理证券交易。正如Microsoft所指出的,Web服务是可用URL寻址的自我描述模型。它们可以组合起来创建大型的应用程序(从简单的处理程序到完整的软件产品)。这些Web应用程序能够动态地改变并创建出新的Web服务。更有用的一点是该应用程序可以通过网络连接进行访问并被实时调用。因为应用程序仅在需要时才调用服务,所以在某种意义上来说你获得了一种“即时的应用程序集成方法”。而所有这些并不需要你预先学习专门的知识(如果它正确地运行的话),也无需进行预先的编程。
尽管只是一款Beta版产品,VisualStudio.NET安装起来却毫不费力。安装过程包括三个关键步骤:更新系统组件、安装.NET架构和加入VisualStudio.NET。如果你选择“完整安装”方式,你可以获得C++及其相关的类库和工具,此外还有C#和VisualBasic。另外,你还能拥有CrystalReport系统、服务器组件和用来发布应用程序的工具。
当你加载该运行环境时,会出现一个类似浏览器的窗口,其中包含一个带有在线资源、更新、新闻、下载等链接的起始页面。其中,“下载”链接特别有用,因为它可以把你直接带到Microsoft的MSDN区域去下载最新的软件、工具包、源代码示例和参考资料。在被称作“WebHosting”的链接页面中提供了一批支持ASP.NET的主机托管服务公司的列表。正如Microsoft所说的,“每个公司都为你提供了一块试验场地。”
另一个链接允许你根据开发的类别改变你的简档。比如,Web开发人员可以选择VisualInterDev简档,让键盘和Window版面都模拟VisualStudio6的方案,并设置帮助文件过滤器来筛选出与Internet开发相关的文档。
如同你在图1中所看到的那样,VS.NET也可以对屏幕进行大多数控制。首先,你可以把多个窗口放到屏幕上,然后用TAB键在窗口之间快速切换。此外,你可以把窗口(比如属性窗口)固定在屏幕上或者放到屏幕边缘隐藏起来。当你的鼠标游历到隐藏窗口时,它会立即滑回到屏幕上。这样你可以很轻松地在浏览窗口、工具条、属性检查器和编辑器之间切换。
开发环境是高度可配置的。通过工具菜单,你能对整个开发环境进行总体设置,也可以分别对每种开发语言的选项进行设置。当你需要在VisualBasic和C#之间进行切换的时候,这种可配置特性就会相当有用。除了控制VS.NET的起始环境之外,你还能对编辑器进行定制,比如设置字体和颜色、为项目和解决方案设置缺省的显示位置。
VS.NET开发环境中还有大量的特点无法一一提及。它对调试程序和监测程序进行了改进,提供的工具可以支持新的开发模型、新的源代码控制方式等等。图2中的表格概括了许多这样的新特点,可以作为一个快速参考。
编辑环境
VisualStudio为VS.NET支持的所有开发语言提供统一的代码编辑器,同时也针对每种语言提供专用的特性。编辑器的某几个地方有了改进,比如自动换行、增量搜索、代码划线、文本折叠、行号、彩色打印和快捷方式等。并且,编辑器针对开发语言提供了很多特殊的功能,比如当你输入原型和函数调用时编辑器会自动帮你完成某些内容的输入。
除了支持程序设计语言的编辑之外,编辑器还支持HTML文档、层叠样式表甚至是XML的开发。实际上,当我装入一个XML文档并看到某些关键标记(如XML声明和属性)被彩色加强显示时,确实感到相当兴奋。此外,编辑器提供了源代码和数据两种查看方式。
在数据查看方式下,文档的结构显示在左边的窗口中。当你在某个层次选中一个XML元素,右边的窗口中就会显示出该元素的子元素并允许你修改元素数据。不过我发现并不是所有的XML文档都可以被正常地载入数据查看模式进行观看。如果你试图在数据查看方式下装入带有不可预见的结构的文档的话,编辑器就会发生混乱。
另一个让人兴奋的发现是VisualStudio.NET允许你在文档实例的基础上建立一个XMLschema模式。缺省情况下当你打开文档实例时会进入源代码查看方式。你可以停留在源代码查看方式或者切换到数据查看方式,然后在窗口中按鼠标右键并从弹出菜单中选择“CreateSchema”。这时会弹出一个对话框让你指定schema模式文档的文件名。一旦schema模式被创建出来,编辑器就会在原始文档实例中加入一个指向该schema模式的指针。对于那些不想费力从头开始编写XMLschema模式的人来说,VisualStudio.NET的确提供了一个省事的办法。
项目和解决方案
VisualStudio.NET的另一个方便之处是解决方案。它的思路是单个解决方案中可以涉及多个项目。你可以在解决方案窗口中象管理独立的项目那样去管理解决方案。因此,你能够对某个解决方案所定义的任一项目中的文件进行访问、创建、编辑和删除。
使用VS.NET来建立独立的项目对于VB、C#和C++的程序员来说是轻而易举的事情。我可以利用VB在几分钟之内创建一个以命令行方式运行的ASP.NET应用程序。开发环境会自动在我的本地Web服务器上创建一个目录,把ASPX和global.asax文件、CSS样式表、一些组成文件和Web.config文件(包含项目配置信息的XML文档)都放进去。你所必须要做的只是在Web浏览器中加载你的aspx文档来运行你的应用程序。
另一方面,对于JScript开发人员来说可能会遇到一些困难,因为JScript没有完全被集成到Microsoft的开发环境(MDE)中。这意味着你必须手工建立文件夹,然后还要手工创建并管理里面的很多文件。
语言上的改变
如同它所支持的平台一样,VisualStudio.Net从编程语言角度来看也有重大的改变。特别是,VisualBasic的程序员会发现明显的变化,因为VB已经与通用语言运行时集成到一起。其结果就是,你也许不得不重新设计并修改大段的代码。对初学者来说,继承和多态性的增加意味着VB最终成为了真正面向对象的程序语言。现在VB不再需要你去考虑诸如程序调用越界之类的问题。VB也引入了结构化的意外处理、对类COM界面的支持和多线程处理。另外,很多语言元素已经被去掉,还有一些则被新的属性、方法和函数所代替。Microsoft公司对那些不再有用的、过时的VB元素进行了清除。在图3中对VB所发生的改变进行了总结和概括。
另外,JScript开发人员也会感觉到很明显的变化。由于语言编译方面的需要,现在所有的JScript变量都必须被事先声明。此外,数据类型的概念已经被引入。以前,JScript程序员创建变量时不需要把它们同某种数据类型联系起来。不过现在.NET应用程序要求你必须为变量指定数据类型。数据类型本身并不会让JScript程序员过分为难,但数据类型可能会给JScript程序员带来他们以前从来没有碰到过的一系列新问题(比如数据类型兼容性)。JScript也引入了类、函数越界的概念,并可以获取和设置属性。其他的语言改进还包括常量声明、枚举类型和新的导入语句。它已经和上一代Script语言完全不同。
VisualStudio.NET确实是一个功能强大的开发环境。对于VisualStudio.NET所提供的功能,我在这里只能给读者做一些表面的介绍。对通用语言的支持让开发者可以在C++、VisualBasic和C#之间自由地切换。编辑器同样能够支持XML文档、XMLschema、HTML和层叠样式表的创建。调试和监测程序有所改善,并提供了支持软件发布、源代码控制等等功能的新工具。是的,对于想要成为.NET程序员的人来说开发环境发生了全新的改变。没有VisualStudio的帮助,你根本无法想象如何去创建.NET应用程序。
46. 邮件代理服务器及其实现
邮件代理服务器
代理服务器是一种中转机构,它将来自客户端的请求转至远程服务器,同时将服务器的应答返回到客户端。其目的有二:一是使不可达路径变为可达,即路由型;二是对通信过程实施预定的监控,即应用型。
邮件代理服务器专用于电子邮件的收发传递,其中用于邮件发送的称为SMTP代理,用于邮件接收的称为POP3代理。电子邮件的收发分别采用SMTP和POP3协议,邮件客户端程序和邮件服务器之间通过一系列约定的命令序列和相应的应答信息,完成邮件收发。应用型邮件代理对邮件收发过程实施监控,必须对邮件的协议、编码等进行具体处理;而路由型代理只负责邮件请求和回应的中转,实现较为简单。本文给出一个路由型代理示例,在此基础上,通过截获和深入分析了解邮件收发过程全部信息,可以按需要进行应用型邮件代理的设计。本文程序中,SMTP和POP3都基于TCP/IP协议,编程接口为SOCKET,采用比较简明的面向对象设计方法,用VB语言实现。
SMTP代理服务器的实现
在FORM中设计两个SOCKET控件SktClient和SktServer,前者作为服务器方与邮件客户程序通信,后者作为客户方与远程邮件服务器通信。设计两个TEXT控件TxtClient和TxtServer,用于显示邮件客户端发送的命令请求及邮件服务器回应信息,置其属性为多行可滚动。在Form_Load()过程中填充以下语句,使SMTP代理运行后处于监听状态:
’SMTP缺省端口号
SktClient.LocalPort=25
’监听邮件客户端连接请求
SktClient.Listen
监听到邮件客户端连接请求后,SMTP代理服务器立即连接远程邮件服务器,在SktClient_ConnectionRequest()中加入:
’关闭监听,进行处理
IfSktClient.State〈〉sckClosedThenSktClient.Close
’取得连接的ID号,连接远程邮件服务器
SktClient.AcceptrequestID
’远程邮件服务器地址
SktServer.RemoteHost=“10.120.15.205“
’端口号25=SMTP
SktServer.RemotePort=25
’连接
SktServer.Connect
SktServer收到应答后在文本框TxtServer中显示数据,并转发给邮件客户端:
PrivateSubSktServer_DataArrival(ByValbytesTotalAsLong)
DimstrdataAsString
’取出数据
SktServer.GetDatastrdata
’取出数据
TxtServer.Text=TxtServer.Text+vbCrLf+strdata
TxtServer.Refresh
’服务器回应并转发到客户端
SktClient.SendDatastrdata
SktClient收到客户端数据后,取出数据在文本框TxtClient中显示,并将数据发送给远程邮件服务器:
SktClient_DataArrival(ByValbytesTotalAsLong)
DimstrdataAsString
’取出数据,放在字符串strdata中
SktClient.GetDatastrdata
’在文本框中分行显示
TxtClient.Text=TxtClient.Text+vbCrLf+strdata
TxtClient.Refresh
’邮件客户端请求转发到远程服务器
SktServer.SendDatastrdata
在TCP/IP通信中,断开与连接一样由客户方发起,服务器方接收CLOSE消息,因此在SktClient_Close()中进行下列处理:
’关闭与邮件客户端的连接
SktClient.Close
’关闭与远程邮件服务器的连接
SktServer.Close
’继续监听下一次连接请求
SktClient.Listen
通过分析两个文本框中的信息,可以了解邮件发送整个过程的操作。由此根据SMTP协议可分离出发信人、收信人、主题、信件编码与内容、附件编码与内容等关键信息,使之扩展为应用型代理,对邮件发送进行监控。
POP3代理服务器的实现
POP3代理的难点在于要支持多信箱取件,因邮件客户端程序一般支持多个POP3信箱,所以设计前要了解邮件收取的过程。邮件客户程序首先向服务器发送连接请求,服务器应答后,客户端发送USER命令传递POP3用户名,服务器确认后,客户端发送用户口令,服务器再次确认,之后进入取信操作。多个POP3信箱下,远程邮件服务器是不确定的,故POP3代理要连接的对象不定。使用POP3代理时,通常在邮件客户程序的服务器处填上代理地址,再在用户名之后加隔离符注明实际POP3服务器地址(如hzh/10.120.15.205)。下面给出其实现代码,该控件设置与SMTP代理相同。在Form_Load()中添加如下代码:
’全局变量,1表示客户端已发出连接请求
First=0
’POP3端口号
SktClient.LocalPort=110
’监听邮件客户端收取信件的连接请求
SktClient.Listen
SktClient收到邮件客户端请求时,远程POP3服务器地址还是未知的,不能进行连接,为使邮件客户程序继续进行操作,POP3代理必须向其发送伪应答信息:
SubSktClient_ConnectionRequest()
’关闭监听,处理连接
IfSktClient.State〈〉sckClosedThenSktClient.Close
’取得连接标识
SktClient.AcceptrequestID
’设置全局标志
First=1
’向邮件客户端发送伪应答信号
SktClient.SendData“+OKPOP3serverready“+vbCrLf
邮件客户端收到伪应答后,接着向POP3代理发送USER命令,其中带有远程邮件服务器地址。POP3代理分离出邮件服务器地址,进行实际连接:
SubSktClient_DataArrival(ByValbytesTotalAsLong)
Dimstrdata,RemotePOP3SrvAsString
DimPosAsInteger
’取得邮件客户端发来数据
SktClient.GetDatastrdata
’分行显示
TxtClient.Text=TxtClient.Text+vbCrLf+strdata
TxtClient.Refresh
’邮件客户端发送USER命令时,POP3代理服务器解析远程服务器地址,并进行实际连接
IfLeft(strdata,4)=“USER“Then
’找到字符“/“在串中的位置
Pos=InStr(strdata,“/“)
’用户名,为全局变量
UserInfo=Left(strdata,Pos)
’远程POP3服务器地址
RemotePOP3Srv=Right(strdata,len(strdata)-Pos)
SktServer.RemoteHost=RemotePOP3Srv
’pop3端口号
SktServer.RemotePort=110
’连接远程POP3服务器
SktServer.Connect
’其他情况下向服务器转发
Else
SktServer.SendDatastrdata
EndIf
远程POP3服务器收到代理的连接请求后,回送应答信息,该信息不再回传客户端。由于此时邮件客户端已经发出USER命令,POP3代理再立即将此命令补发给远程POP3服务器:
SubSktServer_DataArrival()
DimstrdataAsString
’取服务器信息
SktServer.GetDatastrdata
’显示应答信息
TxtServer.Text=TxtServer.Text+vbCr+strdata
TxtServer.Refresh
’第一次应答
IfFirst=1Then
’复位标志
First=0
’补发USER命令
SktServer.SendDatauserinfo+vbCrLf
’非第一次应答
Else
’转发到邮件客户端
SktClient.SendDatastrdata
EndIf
其他情况下POP3代理只做信息中转并记录,通过分析两个文本框中的内容,可以了解电子邮件收取全过程的所有信息。根据POP3协议,可以分离出发信人、收信人、主题、信件编码与内容、附件编码与内容等关键信息,为实现收信监控奠定基础。
邮件代理服务器的应用
邮件代理服务器可分为单用户和多用户两种版本。单用户版本在某一时间只支持一个用户操作。如本文的例子,它安装在邮件客户机器上,邮件程序中服务器地址可简单设为127.0.0.1或localhost。多用户版本可同时为多个邮件客户端做代理,其思想是由父线程专门负责侦听,侦听到连接后创建一个子线程具体处理该路通信,它通常安装在服务器上,在邮件客户程序中将服务器地址指向此机器。
要实现邮件代理服务器的应用,必须理解邮件编码格式。PC机上电子邮件的编码分两类,即UUENCODE格式和MIME格式。前者起源于UNIX系统,后者为一种多目标扩展,应用越来越广。MIME格式中,BASE64主要用于附件的编码,信件主体多使用QP、UTF-8(UNICODE)、UTF-7和HZ格式。它们具有明显的标志,判明编码格式后进行解码得到明文,处理完毕再编码转发或换码转发。应用型邮件代理服务器在实际中有十分广泛的应用,如邮件在线加密/解密、邮件过滤和病毒在线检查等。
47. 一个小巧的数据库压缩算法
是这样一件尴尬的事促使我们寻找一种压缩算法:我们刚刚制作完成的欧洲十五国进出口商数据库及其检索系统要占用700兆空间,它刚好放不到一张光盘上去!
常用的压缩工具(如WinZip或ARJ)现在也不起作用,因为我们并不是对整个数据库进行压缩(那样光盘上的数据将无法检索),而只是要将数据库中的某些字段的内容压缩后存入库中——以减少整个数据库占用的空间——然后在使用中动态解压将数据还原为本来面目。
最理想的办法当然是数据库系统(DBMS)本身直接支持数据压缩存储,但令人遗憾的是:常见的DBMS均未提供该功能。
在互联网上确实能找到一些有关数据压缩的思想、算法甚至C语言的部分源代码,但它们大都过于复杂,或应用范围有限(如仅对图像或纯数字数据有效),或是在版权方面有太苛刻的要求。
最后我们采用了自行设计的算法。该方法的压缩率只有20%至25%,但它小巧、容易实现,在实际应用中取得了良好的效果。
一、算法概述
压缩冗余的比特位(bit)是常见的压缩思想之一。
例如,字符串CCW-2000的二进制表示为:
01000011,01000011,01010111,00101101,00110010,00110000,00110000,00110000
其中每个二进制的前导“0”是冗余的,去掉前导“0”后的表示为:
1000011,1000011,1010111,101101,110010,110000,110000,110000
这显然达到了数据压缩的目的,但同时也带来一个很大的问题:二进制流仅仅由“0”和“1”组成,并不存在上面为了表述清晰而加入的“,”。即:由于压缩后的二进制不再是“定长”的,两个二进制之间如何“划界”成了难题。
常见的解决方案有:霍夫曼表(Huffmancodes)、LZW的动态查询表、Markov的多维长度字典表,以及根据前值和前长度变换应用规则的DAKX方法等。但这些方法大都过于繁杂或未公布技术细节而难以实现。
为了实现起来简捷,避免用“宰牛刀”,我们设计的算法采用一种折衷的思想:用“缩短”了的“定长”二进制来实现压缩,同时又避免了“划界”的难题。
具体思路是:
(1)所有字符的二进制长度均为6个bit而不是一般的8个bit。即每个字符节约2个bit。这决定了本方法的理想(最高)压缩率为25%。
(2)由于6个bit仅能区分64种字符,故将内码小于127的字符分为“常用”和“不常用”两组。
(3)第一组由英文字母(大小写)、0至9的数字、空格以及一个控制字符
组成;第二组由内码小于127的其它字符组成。
(4)每组内的字符均重新编码,以使其“新内码”均小于64。
(5)第二组的每个字符前均以第一组中的控制字符做前缀。
(6)内码大于等于127的字符用普通的8-bit表示,前缀是两个连续的第一组中的控制字符。
(7)本方法显然仅适用于西文文本。
(8)压缩后的数据相当于被加密了,这大大提高了信息安全性。
二、算法实现
我们制作的西欧进出口商数据库中有大量大段的企业英文描述,故应用上述算法非常成功,如愿以偿地将整个系统的大小降到了600兆以下,可以轻松地装入一张光盘中。
检索系统用VB编程,数据库用的是MS-Access。
后附程序为清晰起见,将压缩数据库的功能改为压缩文本文件的功能。该程序已在机器上测试通过,可作为一个压缩工具单独运行(为了同时说明压缩及解压缩这两个函数的使用,下面的程序会将刚被压缩的文件再进行解压缩,解压后文件与源文件是完全一样的)。
附完整的VB源代码:
OptionExplicit
Dimcomped$,compingAsString*1,comping_p%,bit_p&
Dimebitmask(1To8)AsInteger’存储掩码的数组
SubCommand1_Click()
Dimibuf$,obuf1$,obuf2$
MousePointer=11
’设置掩码
ebitmask(1)=128:ebitmask(2)=64:ebitmask(3)=32:ebitmask(4)=16
ebitmask(5)=8:ebitmask(6)=4:ebitmask(7)=2:ebitmask(8)=1
Open“d:\temp\compress\theory.txt“ForInputAs#1’压缩前的源文件
Open“d:\temp\compress\theory.6bt“ForOutputAs#2’压缩后的文件
Open“d:\temp\compress\theory_2.txt“ForOutputAs#3’解压后的文件
DoWhileNotEOF(1)
LineInput#1,ibuf
obuf1=DoCompress(ibuf)
Print#2,obuf1
obuf2=UnDoCompress(obuf1)
Print#3,obuf2
Loop
Close#1,#2,#3
MousePointer=0
EndSub
FunctionDoCompress(in_buf$)AsString’对输入的字符串进行压缩
Dimi&,buf_len&,cAsString*1
comped=““:comping=Chr(0):comping_p=0
buf_len=Len(in_buf)
Ifbuf_len〉0Then
Fori=1Tobuf_len
c=Mid(in_buf,i,1)
SelectCasec
Case““,“A“To“Z“,“a“To“z“
putbits0,c’第一组中的字符
Case“!“To“/“,“:“To“@“,“[“To“`“,“{“To“~“,Chr(1)ToChr(31)
putbits1,c’第二组中的字符
CaseElse
putbits2,c’其它字符
EndSelect
Nexti
putbits3,Chr(0)
EndIf
DoCompress=comped
EndFunction
Subputbits(flag%,cc$)’压缩冗余的比特位(bits)
Dimi%,cAsString*1
c=cc
SelectCaseflag
Case0’对第一组中的字符内码进行重新定位
SelectCasec
Case““
c=Chr(1)
Case“0“To“9“
c=Chr(Asc(c)-46)
Case“A“To“Z“
c=Chr(Asc(c)-53)
Case“a“To“z“
c=Chr(Asc(c)-59)
EndSelect
Case1’对第二组中的字符内码进行重新定位
SelectCasec
Case“!“To“/“
c=Chr(Asc(c)-32)
Case“:“To“@“
c=Chr(Asc(c)-42)
Case“[“To“`“
c=Chr(Asc(c)-68)
Case“{“To“~“
c=Chr(Asc(c)-94)
CaseChr(1)ToChr(31)
c=Chr(Asc(c)+32)
EndSelect
Fori=1To6
putbit0
Nexti
Case2
Fori=1To12
putbit0
Nexti
Fori=1To8
If(Asc(c)Andebitmask(i))〈〉0Then
putbit1
Else
putbit0
EndIf
Nexti
Case3
Fori=comping_p+1To9
putbit0
Nexti
EndSelect
Ifflag〈2Then
Fori=1To6
If(Asc(c)Andebitmask(i+2))〈〉0Then
putbit1
Else
putbit0
EndIf
Nexti
EndIf
EndSub
Subputbit(bit%)’设置比特位(bit)
comping_p=comping_p+1
Ifcomping_p〉8Then
comped=comped+comping
comping=Chr(0)
comping_p=1
EndIf
Ifbit=1Then
comping=Chr(Asc(comping)Orebitmask(comping_p))
EndIf
EndSub
FunctionUnDoCompress(in_buf$)AsString’对输入的字符串进行解压缩
Dimbits_buf%,out_buf$,cAsString*1,comped_len&
comped=in_buf:bit_p=1:comped_len=Len(comped)*8
DoWhilebit_p〈=comped_len
Ifcomped_len-bit_p〈5Then
ExitDo
EndIf
bits_buf=getbits(6)
Ifbits_buf〈〉0Then’根据控制字符判断字符的组别
SelectCasebits_buf
Case1
c=““
Case2To11’“0“To“9“
c=Chr(bits_buf+46)
Case12To37’“A“To“Z“
c=Chr(bits_buf+53)
Case38To63’“a“To“z“
c=Chr(bits_buf+59)
EndSelect
out_buf=out_buf+c
Else
Ifbit_p〉comped_lenThen
ExitDo
EndIf
bits_buf=getbits(6)
Ifbits_buf〈〉0Then
SelectCasebits_buf
Case1To15’“!“To“/“
c=Chr(bits_buf+32)
Case16To22’“:“To“@“
c=Chr(bits_buf+42)
Case23To28’“[“To“`“
c=Chr(bits_buf+68)
Case29To32’“{“To“~“
c=Chr(bits_buf+94)
Case33To63’Chr(1)ToChr(31)
c=Chr(bits_buf-32)
EndSelect
out_buf=out_buf+c
Else
bits_buf=getbits(8)
out_buf=out_buf+Chr(bits_buf)
EndIf
EndIf
Loop
UnDoCompress=out_buf
EndFunction
Functiongetbits(numbAsInteger)AsInteger’获取比特位(bit)
Dimbyte_p&,bit_o%,byte1AsString*1,i%,j%,k%,m%
byte_p=(bit_p\8)+1:bit_o=bit_pMod8
byte1=Mid(comped,byte_p,1):j=0:k=0
Fori=bit_oTo8
k=k+1
Ifk}numbThen
ExitFor
EndIf
If(Asc(byte1)Andebitmask(i))〈〉0Then
j=jOrebitmask(IIf(numb=6,k+2,k))
EndIf
Nexti
Ifk〈numbThen
byte1=Mid(comped,byte_p+1,1):m=numb-k
Fori=1Tom
k=k+1
If(Asc(byte1)Andebitmask(i))〈〉0Then
j=jOrebitmask(IIf(numb=6,k+2,k))
EndIf
Nexti
EndIf
bit_p=bit_p+numb
getbits=j
EndFunction
SubCommand2_Click()
End
EndSub
48. 浅谈电话语音查询系统
电话语音查询系统已广泛应用于电话银行、话费查询、证券委托、自动缴费(水、电、气等费用)、语音信箱、自动声讯服务、民航、铁路等部门的信息查询以及各种公共场所自动回答顾客提问等领域。
电话语音查询系统的工作流程是用户拨通电话语音查询系统的热线电话,并根据电话中的语音提示,通过按电话键来查询电脑中存储的各种信息;电脑自动对用户的操作进行应答,并以语音形式将信息反馈给用户。
系统构成
电话语音查询系统是现代电信技术与计算机技术高度结合的产物,一般由硬件和软件两部分组成。
1.硬件
电话语音查询系统的硬件部分主要包括:电脑、电话语音卡、外线(普通市话线路)或内线。
硬件部分的安装过程极为简单。只要将电话语音卡插入电脑的扩展槽中,再将电话线接至电话语音卡提供的电话插座上,即可构成电话语音查询系统的硬件部分。
在电话语音卡的选择方面,首先应选择符合国家电信标准和国际电信标准,达到原邮电部入网规范,同时动态范围大、信噪比高、音质好的电话语音卡产品。其次应选择采用电话接口模块与语音处理板分离结构的产品,以便可以根据实际需要灵活配置电话线数和内外线比例。电话接口模块分用户模块(也称内线模块)和中继模块(也称外线模块)两种。其中用户模块可以直接驱动电话,所连电话一摘机即可开始工作,常用来调试程序或放到营业大厅供用户使用。中继模块连接市话网的电话线或小型程控交换机的用户线,本身相当于一部电话机。
2.软件
软件一般由操作系统、电话语音卡底层驱动软件和二次开发接口软件、电话语音查询系统应用软件三部分构成。由于目前电话语音卡的国际标准尚未制订,因此不同生产厂家仍需随卡提供支持各种电话语音功能(如录音、放音、接收和发送双音频码等)的底层驱动软件。驱动软件与应用软件的接口一般采用软件中断调用方式(如INT9FH),可以直接在汇编语言或C语言等高级语言编写的程序中调用。此外,很多生产厂家还提供了方便用户进行二次开发的各种编程语言接口。
电话语音查询系统应用软件由电话语音处理和数据库处理两大程序模块组成。
电话语音处理程序模块的主要任务是负责完成(通过调用底层驱动软件)每条线路的摘挂机控制、放音、录音、接收由用户按键产生的双音频信号以及发送双音频信号等功能,并能够检测各种信号音,如占线忙音等。
DTMF(DualToneMultiFrequency)双音多频(简称双音频)信号由CCITT制订并推荐作为按键式电话的标准,目前广泛用于电话拨号。双音频信号由两种频率组合而成,每个DTMF信号由一个低频信号和一个高频信号组成,一共可以产生16种信号,分别代表0~9、*、#等。
电话语音处理程序模块中信号音检测部分用于确定电话线路的状态,如拨号音、占线忙音(即被叫用户忙音)、振铃音(外线振铃或内线摘挂机)、挂机忙音(检测对方是否挂机)等。
由于绝大多数电话语音查询系统允许一边进行电话语音查询作业,一边进行数据库的增、删、改及系统维护管理,因此,电话语音处理程序模块一般均在后台运行。
电话语音查询最终都要对数据库进行操作。数据库处理程序模块是电话语音查询系统最重要的组成部分之一。
由于数据库处理程序模块除了允许电脑操作员实时进行数据库的增、删、改操作外,还必须同时对用户通过电话提出的各种数据处理要求(可能包括写数据库操作)进行实时响应,因此,保证数据库的完整性是数据库处理程序模块的一项重要任务。
系统的开发设计
根据电话语音查询系统的电话操作与电脑操作能否并行运作,可以将电话语音查询系统分为单任务系统和多任务系统两大类。
1.单任务系统
若电话语音查询系统在进行电话语音查询作业时,操作员不能同时再用电脑对其进行操作(除非暂停电话语音查询作业),则所有的查询与任务均可以在前台完成。这类系统属于典型的单任务系统,程序设计较为简单,应用面较窄。
这类系统的电话处理程序模块较为简单,一般采用循环方式依次对各条电话线路的请求进行处理。
//无限循环,直至满足结束条件(如按ESC键)为止
while(!FINISHED)
{
for(ChannelNo=0;ChannelNo/td>
{//循环处理每条线路
processing(ChannelNo);//电话语音查询处理函数
}/*for*/
}/*while*/
其中电话语音查询处理函数(processing)的功能如下:
●若检测到某条电话线路的振铃信号(外线)或摘机信号(内线),则转入相应的子程序进行放音、录音、接收用户按键码或自动拨号等处理;
●大多数电话语音卡上每条线路的实际录放音以及收发双音频码工作均由底层驱动软件来控制完成,且可以同时进行,互不影响。这样,应用程序只需发出录放音或收发双音频码命令即可继续往下执行程序(如继续处理其他线路的请求等),无需等待。当然,有时可能希望某个线路在录放音或收发双音频码工作尚未完成之前,不能对该线路进行其他操作。在这种情况下,应用程序可以调用底层驱动软件对该线路进行检测。若前面工作尚未完成,则循环至下一条线路继续进行处理。若前面工作已完成,则视程序的具体要求进行相应处理,例如根据用户的按键码进行相关的数据库处理,并将处理结果通过电话反馈给用户。
●自动拨号后,应根据检测到的信号音做出相应的处理。例如:若检测到被叫用户占线忙音,则可以重新拨号或者隔一段时间之后再重新拨号;若检测到对方无人接听,则放弃拨号操作或隔一段时间之后再重新拨号。
●当处理完某条电话线路的请求或检测到用户挂机信号(通过检测挂机忙音进行判别),则执行该线路的挂机动作,并重新对该线路进行初始化,以便处理来自该线路的下一个请求任务。
●当需要读出一系列数字(如123,1996等)时,可以采用组合放音法,即依次播放每个数字对应的语音文件。由于每读一个数字均需打开和关闭该数字对应的语音文件,因此势必造成时间上的一些延迟,进而影响到语音的连贯性。建议将每个数字的语音文件事先读入内存中,这样就避免了因读写磁盘而影响语音的连贯性。
2.多任务系统
若电话语音查询系统允许电话操作与电脑操作并行运作,亦即所谓的多任务系统,则相应的程序设计工作略为复杂。由于此类系统功能强,且具有较大的灵活性,因此,大多数电话语音查询系统均属此类。
与第一类电话语音查询系统相比,为了实现并行操作,此类系统的电话处理程序模块一般由时钟中断处理程序按照设定的时间间隔(如每秒18次)自动调用。由于间隔时间相对较短,因此可以用来模拟实时多任务系统,而数据库处理程序模块要求既能被电话处理程序模块调用,又能被系统主程序(前台)调用。
●对各条电话线路的处理采用循环方式,即每次产生时钟中断时,依次对各条线路进行循环处理。时钟中断处理程序与电话处理程序模块的相互关系大致如下:
//时钟中断处理程序
timer_interrupt()
{
for(ChannelNo=0;ChannelNo/td>
{//循环处理每条线路
processing(ChannelNo);//电话语音查询处理函数
}/*for*/
}/*timer_interrupt*/
为避免前面指令尚未处理完又产生新的定时器事件,应在时钟中断处理程序前面设置一个进出标记(应为static静态变量)。若此标记置0,则可以进入循环进行处理;若此标记置1,则说明上一次处理尚未完成,中断程序无条件返回。
对于像DOS这样的单任务操作系统来说,由于上述程序中对所有电话线路的处理均集中在一次中断调用过程中(视任务繁忙情况可能跨越几个时钟周期)完成,因此,若前台任务量大,则可能因前台分配到的时间片过少而影响前台任务的执行效率。这时可采用每次中断仅处理一条线路的方法轮流进行处理:
//全局变量,表示正在处理的电话线路号
intCurrentChannel=0;
timer_interrupt()//时钟中断处理程序
{
//若一轮循环结束,则开始新一轮循环
if(CurrentChannel==MAXCHANNELS)
CurrentChannel=0;
//电话语音查询处理函数
processing(ChannelNo);
//准备处理下一条电话线路
++CurrentChannel;
}/*timer_interrupt*/
注意:此类电话语音查询系统的电话处理程序模块中不允许出现无限等待的现象,如等待用户按键盘某个键等。
●若电话语音查询系统不需要对数据库进行写操作,则情况要简单得多。只要以共享方式,而不是以独占方式打开数据库即可,当然还可以进一步指定以只读方式打开数据库。当两个或两上以上的进程同时对数据库中的同一个数据进行读写操作时,将导致数据库数据的不一致。为确保数据的正确性,必须采取数据保护措施。首先,只能以共享方式,而不允许以独占方式打开数据库。其次,必须以读写方式打开数据库。最后,还要确保通过电话提交的数据库操作请求与电脑操作员正在进行的数据库操作不发生冲突,即两个进程不能同时读写同一数据。为防止这种情况,可以采用自动文件加锁与解锁方式,如某些数据库操作命令(如append命令)在执行时自动给数据库文件加锁,并且在执行完后自动解锁;或者使用lockfile和unlockfile等命令显式地进行数据库文件的加锁与解锁,还可以使用lockrecord或unlockrecord等命令显式地进行数据库记录的加锁与解锁。
●对于采用支持多任务的操作系统作为开发和运行平台的电话语音查询系统来说,电话操作(作为定时器中断事件处理程序的一部分)与电脑操作,作为同一程序的两个进程并发地运行没有任何问题,程序设计也较为简单,故不再赘述。
相比之下,对于把DOS这样的单任务操作系统作为开发和运行平台的电话语音查询系统来说,程序设计要困难得多。为了实现电话语音查询系统的电话操作与电脑操作并发运行,必须将其模拟成一个实时多任务系统。
实时多任务系统的特点是事件驱动和多任务处理。任务是多任务系统中独立运行的基本单元,也是系统分配和调度的基本单元。每个任务均由程序段、数据段、堆栈段和一个任务控制块(包括当前任务的优先级标识、堆栈指针和运行状态等)组成。系统通过任务控制块PCB确定任务的运行状态,并按照任务的优先级调度多任务并发运行,即允许就绪任务中具有较高优先级的任务抢占CPU运行,并暂时中断较低优先级任务的执行。
49. 如何写程序维护手册
相信看帖的朋友都有这么一段难熬的时间,就是程序开发出来之后,需要撰写程序维护手册。每次总是绞尽脑汁,但是感觉写出来的维护手册效果不是很好。而往往一本好的程序维护手册对于程序将来的使用和维护都是很重要的。因为我们不能要求使用者或者是后来看此程序的人再花很多时间来研究这个程序,必须通过该程序的维护手册,让后续人员阅读以后能快速的掌握该程序的大体。
我现在提供一种比较通用的程序维护手册书写格式。希望能给大家带来方便。
文档编号:
版本号:
密级:
文档名称:XXXX程序维护手册
项目编号:
项目名称:
开发部门:
项目负责人:
编写年月日
校对年月日
审核年月日
批准年月日
程序维护手册
1引言
1.1编写目的
[阐明编写维护手册的目的,简述其内容。指出读者对象(程序维护人员、研发人员)。]
1.2开发单位
[说明项目的提出者、项目的委托单位、开发单位和使用场所。]
1.3定义
[列出本文挡中用到的专业术语的定义和缩写词的原文。]
1.4参考资料
[可包括:a.用户操作手册;b.于本项目有关的文档。列出这些资料的作者、标题、编号、发表日期、出版单位或资料来源以及保密级别。]
2系统说明
2.1系统用途
[说明系统具备的功能,输入和输出。]
2.2安全保密
[说明系统安全保密方面的考虑。]
2.3总体说明
[说明系统的总体功能、对子系统和作业作出综合性的介绍,并用图表方式给出系统主要部分的内部关系。]
2.4程序说明
[说明系统中每一程序、分程序的细节和特性。]
2.4.1程序1的说明
2.4.1.1功能[说明程序的功能。]
2.4.1.2方法[说明实现方法。]
2.4.1.3输入[说明程序的输入、媒体、运行数据记录、运行开始时使用的输入数据的类型和存放单元、与程序初始化有关的入口要求。]
2.4.1.4处理[处理特点和目的,如:a.用图表说明程序的运行的逻辑流程;b.程序主要转移条件;c.对程序的约束条件;d.程序结束时的出口要求;e.与下一个程序的通信与联结(运行、控制);f.由该程序产生并供处理使用的输出数据类型和存放单元。g.程序运行所用存储量、类型及存储位置等。]
2.4.1.5输出[程序的输出。]
2.4.1.6接口[本程序与本系统其他部分的接口。]
2.4.1.7表格[说明程序内部的各种表、项的细节和特性。对每张表的说明至少包括:a.表的标识符;b.使用目的;c.使用此表的其他程序;d.逻辑划分,如块或部,不包括表项;e.表的基本结构;f.设计安排,包括表的控制信息。表目结构细节、使用中的特有性质及各表项的标识、位置、用途、类型、编码表示。]
2.4.1.8特有的运行性质[说明在用户操作手册中没有提到的运行性质。]
2.4.2程序2的说明[与程序1的说明相同。以后其他各程序的说明相同。]
3操作环境
3.1设备
[逐步说明系统的设备配置极其特性]
3.2支持文件
[列出系统使用的支持软件、包括他们的名称和版本号。]
3.3数据库
[说明每个数据库的性质和内容,包括安全考虑。]
3.3.1总体特征
[如:a.标识符b.使用这些数据库的程序;c.静态数据;d.动态数据;e.数据库的存储媒体;f.程序使用数据库的限制。]
3.3.2结构及详细说明
3.3.2.1说明该数据库的结构,包括其中的记录和项;
3.3.2.2说明记录的组成,包括首部或或控制段、记录体;
3.3.2.3说明每个记录结构的字段,包括:标记或标号、字段的字符长度和位数该字段的允许值范围。
3.3.2.4扩充:说明为记录追加字段的规定;
4维护过程
4.1约定
[列出该软件系统设计中所使用全部规则和约定,包括:a.程序、分程序、记录、字段和存储区的标识或标号助记符的使用规则;b.图表的处理标准、卡片的连接顺序、语句和记号中使用的缩写、出现在图表中的符号名;c.使用软件的技术标准;d.标准化的数据元素极其特征。]
4.2验证过程
[说明一个程序修改后,对其进行验证的要求和过程(包括测试程序和数据)及程序周期性验证的过程。]
4.3出错及纠正方法
[列出出错状态及其纠正方法。]
4.4专门维护过程
[说明文档其他地方没有提到的专门维护过程,如:a.维护该软件系统的输入部分(如数据库)的要求、过程和验证方法;b.运行程序库维护系统所必须的要求、过程和验证方法;c.对闰年、世纪变更所需要的临时性修改等。]
4.5专用维护程序
[列出维护软件系统使用的后备技术和专用程序(如文件恢复程序、淘汰过时文件的程序等)的目录,并加以说明,内容包括:a.维护作业的输入输出要求;b.输入的详细过程及硬件设备上建立、运行并完成维护作业的操作步骤。]
4.6程序清单和流程图
50. B/S模式下实现EXCEL报表的生成与打印
1、前言
报表打印通常是管理信息系统中的一个重要模块,而Excel凭借它功能强大、应用灵活、通用性强等的优势在报表打印中获得了广泛的应用。
最初的管理信息系统基本上是采用客户机/服务器(C/S)模式开发的,但随着WWW的广泛应用,目前的管理信息系统已经逐渐开始从C/S模式向浏览器/服务器(B/S)模式转变。B/S模式具有传统C/S模式所不及的很多特点,如更加开放、与软硬件无关、应用扩充和系统维护升级方便等等,目前已成为企业网上首选的计算模式,原先在C/S下的很多软件都开始移植到B/S模式下。由于B/S模式的特殊性,在C/S下相对较易实现的Excel报表打印功能在B/S下却成为一个难点。本文根据在实际的项目中总结的经验,以ASP为例,给出了一个较好的通用方法。
2、功能实现
为了说明问题,这里举一个例子。系统平台是Windows2000+SQLServer2000+IIS5.0+ASP3,报表采用的是Excel,要求按照给定的报表格式生成图书销售统计的报表,并能够打印。
2.1Excel报表模板的制作
首先根据给定的报表格式,制作一个Excel模板(就是要打印的报表的表格),当然其中需要从数据库中动态统计生成的数据留着空白。这个报表先在Excel中画好,然后保存为模板,存放在起来,这里为\test\book1.xlt。
2.2Excel报表的生成与打印
这里采用了Excel的Application组件,该组件在安装Excel时安装到系统中。我们的操作也都是针对该组件。
(1)建立Excel.Application对象
setobjExcel=CreateObject(“Excel.Application“)
(2)打开Excel模板
objExcel.Workbooks.Open(server.mappath(“\test“)&“\book1.xlt“)’打开Excel模板
objExcel.Sheets(1).select’选中工作页
setsheetActive=objExcel.ActiveWorkbook.ActiveSheet
(3)Excel的常规添加操作
例如sheetActive.range(“g4“).value=date()‘这里添加的是时间,当然也可以是你指定的任何数据
(4)Excel中添加数据库中的纪录
这里假设已有一个数据集adoRset,存放由Sql操作生成的统计数据。
num=7‘从Excel的第七行开始
dountiladoRset.EOF‘循环直至数据集中的数据写完
strRange=“d“&num&“:f“&num‘设定要填写内容的单元区域
sheetActive.range(strRange).font.size=10‘设定字体大小
sheetActive.range(strRange).WrapText=false‘设定文字回卷
sheetActive.range(strRange).ShrinkToFit=true‘设定是否自动适应表格单元大小
sheetActive.range(strRange).value=array(adoRset(“bookid“),adoRset(“bookname“),adoRset(“author“))‘把数据集中的数据填写到相应的单元中
num=num+1
adoRset.MoveNext
loop
(5)Excel临时报表文件的保存及处理
实际运行中应该注意每次一个用户进行报表打印时都采用一个临时的Excel文件,而不是硬性规定文件名,因为如果用固定的文件名的话,只有第一次生成是成功的,后面的操作都会因为已存在同名文件而导致失败。所以我们需要每次都产生一个临时的而且不重复的文件名,这里可以采用自定义的getTemporaryFile()函数由来生成,然后存放在变量filename中,用变量filepos表示这些临时文件的路径。
此外如果这些临时文件不处理的话久而久之会成为文件垃圾,因此在每个用户提交Excel报表打印请求时要先删除临时目录下所有原先产生的临时打印文件。
临时文件的处理主要代码如下:
functiongetTemporaryFile(myFileSystem)
dimtempFile,dotPos
tempFile=myFileSystem.getTempName
dotPos=instr(1,tempFile,“.“)
getTemporaryFile=mid(tempFile,1,dotPos)&“xls“
endfunction
setmyFs=createObject(“scripting.FileSystemObject“)
filePos=server.mappath(“\test“)&“\tmp\“’要存放打印临时文件的临时目录
fileName=getTemporaryFile(myFs)’取得一个临时文件名
myFs.DeleteFilefilePos&“*.xls“’删除该目录下所有原先产生的临时打印文件
setmyFs=nothing
Excel临时文件的保存代码为:
objExcel.ActiveWorkbook.saveasfilePos&filename
(6)退出Excel应用
objExcel.quit
setobjExcel=Nothing
(7)Excel报表的打印
前面的步骤已经生成了Excel报表,下一步进行打印,采用的策略可以有两种:
方案一:提供上面生成的Excel报表临时文件链接给用户,用户可以直接点击在浏览器中打开Excel报表并通过浏览器的打印功能进行打印,也可以点击右键然后另存到本地后再作打印等相关处理。
方案二:生成Excel报表后直接在客户端加载到浏览器,当然在没有完全加载时应该提示“正在加载,请等待”等字样。
2.3系统配置与注意事项
虽然以上代码很简单,但实际应用不当经常会出现错误,所以下面要讲到的系统配置和注意事项非常关键。
(1)千万要保证以上代码输入的正确性,否则一旦运行错误,Excel对象会滞留内存,难以消除,导致下一次调用时速度狂慢,并产生内存不可读写的Windows错误。这时的解决方法就是注销当前用户,如果还不行,就只能Reset了。
(2)一定要设置好负责打印功能的asp文件的权限。方法是:在IIS管理中,选择该asp文件,右键然后选“属性”/“文件安全性”/“匿名访问和验证控制“,在这里IIS默认是匿名访问,应该选择验证访问(这里基本验证和集成Windows验证两种方式均可,但前者不够安全),这一点无比重要,否则应用当中会出错的。
(3)有的时候报表分为多页,而且我们希望每一页有相同的表头,要求表头每页都自动打印,可以在Excel模板中进行设置。方法如下:选择菜单“文件“/“页面设置“/“工作表“,然后在“顶端标题行“输入你表头的行数(如:表头为1-3行即填入:$1:$3)。
3、总结
以上我们给出了一个采用ASP写的在B/S模式下实现EXCEL报表的生成与打印的例子,在实际当中已经得到了良好的应用。事实也证明,这个例子的代码虽然不难写,但一定要注意系统的配置,这也是无数次失败后得出的经验。