上次,我们写了个简单的网络应用,这次我们写各稍微复杂些的网络应用。在新工程中,我们将完成Winsock制作单服务器对多客户端、可收发自定义数据流、使用数据库增加客户端登陆验证等功能。当然,这也是个简单的例子,旨在帮助大家学习如何写这类型的程序,当然换做其它编程语言,总的思路也是可以使用的。
我们先说说我们的服务器都要干什么?
1、提供侦听服务,与客户端建立连接;
2、提供数据库的支持,用以完成检查客户是否允许登陆等;
3、为各客户端之间的消息提供转发功能;
4、各客户端可注册用户以及相互加好友。
一样是新建两个工程:一个Server,一个Client,两个工程都需要引用“Microsoft Winsock Control 6.0”,对于Server来讲,我们还要引用“Microsoft DAO 3.6 Object Library”,当然你也可以将代码变为 ADO 等各种其它数据库的支持库。
除了Winsock和DAO的库,其它工程所需要的文件,我已经都上传到百度网盘了,大家可以下载后自己编译成EXE,因为凡事涉及到VB6+Winsock的代码,一般杀软都会报告有病毒或木马,所以大家自己编译吧。
链接:https://pan.baidu.com/s/1dFVr5Cl
密码:69oo
上面是个客户端的截图,大家可以好好看看代码
由于时间的关系,程序中很多功能没能完全都实现,不过重要的单服务器与多客户端以及发送自定义数据的功能都实现了,其实拿到这些代码完全可以自己改改界面,搞个更厉害的聊天工具。
而且,你可以完全在代码中的增加数据将加密来提升安全度,增加数据压缩来降低网络数据传输量。
在给大家将这俩工程之前,我觉得有必要先讲讲两个工程共用的部分。
(其中以下这3个文件都是以前的文章里介绍过的)
GdiP.bas:请参阅《我们继续研究VB6和GDI+能干啥?》等文章;
ASM.Bas:请参阅《为VB6增加所有移位函数(第二版)》;
AttacherTimeEvent.cls:请参阅《今天发布一个定时器类》;
其中 GdiP.bas 比以前新增了两个函数:
Bitmap_CreateJPGBytes:将GDIPlus的Bitmap对象转换成JPG数据流;
JPGBytesToiPicture:将JPG数据流转换为iPicture对象以便显示;
(后面这部分都是这次发布的这两个工程新加入的文件)
1、MD5API:通过调用AdvAPI32.DLL来为程序提供计算MD5值的功能。
其实这些代码在网上可以随意搜到,我只不过进行了相应的处理,来增强某些内容:
调用 MD5File 时提供了回调函数,不过是最简单的VB6回调方式,就是通过CallByName来调用而已,在代码中给出了详细的说明,在服务器程序的主窗体的最上面两个 Sub 就是了调用示例。当然大家完全可以把这个MD5API搞成一个类,并将回调弄成事件,效率就更高了,不过这只是个试验而已,咱的程序中并不存在那么大且需要回调的文件,所以我就没细弄。
为 MD5Fille 的回调提供了一个状态枚举值并提供了 ProcStepMemo 来返回各中状态的文字说明,大家可以调用看看就知道了;
为 MD5File 提供了一个 可选的运算结果返回值 Result ,这样在调用 MD5File 时可以返回一个结果值来判断是否正确的得到了要计算的文件的 MD5 值;
在 MD5String 和 MD5Bytes 中提供了一个可选的 Continue 参数,也就是如果你要计算很多分段内容的 MD5 值时,第一次调用时设 Continue 为假,之后的调用设 Continue 为真,最后用 GetLastMD5 得到所有这些内容计算后的 MD5 ;
在 MD5String 中增加了一个 IsUnicodeText 参数,用来区分你传入的字符串是 UniCode 编码的还是 ANSI 编码的,我们 VB6 直接调用的话基本就是 UniCode 编码,并不需要设置该参数,如果是从某些文件、API函数之类的取来的字符串很有可能是 ANSI 编码的,这要根据实际情况自己去区分了;
2、NetInfo.bas:主要提供两个程序都需要且必须统一的网络命令定义、网路数据包结构以及文件版本的函数。对于咱们这个小工程,这个模块只能写这么点儿东西了,如果你要扩充工程的能力,这里会写更多的东西。也许以后我会写一个类似QQ这种的聊天工具给大家提供更多的学习资料时在说吧。
3、iBinaryStream.cls:用来将 VB6 标准数据类型封装成一个长数据流,以方便在网络传输前的写入及网络传输后的读出。当然,这是个基类,以后我还将发布更多类型的BinaryStream类来提供更多的功能,例如自动增加长度的、可封装自定义类型的、可带数据类型及属性名称的等等,以后再说吧。
接下来我们来讲服务器的代码,也就是DemoServer.vbp。
这个工程中,除了上面介绍的公用部分,自己有 iUsers 和 iUser 两个类模块,这两个类模块主要是完成对客户端连接的管理。
iUsers 是个集合,主要提供对元素 iUser 这种对象的增删及为每个 iUser 提供事件驱动机制。有些类似于 VB6 的控件数组,但在事件方面与 VB6 的控件数组还是有差别的。我并没有想出在不掉用 API 的情况下如何处理众多元素的事件挂钩的方法,所以只能使用 Friend 类型的 Sub 来处理内部事件了。大家如果有更好的方法,可以自行改编,当然如果您愿意分享出来,也可以给我发邮件,我将为您的方法专门发一篇注明来源的文章。
iUsers很简单,我想大家看看代码都能明白它是干什么和怎么干的,无外乎就是一个以 iUser 为元素的集合,并为 iUser 的信息反馈提供了几个事件而已。
下面我们来讲讲 iUser 中的重点:
在 iUser 中,最主要的就是SetSocket、SetUser和SendPackage三个函数。
SetSocket:这是在创建新的 iUser 后必须马上调用的,它为这个 iUser 设置最基本的网络接口及初始化数据。
SetUser:这是在客户端登陆后,为这个 iUser 设置客户信息的。
SendPackage:这是这个 iUser 发送数据包的函数。
额外需要说明的是我们在 iUser 中使用了 AttacherTimerEvent 类(请参阅《今天发布一个定时器类》)来定时监测是否有足够的网络数据能组成一个数据包,当组成一个数据包时调用 iUsers 的Friend 类型的 Sub 来向主窗体提出 UserGotPackage(获得一个完整的数据包) 和 MustDisConnect(必须要断开连接) 事件的调用。
在咱的程序中,每个数据包都是由三方面组成:
数据长度:Long 型,占 4 字节
数据命令:Long 型,占 4 字节
数据内容:Byte() 型,占 “数据长度” 字节
所以每个数据包最少占8个字节(数据长度为0的时候)。
接下来就说道我们服务器的主窗体,因为这是个很简单的服务器程序,所以我们把主要的功能都写进这个主窗体里了,实际上,我们的在写服务器程序的时候基本上都会把主代码写进主线程的某个模块中,而所有的 UI 元素都应该写到其它辅助线程中,这样就能避免因为 UI 元素被挂起时导致的程序崩溃。由于我们这个程序非常简单,所以就不需要大动干戈了,毕竟对于 VB6 来讲多线程调用就像让你现在立刻爬上喜马拉雅山一样:不是不可能,但难于上青天。以后我们会专门发几篇文章来讲中如何安全的制作VB6的多线程程序的文章。
在主窗体中,最上来的两个 Sub 是用来测试 MD5File 回调的代码,对于咱们这个服务器程序没有任何意义,大家看看运行效果,然后删除即可,记得也把窗体上的 Command2 删除。哦,对了,运行效果是 Debug.Print 出来的,所以只能在 VB6 IDE 中观察效果,编译后是啥也看不到的哦。由于咱的程序中并没有提供对这种方式的回调的例子,所以我就留着这两过程没删,也是为了给某些新手提供一种简单的回调方法。
我们的主窗体主要完成了对网络服务提供侦听、为每个客户端分配 iUser 对象及 Winsock 控件,对收到的数据包进行分析处理并反馈至客户端。
首先,我们的程序使用了最简单的 DAO+MDB 的数据库调用方式,因为我们不主要讲数据库,所以就用了这种最简单且没啥毛病的方式来处理非常小的数据库的问题。可能某些人的电脑中不存在 DAO360.DLL ,没关系,在我提供给大家的工程包中包含了这个文件,您只需要复制这个文件到“\Program Files (x86)\Common Files\Microsoft Shared\DAO\”中并 Regsvr32 它即可,记得如果你用命令行的方式 Regsvr32 时,记得给 CMD.EXE 提供管理员权限就行了。
以上视频To:新手
对于咱这个非常简单的工程,咱的数据库就才弄了3个表:
UserBaseInfo:保存客户的最基本的信息:
UserID:自动生成的每个客户的唯一编号;
RegTime:注册时间(为MD5密码提供加盐值,以后也可以计算网龄);
UserName:登陆用的用户名;
Password:加盐后的MD5密码;
NickName:用户昵称;
LastLoginTime、LastLoginIP:最后一次登陆的时间和数字IP(将来可以用来提示用户判断上次是否自己登陆的);
Friends:好友列表(UserID数组);
DynamicKey:iUser的Key,用以迅速找到 iUser 并可判断是否是正确的。
TempTickets:保存临时连接的信息,其实应该交Token:
ID:就是TempTicket值,由 This.bas 的 GetTempTicket 函数产生,用以标识每个未登陆连接的唯一性与合法性(应该是Token,但已经这么写好了,就这样吧,后面的文章中就都叫“临时票据”吧);
SNCode:验证码字符串,现在没用了,但最早写在代码中就懒得删了;
CreateTime:创建本条记录的时间,用以在10分钟后删除本条记录。
ToSendMessages:保存用户不在线时该发的消息:
Time:产生这条消息的时间;
From:UserID,指谁要发的这条消息;
To:UserID,指这条消息要发给谁;
Type:消息类型;
Message:消息内容。
整个工程由 This.Main 开始,不过就是简单的初始化、连接压缩后的数据库、获取客户端版本而已,其中压缩数据库就是调用DAO的CompactyDataBase函数,实际上就是清理已经被删除的数据,并对某些可以用允许被压缩的字符串进行UniCode转ANSI编码的工作。
This.Bas中最值得讲的就是 GetTempTicket 这个函数,它主要是完成生成随机的 TempTicket、验证码及验证码图片。这个函数的主要部分就是生成验证码图片,其实就是使用GDIPlus的修改输出的字符串路径,我用了最简单的办法,随机偏移量来解决字符路径的修改,其实你可以拆分每个字的路径然后进行旋转倾斜等各种操作,然后在生成验证码图片,最后转换成JPG数据流传输给客户端。关于调用GDIPlus制作字符串路径及修改路径后在显示新图像的具体方法,请参阅《VB6与GDI+更近一步》。
大部分服务器功能都是在主窗体完成的,包括开始侦听、与客户端建立连接、处理所有客户端发来的请求等。
向侦听、建立连接等基本操作,在上篇文章咱们基本都介绍完了,这次主要是发送多种类型的数据流以及对不同的网络数据包进行不同的处理。
关于多种数据类型组成的数据流,也就是我们在文章开始不久就介绍的iBinaryStream.Cls类,这个类的内容非常简单,只是写起来比较繁琐,所以大家应该都会,只不过是懒得写出来而已,其实更简单的办法是用自定义类型写进文件中,然后读出来在发送,不过那样我们就会大量进行硬盘的读写,这并不是好习惯,而在系统中弄一小块 RamDisk 却又需要外挂动态库和驱动程序,那就更麻烦了,我还没有好好研究是否有更好的办法使 VB6 的文件读取函数能直接应用于内存、共享内存或文件映射之类的办法,所以只能傻傻的写这么个类来解决数据流序列化的问题。而这里也没啥需要解释的东西,都是最基本的内存读写指定位置而已,唯一可能需要解释的就是 VB6 的数组其实是 COM 的 SafeArray 结构和一段内存,关于SafeArray结构,我们以后将发布一篇专门的文章来为新手介绍,关于代码中的通过 Not Not ArrayName 来获取 SafeArray 指针的方式,请参阅《一个错误带来的高效代码》。
关于服务器中可能也没啥别的需要详细说明的了,也就是 Users_GotPackage 这个事件需要说明了,其实就是 iUser 类收到数据后拆分为一个完整的数据包,并调用 iUsers 的 Friend 类型的函数,然后产生一个 iUser 的事件来让主控程序处理收到的每个客户的每个数据包,对于咱们这个简单的服务器而言,需要处理的数据包非常少,它们有:
1、csGetNewSNCode:客户端获取临时票据、验证码图片;
2、csLong:客户端要登陆;
3、csUserRegister:客户端要注册新用户;
4、csGetFile:下载文件,现在只完成了更新客户端;
上面是任何人的都可发送的数据包。
下面是只有成功登陆的客户端才能发送的数据包:
5、csTextMessage:客户端发送文本信息;
6、csSearchUsers:客户端搜索其它用户;
7、csApplyFriend:不同客户端加好友相关。
接下来我们就说说客户端的情况。客户端的处理比服务器稍微复杂了些,因为需要面对客户,所以界面上的东西多了些。
首先我们来看看客户端自己的那些东西:
1、AttacherWinMsgEvent.cls:请参阅《拦截窗口消息类》;
2、FriendApplicant:当有一个好友申请的时候,我们使用这个对象生成一个新的询问框,来解决在不影响客户端正常操作的情况下等待用户判断是否同意加这个好友的问题;
3、fClient:完成主要的网络数据收发及聊天消息收发;
4、fLogin:登陆对话框;
5、fRegister:注册新用户;
6、fSearchFriend:搜索符合条件的用户,准备加好友;
7、MsgBoxEX:这是自己做了一个MsgBox的加强版;
在客户端中,我们首先要介绍的是 fClient 中的 DoCommand 这个过程,这里主要是处理收到的网络命令,并做出响应。
这里处理了:收发文本消息、服务器广播文本消息、收到验证码信息、收到登陆结果信息、返回搜索用户的结果、加好友的相关问答、注册用户。当然,如果我们要把这个工程做大,这里要写的东西还多了去了,不过作为一个例子,这些足够了,剩下的东西大家就自由发挥吧。
在主窗体里,我们用了两个 AttacherWinMsgEvent 来分别处理主窗体和Combo1 的窗口消息,主窗口只是处理了一个最大最小拖拽尺寸的问题;Combo1 是为了处理了当单击后或通过上下按钮+回车按钮选择某一发送对象后才让消息填写框(Text4)得到焦点。
主窗体中还增加了 Command3 这个按钮,当收到客户端版信息后,如果版本好不同或者MD5值不同,则显示这个按钮,这个按钮就是启动更新程序的按钮。
当收到服务器要求登陆的消息后,则启动 fLogin ,并将验证码图片和临时票据保存到 fLogin 中,让 fLogin 填写好必要的信息后可以按登陆按钮将登陆信息发送到服务器。各种论坛及问答上也有很多这种如何写登陆框的问题,这次这篇文章就算统一给解答了吧。
然后在 fLogin 中还提供了注册按钮,单击注册按钮就调用 fRegister 来让大家可以注册用户,当然,我们的服务器并没有设置什么用以避免某些不符合注册要求的文字等条件,大家写工程的时候肯定会自己加进去的,例如违法的言论、广告信息等各种不允许出现的内容。
作为一个例子,咱的工程弄这点儿代码和数据库内容基本够用了,而且由于最近家里有病人,我实在没太多工夫来好好弄这个工程,所以工程中肯定存在一些问题或缺少很多功能,不过对于讲解 VB6 制作网络服务器/客户端的程序示例,应该是足够了,毕竟遗憾和缺失才是大家自己搞出高质量代码的动力。
为了不再继续拉长我发文的时间间隔,这次这个示例只能这么粗糙了。以后也许会发一个模拟QQ之类聊天工具的大型工程来详细讲解:自定义数据的网络收发、单客户多连接进行不同内容的收发、客户端自动更新等附加功能。
也许随着大家问题的增加,我会再发一篇文章来详细解释该工程中的内容,这就要看大家的反应了。
好了,今天就说这么多吧,最近发文少,一个是家里的近况不允许我有太多的时间来写文章,另一个就是这种稍微多一些内容的工程,我也需要更多的时间来写、来调试,大家见谅吧。以后我会多挤些时间来增加发文频率的。
领取专属 10元无门槛券
私享最新 技术干货