BLUE BBS HELP开发文档
NOTE:本程序只在win2k和winxp上使用Sterm2.052测试过
1 引言
目前smth和ytht以及FireBird都有了Windows NT的版本。它们的程序结构都是过程驱动和面向函数的。不考虑在用户访问量上的优势,分析其源码后会发现存在如下不足:
1)可读性较差。
在纷繁复杂的全局变量以及goto面前,需要很有经验的开发者,以及花费大量的时间才能够对系统架构有所了解,无疑加大了系统升级的难度。
2)独占进程。
例如FB,每一个用户请求都会为其创建一个进程,这样会增大系统资源的开销。为了进一步压缩系统资源的消耗,可采用多线程、共享服务或者线程池的方法。
3)未接入数据库系统
BBS Server每天都要存储和修改大量的数据,FB目前都是使用文件的形式进行存档的,无疑会失去很多使用数据库的好处。
4)封装性和可重用性差
在系统各个板块的设计中会有很多重复的页面元素,例如列表等。由于面向过程的设计缺乏继承、重载之类的机制,在制作页面元素以及处理其响应时,即使只有一点小小的变化,都需要调派各类函数,进行很多重复工作。
5)…
Blue BBS系统针对以上这些问题作了一些改进尝试,由于时间和工作量的问题,不可能完善所有的功能,但是它实现了以下的部分。
1)采用面向对象的表达方式
BBS中需要用到的各种元素,例如列表、文本框等常用页面元素都具有较好的封装性和可重用性。这样每次需要使用一个列表时都不需拷贝大量的代码了,直接控制类表类的对象即可。
2)采用事件驱动的设计方式
将网络数据传输、页面加载、数据库更新等这样一些系统事件都做成消息的形式,通过消息泵bump到各个模块。各个模块通过响应消息来获取自己需要的信息。
3)使用多线程来处理用户会话
每来一个用户请求,Blue BBS都会启动一个线程,并创建一个用户会话来给特定用户提供服务。这个还可以进一步改进成线程池与进程池相结合的形势,以减小系统开销。
4)使用数据库管理用户数据
用户信息的管理采用了Access数据库。稍做修改可以更换为其它类型的数据库。
2 系统机制
2.1 运行时识别(RTTI)
当我们获得一个某种基类类型的指针时,通过RTTI可以获得其准确的类型,这可以被看作C++的第二大特征。
在MFC中CObject类实现了RTTI功能。参考MFC源代码、《Thinking in C++》以及《深入浅出MFC》,在Blue BBS系统中实现了自己的RTTI。
它是由krObject、krRuntimeClass、RUNTIME_CLASS宏、DECLARE_DYNAMIC宏、IMPLEMENT_DYNAMIC构成的一个体系。大部分的Blue BBS类都是由krObject类派生的,派生类在声明文件中通过DECLARE_DYNAMIC声明支持RTTI,例如:
并且在定义文件中执行IMPLEMENT_DYNAMIC宏,例如:
当我们需要确定一个对象是否为某种类型时,调用krObject的IsKindOf函数,例如:
每个krObject派生类都会有一个krRuntimeClass对象,里面包含了该类的名字一起其它信息。通过krObject的GetRuntimeClass()函数我们可以获得一个指向它的指针。
每次当我们在一个krObject派生类中声明一个DECLARE_DYNAMIC时,都为其声明了一个krRuntimeClass对象,该对象是一张链表中的一个元素,包含了指向父类中krRuntimeClass和链表起始单元(即krObject类的krRuntimeClass成员对象)指针。
而每次我们为某个krObject派生类执行IMPLEMENT_DYNAMIC宏时,我们都会在其krRuntimeClass对象中设置类的名称、父类中krRuntimeClass成员对象的指针等。这样每次我们调用该krObject派生类的IsKindOf(类A)函数时,它通过查看这两个类的krRuntimeClass对象是否相同来判断是否是同一类属。
RTTI的实现代码在kr.h和kr.cpp文件中。
2.2 动态创建
当我们创建一个对象时,我们需要指定它的类型,例如:A a; 如果我们需要创建的类型是未知的,比如说有一个字符串指定了类型,这个字符串是变量,有可能是”A”,也有可能是”B”,这时候就需要用到动态创建的功能了。
动态创建功能使我们可以通过类名来创建该类的对象。这个功能是通过krObject、krRuntimeClass、DECLARE_DYNCREATE和IMPLEMENT_DYNCREATE宏架构起来的。
派生类在声明文件中通过DECLARE_DYNCREATE声明支持动态创建,例如:
并且在定义文件中执行IMPLEMENT_DYNAMIC宏,例如:
在Blue BBS系统中,DECLARE_DYNCREATE和IMPLEMENT_ DYNCREATE宏包含了DECLARE_DYNAMIC和IMPLEMENT_DYNAMIC,也就是说进行如上声明后krObject的派生类同时支持了运行时识别和动态创建功能。
如何根据类名创建对象呢?下面是个例子,在后面被用到krSession的加载页面函数LoadPage中。
2.3 多线程会话
在Blue BBS中系统为每一个用户的服务请求创建一个线程,同时并创建一个krSession对象来提供服务,和进行会话管理。
回到Blue BBS的程序入口,我们可以看到如下代码:
krServer类是一个重要的系统类,它负责系统层面上的控制工作,并协调会话之间的交互。创建线程和会话的工作是在krServer的Start函数中完成的:
krServer的Start函数在23端口建立监听,并且启动一个监听线程。在监听线程中又干什么事情了呢?
呵呵,调用了krServer的ProceedConnectionRequest函数。
ProceedConnectionRequest函数会等待用户连接,一旦有用户访问,则本线程负责处理该用户访问,同时开启一个新的监听线程,用于接收下一个用户的访问。
注意,krSession是一个非常重要的类,它的功能类似于MFC中的CwinApp,它负责创建一个消息循环,并且将系统消息bump到相应的窗口中。这些窗口实现了与用户的交互,并提供给用户服务。
2.4事件驱动
2.4.1 事件(消息)的类型
每一个krSession都负责维护一个krMsg类型的消息列表。一个krMsg消息包含如下的内容:
即处理消息的对象pWnd以及消息代码以及参数等。
这些消息分为三类:一类是所谓系统消息,即需要krServer或者krSession处理的;第二类是窗口消息,即发送给特定窗口处理的;第三类是KR_WM_KEY消息,其实它也是一种窗口消息,它的特殊之处在于它来源于网络上的数据传输(即用户在客户端按下一个键,这个键值会通过网络传送到Blue BBS,而Blue BBS将这些键动作都视作KR_WM_KEY消息发送给当前窗口)。不同的消息是通过krMSG中的nMessage来区别的。
2.4.2事件的产生
上述的消息是如何产生的呢?
1)KR_WM_KEY消息:在消息循环中产生。在krSession的每一个Run循环中都会执行一次GetMessage,它除了从消息列表中提取待处理的消息外,还要检测网络上是否有按键传送过来。如果有的话,它就在消息列表中添加一个KR_WM_KEY消息,让当前窗口处理
2)窗口消息
窗口消息是在窗口行为中产生的,例如
3)系统消息
系统消息是一些由krSession或者krServer处理的特殊消息,例如:
这些消息都是在用户执行特殊功能时产生的。
2.4.3 事件的驱动
事件的驱动是由krSession的Run函数完成的。 我们来看一看krSession的Run函数就知道了。
首先它通过GetMessage函数在消息列表中取出最近需要处理的消息,判断它的类型,如果是系统消息,krSession就处理了,如果是窗口消息或者KR_WM_KEY消息就让krMSG中指定的窗口处理。下面是GetMessage从消息列表中提取消息部分的代码:
2.4.4 事件的分发
事件的处理分布在krSession、krServer、krCmdObject、krWnd以及krWnd的派生类中。
系统消息消息由krSession类的TranslateMessage函数处理和分发。
窗口消息krSession会通过DispatchMsg函数有条件地分发给窗口,由其WindowProc函数处理。
2.5 窗口机制
Blue BBS系统中引入窗口概念与Windows中引入窗口的概念相似,只不过后者将图形化的结果展现在本地用户的显示器上,而前者是通过网络将结果输出到远程用户的虚拟终端(例如Cterm,Sterm或者netterm)。
引入窗口机制的目的在于以窗口作为服务器与用户交互的核心,结果显示和用户指令响应都是围绕窗口完成的。同时也符合长期从事Windows Application开发的同志们的习惯。
2.6消息映射
消息映射的目的是将消息和处理消息的窗口函数联系起来。这个功能是通过krWnd、DECLARE_MESSAGE_MAP、BEGIN_MESSAGE_MAP、END_MESSAGE_MAP、ON_COMMAND、ON_WM_CREATE、ON_WM_PAINT这样一些宏构成的。
DECLARE_MESSAGE_MAP的作用在于为窗口类声明一个数组lpEntries,数组中的成员包含了消息函数对,还声明了一个指针,通过它可以找到父类的lpEntries。
BEGIN_MESSAGE_MAP、END_MESSAGE_MAP以及ON_COMMAND等宏用于往lpEntries数组中添加消息函数对。
前面已经讲了,窗口消息会发送给窗口类的WindowProc函数处理。它做的工作就是根据lpEntries中的消息函数对和待处理的消息,来确定调用哪个函数处理该消息。
如果当前窗口中没有处理该消息的函数,那么它会向上查找父类的lpEntries,将该消息交给父类处理。如此类推。
2.7 窗口绘制
Blue BBS中窗口的绘制是向远程用户的虚拟终端(NVT)发送控制字符,通过虚拟终端来将结果显示给远程用户。
一旦服务器端要更新显示时,就将绘制每个窗口的控制字符都发送出去,是不划算的。一个很明显的例子就是,如果这些窗口是重叠的,对于某个窗口而言,如果它有很大一部分被上层的窗口给遮挡住了,那么发送被遮挡住的这部分控制代码即没有意义又增加了网络负担。为此Blue BBS参考FireBird设置了屏幕缓冲区。它由krScreen类负责,每个krSession都有一个krScreen对象用以向远程用户展示处理结果。
krScreen实现了一个虚拟屏幕,是一个大的KR_SCRN_LINE类型的数组m_pScreen,数组中每一个元素代表屏幕上的一行。
每次向屏幕上写控制字符时,都会将所在行标记为已被修改(设置nMode为1),并且通过该行中设置被修改的区域(设置nStartModified和nEndModified)。每次我们调用Refresh函数刷新屏幕时,它实际上只向远程客户端输出被修改区域的控制字符。这些区域被输出(对用户而言,就是刷新了)后,便又被标记为未修改状态。
当有两个窗口重叠时,它们都会修改重叠区域,但是刷新时实际上只输出上层窗口的内容。
Blue BBS 中窗口类不直接向krScreen中进行窗口绘制,而是通过krDC类进行绘制。krDC类负责窗口坐标系和屏幕坐标系之间的转换,保证窗口绘制区域范围(通过裁剪区域和排斥区域以及换行控制),控制输出字符的前景色和背景色,设置屏幕光标等。
针对重叠窗口的绘制问题,参考MFC采用了Z-Order方式。在krSession中创建了三个窗口指针列表m_TopMostWnds、m_OverLappedWnds和m_ChildWnds用于存放不同类型的窗口指针。
窗口类型分为三种:TOPMOST(最顶层)、OverLapped(层叠)、CHILD(子窗口)。TOPMOS类型的窗口总是在其它类型窗口的上层。CHILD类型的窗口,其显示随其父窗口。每一种类型的窗口创建时,都会向其所在的krSession的相应Z-Order列表中添加自己的窗口指针。
当一个窗口失去激活时,他会激活所在Z-Order 列表中下一层的窗口。当一个窗口被激活时,他会移动到Z-Order 列表的顶层,激活子窗口,并且重画前一激活窗口所占的区域。
在Blue BBS中没有实现TOPMOST类型的窗口,实现了OVERLAPPED和CHILD窗口。其中krPage总是OVERLAPPED的,krCtrl总是CHILD类型的。
krWnd类中的InvalidateRect函数可以用来重画窗口中指定的区域,并且可以指定是否重画背景。
2.8页面加载
页面加载方式是用来处理一类特殊的窗口的。这种窗口总是占了虚拟终端的整个屏幕,逻辑上也是一个独立的表现某种功能的实体。类似于我们使用IE浏览的网页页面。经常会从当前页面向前链接到下一页面,又会向后回到上一页面。这种窗口不宜频繁创建和销毁,而应该暂留在内存中。
为此Blue BBS在krSession中建立了一个页面指针列表,所有的页面被创建后会向页面指针列表添加自身的指针。使用完后,并不销毁,以备后用。每个页面内又含有一个指向创建它的父页面指针,用以返回时能找到父页面。
krSession中为此添加了一个LoadPage函数,参数为所要加载的页面类名称。
其功能是:如果该页面已存在,则激活该页面;如果不存在,则创建一个该页面,并将当前页面设置为其父页面。
2.9 数据操作
为了对数据库进行操作,引进了krDBDataObj和krDbObjSet类
krDbDataObj是Blue BBS中的实体类(即有数据存取的类)。krAccount、
krArticle、krMail、krBoard、krZone都是其派生类。krDbDataObj有两个重要的虚函数:SerializeIn和SerializeOut,用于与krDbObjSet捆绑时,从/向krDbObjSet读入/输出数据。
例如krAccount的SerializeIn 和SerializeOut实现分别为:
krDbObjSet实现了对ADO Recordset(纪录集)对象的封装。
什么是捆绑呢?捆绑(bind)是Blue BBS中为了方便数据操作而采取的一种方式。所谓捆绑是指将krDbObjSet和krDbDataObj组合在一起进行数据操作。捆绑的原理是:当对krDbObjSet进行移动、查询等操作时,被捆绑的krDbDataObj对象总是被更新为纪录集中当前纪录的内容。
例如,在登陆页面中,当用户输入用户名并按回车后,Blue BBS需要查询数据库中的Account表,以确认是否有该用户。利用捆绑是这样做的:
很显然,捆绑在文章列表、版面列表这样一些对数据库进行操作的控件上是很有用户的,在移动光标的同时,有一个东西就记录下了数据库中当前纪录的内容。
在更新数据时捆绑也很管用,我们只需修改被捆绑对象的成员变量的值,然后调用krDbObjSet的Update函数即可,不需要跟VARIANT类型的数据打交道。
2.10 通用窗口类
1)页面类
与krSession一起实现了页面加载的功能。特殊的窗口类。
2)对话框类 krDialog
krDialog实现一个类似MFC中CDialog的模式对话框功能。当它DoModal时,会截取会话的按键消息,直接发送给krDialog对象,直到它被销毁。在主界面中的搜索讨论区对话框就是派生于krDialog类。它是这样使用的:
2)Static控件类 krStaticCtrl
用于显示静态文本
3)文本框控件类 krEditCtrl
用于接收用户输入文本
4)列表控件类 krListCtrl
用于显示列表。可以自由选择列表中的选项
5)菜单类控件类 krMenu
实现菜单的功能。选中一项后按回车会启动相应的响应函数
6)数据捆绑列表类 krDbListCtrl
这个列表控件可以与数据库中的表进行捆绑,显示其内容。是由krDbDataObj、krDbObjSet和krListCtrl的功能组合
2.11如何快速创建一个页面?
很简单。上面提供的通用窗口类应该够你使用。
1)创建一个krPage的派生类A
例如 krMainPage
2)在krMainPage的头文件中声明动态创建和消息映射表,如果你需要处理消息的话。
3)在krMainPage的定义文件中定义动态创建和消息映射表
4)给krMainPage添加几个控件成员变量
例如:krMenu m_MainMenu;
5)重载OnCreate函数,并在On_Create函数中创建控件,并设置其中一个为当前活跃控件。
6)添加你的菜单响应函数,例如
7)在需要调用该页面的地方使用
OK大功告成!
3、数据库
一个BBS系统中要处理的数据很复杂。Blue BBS系统到目前为止处理了以下几个数据,内容请参考bbs目录下的system.mdb
1)账号
2)文章
3)分类区记录
4)讨论区记录
拟处理:
4 Blue BBS使用方法
4.1 说明
1)Blue BBS是使用Sterm2.502进行测试的。采用VT220键集。使用telnet.exe时根据不同的操作系统,按键对应发送的字符会有所差异。
2)请在控制面板中设置数据源 DSN=bbs Access的驱动,数据库文件在bbs目录下的system.mdb
3)因为使用了c:\Program Files\Common Files\System\ADO\msado15.dll,如果该dll没有,请使用bbs目录下的msado15.dll
4)客户端请使用bbs目录下的Sterm
4.2 登陆
1)打开Sterm2.502,并在标准工具栏上单击QuickLogin按钮。
![[原始尺寸:677x487 点击查看大图]](/upload/2004/05/1047343490953.jpg)
2)在快速登陆对话框的Host文本框中输入Blue BBS所在的主机ID,本机测试时请使用localhost,端口请使用23,即telnet的默认端口。单击Connect按钮进行连接。

3)按照屏幕上的提示输入用户名和密码,如果需要注册在用户名“文本框”中输入new,如果是匿名访问,则输入guest,不区分大小写,按回车完成输入。
![[原始尺寸:704x508 点击查看大图]](/upload/2004/05/1047343490955.jpg)
如果提供的信息错误,系统会提示错误,用户可以再修改按Ctrl+Q键可以放弃登陆
![[原始尺寸:704x508 点击查看大图]](/upload/2004/05/1047343490956.jpg)
4)如果用户名和密码都正确,则登陆成功
4.3 注册
1)在登陆界面的用户名“文本框”中输入“new”。按回车键进入注册页面
2)供自己的用户代号、密码信息。如果用户存在或者密码有问题,系统会有提示。
![[原始尺寸:704x508 点击查看大图]](/upload/2004/05/1047343490957.jpg)
3)如果注册成功,会登陆到主界面,缺省的用户级别为1,即一般用户。
4.4 主界面
1)主界面中根据不同的用户级别显示可提供的服务。如果用户为admin,即系统管理员,则会出现比一般用户多一项功能?D?D系统管理,不过这个功能没有提供。如果用户为guest则没有站内信件和个人设置选项。
移动↑↓键或者功能项对应的快捷键(例如Z表示选择分类区列表)可以选择功能,回车键或者→键可以启动相应功能。
![[原始尺寸:704x508 点击查看大图]](/upload/2004/05/1047343490958.jpg)
一般用户
![[原始尺寸:704x508 点击查看大图]](/upload/2004/05/1047343490959.jpg)
系统管理员
![[原始尺寸:704x508 点击查看大图]](/upload/2004/05/1047343490960.jpg)
匿名用户
4.5 阅读当前讨论区
1)在主界面中按R键可以选中阅读当前讨论区选项。当前讨论区指的是最近一次阅读过的讨论区。如果用户刚登陆,则当前讨论区为SystemBoard,水母上为Test,呵呵。
![[原始尺寸:704x508 点击查看大图]](/upload/2004/05/1047343490961.jpg)
2)在当前讨论区中通过↑↓或者Pageup、PageDown键选择文章,按→或者回车键阅读文章。
![[原始尺寸:704x508 点击查看大图]](/upload/2004/05/1047343490962.jpg)
3)在版面中按CTRL+A键启动发表文章页面。输入文章标题和内容后按CTRL+W键发表文章。
![[原始尺寸:704x508 点击查看大图]](/upload/2004/05/1047343490963.jpg)
4.6 分类区列表
1)在主界面中按“Z”键选择“分类讨论区”,按回车键进入分类讨论区列表
2)在分类区讨论区列表中可以看到Blue BBS中的分类讨论区
![[原始尺寸:704x508 点击查看大图]](/upload/2004/05/1047343490964.jpg)
3)通过↑↓键选择要浏览的分类区,按回车或者→键进入相应的分类区的版面列表。要回到主界面,按←键。
![[原始尺寸:704x508 点击查看大图]](/upload/2004/05/1047343490965.jpg)
4)在版面列表中通过↑↓键选择要版面(即讨论区),按回车或者→键进入相应的讨论区。要回到上一页面,按←键。
![[原始尺寸:704x508 点击查看大图]](/upload/2004/05/1047343490966.jpg)
5)如果你是系统管理员,在第二步的分类区列表中,按CTRL+A键,启动创建分类区页面。输入分类区代号,和其中文名称,按回车键完成。选中要删除的分类区,按CTRL+D键删除该分类区。
![[原始尺寸:704x508 点击查看大图]](/upload/2004/05/1047343490967.jpg)
6)如果你是系统管理员,进入第三步的版面列表中,按CTRL+A键,启动创建版面(讨论区)页面。输入代号和中文描述,按回车完成。选中要删除的版面(讨论区)按CTRL+D键删除该版面。页面内容同上一步。
4.7查找讨论区
1)在主界面中按“S”键选中查找讨论区功能项,按回车或者→键启动查找对“话框”。
![[原始尺寸:704x508 点击查看大图]](/upload/2004/05/1047343490968.jpg)
2)在查找“对话框”中输入要查找的讨论区代号。按回车键完成。如果该讨论区存在,主界面功能列表中会自动选中阅读当前讨论区,将当前讨论区设置为搜索到的版面,在按回车键可以进入浏览该讨论区。如果不存在,功能列表依然选中“查找讨论区”选项。
4.8个人设置
1)在主界面中按“P”键选择个人设置功能项,按回车启动个人设置页面
2)在个人设置页面中修改自己的昵称,个人介绍等资料。按回车或者CTRL+W键完成。
![[原始尺寸:704x508 点击查看大图]](/upload/2004/05/1047343490969.jpg)
4.9 站内信件
按前面介绍的系统结构,读者应该可以很轻松完成。
4.10 系统管理
按前面介绍的系统结构,读者应该可以很轻松完成。
4.11退出系统
在主界面的菜单内选择“退出系统”,按回车或者→键,进入退出系统页面。按回车键退出,按ESC键回到主页面。
![[原始尺寸:704x508 点击查看大图]](/upload/2004/05/1047343490970.jpg)