分类

课内:
不限
类型:
不限 毕业设计 课程设计 小学期 大作业
汇编语言 C语言 C++ JAVA C# JSP PYTHON PHP
数据结构与算法 操作系统 编译原理 数据库 计算机网络 软件工程 VC++程序设计
游戏 PC程序 APP 网站 其他
评分:
不限 10 9 8 7 6 5 4 3 2 1
年份:
不限 2018 2019

资源列表

  • 基于汇编语言的音乐盒设计与实现

    基于汇编语言的音乐盒设计与实现—汇编课设
    一 需求分析设计一个音乐盒,可用在诸如生日礼品等场景里。
    包含的功能有播放音乐、切换音乐。默认播放第一首音乐,单曲循环。当拨动控制开关时切换歌曲,总共三首,分别由三个开关控制。当且仅当一个开关开启其它开关关闭时有效,多个开关同时开启时无效。
    要求所选多个芯片或模块的加权值总和≥2,并且,所选芯片或模块中必须包含8259或8254芯片的其中一个作为功能模块部分。
    二 设计思路发声:通过将8254(8253)模块,设置为第三种模式,选择0号计数器,00110110B,作为波形发生器。通过输出波形的频率控制音调,通过维持频率波形的时间控制节拍。(1Mhz时钟信号下,频率与音阶对应如下:1=247,2=277,3=311,4=399,5=370,6=415,7=466, 1’=494)。
    控制:通过将8255模块,设置为方式0,A端口输入,10010000B,作为控制器。通过读取A端口输入的数据来选择相应的歌曲。每次读取到的数据存入寄存器,与下次读取到的做对比,来判断是否需要切换。切换时,不同开关位状态分别代表不同歌曲,设置的三首音乐分别对应001B,010B,100B。
    三 接线图设计8254:

    CLK0 => 1Mhz
    GATE0 => +5V
    OUT0 => 扬声器
    CS => 280H~287H

    8255:

    PA0~3 => SW0~3
    CS => 288H~28FH

    四 仿真设计使用Proteus实现仿真设计,设计图如下所示:


    五 程序设计5.1 程序流程图设计如下图所示:

    5.2 程序源代码 IOBASE EQU 280H IO8253_MODE EQU IOBASE+06H IO8253_0 EQU IOBASE+00H IO8255_MODE EQU IOBASE+0EH IO8255_A EQU IOBASE+08H PAGE 50,70 DATA1 SEGMENT FREQ1 DW 247,277,311,330,370,415,466,494,0 TIME1 DW 100,100,100,100,100,100,100,200,0 FREQ2 DW 311,311,277,311,311,370,311,277,311,247,247,277,311,370,310,277,277,247,277 DW 311,370,311,415,370,415,377,377,311,370 DW 311,277,311,370,311,277,277,247,0 TIME2 DW 100,50,50,200,50,50,50,50,200,100,50,50,50,50,100,100,50,50,200 DW 150,25,25,50,150,50,50,50,50,200 DW 100,50,50,100,50,50,50,200,0 FREQ3 DW 265,294,330,262,262,294,330,262,330,349 DW 392,330,349,392,392,440,392,349,330,262 DW 392,440,392,349,330,262,294,196,262,294 DW 196,262,0 TIME3 DW 50,50,100,100,100,100,100,50,50,100 DW 100,100,100,100,50,50,100,100,100,100 DW 100,100,50,50,100,100,100,100,100,50 DW 100,100,0,0 DATA1 ENDS STACK1 SEGMENT PARA STACK DW 100 DUP(?) STACK1 ENDS;==================== CODE SEGMENT ASSUME CS:CODE,DS:DATA1 ASSUME SS:STACK1 START: MOV AX,DATA1 MOV DS,AX MOV AX,0 MOV SI,AX MOV DX,IO8255_MODE MOV AL,90H ;A端口方式0输人 OUT DX,AL MOV DX,IO8253_MODE ;连接8253的控制端口 MOV AL,36H ;定义8253为通道0,方式3, OUT DX,AL ;二进制,先读低位/后读高位;;;MUSIC1=== MUSIC1: LEA DI,FREQ1 ;取偏移地址 LEA BP,TIME1 ;取时间偏移地址 LOOP1: MOV AX,[DI] ;取时间偏移地址 CMP AX,0 JE MUSIC1 CALL SPEAKER XOR AX,AX MOV DX,IO8255_A ;连接8255输入端口 IN AL,DX ;从手动控制端读入控制选择信息 MOV AH,0 CMP AX,SI ;判断输入的信息变化没有, JZ AAA1 ;没变则表示没有改变原来的选择 MOV SI,AX CMP AL,1H ;判断输入的信息 JNZ AAA2 ;选择播放那首音乐 JMP MUSIC1 AAA2: CMP AL,2H JNZ AAA3 JMP MUSIC2 AAA3: CMP AL,4H JNZ AAA1 JMP MUSIC3 AAA1: ADD DI,2 ADD BP,2 JMP LOOP1;;;===MUSIC1;;;MUSIC2=== MUSIC2: LEA DI,FREQ2 ;取偏移地址 LEA BP,TIME2 ;取时间偏移地址 LOOP2: MOV AX,[DI] CMP AX,0 JE MUSIC2 CALL SPEAKER XOR AX,AX MOV DX,IO8255_A ;连接8255输入端口 IN AL,DX ;从手动控制端读入控制选择信息 MOV AH,0H CMP AX,SI ;判断输入的信息变化没有,没变 JZ AAA6 ;则表示没有改变原来的选择 MOV SI,AX CMP AL,1H ;判断输入的信息,选择播放那首音乐 JNZ AAA4 JMP MUSIC1 AAA4: CMP AL,2H JNZ AAA5 JMP MUSIC2 AAA5: CMP AL,4H JNZ AAA6 JMP MUSIC3 AAA6: ADD DI,2 ADD BP,2 JMP LOOP2;;;===MUSIC2;;;MUSIC3=== MUSIC3: LEA DI,FREQ3 ;取偏移地址 LEA BP,TIME3 ;取时间偏移地址 LOOP3: MOV AX,[DI] CMP AX,0 JE MUSIC3 CALL SPEAKER XOR AX,AX MOV DX,IO8255_A ;连接8255输入端口 IN AL,DX ;从手动控制端读入控制选择信息 MOV AH,0H CMP AX,SI ;判断输入的信息变化没有,没 JZ AAA9 ; 变则表示没有改变原来的选择 MOV SI,AX CMP AL,1H ;判断输入的信息,选择播放那首音乐 JNZ AAA7 JMP MUSIC1 AAA7: CMP AL,2H JNZ AAA8 JMP MUSIC2 AAA8: CMP AL,4H JNZ AAA9 JMP MUSIC3 AAA9: ADD DI,2 ADD BP,2 JMP LOOP3;;;===MUSIC3;========播放音乐子程序======== SPEAKER PROC PUSH AX PUSH BX PUSH CX PUSH DX MOV AX,[DI] MOV BX,AX MOV DX,0FH MOV AX,4240H ;送入记数值clk=1MHZ 0x0F4240 = 1000000 DIV BX MOV DX,IO8253_0 ;将数送入8253计数器中 OUT DX,AL MOV AL,AH OUT DX,AL ;将每个音节的时间周期存入CX ;MOV CX,2710H ;设0.5s的时间周期所需的循环次数 MOV BX,WORD PTR DS:[BP] ADD BX,BX ADD BX,BX LOOP_TIMES: MOV AX,2710H ;设最小节拍播放时间为0.5s ADD AX,AX ADD AX,AX DELAY_TIME: DEC AX JNZ DELAY_TIME DEC BX JNZ LOOP_TIMES POP DX POP CX POP BX POP AX RET SPEAKER ENDP CODE ENDS END START六 实验总结该小程序锻炼了包括8086汇编语言的编写、各元器件微机接口的运用等。
    扬声器发出了音乐声,总体效果还行,第一首是声调测试,1234567;第二首是菊花台;第三首是两只老虎。其中,两只老虎的曲调是百度到的,实际播放节拍不对,推测是时钟信号频率不同导致的。
    1 评论 6 下载 2018-10-15 09:36:17 下载需要6点积分
  • 基于贪心策略直接搜索算法和极大极小博弈树算法的智能人机博弈五子棋游戏

    一、问题分析五子棋是双人博弈棋类益智游戏,由围棋演变而来,属纯策略型。棋盘通常15*15,即15行,15列,共225个交叉点,即棋子落点;棋子由黑白两色组成,黑棋123颗,白棋122颗。游戏规则为黑先白后,谁先五子连成一条直线谁赢,其中直线可以是横的、纵的、45度、135度。
    本次Java编程我的目的是现实人机对战,即游戏者一方是人,另一方计算机。这就要求程序不仅要具备五子棋的基本界面,还要编程指导计算机与人进行对弈。为了使程序尽可能智能,我采用了贪心策略、传统搜索算法、极大极小博弈树算法,对应游戏玩家的3个等级:简单、中等、困难。
    二、功能设计我的程序基本功能是实现人机对弈五子棋。人和电脑交替下棋,谁先五子连成一条直线谁就赢。下面是我程序的功能模块:

    等级设置核心功能是实现不同策略与算法的对比运用,纯贪心策略实现简单等级对手,直接搜索算法实现中等等级对手,极大极小博弈树算法实现困难等级对手。对应程序中的3选1单选按钮。
    悔棋功能模拟栈机制实现人悔棋,不限步长的悔棋。对应程序中的悔棋按钮。
    棋面绘制根据不同机计算机的屏幕分辨率,绘制逼真的棋盘。
    图片引入两张古典的人物图片,生动模拟对弈双方。人物图片旁的黑白棋钵图片显示黑白棋归属。
    背景设置支持用户选择背景,包括棋盘、棋盘边框、窗口边框,彰显个性。
    音乐播放下棋时有棋子落地的声音,一方胜利时有五子连成一片的声音。同时在设置背景时相应的改变整个对弈过程中的背景音乐。
    时间显示在棋盘正上方有一模拟文本框显示当前棋局用时。
    其他小功能支持和棋、认输、开启新游戏、退出游戏等操作。

    三、数据结构与算法设计3.1 数据结构部分3.1.1 当前棋局的存储结构我的五子棋程序选择通常用到的15*15棋盘,可以开二维数组PositionFlag = new int[15][15],PositionFlag[i][j]为0表示(i, j)点尚无棋,为1表示(i, j)点是人的棋子,为2表示(i, j)点是机器的棋子。之所以选择二维数组,主要原因有两点:

    本程序需要频繁随机访问15*15的交叉点,对应查询该点状态以及改变该点状态,随机访问是数组的特点。
    15*15=225开二维数组的内存需求相对现在内存为2G及以上的计算机完全可以接受,且数组实现简单、操作方便。

    基于以上两点,尽管创建动态的顺序表—链表可能可以节省少量内存(可以只存当前有棋的点,原数组对应位置为0的点可以不存),但选择数组的优势完全在上述两点体现了出来。
    3.1.2 实现悔棋操作的数据结构由于每次悔棋只需回退当前几步,后进先出原则,这正是栈这种典型数据结构的设计思想,于是我选择栈。我自己先写了用自定义数组模拟的栈,但由于是学Java语言且由于悔棋的存储空间需要随当前步数增大而增大(由于每局最多下225步,即最多要悔225步,所以自己开个225的数组完全可以避免存储空间自增长的问题且内存完全可以接受,之所以不用自定义数组而用ArrayList类主要是为了尝试Java中STL的用法),所有我最终改为用Java类库中的ArrayList类。
    确定用ArrayList类实现栈机制后就必须考虑每个ArrayList单元具体存储什么。刚开始我存储的是当前的棋局,即整个局面,而每个局面对应一个二维数组,这样是很占用内存的。试想一下,在最坏情况下,225个ArrayList单元,每个单元存放一个15*15的二维数组,尽管225*15*15在Java的内存管理机制下不会爆栈,但也是极不划算的。之所以说不划算,是因为有更好的解决方案。由于每次悔棋只是在回退倒数一步,多步悔棋只需循环回退,所以可以只存储当前棋局最后一步的下法,对应一个二维点,完全可以自定义一个二维坐标类chessOneStep。
    3.2 算法设计部分Java语言是面向对象的语言。我在进行五子棋游戏编程是总共传创建了11个自定义的类。在编写程序的过程中,我有一个明显的体验就是面向对象编程就是一项有关对象设计和对象接口技术,很多关键的技术就是如何设计自定义的对象。
    下面我先概括给出我的所有类的作用:

    mainFrame类:主框架类,我应用程序的入口
    chessPositon类:主控类,这个类是我程序的核心类,负责控制双方的下棋,以及调用其他的类完成当前棋局的显示绘制
    chessPanel类:面板类,调用其他底层类完成当前棋局的显示绘制
    chessBoard类:棋盘绘制类,负责棋盘的绘制
    chessImage类:文件类,包含各种资源(背景图片、背景音乐)以及静态全局变量(public static Type)
    chessButton类:组件类,定义各种组件,包括按钮、单选按钮、文本框等
    chessMusic类:音乐类,负责调用Java类库完成背景音乐、下棋音乐、取胜音乐等的播放
    chessPiece类:棋局类,定义棋局二维数组数据结构并完成相关操作
    chessList类:栈类,完成悔棋等操作
    chessOneStep类:棋子类,定义每步坐标以及下在该处获得的估价值
    myCompare类:排序类,完成chessOneStep类的自定义排序

    五子棋程序类调用关系图如下所示:

    四、详细设计4.1 mainFrame类作为我的五子棋程序的主类,mainFrame类主要实例化相关的对象,如chessbutton,chessborad等,从而完成框架的创建。更重要的是实例化chessposition,这是本程序的核心类,控制游戏双方行棋过程完成人机互动下棋,然后将MyChessPosition与鼠标响应addMouseListener()关联起来。
    4.2 chessMusic类一个好的游戏必须给人一种身临其境的感觉,而声音是营造这种氛围的重要因素。参照网上各游戏运行商的音乐配置,我选择相关逼真的声音。包括背景音乐、下棋棋子落到棋盘发出的声音以及一方胜出的配乐。所有这些功能的实现,依赖于自定义的chessMusic类,采用AudioInputStream配合Clip的方式完成音乐播放的软硬件工作,然后定义两个接口chessmusic(String Name)和Stop(),前者完成播放功能,后者完成关闭当前音乐功能。因为音频文件相对较大,而我的程序提供在不同背景乐之间切换的功能,所以在打开另一个音频文件之前必须关闭前一个正在播放的音频文件,防止出现溢出。
    4.3 chessImage类适当的动画或图片能给游戏玩家带来美的体验。所以我的五子棋程序界面在不失和谐的前提下引入了尽可能多的图片,包括对弈双方、棋钵等。图片引入的具体工作通过语句import javax.imageio.ImageIO完成。同时,由于图片要在用到它的类中被访问,为了避免频繁调用函数,我直接将图片相关联的对象定义为public static,表明是公用的、静态的。进一步引申开去,我将程序中用到的静态全局变量都定义在chessImage类中。具体如下:
    public static Date begin;//每局开始时间 public static Date cur;//每局结束时间 public static chessOneStep LineLeft;//结束端点1 public static chessOneStep LineRight;//结束端点2 public static boolean IsGameOver;//是否只有一方获胜 public static int ColorOfBackGround[][]= {{255, 227, 132},{0,255,127},{218,165,32}};//背景颜色 public static int ColorOfWindows[][]= {{ 60,179,113},{245,245,245},{122,122,122}};//背景颜色 public static int WitchMatch;//背景搭配 public static String MusicOfBackGround;//背景音乐 public static int CurrentStep;//记录当前步数 public static int Rank;//设置难度等级 public static boolean IsSurrender;//判断是否认输 public static boolean IsTie;//判断是否认输 public static String Message;//输出提示信息 public static Image IconImage;// 图标 public static Image blackBoard;//白棋盘 public static Image whiteBoard;//黑棋盘 public static Image blackChess;// 白棋棋子图片 public static Image whiteChess;// 白棋棋子图片 public static Image RightPlayer;//白棋棋罐图片 public static Image LeftPlayer;//白棋玩家头像图片 public static String path = "src/";// 图片的保存路径
    4.4 chessButton类这个是程序的组件类。定义了各种功能键,完善程序功能,营造逼真的人机对战游戏效果。分为3类:
    4.4.1 按钮组件本程序有5个按钮,支持和棋、认输、新游戏、退出、悔棋等。认输和和棋按钮终止当前的棋局,给出相应的提示信息;退出按钮调用系统System.exit(0)的函数正常返回;悔棋按钮调用后面要介绍的chessList类实现悔棋;新游戏按钮则刷新当前棋局准备下一轮,要将记录当前棋局的二维数组全部置0,刷新当前棋局开始时间等。
    4.4.2 单选按钮组件游戏界面支持设置个性化界面,包括背景颜色与背景音乐,跟重要的一点是设置难度(简单、中等、困难)。单选按钮只能多选一。背景颜色主要是存储相关颜色搭配方案的RGB颜色,开2维数组,即对应RGB3原色数组的一维数组,然后通过改变WitchMatch全局变量的值来有用户自己选择颜色搭配,不同的颜色搭配对应不同的背景音乐表达一致的主题。难度设置主要是改变计算机的下棋算法,不同难度通过Rank判断进入不同的程序分支,实现不同智能等级的计算机下棋水平。
    4.4.3 文本框在不同的单选按钮前添加相应的文本框,提示用户可以实现的功能。同时我用颜色模拟出显示当前棋局耗用时间的文本框。
    不论按钮还是单选按钮都要关联相应的消息,把相应功能的实现放在消息响应处理函数理。这些主要是实现Java库提供的消息响应接口里的方法。
    4.5 chessPiece类主要完成当前棋面的存储,存储棋面的数据结构为二维数组int [][]PositionFlag;然后定义获取、设置某点以及整个棋面的状态的方法。
    SetPositionFlag(intx, int y, int flag) //设置(x,y)处的状态为flagGetPositionFlag(intx, int y) //获取(x,y)处的状态SetAllFlag(intNewFlag) //设置当前整个棋面的状态为NewFlagGetAllFlag() //获取当前整个棋面的状态DrawChessPiece(Graphicsg) //绘制当前局面的棋子
    4.6 chessBoard类功能为绘制棋盘线。由于围棋的棋盘比较复杂,横线、竖线较多,且为了使棋盘美观,还要自定义窗口边框、棋盘边框、对弈双方边框等,对线宽、线型也有一定要求。有时要单像素线条,有时要多像素线条。对于多像素线条,我主要用了2种方法。
    4.6.1 方法一在需要绘制多像素线条处首先绘制一条单像素线,然后根据线宽要求上下平移适当像素达到绘制多像素的目的。这样的方法适合绘制水平线或竖直线,绘制其他斜率的线条容易造成走样。在没有想到比较好的反走样编程思想后我选择了调用Java库中已经封装好的函数。
    4.6.2 方法二为了克服方法一绘制非水平或竖直线时造成的走样,同时也为了更进一步学习Java语言,我猜想肯定会有类似OpenGL中设置线宽的画刷,于是上网百度找到了相应的画刷Stroke类。通过Java库实现绘制不同线宽的直线,达到了反走样效果。
    4.7 chessOneStep类这个类是为了配合chessList类实现悔棋以及在计算机下棋算法实现返回有效状态点而设计的。主要数据成员为
    private int x, y, weight; //其中x,y表示点坐标,weight表示将棋下到该点获得的估价值
    主要方法如下:
    GetX() //获得当前对象的x坐标GetY() //获得当前对象的y坐标GetWeight() //获得当前对象的(x,y)处的估价值
    4.8 chessList类程序支持悔棋功能,为了实现悔棋,自定义了chessList类。这个类主要通过引入java.util.ArrayList和java.util.List实现集合的数据类型。然后自定义一些方法,如下:
    AddStep(chessOneStep OneStep) //添加一步棋到List中GetSize() //获得当前List的大小ClearList() //清空ListRemoveLast() //删去List中的最后元素
    由于每次删除当前List中的最后一个元素,实现后进先出,所以可以模拟栈的功能实现悔棋。
    4.9 myCompare类由于在计算机下棋的极大极小博弈树算法中需要对自定义对象chessOneStep按weight进行排序,所以引入了myCompare类,通过实现Comparator接口中的compare方法完成自定义对象排序。
    4.10 chessPanel类程序的自定义面板类,主要负责完成当前框架内容的显示。这是一个重要的与框架和图形显示密切相关的类。主要数据成员为
    private chessboard MyChessBoard; //当前显示棋盘private chesspiece MyChessPiece; //当前显示整个棋面的状态
    主要方法如下:
    chesspanel(chessboard MyChessBoard1, chesspiece MyChessPiece1)//构造函数,分别用MyChessBoard1和MyChessPiece1初始化MyChessBoard和MyChessPiecedisplay(chessboard MyChessBoard1, chesspieceMyChessPiece1)//自定义显示回调函数,调用repaint()完成重新绘制游戏界面paintComponent(Graphics g)//核心方法,调用各种函数完成具体的绘制工作
    4.11 chessPositon类程序算法核心类,总的功能是控制人和计算机轮流下棋,以及调用chessPanel类中的display(chessboard, chesspiece )方法完成界面的实时刷新。关于chessPositon类,我在此将重点介绍。chessPosition类的主要数据成员如下:
    privatestatic chessboard MyChessBoard; //当前显示棋盘publicstatic chesspiece MyChessPiece; //当前显示整个棋面的状态privatestatic chesspanel Mychesspanel; //当前显示面板publicstatic chesslist MyChessList=new chesslist();//当前下棋集合,用于悔棋finalprivate static int INF = (1 << 30);// 表示正无穷大的常量,用于极大极小博弈数搜索算法publicstatic boolean CanGo; //控制当前下棋一方
    类的设计集中体现在成员方法的设计上。实现人机对战,只有语言是远远不够的,还要加入算法,用算法引导计算机下棋。下面介绍该类的方法成员:
    chessposition(chesspanel , chessboard ,chesspiece ) //带有参数的构造函数chessposition() // 不带参数的构造函数mouseClicked(MouseEvent event)
    鼠标响应函数,负责人的下棋,根据鼠标点击的位置转换得到所在棋盘的相对位置。如果该位置不合法,即超出棋盘有效范围,点击无响应;如果该位置上已有棋,弹出消息框给出提示。这二者都要求重新给出下棋位置,即当前鼠标响应无效…直到点击到棋盘有效区域。
    IsOver(int Array,int x,int y)
    判断当前int[][]Array对应的棋局是否结束,即一方五子连成一条直线。此处有两种思路,一种对当前棋面上的所有棋子都进行一次判断,具体为水平方向、竖直方向、与水平线成45度方向、与水平线成135度方向,只要有一个方向五子连成一条直线就说明有一方获胜,游戏结束;另一种思路为只在当前下棋的4个方向进行判断,我的程序采用的是第二种,所以IsOver方法除了int[][]Array参数外,还有x, y参数,(x, y)表示当前下棋的坐标点。
    display()
    通过调用自定义面板类的显示回调函数用于重新显示游戏界面,达到每下一步棋及时更新游戏界面的目的。
    GetValue(int flag, int num)
    估值函数,根据经验把棋局分成只有1颗棋相连,2颗棋相连且两端被封死,2颗棋相连且一端封死另一端活的,2颗棋相连且两端都是活的,同理3颗棋、4颗棋也各自可分3种情况。不同的情况对应不同的估价值。估价值的设定是决定计算机一方是否智能的一个关键因素。
    GetPredictValue(int flag, int num)
    对未连成一片但通过再下一颗子就能连成一片的局面进行估值,这在双方下棋的有限步骤内是能产生重要影响的。如果每局棋仅考虑当前一步,是不可取的。
    Evaluate(int Array, int x, int y)
    根据棋面具体情况以及预先设定的估值函数,对某个点对应的局面进行评估。由于每次双方只能下一颗棋,所以可以每次取当前局面的所有点中对应估值最大值点的估值作为整个局面的估值。
    计算机下棋方法1
    GetGreedNext()
    对应难度等级为简单,采用贪心思想。每次下棋前在求得最有利点下棋,而是否最有利只是通过一步评估。算法伪码描述为(Max取负无穷大):
    for(行i从0到15) { For(列j从0到15) { If((i,j)对应的位置无棋) { a.假设放上一颗由人控制的棋,求估价值; b.假设放上一颗由计算机控制的棋,求估价值; c.取二者中较大值作为(i,j)处的估价值tmp; d.取tmp与Max较大值赋值给Max. } } }
    最终Max对应的点就是当前整个局面中最大的估值点。至于上述为什么要考虑双方都在该点下棋的情况呢?主要原因为下五子棋是个攻防兼备的过程,不仅要考虑自己对自己最有利,还要考虑对对手最不利,通俗来讲就是在自己赢的时候不能让对手先赢。
    计算机下棋方法2
    GetSearchNext(int LookLength)derectSearch(int [][]Array,boolean who,int deepth)
    直接搜索法,对应难度等级为中等。每步棋最多有225个不同下法,若采用直接搜索法则对应的孩子节点有225个(在下棋过程中会逐渐减少),即每层有最多225个节点待扩展,这就决定了直接搜索进行不超过2次,主要原因有两点:

    采用深度优先搜索需要递归,递归中状态过多可能会爆栈,我们知道递归是用栈机制来实现的;采用宽度优先搜索又需要存储为扩展的节点,这对内存容量要求很高。不管深搜还是广搜,在时间复杂度为O(N^m)的情况下都是不能接受的。其中N为当前棋局的待扩展节点,最大225;m为搜索的深度。
    综上所述,在采用直接搜索法时搜索深度不能太深,严格来说是应该控制在2层以内,在计算机运算速度在10^7次每秒的情况下,理论和实验都表明超过2层就会变得很慢且这种趋势成指数级增长。直接搜索算法伪代码为:
    GetSearch(boolean flag,int deep) { 如果deep等于0,返回当前棋局估值; for(行i从0到15) { For(列j从0到15) { If((i,j)对应的位置无棋) { 如果轮到计算机下棋,置标志位为2 GetSearch(!flag,deep-1); 如果轮到人下棋,置标志位为1; GetSearch(!flag,deep-1); } } } }
    计算机下棋方法3
    GetMinMaxsearchNext(int LookLength)MinMaxsearch(int [][]Array,boolean who, int deepth)
    极大极小博弈树法,对应难度等级为困难。五子棋是个博弈游戏,当前在寻找对自己最有利的下棋点时要尽可能保证对对手最不利,这种思想可以用极大极小博弈树完美阐释。关于极大极小博弈树在这里就不多说了。就算用极大极小博弈树,关于每次扩展节点与扩展深度的问题也是无法回避的。每次扩展节点不能太多,扩展深度不能太深,但若简单限制二者,就会使计算机游戏方表示出的智能水平有限。在每次扩展节点时可以采用贪心策略,首先对当前所有可扩展节点进行一次估值,选择其中估价值最大的节点进行扩展,这是一个很好的优化。好的开始很可能产生好的结果,良性循环。本题我是选择最好的5个节点进行扩展,扩展深度为4层,因为五子连成一条直线就可以取胜,故选择4层是合理的。
    下面给出极大极小博弈树算法的伪代码
    MinMaxsearch(int [][]Array,boolean who, int deepth) { 如果到达了搜索深度,返回当前棋局估值; for(行i从0到15) { For(列j从0到15) { If((i,j)对应的位置无棋) { 对当前棋局进行估值,结果存在List表中; } } } 取其中最大的5个节点进行MinMaxsearch()估值。 }
    由于取到对方最大的可以干扰对方,即在对方最有利的坐标点下上自己的棋可以“损人利己”,何乐不为?每次都取最大的这与极大极小博弈树算法有些不太一样,但对于本题是有效的。
    五、源程序清单由于整个程序比较长,在这里只贴出我认为较为重要的代码。

    源程序1 每局棋数据结构源程序2 控制人机交互下棋源程序3 完成面板的绘制
    5.1 源程序1 每局棋数据结构import java.awt.Graphics;public class chesspiece { private int[][] PositionFlag = new int[15][15];// 表示格子上的棋子类型,0表示黑子,1表示白字 public void SetPositionFlag(int x, int y, int flag) { PositionFlag[x][y] = flag; } public void SetAllFlag(int[][] NewFlag) { PositionFlag = NewFlag; } public int GetPositionFlag(int x, int y) { return PositionFlag[x][y]; } public int[][] GetAllFlag() { return PositionFlag; } public void DrawChessPiece(Graphics g) {// 画棋子 for (int i = 0; i < 15; i++) {// 扫描棋盘中所有的棋子 for (int j = 0; j < 15; j++) { int x = (int) (chessboard.Left) + i * (int) (chessboard.Inc) - 15;// 把棋子在棋盘中对应的下标转化成在游戏中的坐标 int y = 25 + j * (int) (chessboard.Inc) - 15; if (GetPositionFlag(i, j) == 1) {// 如果指定位置的棋子是黑色棋子 g.drawImage(chessimage.whiteChess, x, y, null); } else if (GetPositionFlag(i, j) == 2) {// 如果指定位置的棋子是白色棋子, g.drawImage(chessimage.blackChess, x, y, null); } } } }}
    5.2 源程序2 控制人机交互下棋import java.awt.event.MouseAdapter;import java.awt.event.MouseEvent;import java.util.ArrayList;import java.util.Collections;import javax.swing.JOptionPane;public class chessposition extends MouseAdapter { private static chessboard MyChessBoard; public static chesspiece MyChessPiece; private static chesspanel Mychesspanel; public static chesslist MyChessList = new chesslist(); final private static int INF = (1 << 30); // 表示正无穷大的常量 public static boolean CanGo; public chessposition(chesspanel Mychesspanel1, chessboard MyChessBoard1, chesspiece MyChessPiece1) { chessposition.Mychesspanel = Mychesspanel1; chessposition.MyChessBoard = MyChessBoard1; chessposition.MyChessPiece = MyChessPiece1; CanGo = true; } public chessposition() { } public void mouseClicked(MouseEvent event) { if (!CanGo) return; // 获取鼠标点击的棋盘相对位置 int x = event.getX() - (int) (chessboard.Left); int y = event.getY() - 25; int Max = (int) (chessboard.Inc * 14) + 39;// 棋盘最右、下边 int Min = 0;// 棋盘最左、上边 if ((x < Min) || (x > Max) || (y < Min) || (y > Max))// 无效棋子 return; int Inc = (int) chessboard.Inc; int NextX = x / Inc; int NextY = y / Inc; new chessmusic("下棋.wav"); if (0 != MyChessPiece.GetPositionFlag(NextX, NextY)) { JOptionPane.showMessageDialog(JOptionPane.getRootFrame(), "您的走法违规,该处已有棋子!", "温馨提示", JOptionPane.ERROR_MESSAGE); return; } chessOneStep OneStep = new chessOneStep(NextX, NextY, 0); MyChessList.AddStep(OneStep);// 保留当前下得棋 MyChessPiece.SetPositionFlag(NextX, NextY, 1); chessimage.CurrentStep++; if (chessimage.CurrentStep == 225) { chessimage.Message = "伯仲之间 ,胜负难分!"; CanGo = false; } if (IsOver(MyChessPiece.GetAllFlag(), NextX, NextY)) new chessmusic("取胜.wav"); display(); if (0 == chessimage.Rank) GetGreedNext(); else if (1 == chessimage.Rank) GetSearchNext(1); else if (2 == chessimage.Rank) GetMinMaxsearchNext(3); } // 计算机采用一步攻防贪心策略下棋 public void GetGreedNext() { int NextX, NextY; if (!CanGo) return; // 完全裸下 int MaxWei = -INF; int idX = -1; int idY = -1; for (int i = 0; i < 15; i++) { for (int j = 0; j < 15; j++) { if (0 == MyChessPiece.GetPositionFlag(i, j)) { MyChessPiece.SetPositionFlag(i, j, 2); int tmp = Evaluate(MyChessPiece.GetAllFlag(), i, j); if (tmp >= MaxWei) { MaxWei = tmp; idX = i; idY = j; } MyChessPiece.SetPositionFlag(i, j, 1); tmp = Evaluate(MyChessPiece.GetAllFlag(), i, j); if (tmp > MaxWei) { MaxWei = tmp; idX = i; idY = j; } MyChessPiece.SetPositionFlag(i, j, 0); } } } NextX = idX; NextY = idY; new chessmusic("下棋.wav"); if (-1 == NextX && -1 == NextY) { if (chessimage.CurrentStep == 225) { chessimage.Message = "伯仲之间 ,胜负难分!"; CanGo = false; } return; } chessimage.CurrentStep++; chessOneStep OneStep = new chessOneStep(NextX, NextY, 0); MyChessList.AddStep(OneStep);// 保留当前下得棋 MyChessPiece.SetPositionFlag(NextX, NextY, 2); if (IsOver(MyChessPiece.GetAllFlag(), NextX, NextY)) new chessmusic("取胜.wav"); display(); } public void display() {// 用于重新显示游戏界面 Mychesspanel.display(MyChessBoard, MyChessPiece); } // 添加棋子后只需判断水平、竖直、成45、135度角上是否连成5个 public boolean IsOver(int[][] Array, int x, int y) { boolean flag = false; int num = 1; int k = 1; while (x - k >= 0 && Array[x][y] == Array[x - k][y]) { num++; k++; } chessimage.LineLeft = new chessOneStep(x - k + 1, y, 0); k = 1; while (x + k < 15 && Array[x][y] == Array[x + k][y]) { num++; k++; } chessimage.LineRight = new chessOneStep(x + k - 1, y, 0); if (num >= 5) flag = true; if (!flag) { num = 1; k = 1; while (y - k >= 0 && Array[x][y] == Array[x][y - k]) { num++; k++; } chessimage.LineLeft = new chessOneStep(x, y - k + 1, 0); k = 1; while (y + k < 15 && Array[x][y] == Array[x][y + k]) { num++; k++; } chessimage.LineRight = new chessOneStep(x, y + k - 1, 0); if (num >= 5) flag = true; } if (!flag) { num = 1; k = 1; while (y - k >= 0 && x - k >= 0 && Array[x][y] == Array[x - k][y - k]) { num++; k++; } chessimage.LineLeft = new chessOneStep(x - k + 1, y - k + 1, 0); k = 1; while (y + k < 15 && x + k < 15 && Array[x][y] == Array[x + k][y + k]) { num++; k++; } chessimage.LineRight = new chessOneStep(x + k - 1, y + k - 1, 0); if (num >= 5) flag = true; } if (!flag) { num = 1; k = 1; while (y + k < 15 && x - k >= 0 && Array[x][y] == Array[x - k][y + k]) { num++; k++; } chessimage.LineLeft = new chessOneStep(x - k + 1, y + k - 1, 0); k = 1; while (y - k >= 0 && x + k < 15 && Array[x][y] == Array[x + k][y - k]) { num++; k++; } chessimage.LineRight = new chessOneStep(x + k - 1, y - k + 1, 0); if (num >= 5) flag = true; } if (flag) { chessimage.IsGameOver = true; if (1 == Array[x][y]) chessimage.Message = "获胜了,恭喜您!"; else chessimage.Message = "失败了,振作点!"; CanGo = false; } return flag; } public int GetMax(int a, int b) { return a < b ? b : a; } // 预先设定一些规则估值,对已连成一片的 public int GetValue(int flag, int num) { int ret = 0; if (1 == num) ret = 0; if (2 == num) { if (0 == flag)// 死2 ret = 3; else if (1 == flag)// 单活2 ret = 50; else ret = 100;// 双活2 } else if (3 == num) { if (0 == flag)// 死3 ret = 5; else if (1 == flag)// 单活3 ret = 200; else ret = 5000;// 双活3 } else if (4 == num) { if (0 == flag)// 死4 ret = 10; else if (1 == flag)// 单活4 ret = 8000; else ret = 500000; } else if (5 == num) { ret = 10000000; } return ret; } // 对未连成一片但通过再下一颗子就能连成一片的局面进行估值 public int GetPredictValue(int flag, int num) { int ret = 0; if (0 == flag || num <= 2) ret = 0; else { if (1 == flag) { if (3 == num) ret = 10; else if (4 == num) ret = 50; else ret = 200; } else { if (3 == num) ret = 100; else if (4 == num) ret = 5000; else ret = 8000; } } return ret; } // 以下棋点为中心,查看总得分,此评判方法为贪心法 public int Evaluate(int[][] Array, int x, int y) { int ret = 0; int num, k, tag; boolean lflag, rflag; // 先估值一连成一片的 // 水平线 k = 1; num = 1; lflag = true; rflag = true; while (x - k >= 0 && Array[x][y] == Array[x - k][y]) { num++; k++; } if (!(x - k >= 0 && 0 == Array[x - k][y])) lflag = false; k = 1; while (x + k < 15 && Array[x][y] == Array[x + k][y]) { num++; k++; } if (!(x + k < 15 && 0 == Array[x + k][y])) rflag = false; num = (num < 5 ? num : 5); if (lflag && rflag) { tag = 2; } else { if (lflag || rflag) tag = 1; else tag = 0; } ret += GetValue(tag, num); // 竖直线 k = 1; num = 1; lflag = true; rflag = true; while (y - k >= 0 && Array[x][y] == Array[x][y - k]) { num++; k++; } if (!(y - k >= 0 && 0 == Array[x][y - k])) lflag = false; k = 1; while (y + k < 15 && Array[x][y] == Array[x][y + k]) { num++; k++; } if (!(y + k < 15 && 0 == Array[x][y + k])) rflag = false; num = (num < 5 ? num : 5); if (lflag && rflag) { tag = 2; } else { if (lflag || rflag) tag = 1; else tag = 0; } ret += GetValue(tag, num); // 135度 k = 1; num = 1; lflag = true; rflag = true; while (y - k >= 0 && x - k >= 0 && Array[x][y] == Array[x - k][y - k]) { num++; k++; } if (!(y - k >= 0 && x - k >= 0 && 0 == Array[x - k][y - k])) lflag = false; k = 1; while (y + k < 15 && x + k < 15 && Array[x][y] == Array[x + k][y + k]) { num++; k++; } if (!(y + k < 15 && x + k < 15 && 0 == Array[x + k][y + k])) rflag = false; num = (num < 5 ? num : 5); if (lflag && rflag) { tag = 2; } else { if (lflag || rflag) tag = 1; else tag = 0; } ret += GetValue(tag, num); // 45度 k = 1; num = 1; lflag = true; rflag = true; while (y + k < 15 && x - k >= 0 && Array[x][y] == Array[x - k][y + k]) { num++; k++; } if (!(y + k < 15 && x - k >= 0 && 0 == Array[x - k][y + k])) lflag = false; k = 1; while (y - k >= 0 && x + k < 15 && Array[x][y] == Array[x + k][y - k]) { num++; k++; } if (!(y - k >= 0 && x + k < 15 && 0 == Array[x + k][y - k])) rflag = false; num = (num < 5 ? num : 5); if (lflag && rflag) { tag = 2; } else { if (lflag || rflag) tag = 1; else tag = 0; } ret += GetValue(tag, num); // 能成连成一片的 // 水平线 int add; int leftadd, rightadd; boolean leftflag, rightflag; int lvalue, rvalue; k = 1; num = 1; lflag = true; rflag = true; leftflag = true; rightflag = true; leftadd = 0; rightadd = 0; while (x - k >= 0 && Array[x][y] == Array[x - k][y]) { num++; k++; } if (!(x - k >= 0 && 0 == Array[x - k][y])) lflag = false; else { add = k + 1;// 跳过空格 while (x - add >= 0 && Array[x][y] == Array[x - add][y]) { leftadd++; add++; } if (!(x - add >= 0 && 0 == Array[x - add][y]))// 堵死了 leftflag = false; } k = 1; while (x + k < 15 && Array[x][y] == Array[x + k][y]) { num++; k++; } if (!(x + k < 15 && 0 == Array[x + k][y])) rflag = false; else { add = k + 1;// 跳过空格 while (x + add < 15 && Array[x][y] == Array[x + add][y]) { rightadd++; add++; } if (!(x + add < 15 && 0 == Array[x + add][y]))// 堵死了 rightflag = false; } if (leftflag && rflag) { tag = 2; } else { if (leftflag || rflag) tag = 1; else tag = 0; } lvalue = GetPredictValue(tag, num + 1 + leftadd); if (lflag && rightflag) { tag = 2; } else { if (lflag || rightflag) tag = 1; else tag = 0; } rvalue = GetPredictValue(tag, num + 1 + rightadd); ret += GetMax(lvalue, rvalue); // 竖直线 k = 1; num = 1; lflag = true; rflag = true; leftflag = true; rightflag = true; leftadd = 0; rightadd = 0; while (y - k >= 0 && Array[x][y] == Array[x][y - k]) { num++; k++; } if (!(y - k >= 0 && 0 == Array[x][y - k])) lflag = false; else { add = k + 1;// 跳过空格 while (y - add >= 0 && Array[x][y] == Array[x][y - add]) { leftadd++; add++; } if (!(y - add >= 0 && 0 == Array[x][y - add]))// 堵死了 leftflag = false; } k = 1; while (y + k < 15 && Array[x][y] == Array[x][y + k]) { num++; k++; } if (!(y + k < 15 && 0 == Array[x][y + k])) rflag = false; else { add = k + 1;// 跳过空格 while (y + add < 15 && Array[x][y] == Array[x][y + add]) { rightadd++; add++; } if (!(y + add < 15 && 0 == Array[x][y + add]))// 堵死了 rightflag = false; } if (leftflag && rflag) { tag = 2; } else { if (leftflag || rflag) tag = 1; else tag = 0; } lvalue = GetPredictValue(tag, num + 1 + leftadd); if (lflag && rightflag) { tag = 2; } else { if (lflag || rightflag) tag = 1; else tag = 0; } rvalue = GetPredictValue(tag, num + 1 + rightadd); ret += GetMax(lvalue, rvalue); // 135度 k = 1; num = 1; lflag = true; rflag = true; leftflag = true; rightflag = true; leftadd = 0; rightadd = 0; while (y - k >= 0 && x - k >= 0 && Array[x][y] == Array[x - k][y - k]) { num++; k++; } if (!(y - k >= 0 && x - k >= 0 && 0 == Array[x - k][y - k])) lflag = false; else { add = k + 1;// 跳过空格 while (y - add >= 0 && x - add >= 0 && Array[x][y] == Array[x - add][y - add]) { rightadd++; add++; } if (!(y - add >= 0 && x - add >= 0 && 0 == Array[x - add][y - add]))// 堵死了 rightflag = false; } k = 1; while (y + k < 15 && x + k < 15 && Array[x][y] == Array[x + k][y + k]) { num++; k++; } if (!(y + k < 15 && x + k < 15 && 0 == Array[x + k][y + k])) rflag = false; else { add = k + 1;// 跳过空格 while (y + add < 15 && x + add < 15 && Array[x][y] == Array[x + add][y + add]) { rightadd++; add++; } if (!(y + add < 15 && x + add < 15 && 0 == Array[x + add][y + add]))// 堵死了 rightflag = false; } if (leftflag && rflag) { tag = 2; } else { if (leftflag || rflag) tag = 1; else tag = 0; } lvalue = GetPredictValue(tag, num + 1 + leftadd); if (lflag && rightflag) { tag = 2; } else { if (lflag || rightflag) tag = 1; else tag = 0; } rvalue = GetPredictValue(tag, num + 1 + rightadd); ret += GetMax(lvalue, rvalue); k = 1; num = 1; leftflag = true; rightflag = true; leftadd = 0; rightadd = 0; while (y + k < 15 && x - k >= 0 && Array[x][y] == Array[x - k][y + k]) { num++; k++; } if (!(y + k < 15 && x - k >= 0 && 0 == Array[x - k][y + k])) lflag = false; else { add = k + 1;// 跳过空格 while (y + add < 15 && x - add >= 0 && Array[x][y] == Array[x - add][y + add]) { rightadd++; add++; } if (!(y + add < 15 && x - add >= 0 && 0 == Array[x - add][y + add]))// 堵死了 rightflag = false; } k = 1; while (y - k >= 0 && x + k < 15 && Array[x][y] == Array[x + k][y - k]) { num++; k++; } if (!(y - k >= 0 && x + k < 15 && 0 == Array[x + k][y - k])) rflag = false; else { add = k + 1;// 跳过空格 while (y - add >= 0 && x + add < 15 && Array[x][y] == Array[x + add][y - add]) { rightadd++; add++; } if (!(y - add >= 0 && x + add < 15 && 0 == Array[x + add][y - add]))// 堵死了 rightflag = false; } if (leftflag && rflag) { tag = 2; } else { if (leftflag || rflag) tag = 1; else tag = 0; } lvalue = GetPredictValue(tag, num + 1 + leftadd); if (lflag && rightflag) { tag = 2; } else { if (lflag || rightflag) tag = 1; else tag = 0; } rvalue = GetPredictValue(tag, num + 1 + rightadd); ret += GetMax(lvalue, rvalue); return ret; } // 计算机人工智能中直接搜索下棋,向前看LookLength步 public void GetSearchNext(int LookLength) { if (!CanGo) return; chessOneStep Option = derectSearch(MyChessPiece.GetAllFlag(), true, LookLength); int NextX = Option.GetX(); int NextY = Option.GetY(); new chessmusic("下棋.wav"); if (-1 == NextX && -1 == NextY) { if (chessimage.CurrentStep == 225) { chessimage.Message = "伯仲之间 ,胜负难分!"; CanGo = false; } return; } chessimage.CurrentStep++; chessOneStep OneStep = new chessOneStep(NextX, NextY, 0); MyChessList.AddStep(OneStep);// 保留当前下得棋 MyChessPiece.SetPositionFlag(NextX, NextY, 2); if (IsOver(MyChessPiece.GetAllFlag(), NextX, NextY)) new chessmusic("取胜.wav"); display(); } // 直接暴搜 public chessOneStep derectSearch(int[][] Array, boolean who, int deepth) { if (0 == deepth)// 返回当前局面的评估函数值 { int MaxWei = -INF; int idX = -1, idY = -1; for (int i = 0; i < 15; i++) { for (int j = 0; j < 15; j++) { // 5000,8000 if (0 == Array[i][j]) { Array[i][j] = 2; int tmp1 = Evaluate(Array, i, j); Array[i][j] = 1; int tmp2 = Evaluate(Array, i, j); if (tmp2 >= 10000000 && MaxWei < 10000000)// 机器未到死四且人到了活3 { MaxWei = tmp2 + 10000000; idX = i; idY = j; } else if (tmp2 >= 500000 && MaxWei < 500000) { MaxWei = tmp2 + 500000; idX = i; idY = j; } else if (tmp2 >= 10000 && MaxWei < 10000) { MaxWei = tmp2 + 10000; idX = i; idY = j; } else if (tmp1 > tmp2 && tmp1 > MaxWei) { MaxWei = tmp1; idX = i; idY = j; } else if (tmp2 > tmp1 && tmp2 > MaxWei) { MaxWei = tmp2; idX = i; idY = j; } Array[i][j] = 0; } } } return new chessOneStep(idX, idY, MaxWei); } chessOneStep ret = new chessOneStep(-1, -1, -INF); for (int i = 0; i < 15; i++) { for (int j = 0; j < 15; j++) { if (0 == Array[i][j]) { if (who) Array[i][j] = 2; else Array[i][j] = 1; chessOneStep tmp = derectSearch(Array, !who, deepth - 1); Array[i][j] = 0; if (tmp.GetWeight() > ret.GetWeight()) ret = tmp; } } } return ret; } // 计算机人工智能中极大极小法搜索下棋,向前看LookLength步 public void GetMinMaxsearchNext(int LookLength) { chessOneStep Option = MinMaxsearch(MyChessPiece.GetAllFlag(), true, LookLength); int NextX = Option.GetX(); int NextY = Option.GetY(); new chessmusic("下棋.wav"); if (-1 == NextX && -1 == NextY) { if (chessimage.CurrentStep == 225) { chessimage.Message = "伯仲之间 ,胜负难分!"; CanGo = false; } return; } chessimage.CurrentStep++; chessOneStep OneStep = new chessOneStep(NextX, NextY, 0); MyChessList.AddStep(OneStep);// 保留当前下得棋 MyChessPiece.SetPositionFlag(NextX, NextY, 2); if (IsOver(MyChessPiece.GetAllFlag(), NextX, NextY)) new chessmusic("取胜.wav"); display(); } // 极大极小博弈搜索 public chessOneStep MinMaxsearch(int[][] Array, boolean who, int deepth) { if (0 == deepth)// 返回当前局面的评估函数值 { int MaxWei = -INF; int idX = -1, idY = -1; for (int i = 0; i < 15; i++) { for (int j = 0; j < 15; j++) { if (0 == Array[i][j]) { Array[i][j] = 2; int tmp = Evaluate(Array, i, j); if (tmp >= MaxWei) { MaxWei = tmp; idX = i; idY = j; } Array[i][j] = 1; tmp = Evaluate(Array, i, j); if (tmp > MaxWei) { MaxWei = tmp; idX = i; idY = j; } Array[i][j] = 0; } } } return new chessOneStep(idX, idY, MaxWei); } if (who)// 轮到己方,取极大值 { chessOneStep ret = new chessOneStep(-1, -1, -INF); ArrayList<chessOneStep> TmpList = new ArrayList<chessOneStep>(); for (int i = 0; i < 15; i++) { for (int j = 0; j < 15; j++) { if (0 == Array[i][j]) { Array[i][j] = 2; TmpList.add(new chessOneStep(i, j, Evaluate(Array, i, j))); Array[i][j] = 0; } } } Collections.sort(TmpList, new MyCompare()); int num = TmpList.size() < 5 ? TmpList.size() : 5; for (int i = 0; i < num; i++) { chessOneStep t = TmpList.get(i); Array[t.GetX()][t.GetY()] = 2; chessOneStep tmp = MinMaxsearch(Array, !who, deepth - 1); if (tmp.GetWeight() > ret.GetWeight()) ret = tmp; Array[t.GetX()][t.GetY()] = 0; } return ret; } else // 轮到对手,取极小值 { chessOneStep ret = new chessOneStep(-1, -1, INF); ArrayList<chessOneStep> TmpList = new ArrayList<chessOneStep>(); for (int i = 0; i < 15; i++) { for (int j = 0; j < 15; j++) { if (0 == Array[i][j]) { Array[i][j] = 1; TmpList.add(new chessOneStep(i, j, Evaluate(Array, i, j))); Array[i][j] = 0; } } } Collections.sort(TmpList, new MyCompare()); int num = TmpList.size() < 5 ? TmpList.size() : 5; for (int i = 0; i < num; i++) { chessOneStep t = TmpList.get(i); Array[t.GetX()][t.GetY()] = 1; chessOneStep tmp = MinMaxsearch(Array, !who, deepth - 1); if (tmp.GetWeight() < ret.GetWeight()) ret = tmp; Array[t.GetX()][t.GetY()] = 0; } return ret; } }}
    5.3 源程序3 完成面板的绘制import java.awt.BasicStroke;import java.awt.Color;import java.awt.Font;import java.awt.Graphics;import java.awt.Graphics2D;import java.awt.Stroke;import java.awt.geom.Line2D;import java.util.Date;import javax.swing.JPanel;public class chesspanel extends JPanel { private static final long serialVersionUID = 1L; private chessboard MyChessBoard = new chessboard(); private chesspiece MyChessPiece = new chesspiece(); public chesspanel(chessboard MyChessBoard1, chesspiece MyChessPiece1) { MyChessBoard = MyChessBoard1; MyChessPiece = MyChessPiece1; } //自定义显示回调函数 public void display(chessboard MyChessBoard1, chesspiece MyChessPiece1) { MyChessBoard = MyChessBoard1; MyChessPiece = MyChessPiece1; this.repaint(); } //Java库刷新函数 public void paintComponent(Graphics g) {// paint(Graphics g)// // {//此时遇到的问题是只有鼠标经过是才显示button super.paintComponent(g); setBackground(new Color( chessimage.ColorOfBackGround[chessimage.WitchMatch][0], chessimage.ColorOfBackGround[chessimage.WitchMatch][1], chessimage.ColorOfBackGround[chessimage.WitchMatch][2]));// 设置背景色 //画棋盘、棋子 if (MyChessBoard != null && MyChessPiece != null) { MyChessBoard.DrawChessBoard(g);// 绘制棋盘背景 MyChessPiece.DrawChessPiece(g);// 绘制盘面棋子 } // 绘制两个玩家 g.drawImage(chessimage.LeftPlayer, 25, 25, this); g.drawImage(chessimage.RightPlayer, (int) (chessboard.scrSize.width - chessboard.Left + 50), 25, this); //画棋盘 g.drawImage(chessimage.whiteBoard, 25, 25, this); g.drawImage(chessimage.blackBoard, (int) (chessboard.scrSize.width - chessboard.Left + 250), 25, this); //显示文字提示信息 if (chessimage.Message != "") { if (chessimage.IsGameOver) { Graphics2D g2d = (Graphics2D) g; Stroke stroke = g2d.getStroke(); g2d.setStroke(new BasicStroke(10, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_ROUND)); g2d.setColor(Color.pink); g2d.draw(new Line2D.Float( (float) (chessboard.Left + chessboard.Inc * chessimage.LineLeft.GetX()), (float) (25 + chessboard.Inc * chessimage.LineLeft.GetY()), (float) (chessboard.Left + chessboard.Inc * chessimage.LineRight.GetX()), (float) (25 + chessboard.Inc * chessimage.LineRight.GetY())));// 五子连线 g2d.setStroke(stroke); } g.setColor(Color.red); g.setFont(new Font("楷体", Font.BOLD, 86)); g.drawString(chessimage.Message, (int) (chessboard.Left - 50), (int) (chessboard.Low - 7 * chessboard.Inc)); } // 设置游戏时间 g.setColor(Color.blue); g.fillRect((int) (chessboard.Left + 260), 0, 140, 20);// 左边人的下角 g.setColor(Color.yellow); g.setFont(new Font("楷体", Font.BOLD, 20)); chessimage.cur = new Date(); Long m = chessimage.cur.getTime() - chessimage.begin.getTime(); Long H = m / (60 * 60 * 1000); m = m % (60 * 60 * 1000); Long M = m / (60 * 1000); m = m % (60 * 1000); m = m / 1000; String dif = "时间" + H + ":" + M + ":" + m; g.drawString(dif, (int) (chessboard.Left + 280), 20); }}
    六、运行结果截图 游戏界面1(背景乐《高山流水》)

    游戏界面2(背景乐《赛马》)

    游戏界面3(背景乐《笑傲江湖琴箫合奏》)

    用户一方取胜界面

    计算机一方取胜界面

    七、课程总结总的感觉,跟着老师学习Java很容易上手。Java与C、C++是当今最为流行的语言,而Java是其中产生最晚、最充满活力的编程语言。这也就促使我对学习Java有着浓厚的兴趣。
    学习Java与学习其他语言一样,首先是掌握基本语法与控制结构。Java是C语言的升华版,没有指针、没有运算符重载、没有头文件等,而且在编程过程中不用用户手动管理内存,Java提供垃圾回收站自动收回运行过程中释放过的内存空间。Java不提供boolean型与整型数据的自动转换,如if(int)会报语法错,这在C系语言中是完全可以的。而且不同数据类型的转换都要强制进行,如Int x=1.0会报语法错误,必须强制类型转换。这样的风格使Java编程简单、规范。
    Java是一种面向对象的编程语言。对象定义为相关数据与方法的集合。Java编程实现可视化比其他语言方便易懂,只需new 实例化该类型对象,然后添加自定义的界面。Java不支持多继承,多继承会使程序结构较为复杂,Java中单继承与接口的引入可以实现多继承的功能。接口是一些共用的、抽象的数据与方法,但实现接口时必须实现接口中所有的抽象方法;为了避免这种问题,可以引入事件适配器类,这是一种抽象类,但在继承他们创建新类时,可以不现实所有的方法,只需实现需要实现的方法即可。
    Java提供功能齐全的异常类,处理程序运行时的出错问题。Try块监视程序段,catch进行异常处理。用户可以自定义异常类及异常处理机制。
    此外,Java在web网页应用方面有着很重要的应用。由于目前我没有进行网络方面的编程,所以在此就不展开。
    此外,我觉得老师的授课方式很好:在结课后要求每人上交自己的程序,这就能充分调动学生的编程积极性。之前我就对游戏有着浓厚的兴趣,尤其是棋类游戏,在老师布置任务后,我选择了五子棋来挑战自己。虽说之前做ACM,对算法比较敏感,但做游戏需要可视化,而且是自己不熟悉的Java语言,这是一个很大的挑战。但我眼中的IT从业人员应该是勇于挑战、善于学习、迎难而上的,于是我毅然选择了五子棋,最终达到了自己想要的效果。
    1 评论 8 下载 2018-10-06 22:18:20 下载需要8点积分
  • 基于WIN32 API界面编程实现的华容道小游戏

    1 游戏简介华容道是古老的中国智力游戏,由“曹操败走华容道,正与关公狭路逢。只为当初恩义重,放开金锁走蛟龙”这一故事情节引申而来。华容道有一个4*5的二十个方块构成的棋盘,下方边界中间有一个宽为2方格的出口,还包含1个2*2的大滑块代表曹操,5个2*1的滑块代表五虎上将,4个1*1的小滑块代表小兵。游戏的目的是利用棋盘中两个1*1的空格来移动所有滑块,不允许跨越棋子,设法用最少的步数,最终使最上方被包围的曹操从出口成功脱逃。我在保持“横刀立马阵型”的基础上美化了滑块的图案,借用了时下流行的游戏“阴阳师”这一主题,赋予这个古老游戏一个新的面貌。
    2 程序功能在Win32下以图形界面提供给用户游戏平台,游戏开始时,所有滑块的起始位置都是确定的,用户通过鼠标点击选中滑块,被选中的滑块会闪烁,鼠标点击空白处,滑块会移动到相应位置,每次只能移动一步。
    3 设计思路WIN32界面编程要点:

    主函数WinMain函数最重要的是注册窗口和创建窗口以及message的get、translate和dispatchGamePaint函数运用window的GDI函数完成了核心步骤,即所有图像的绘制过程CallBack回调函数则是从客户的所有操纵中接受信息并把它处理后反应为在游戏中的各步操作
    用户操作方面包含数据表示和操作控制两个方面。
    3.1 数据表示将棋盘分为4*5大小的20个块,即建立一个橫为4,纵为5的二维数组,首先用结构体定义游戏中使用的坐标:
    typedef struct _coord{ short x;④ short y; int select;}game_coord, *Pgame_coord;
    x为横坐标,y为纵坐标,select为判断滑块是否选中,初始值为0,当被选中后select变为1,画边框函数根据判断函数勾勒对应边框。
    游戏中,兵将需要使用不同的图片,故需要用不同的数字标志不同的棋子,在主函数WM_CREATE的消息中,创建一个新游戏时,需要同时创建4个兵,5个将和一个核心滑块,因此需要以下函数:
    void CreateGame(HWND hwnd){ SetBoundary(); CreateSoldier(); CreateGeneral(); CreateCMAN(); CreateEmpty();}

    Setboundary用于设置x=4,y=5的边界CreateSoldier用于创建四个小兵,分别赋予他们对应的x和y值CreateGeneral用于创建五个将,分别赋予他们每人对应的两个部分的x和y值CreateCMAN用于创建核心滑块,赋予其四个部分对应的x和y值CreateEmpty用于创建两个空白,分别赋予他们对应的x和y值
    棋盘上的所有部分都用(x, y)数据点来表示,在GamePaint绘制函数中将图案赋予他们,成为区域,得到如下效果。

    3.2 操作控制操作包含选中和移动。
    设置全局变量nCode[2]来保存两次鼠标点击的滑块数据。当主函数第一次接收到WM_LBUTTONDOWN鼠标点击信息时,通过TransferPos(LOWORD(lParam), HIWORD(lParam))函数找到鼠标点击的对应位置,并将它转换成对应滑块的序号,用nCode[0]来记录,此刻再调用Dispatch函数,将选中的滑块序号转化为对应的某种类型的确定的某一滑块,并将其对应的select值设置为1,调用GamePaint函数将被选中的滑块勾出黑色边框。
    当主函数第二次接收到WM_LBUTTONDOWN鼠标点击信息时,通过TransferPos(LOWORD(lParam), HIWORD(lParam))函数找到鼠标点击的对应位置,并将它转换成对应滑块的序号,用nCode[1]来记录,此刻调用DispatchCode(nCode)函数,将两次鼠标点击信息传入,在函数内部设置flag变量,来控制鼠标第一次选中的滑块类型,和判断鼠标第二次是否选中空白,然后进入CanMove(p, i, flag, j)函数,p为第一次选中滑块的坐标,i为选中某类型的第几个滑块,flag为选中滑块的类型,j为选中第几个空白。CanMove函数只需判断被选中单元到目的地是否可行即可,程序定义了上下左右四个方向,要判断所到之处是否全为空格,若成功则互换两个滑块的坐标,若不成功则返回0值。DispatchCode函数返回的值表示是否有移动,若有移动则需要调用GamePaint函数进行重绘,效果如下图所示。



    效果1
    效果2









    3.3 其他游戏中定义了全局变量step来记录玩家移动了多少步,并实时显示在屏幕下方,玩家一共有88次机会,若步数用尽,则会弹出提示框游戏结束,若在步数限制内成功解救大滑块,则会弹出提示框游戏胜利,如下图所示。



    游戏失败
    游戏胜利









    4 总结
    首先是了解每个函数的作用和主要函数的参数形式,然后对老师的代码进行模仿,第一步实现了游戏坐标的设置,对每个滑块的坐标进行了生成、定义以及获得函数,并通过理解该写老师的ui.c文件,实现了将抽象的点类数据以色素块的形式展现在屏幕上。第二步是获取用户的鼠标操作和滑块移动,先开始不会处理将两次鼠标点击信息同时储存的步骤,于是只能实现鼠标点击相应滑块后,滑块自己向周围空白可移动的方向移动,但是这样的信息处理出现了bug,即当所选中的滑块可以同时向左向右(或向上向下)移动时,它只能单调地向某个方向移动,这样就使得当用户想让滑块右移的时候无法满足其需求,于是只能回头,依然是得实现鼠标两次点击实现滑块的移动。第三步即前文所述,创建了全局变量nCode[2]和count,通过count++来判断鼠标点击的次数,同时nCode储存两次鼠标点击的信息,从而成功实现了用户第一次点击滑块,第二次点击空白,使用户能顺着自己的心意使滑块向自己想要的目的地移动。但同时出现的一点不妥,因为count在鼠标点击的同时自加,只有在两次点击都有效时才能实现移动功能,如果用户不小心点错,界面又没有是否选中的提示,很容易在一次次点击中使count的值错乱,从而点击屏幕没有响应,于是给选中的滑块进行标示就变成一件不得不实现的事情了。第四步,在游戏坐标结构体中增加了select变量,用来判断该滑块是否被选中,通过模仿画边框的函数成功实现了用户点击某滑块,该滑块会加上黑色边框的效果。第五步,为了对游戏进行美化,将色素块换成了“阴阳师”游戏中可爱的人物图片,在这一过程中经历了麻烦的事情,像同学求助贴图操作,明明是一样的代码却一直不能实现位图的成功粘贴,最终在老师的帮助下发现了函数调用的问题,成功实现了贴图操作,非常感谢老师和伸出援手帮助我的同学们~第六步,给游戏加上了背景音乐,先开始根据网上教程使用了PlaySound函数,但因为头文件以及字符串无法读取的问题不能成功使用,在终于成功播放出音乐后又遇到了新的问题——该函数必须在音乐全部播放结束后才会接受和处理下一步操作,导致把它加在哪条语句之后都很不方便,于是改用mciSendString函数,成功实现了背景音乐的添加。
    至此,暑期小学期实现华容道小游戏的作业完美收工!对自己的努力和成果较为满意。在这个过程中,学到了很多东西,不光是学习上的知识还有收集信息处理问题以及自己尝试学习解决问题的能力,附上最终效果图,可惜听不到音乐^_^



    效果图1
    效果图2








    1 评论 9 下载 2018-10-06 22:07:42 下载需要8点积分
  • 基于Cocos2d-x实现的RunOrDie小游戏

    一、项目阐述1.1 简介本作是玩家通过鼠标控制角色移动,利用道具击杀怪物得分的一款游戏。
    1.2 功能玩家通过鼠标控制角色移动,地图上会不定时在随机位置出现随机移动的怪物,玩家需要避开这些怪物,并且利用随机出现的道具击杀怪物,击杀怪物数量越多,分数越高。
    1.3 道具
    小刀:角色周围会出现一把围绕着其旋转的小刀,在小刀攻击范围内的怪物将会被消灭,效果持续一段时间 炸弹:形成一次大范围伤害,消灭爆炸区域内的怪物 散射子弹:角色向 8 个方向各发射一枚有穿透性的子弹,子弹路径上的怪物将被消灭
    1.4 亮点
    操作简单,易于理解 简单的色调,清新的画风
    二、项目展示游戏主界面
    主界面有 4 个选项,分别跳转到另外 4 个子界面;主界面的背景有着不同的动画效果。

    游戏界面
    玩家用鼠标光标控制角色移动,用道具消灭怪物,道具视觉效果如下 。
    小刀道具:

    爆炸:

    散射子弹:

    游戏结束:

    排行榜:
    本地储存玩家的成绩,并按先后排名,背景图案有着不同的动画效果。

    规则说明:

    五、 项目难点及解决方案5.1 素材问题在期中 Project 展示的时候,就提到我们的期末 Project 主要卡在了动画素材的寻找上面,算是为了避开这个问题吧,我们以对称的几何图形为基本素材,对图片进行加工处理后,利用 cocos 的基础动画也能做出很不错的效果。至于颜色方面,由于几何图形的颜色不好互相搭配,所以与期中 Project 时我们的项目欢快的颜色搭配相反,这次我们干脆统一一个黑白的色调,这样看起来反而更加简洁精炼。素材来源方面的困难就这样解决了吧。
    5.2 素材处理虽然素材来源方面的困难这样解决了,但是不对图片进行后续的处理时很难达到想要的效果的,而其实我们小组 4 位成员都不会用 PS 软件,也对图像处理没什么经验。但为了更好的效果,只能下载了 PS,一边问身边的同学,一边百度摸索,一步步做出自己想要的图片效果,才有了现在的这个看起来很不错的游戏!
    5.3 在游戏玩家获得道具且道具结束时会出现报错由于道具在结束后将其从场景中移除,而在判断碰撞的调度器里,我们会获取道具的位置,这道具精灵已为空,所以出现错误。对此我们在道具消失前的 0.05 秒便停止判断碰撞,这样避免了访问空精灵的位置。
    5.4 在创建数据库过程中,无法引用 sqlite3.h这个问题虽然困扰了不少时间,但是解决方法却比较简单,就是要先下载一个sqlite3 的包,然后需要在加入一行代码:#pragma comment(lib, “sqlite3.lib”)。
    5.5 对于怪物的移动由于怪物如果只追着玩家时,最终所有怪物都会比较聚集在中心位置。而这里我们添加了另一组随机移动的怪物,尽量使得怪物都能比较分散地分布在地图上。
    5.6 在制作发散子弹由于发散子弹是从 8 个方向发射出去,而一开始打算只创建一个精灵,去实现 8个方向发射。而显然这难以实现,后来跟组员讨论一波后,觉得通过创建 8 个精灵去实现这一效果,虽然这样的话在判断碰撞时要考虑 8 个精灵比较多,但是这也是我们能想到相对较好的方法了。
    1 评论 2 下载 2018-10-06 21:35:59 下载需要7点积分
  • 基于WIN32汇编实现的仿Windows计算器

    摘要使用Win32编程设计一个功能及界面风格类似于Windows计算器的计算器程序,只要求实现标准型计算器。
    主要实现的功能:包含基本的四则运算、倒数运算、平方根运算。并支持存储区的存储、清除、调出、累加等功能。
    关键词:win32,Windows计算器,汇编,四则运算,倒数运算,平方根运算
    AbstractUsing Win32 programming to design a calculator program with a functional and interface style similar to the Windows calculator.It’s only a standard calculator.
    Mainly implemented functions including basic four arithmetic operations, reciprocal operations, and square root operations. It also supports the clearing, recalling, and accumulating functions of the storage area.
    Keywords:Win32, Windows calculator program, Assembly Language , Arithmetic, Countdown, Square root operation
    1 系统分析与设计1.1 系统分析本程序为Win32窗口应用程序,因此采用Windows开发包的文档中规定的Windows程序标准框架进行编程设计。

    1.2 系统设计按照Windows程序标准框架,主程序用于获得并保存本程序的句柄,并调用窗口主程序WinMain创建窗口并进入消息循环。WinMain程序将获取的消息分发给消息处理程序Calculate进行处理。
    消息处理程序Calculate用于相应窗口创立、销毁、按键等消息并进行处理。
    系统总体架构如下图:

    1.3 界面设计系统界面仿照Windows计算器程序界面设计,并使用资源文件进行定义,设计界面如下:

    1.4 功能分析与设计
    数字:添加文本框字符串添加数字字符,调用函数BtnNum完成该功能小数点:为当前输入数字添加小数点,将判断是否小数点的变量HasPoint赋值为1正负号:将当前数字取相反数并在对话框显示,拟通过浮点运算求相反数并调用ShowNum函数显示数字双目运算符:计算结果,调用函数BtnOperator实现运算功能等号:计算结果,调用函数BtnEqual实现运算功能单目运算符:立即对当前数字进行运算并输出结果MS:将当前数据保存在变量Remember中,并在记忆区存储情况的标签中显示相应的信息M+:将当前数据加到变量Remember上,并在记忆区存储情况的标签中显示相应的信息MR:将变量Remember数据显示到文本框中MC:将变量Remember归零,并在记忆区存储情况的标签中显示相应的信息C:初始化计算器,调用函数Init实现该功能,并在文本框显示0.CE:将当前数字清零Backspace:删除当前数据的末位数字
    1.5 文件设计
    头文件(Calculator.inc):头文件中引入程序所需要的库以及常量和函数申明源文件(Calculator.asm):汇编程序源代码资源文件(Calculator.rc):定义程序的窗口界面以及相关资源说明文件(Calculator.exe.manifest):说明程序的相关配置及信息
    2 系统实现2.1 创建计算器界面利用资源文件定义系统界面,代码如下,文件分别定义了对话框,菜单和Icon图标等资源,为了在程序中方便对消息的处理,此处有意连续定义了ID_NUM0~ID_NUM9。
    #include "resource.h"#define ISOLATION_AWARE_ENABLED#define ID_NUM0 300#define ID_NUM1 301#define ID_NUM2 302#define ID_NUM3 303#define ID_NUM4 304#define ID_NUM5 305#define ID_NUM6 306#define ID_NUM7 307#define ID_NUM8 308#define ID_NUM9 309#define ID_NEG 310#define ID_POINT 311#define ID_MUL 312#define ID_DIV 313#define ID_SUB 314#define ID_ADD 315#define ID_EQU 316#define ID_PER 317#define ID_DAO 318#define ID_SQRT 319#define ID_MC 320#define ID_MR 321#define ID_MS 322#define ID_MPLUS 323#define ID_M 324#define ID_BACK 325#define ID_CE 326#define ID_C 327#define ID_RESULT 328#define ID_COPY 1001#define ID_PASTE 1002#define ID_STANDARD 1003#define ID_SCIENCE 1004#define ID_PACKET 1006#define ID_HELP 1007#define ID_ABOUT 1008#define ID_EXIT 1009Calculator DIALOGEX 0, 0, 170, 133STYLE DS_CENTER | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOXCLASS "Calculator"CAPTION "计算器"FONT 8, "Tahoma"BEGIN PUSHBUTTON "0",ID_NUM0,36,99,23,16,0 PUSHBUTTON "1",ID_NUM1,36,81,23,16,0 PUSHBUTTON "2",ID_NUM2,61,81,23,16,0 PUSHBUTTON "3",ID_NUM3,87,81,23,16,0 PUSHBUTTON "4",ID_NUM4,36,63,23,16,0 PUSHBUTTON "5",ID_NUM5,61,63,23,16,0 PUSHBUTTON "6",ID_NUM6,87,63,23,16,0 PUSHBUTTON "7",ID_NUM7,36,44,23,16,0 PUSHBUTTON "8",ID_NUM8,61,44,23,16,0 PUSHBUTTON "9",ID_NUM9,87,44,23,16,0 PUSHBUTTON "+/-",ID_NEG,61,99,23,16,0 PUSHBUTTON ".",ID_POINT,87,99,23,16,0 PUSHBUTTON "/",ID_DIV,113,44,23,16,0 PUSHBUTTON "*",ID_MUL,113,63,23,16,0 PUSHBUTTON "-",ID_SUB,113,81,23,16,0 PUSHBUTTON "+",ID_ADD,113,99,23,16,0 PUSHBUTTON "sqrt",ID_SQRT,139,44,23,16,0 PUSHBUTTON "%",ID_PER,139,63,23,16,0 PUSHBUTTON "1/x",ID_DAO,139,81,23,16,0 PUSHBUTTON "=",ID_EQU,139,99,23,16,0 PUSHBUTTON "MC",ID_MC,6,44,23,16,0 PUSHBUTTON "MR",ID_MR,6,63,23,16,0 PUSHBUTTON "MS",ID_MS,6,81,23,16,0 PUSHBUTTON "M+",ID_MPLUS,6,99,23,16,0 PUSHBUTTON "Backspace",ID_BACK,36,23,42,16,0 PUSHBUTTON "CE",ID_CE,79,23,41,16,0 PUSHBUTTON "C",ID_C,122,23,41,16,0 EDITTEXT ID_RESULT,5,2,160,13,ES_RIGHT | ES_NUMBER ,0 CTEXT "",ID_M,9,23,17,14,SS_SUNKEN | NOT WS_BORDERENDMenu MENU LOADONCALL BEGIN POPUP "编辑(&F)" BEGIN MENUITEM "复制(&C) Ctrl+C",ID_COPY MENUITEM "粘贴(&P) Ctrl+P",ID_PASTE MENUITEM SEPARATOR MENUITEM "关闭(&E)",ID_EXIT END POPUP "查看(&V)" BEGIN MENUITEM "标准型(&T)",ID_STANDARD MENUITEM "科学型(&S)",ID_SCIENCE,GRAYED MENUITEM SEPARATOR MENUITEM "数字分组(&I)",ID_PACKET END POPUP "帮助(&H)" BEGIN MENUITEM "帮助主题(&H)",ID_HELP MENUITEM SEPARATOR MENUITEM "关于计算器(&A)",ID_ABOUT END POPUP "", GRAYED BEGIN MENUITEM "复制(&C) Ctrl+C",1001 MENUITEM "粘贴(&P) Ctrl+P",1002 MENUITEM SEPARATOR MENUITEM "标准型(&T)",1003 MENUITEM "科学型(&S)",1004,GRAYED MENUITEM SEPARATOR MENUITEM "数字分组(&I)",1006 MENUITEM SEPARATOR MENUITEM "帮助主题(&H)",1007 MENUITEM "关于计算器(&A)",1008 MENUITEM SEPARATOR MENUITEM "关闭(&E)",1009 END ENDIcon ICON MOVEABLE PURE LOADONCALL DISCARDABLE "Calculator.ico"2.2 引入头文件及库在Calculator.inc头文件中统一定义程序所需的头文件及引入库,代码如下:
    ;--------------------------- 头文件声明--------------------------- include windows.inc include user32.inc include kernel32.inc include comctl32.inc include masm32.inc include shell32.inc;--------------------------- 引入库声明--------------------------- includelib user32.lib includelib comctl32.lib includelib masm32.lib2.3 定义常量在Calculator.inc中定义程序所需常量,代码如下:
    ;---------------------------- 常量声明---------------------------- ID_NUM0 equ 300 ID_NUM1 equ 301 ID_NUM2 equ 302 ID_NUM3 equ 303 ID_NUM4 equ 304 ID_NUM5 equ 305 ID_NUM6 equ 306 ID_NUM7 equ 307 ID_NUM8 equ 308 ID_NUM9 equ 309 ID_NEG equ 310 ID_POINT equ 311 ID_MUL equ 312 ID_DIV equ 313 ID_SUB equ 314 ID_ADD equ 315 ID_EQU equ 316 ID_PER equ 317 ID_DAO equ 318 ID_SQRT equ 319 ID_MC equ 320 ID_MR equ 321 ID_MS equ 322 ID_MPLUS equ 323 ID_M equ 324 ID_BACK equ 325 ID_CE equ 326 ID_C equ 327 ID_RESULT equ 328 ID_COPY equ 1001 ID_PASTE equ 1002 ID_STANDARD equ 1003 ID_SCIENCE equ 1004 ID_PACKET equ 1006 ID_HELP equ 1007 ID_ABOUT equ 1008 ID_EXIT equ 1009 ID_NOTIFYICON equ 2000 WM_SHELLNOTIFY equ WM_USER+12.4 函数声明在Calculator.inc声明了自定义函数的原型,代码如下:
    ;---------------------------- 函数声明---------------------------- WinMain PROTO :DWORD, :DWORD, :DWORD, :DWORD ; 窗口主程序 Calculate PROTO :DWORD,:DWORD,:DWORD,:DWORD ; 消息处理程序 PackNum PROTO ; 数字分组子程序 UnpackNum PROTO ; 数字不分组子程序 BtnNum PROTO :DWORD ; 数字按键消息处理程序 ShowNum PROTO ; 显示数据子程序 ShowTextM PROTO ; 显示存储信息子程序 Init PROTO ; 初始化计算器子程序 GetResult PROTO ; 计算结果子程序 BtnOperator PROTO ; 双目运算符消息处理程序 BtnEqual PROTO ; 等于消息处理程序数据段定义代码如下:
    ;===================== Start 数据段定义Start ===================== .data ProgramName db "计算器",0 ;程序名 Author db "作者:桂杨",0 ;作者 HelpFile db "rc.hlp",0 ;帮助文档 hInstance db ? ;主程序句柄 hEdit db ? ;输出文本框句柄 hTextM db ? ;记忆标签句柄 hMenu db ? ;菜单句柄 hIcon db ? ;Icon句柄 DialogName db "Calculator",0 ;对话框名称 MenuName db "Menu",0 ;菜单名称 IconName db "Icon",0 ;Icon名称 TextM db 'M',0 ;M Output db "0.",0,30 dup(0) ;输出字符串 IsStart db 1 ;判断是否运算开始 HasPoint db 0 ;判断是否存在小数点 HasEqueal db 0 ;判断是否存在等号 Remember dq 0.0 ;记忆数据 Number dq 0.0 ;记录临时数据 Result dq 0.0 ;记录结果 Operand dq 0.0 ;记录操作数 IsPacket db 0 ;数字分组 Operator db '.' ;记录运算符 IsError db 0 ;记录是否出现异常 Div0 db "除数不能为零。",0 FunctionError db "函数输入无效。",0 hGlobal HANDLE ? ;剪切板内存块句柄 pGlobal db ? ;pointer to allocate memory NumLittle REAL8 1.0E-12 Num10 REAL8 10.0 ;实数10 Num100 REAL8 100.0 ;实数100 NotifyIcon NOTIFYICONDATA<> ;通知栏图标;======================= End 数据段定义End =======================2.5 程序说明2.5.1 主程序主程序用于获得并保存本程序的句柄,调用WinMain主程序创建窗口并获取和分发消息,然后结束程序。
    主程序流程图及原代码如下:

    invoke GetModuleHandle,NULL;获得并保存本程序的句柄mov hInstance,eaxinvoke WinMain,hInstance,0,0,SW_SHOWDEFAULTinvoke ExitProcess,eax;退出程序,返回eax值2.5.2 WinMain主程序WinMain主程序用于创建窗口并获取和分发消息。
    主程序流程图及源代码如下:

    WinMain proc hInst:DWORD, hPrevInst:DWORD, CmdLine:DWORD, CmdShow:DWORD LOCAL wc:WNDCLASSEX ;窗口类 LOCAL msg:MSG ;消息 LOCAL hWnd:HWND ;对话框句柄 mov wc.cbSize,sizeof WNDCLASSEX ;WNDCLASSEX的大小 mov wc.style,CS_BYTEALIGNWINDOW or CS_BYTEALIGNWINDOW ;窗口风格or CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndProc,OFFSET Calculate ;窗口消息处理函数地址 mov wc.cbClsExtra,0 ;在窗口类结构后的附加字节数,共享内存 mov wc.cbWndExtra,DLGWINDOWEXTRA ;在窗口实例后的附加字节数(!注意点) mov eax,hInst mov wc.hInstance,eax ;窗口所属程序句柄 mov wc.hbrBackground,COLOR_BTNFACE+1 ;背景画刷句柄 mov wc.lpszMenuName,NULL ;菜单名称指针 mov wc.lpszClassName,OFFSET DialogName ;类名称指针 invoke LoadIcon,hInst,addr IconName ;加载Icon mov wc.hIcon,eax ;图标句柄 invoke LoadCursor,NULL,IDC_ARROW mov wc.hCursor,eax ;光标句柄 mov wc.hIconSm,0 ;窗口小图标句柄 invoke RegisterClassEx,addr wc ;注册窗口类 invoke CreateDialogParam,hInst,addr DialogName,0,addr Calculate,0 ;调用对话框窗口 mov hWnd,eax ;保存对话框句柄 invoke ShowWindow,hWnd,CmdShow ;最后一个参数可设置为SW_SHOWNORMAL invoke UpdateWindow,hWnd ;更新窗口StartLoop: ;消息循环 invoke GetMessage,addr msg,0,0,0 ;获取消息 cmp eax,0 je ExitLoop invoke TranslateMessage,addr msg ;转换键盘消息 invoke DispatchMessage,addr msg ;分发消息 jmp StartLoopExitLoop: ;结束消息循环 mov eax,msg.wParam retWinMain endp2.5.3 消息处理程序消息处理程序用于处理用户消息。
    消息处理程序流程图及代码如下:

    Calculate proc hWin:DWORD,uMsg:UINT,aParam:DWORD,bParam:DWORD LOCAL pt:POINT .if uMsg == WM_INITDIALOG invoke GetDlgItem,hWin,ID_RESULT ;获取输出文本框句柄 mov hEdit,eax ;保存文本框句柄 invoke GetDlgItem,hWin,ID_M ;获取记忆标签句柄 mov hTextM,eax ;保存记忆标签句柄 invoke LoadIcon,hInstance,addr IconName ;载入Icon mov hIcon,eax ;保存Icon句柄 invoke SendMessage,hWin,WM_SETICON,ICON_SMALL ,eax invoke LoadMenu,hInstance,addr MenuName ;加载菜单 mov hMenu,eax ;保存菜单句柄 invoke SetMenu,hWin,eax invoke CheckMenuRadioItem, hMenu, ID_STANDARD, ID_SCIENCE,ID_STANDARD,MF_BYCOMMAND ;选中标准型 invoke SendMessage,hEdit,WM_SETTEXT,0,addr Output ;显示"0." .elseif uMsg == WM_SIZE .if aParam==SIZE_MINIMIZED ;最小化 mov NotifyIcon.cbSize,sizeof NOTIFYICONDATA push hWin pop NotifyIcon.hwnd mov NotifyIcon.uID,ID_NOTIFYICON mov NotifyIcon.uFlags,NIF_ICON+NIF_MESSAGE+NIF_TIP mov NotifyIcon.uCallbackMessage,WM_SHELLNOTIFY mov eax,hIcon mov NotifyIcon.hIcon,eax invoke lstrcpy,addr NotifyIcon.szTip,addr ProgramName invoke ShowWindow,hWin,SW_HIDE ;隐藏窗口 invoke Shell_NotifyIcon,NIM_ADD,addr NotifyIcon .endif .elseif uMsg == WM_SHELLNOTIFY .if aParam==ID_NOTIFYICON .if (bParam==WM_LBUTTONDOWN) ;单击通知栏图标 invoke ShowWindow,hWin,SW_SHOW ;显示窗口 invoke Shell_NotifyIcon,NIM_DELETE,addr NotifyIcon ;删除通知栏图标 .elseif (bParam==WM_RBUTTONDOWN) ;右键通知栏图标 invoke GetCursorPos,addr pt invoke GetSubMenu,hMenu,3 invoke TrackPopupMenu,eax,TPM_LEFTALIGN,pt.x,pt.y,NULL,hWin,NULL .endif .endif .elseif uMsg == WM_CHAR ;热键操作 mov eax,aParam sub eax,'0' add eax,ID_NUM0 .if (eax>=ID_NUM0) && (eax<=ID_NUM9) ;数字按钮 invoke Calculate,hWin,WM_COMMAND,eax,0 .elseif (eax==0ffh) ;ID_COPY invoke Calculate,hWin,WM_COMMAND,ID_COPY,0 .elseif (eax==112h) ;ID_PASTE invoke Calculate,hWin,WM_COMMAND,ID_PASTE,0 .elseif (eax==104h) ;ID_BACK invoke Calculate,hWin,WM_COMMAND,ID_BACK,0 .elseif (eax==265) ;ID_EQU invoke Calculate,hWin,WM_COMMAND,ID_EQU,0 .elseif (eax==298) ;ID_POINT invoke Calculate,hWin,WM_COMMAND,ID_POINT,0 .elseif(eax==295) ;ID_ADD invoke Calculate,hWin,WM_COMMAND,ID_ADD,0 .elseif (eax==297) ;ID_SUB invoke Calculate,hWin,WM_COMMAND,ID_SUB,0 .elseif (eax==294) ;ID_MUL invoke Calculate,hWin,WM_COMMAND,ID_MUL,0 .elseif (eax==299) ;ID_DIV invoke Calculate,hWin,WM_COMMAND,ID_DIV,0 .endif .elseif uMsg == WM_COMMAND mov eax,aParam .if eax == ID_CE ;清零按钮CE lea esi,Output mov BYTE PTR[esi],'0' mov BYTE PTR[esi+1],'.' mov BYTE PTR[esi+2],0 .if IsError==1 invoke Init .endif invoke SendMessage,hEdit,WM_SETTEXT,0,addr Output .elseif eax == ID_C ;初始化按钮C invoke Calculate,hWin,WM_COMMAND,ID_CE,bParam invoke Init .elseif IsError==1 ret .elseif eax == ID_BACK ;退格按钮Backspace invoke UnpackNum .if IsStart==0 lea esi,Output .while BYTE PTR[esi]!=0 inc esi .endw .if BYTE PTR[esi-1]=='.' .if HasPoint==1 mov HasPoint,0 .else .if BYTE PTR[esi-3]=='-' lea esi,Output mov BYTE PTR[esi],'0' mov BYTE PTR[esi+1],'.' mov BYTE PTR[esi+2],0 .else mov BYTE PTR[esi-2],'.' mov BYTE PTR[esi-1],0 .endif .endif .else mov BYTE PTR[esi-1],0 .endif lea esi,Output .if BYTE PTR[esi]=='.' mov BYTE PTR[esi],'0' mov BYTE PTR[esi+1],'.' mov BYTE PTR[esi+2],0 .endif invoke ShowNum .endif .elseif (eax >= ID_NUM0) && (eax <= ID_NUM9) ;数字按钮 .if HasEqueal==1 invoke Init .endif invoke BtnNum,eax .elseif eax == ID_POINT ;小数点按钮 mov BYTE PTR HasPoint,1 mov BYTE PTR IsStart,0 .elseif eax == ID_NEG ;正负号按钮 invoke UnpackNum invoke StrToFloat,addr Output, addr Number finit fldz fld Number fsub fstp Number invoke FloatToStr2,Number,addr Output invoke ShowNum .elseif (eax >= ID_MUL) && (eax <= ID_ADD) ;双目运算符按钮 invoke BtnOperator .elseif eax == ID_EQU ;等于按钮 invoke BtnEqual .elseif eax == ID_PER ;百分号按钮 mov Operator,'*' invoke GetResult invoke UnpackNum invoke StrToFloat,addr Output, addr Number finit fld Number fld Num100 fdiv fstp Number invoke FloatToStr2,Number,addr Output invoke ShowNum .elseif eax == ID_DAO ;倒数按钮 invoke UnpackNum invoke StrToFloat,addr Output, addr Number finit fld Number fldz fcomi ST(0),ST(1) jnz NotZero mov IsError,1 invoke SendMessage,hEdit,WM_SETTEXT,0,addr Div0 retNotZero: fstp Number fstp Number fld1 fld Number fdiv .if HasEqueal==1 fst Result .endif fstp Number invoke FloatToStr2,Number,addr Output invoke ShowNum .elseif eax == ID_SQRT ;开方按钮 invoke UnpackNum invoke StrToFloat,addr Output, addr Number finit fld Number fldz fcomi ST(0),ST(1) jb Positive mov IsError,1 invoke SendMessage,hEdit,WM_SETTEXT,0,addr FunctionError retPositive: fstp Number fsqrt .if HasEqueal==1 fst Result .endif fstp Number invoke FloatToStr2,Number,addr Output invoke ShowNum .elseif eax == ID_MC ;MC按钮 fldz fstp Remember invoke SendMessage,hTextM,WM_SETTEXT,0,NULL .elseif eax == ID_MR ;MR按钮 invoke FloatToStr2,Remember,addr Output invoke ShowNum mov IsStart,0 .elseif eax == ID_MS ;MS按钮 invoke UnpackNum invoke StrToFloat,addr Output, addr Remember invoke ShowTextM .elseif eax == ID_MPLUS ;M+按钮 finit fld Remember invoke UnpackNum invoke StrToFloat,addr Output, addr Remember fld Remember fadd fstp Remember invoke ShowTextM .elseif eax == ID_COPY ;复制 invoke GlobalAlloc,GMEM_MOVEABLE,35 ;配置一个内存块 mov hGlobal ,eax invoke GlobalLock,hGlobal ;锁定内存块 mov pGlobal ,eax lea esi,Output mov edi,pGlobal mov ecx,35 rep movsb ;复制字符串 invoke GlobalUnlock,hGlobal ;解锁内存块 invoke OpenClipboard, NULL ;打开剪切板 invoke EmptyClipboard ;清空剪切板 invoke SetClipboardData,CF_TEXT,hGlobal ;把内存句柄交给剪贴簿 invoke CloseClipboard ;关闭剪切板 .elseif eax == ID_PASTE ;粘贴 invoke IsClipboardFormatAvailable,CF_TEXT ;确定剪贴簿是否含有CF_TEXT格式的数据 invoke OpenClipboard,NULL ;打开剪切板 invoke GetClipboardData,CF_TEXT ;得到代表文字的内存块代号 mov hGlobal,eax invoke GlobalLock ,hGlobal ;解锁内存块 mov pGlobal,eax mov ecx,35 lea edi,Output mov esi,eax rep movsb ;复制字符串 invoke GlobalUnlock ,hGlobal ;解锁内存块 invoke CloseClipboard ;关闭剪切板 invoke ShowNum .elseif eax == ID_PACKET ;数字分组 .if IsPacket==0 invoke CheckMenuItem,hMenu,ID_PACKET,MF_CHECKED ;选中数字分组 .else invoke CheckMenuItem,hMenu,ID_PACKET,MF_UNCHECKED ;选中数字分组 .endif xor IsPacket,1 invoke ShowNum .elseif eax == ID_HELP ;帮助 invoke WinHelp,hWin,addr HelpFile,HELP_CONTENTS,1 .elseif eax == ID_ABOUT ;关于 invoke ShellAbout,hWin,addr ProgramName,addr Author,hIcon .elseif eax == ID_EXIT ;关闭 invoke Calculate,hWin,WM_CLOSE,aParam,bParam .endif .elseif uMsg == WM_CLOSE invoke Shell_NotifyIcon,NIM_DELETE,addr NotifyIcon invoke EndDialog,hWin,NULL invoke PostQuitMessage,0 ;退出消息循环 .else invoke DefWindowProc,hWin,uMsg,aParam,bParam ret .endif invoke SetFocus,hWin xor eax,eax ;关于WM_KEYDOWN原因 retCalculate endp2.5.4 工具子程序说明2.5.4.1 PackNumPackNum函数将输出数据的字符串Output进行数字分组。它首先获取小数点以前的数字位数并保存在寄存器eax中,然后将(eax-1)/3即为需要添加的字符‘,’数目,并保存在eax中,对于小数点以后的字符都向后移动eax位,对于小数点以前的字符,向后移动eax位并用ecx计数,当ecx计数到3是添加字符‘,’并将ecx设为1且eax减一,重复上述步骤直到eax等于0。
    函数的流程图及代码如下:

    PackNum proc USES eax ebx ecx edx lea esi,Output mov eax,0 .while (BYTE PTR[esi]!='.') inc eax inc esi .endw .while (BYTE PTR[esi]!=0) inc esi .endw dec eax mov edx,0 mov ecx,3 div ecx .while (BYTE PTR[esi]!='.') mov bx,[esi] mov [esi+eax],bx dec esi .endw mov bx,[esi] mov [esi+eax],bx dec esi mov ecx,0 .while (eax!=0) .if(ecx<3) mov bx,[esi] mov [esi+eax],bx inc ecx .else mov BYTE PTR[esi+eax],',' dec eax mov ecx,1 .endif dec esi .endw lea esi,Output .while (BYTE PTR[esi]!=0) mov bx,[esi] inc esi .endw retPackNum endp2.5.4.2 UnpackNumUnpackNum函数将进行数字分组输出的字符串Output解分组。它首先获取Output地址存在esi中,然后ecx赋0,并将Output中字符向前移动ecx个单位,遇见‘,’字符则将ecx加1,直到字符串结束。
    函数的流程图及代码如下:

    UnpackNum proc USES ecx lea esi,Output mov ecx,0 .while (BYTE PTR[esi+ecx]!=0) .if(BYTE PTR[esi]==",") inc ecx .endif mov bx,[esi+ecx] mov [esi],bx inc esi .endw retUnpackNum endp2.5.4.3 ShowNumShowNum函数将Output字符串处理后在文本框中显示出来。它首先调用UnpackNum函数对Output解分组,然后获取Output地址存在esi、edi中,通过循环将Output尾地址存在esi中,将字符‘.’地址存在edi中,如果edi等于esi则表明Output中无字符‘.’,则在结尾添加字符‘.’。如果IsPacked等于1则对Output调用UnpackNum函数对其分组,最后向文本框发送WM_SETTEXT消息显示数据。
    函数的流程图及代码如下:

    ShowNum proc invoke UnpackNum lea esi,Output lea edi,Output .while (BYTE PTR[esi]!=0) inc esi .endw .while (BYTE PTR[edi]!='.') && (edi<esi) inc edi .endw .if esi==edi mov BYTE PTR[esi],'.' mov BYTE PTR[esi+1],0 .endif .if IsPacket==1 invoke PackNum .endif invoke SendMessage,hEdit,WM_SETTEXT,0,addr Output retShowNum endp2.5.4.4 BtnNumBtnNum函数响应数字按钮消息,向文本框中添加字符。
    函数源代码如下:
    BtnNum proc USES eax,Num:DWORD lea esi,Output mov eax,Num sub eax,252 .if IsStart==1 mov [esi],eax inc esi mov BYTE PTR[esi],'.' inc esi mov BYTE PTR[esi],0 mov IsStart,0 .else .while BYTE PTR[esi]!='.' inc esi .endw .if HasPoint==1 .while BYTE PTR[esi]!=0 inc esi .endw mov [esi],ax inc esi mov BYTE PTR[esi],0 .else .if BYTE PTR[Output]=='0' lea esi,Output mov [esi],eax mov BYTE PTR[esi+1],'.' mov BYTE PTR[esi+2],0 .else mov [esi],eax inc esi mov BYTE PTR[esi],'.' inc esi mov BYTE PTR[esi],0 .endif .endif .endif invoke ShowNum ret BtnNum endp2.5.4.5 BtnOperatorBtnOperator函数响应运算符按钮消息,进行运算并输出结果。首先判断是否为等号,如果不是则调用GetResult函数先进行一次运算,然后将当前操作符存入Operator变量中。
    函数源代码如下:
    BtnOperator proc USES eax .if HasEqueal!=1 invoke GetResult .endif .if eax == ID_MUL mov Operator,'*' .elseif eax == ID_DIV mov Operator,'/' .elseif eax == ID_SUB mov Operator,'-' .elseif eax == ID_ADD mov Operator,'+' .endif mov HasEqueal,0 retBtnOperator endp2.5.4.6 BtnEqualBtnEqual函数响应等号按钮消息,进行运算并输出结果。首先判断是否为起始状态,如果不是则调用GetResult函数,并将HasEqual变量置1。
    函数源代码如下:
    BtnEqual proc .if (IsStart==1) && (HasEqueal==0) fstp Number fst Number fld Number .endif invoke GetResult mov HasEqueal,1 retBtnEqual endp2.5.4.7 GetResultBtnEqual函数响应等号按钮消息,进行运算并输出结果。首先判断是否为起始状态,如果不是则调用GetResult函数,并将HasEqual变量置1。
    函数源代码如下:
    GetResult proc USES eax invoke UnpackNum finit .if (IsStart==1) && (HasEqueal==0) .else .if HasEqueal!=1 invoke StrToFloat,addr Output, addr Operand .endif fld Result fld Operand .if Operator=='.' fst Result jmp Show .elseif Operator=='+' fadd ST(1),ST(0) .elseif Operator=='-' fsub ST(1),ST(0) .elseif Operator=='*' fmul ST(1),ST(0) .elseif Operator=='/' fldz fcomi ST(0),ST(1) jnz NotZero mov IsError,1 invoke SendMessage,hEdit,WM_SETTEXT,0,addr Div0 retNotZero: fstp Operand fdiv ST(1),ST(0) .endif fstp Operand fst ResultShow: mov IsStart,1 mov HasPoint,0 invoke FloatToStr2,Result,addr Output invoke ShowNum .endif retGetResult endp2.5.4.8 ShowTextMShowTextM函数判断Remember中的值是否为0,如果不是是则在标签中显示‘M’,否则清空标签中内容。
    函数源代码如下:
    ShowTextM proc fld NumLittle fldz fsub Remember fabs fcomi ST(0),ST(1) ja NotZero invoke SendMessage,hTextM,WM_SETTEXT,0,NULL jmp PopNumLittleNotZero:invoke SendMessage,hTextM,WM_SETTEXT,0,addr TextMPopNumLittle:fstp Operand fstp Operand mov IsStart,1 mov HasPoint,0 retShowTextM endp2.5.4.9 InitInit函数负责进行必要的初始化操作,如对状态变量的初始化以及的FPU的初始化。
    函数源代码如下:
    Init proc mov IsStart,1 ;初始化 mov HasPoint,0 ;清除小数点 mov HasEqueal,0 fldz fst Number ;清除结果 fst Operand mov Operator,'.' ;清除运算符 mov IsError,0 finit ;初始化FPU ret Init endp3 参考文献
    《80X86汇编语言程序设计》,王元珍、曹忠升、韩宗芬,华中科技大学出版社,2005《Iczelion的Win32汇编教程》《Intel汇编语言程序设计(第五版)》,【美】Kip R Irvine,电子工业出版社,2008《汇编语言编程艺术》,Randall Hyde,清华大学出版社 ,2005《IBM PC汇编语言程序设计(第五版)》,Peter Abel,人民邮电出版社,2002《Win32开发人员参考库第五卷:Windows Shell》,David Iseminger,机械工业出版社,2001《Microsoft MASM 参考手册》《现代操作系统》,【荷】Andrew S. Tanenbaum 机械工业出版社,2009《Windows核心编程(第五版)》,【美】Jeffery Richter清华大学出版社,2008《Windows程序设计(第五版)》,【美】Charles Petzold,北京大学出版社,1999《Intel® 64 and IA-32 Architectures Software Developer’s Manuals》MSDN Library: http://www.microsoft.com/china/MSDN/library/
    1 评论 25 下载 2018-10-06 20:24:31 下载需要11点积分
  • 基于深度学习的医疗图像预分类平台开发与实现

    摘 要本文课题设计是基于深度学习中支持向量机算法的医疗图像预分类平台的开发与实现。现阶段,医疗图像中病情的确认都由医生进行判定,从效率方面来讲,人工判断的效率远远小于使用机器学习判断医疗图像中病情的效率,因此,本文课题设计旨在设计实现算法将医疗图像进行分类,产生新的符合机器进行判断的医疗图像。配合使用深度学习中的卷积神经网络经过训练对医疗图像进行一定程度的解读,用来辅助或代替医生诊断病情。使用机器进行病情的判断可以辅助医生判断病情,大大减少医生的工作量,解放生产力。
    在实现分类医疗图像的算法过程中,核心是机器学习中的支持向量机算法,开发环境是python环境。由于dicom格式是医疗图像标准格式,因此实现算法的第一步是对dicom格式医疗图像文件进行读取解析,使用python中的第三方模块pydicom模块进行读取解析。读取文件之后需要将数据进行一系列操作转换成可用于分类器分类的格式,本文课题设计中使用opencv模块对数据进行处理,首先进行图像的截取,之后进行灰度化处理、二值化处理,将格式转化为可用于分类的格式。紧接着进行核心操作,即使用分类器进行分类,使用python第三方模块中的libsvm模块对训练集数据进行训练,训练出分类器模型,之后使用训练好的模型对数据进行预测,得出预测的结果。将图像数据分类后,需要进行判断,将正确的分类的图像数据保存到固定的位置,生成新的png格式图像以便于后续的再次使用,这将生产最后的结果,使用python第三方模块中的opencv模块。
    本文课题设计的最终结果将会产生新的png格式的医疗图像,生成的新医疗图像是直接截取原始图像的病情部位,去除大量无用信息,实现原始dicom格式医疗图像的有用信息分类转化为易于操作的png格式图像,生成的新医疗图像将被应用到卷积神经网络解读医疗图像病情方面,最终实现病情解读判断效率的大幅度提升,解放生产力。最后会对对程序进行展开分析,展示了程序的设计思路、功能流程及其实现的逻辑,结合程序的源码部分展开描述。
    关键词:深度学习,支持向量机,dicom,pydicom,libsvm
    ABSTRACTThe design of this paper is based on the development and implementation of medical image pre-classification platform based on support vector machine algorithm in depth learning. At present, the confirmation of the condition in the medical image is determined by the doctor. In terms of efficiency, the efficiency of manual judgment is far less than that of using the machine to judge the condition of the medical image. Therefore, the purpose of this paper is to design and implement the algorithm. Medical images are classified, resulting in new medical images that meet the machine’s judgment. Conjugate the use of deep learning in the convolution of the neural network through training to conduct a certain degree of interpretation of medical images to assist or replace the doctor to diagnose the condition. The use of the machine to determine the condition can help the doctor to determine the condition, greatly reducing the work load of doctors, the liberation of productivity.
    In the process of implementing the classification of medical images, the core is the support vector machine algorithm in machine learning, and the development environment is python environment. Since the dicom format is the standard format for medical images, the first step in implementing the algorithm is to read the dicom format medical image file and use the third-party module pydicom module in python for reading and parsing. After reading the file, you need to convert the data into a series of operations that can be used in the classification of the classifier. In this paper, the opencv module is used to process the data. The image is taken first, then the gray scale is processed and binarized , The format into a format can be used for classification.Followed by the core operation, that is, using the classifier to classify, use the libsvm module in the python third party module to train the training set data, train the classifier model, and then use the trained model to predict the data, result. After sorting the image data, it is necessary to make a judgment,save the correct sorted image data to a fixed location, generate a new png format image for subsequent re-use, which will produce the final result using python in the third party module Opencv module.
    The final result of the design of this topic will produce a new png-style medical image, the new medical image generated is a direct removal of the original image of the disease site, remove a large number of useless information to achieve the original dicom format medical image useful information into easy to operate The png format image, the generated new medical image will be applied to the convolution of the neural network to read the medical image of the disease, the ultimate realization of disease interpretation to determine the efficiency of a substantial increase in the liberation of productivity.Finally, the program will be carried out on the analysis, showing the program design ideas, functional processes and the realization of the logic, combined with the source part of the program to start description.
    Key words:deep learning, support vector machine, dicom, pydicom, libsvm
    1 绪论本章旨在说明基于深度学习的医疗图像预分类平台的研究目的、研究背景,以及发展现状和对该平台的研究意义。
    1.1 基于深度学习的医疗图像预分类平台未来的时代是人工智能的时代。人工智能的核心是机器学习,更确切的说,深度学习是人工智能的灵魂。所以研究深度学习和机器学习是有意义的。在互联网时代与大数据时代,机器学习是一门难以避开的学科,任何通过数据进行训练的学习型算法研究都属于机器学习。机器学习涉及许多学科,如概率论、统计学等。机器学习研究的是机器怎样模拟人类,并实现人类的学习行为。机器学习是人工智能的核心,计算机的智能化确切来说离不开机器学习方面的内容。机器学习算法种类较多,适用于不同的场景,例如线性回归、K均值(K-means,基于原型的目标函数聚类方法)、决策树、随机森林、支持向量机(SVM)以及人工神经网络(ANN)等一些算法。
    本次课题设计是开发基于深度学习的医疗图像预分类平台。医疗图像病情的解读从技术角度来讲是可以用机器识别辅助或者代替人工识别,现阶段深度学习中的卷积神经网络技术可以很好的完成这一点,而使用卷积神经网络进行识别需要对图像数据进行一定的格式转化、分类及去噪处理。医疗图像预分类平台就是基于该信息点出发,对原始医疗图像进行去噪处理、分类器分类,产生新的医疗图像。而DICOM标准是医疗图像的规范,因此首先是对dicom格式医疗图像进行读取解析,该平台的核心点在于分类器的分类,由于支持向量机算法较为符合该平台对于分类的要求,因此分类器使用的是机器学习中的支持向量机算法作为分类器进行图像分类。
    1.2 研究意义本文课题设计是研究基于深度学习的医疗图像预分类平台,首先来分析医疗图像预分类的好处。自1985年DICOM(医学数字成像和通信)标准发布以来,被广泛用于放射医疗,心血管成像以及放射诊疗诊断设备,在我国DICOM是唯一被接受的医疗影响国际规范。而解读医疗图像在诊断时期很重要,病变基本都发生在体内,现阶段只能通过X射线、CT、核磁共振、超声等方式获取的扫描图像来判断病情。
    现阶段,医学图像都由医生来进行解译,这需要医生具有一定的经验才能进行。使用计算机读取DICOM格式[6]图片数据,可以将图片进行截取等操作,并对截取后的图像数据通过机器学习中的支持向量机算法处理,由于医疗图片是属于线性不可分的,所以使用支持向量机的方法。支持向量机遇到线性不可分问题时,通过一个非线性映射p,将样本空间映射到一个高维乃至无穷维的特征空间中,也就是Hilber空间,使得原本的样本空间中的非线性可分的问题转化为特征空间的线性可分的问题。一般情况下,升维会增加计算的复杂性,甚至会引起“维数灾难”[2],支持向量机应用核函数[9]的展开定理,引入松弛变量[8],就不需要知道非线性映射的显式表达式,使得不增加计算的复杂性,在某种程度上避免了“维数灾难”。使用支持向量机算法作为分类器对截取后的图像进行分类,对正确分类的图像进行保存,产生新的png格式医疗图像,从而适用于深度学习进行处理。深度学习中的卷积神经网络经过训练可以对新生成的医疗图像进行一定程度的解读,用来辅助或代替医生诊断病情。
    深度学习的方法在处理医学影像数据方面发展迅速,在可见的将来,随着万物互联而来的海量医疗数据甚至可以让深度学习中的卷积神经网络代替医生进行诊断。另外,本次课题设计的研究能够提高自己的学习能力,在发现问题、分析问题和解决问题方面能够有一定深层次的了解。同时,对现今仍然火热的机器学习的知识了解的更进一步,对模型的建立、算法的实现方面有了更深的掌握。
    1.3 论文结构在本篇基于深度学习的医疗图像预分类平台的论文中,第一章说明了医疗图像预分类平台是什么以及研究意义,第二章对图像预分类平台分析需求和可行性,第三章说明了医疗图像预分类平台中支持向量机的应用,第四章说明了医疗图像预分类平台开发的设计,第五章介绍了医疗图像预分类平台开发测试和调试。
    2 医疗图像预分类平台需求分析本章旨在说明开发医疗图像预分类平台的需求,并从经济、技术、运行、操作方面进行可行性分析。
    2.1 医疗图像预分类平台的需求分析本文课题设计是基于深度学习的医疗图像预分类平台和实现,该平台是对医疗图像进行预分类,而dicom格式在我国是唯一被接受的医疗国际规范,研究的基础数据就是dicom格式医疗图像,因此首先要做的就是对dicom格式医疗图像文件的读取。在功能、性能及系统环境方面有一定的要求:
    功能需求:在上述内容中,首先要有批量导入和读取DICOM格式的图片信息功能,在这方面需要使用python中的pydicom第三方模块对dicom格式图像进行读取分析。之后需要有对图片进行截取,截取后返回新的图片,然后需要有将图片处理成libsvm输入格式的功能,在这方面opencv模块中的函数较为符合该需求。紧接着需要对图片使用libsvm进行分类,最后对有用的图片进行保存功能。
    性能需求:由于程序使用python中的libsvm库,需要大量的数据进行训练,同样需要大量数据进行预测分类,还需要对图像进行截取、灰度转化、二值化等操作,因此需要对数据进行处理时考虑到速度因素。
    环境需求:硬件需要一台笔记本电脑,系统环境需要Ubuntu,开发环境需要python,python中需要有libsvm、pydicom、opencv等第三方模块,spyder较为符合作为开发工具。
    现阶段,病人的医疗图像基本都是由医生进行诊断,使得医生的工作量过大,另外,其效率也并不是很高。而通过计算机将病人的医疗图像批量导入,深度学习中的卷积神经网络经过训练可以批量对医疗图像进行一定程度的解读,用来辅助或代替医生诊断病情。在使用深度学习中的卷积神经网络过程中,如果直接使用医疗设备拍出的医疗图像,由于图像中的噪音过大,无用信息过多,因此需要进行预分类处理。本文课题设计中,主要是对颈动脉血管进行分析处理,因此除颈动脉血管的横截面和纵截面外的其他图像都是属于无用的医疗图像,因此需要首先对批量导入的进行分类,分为有用图像的横截面、纵截面和无用图像,然后将有用的图像中的噪音切除,最后保存的图像中噪音大大减少。
    2.2 医疗图像预分类平台的可行性分析医疗图像处理是现代医学的重要手段之一,DICOM是部署最为广泛的医疗图像信息标准之一,医疗图像预分类平台将DICOM格式的医疗图像读取,并进行截取、分类操作后保存成新的图像。深度学习中的卷积神经网络对新的图像读取,之后运行在训练好的模型中并得出结果,对结果进行判断就可以了解图像中的部分是否发生病变。下文中,本文主要从经济可行性、技术可行性、运行可行性、和操作可行性等方面对医疗图像预分类平台进行分析。
    2.1.1 经济可行性开发医疗图像预分类平台所需要的相关资料可以通过互联网进行查询,也可以通过文献资料进行查询。开发该平台所需要的ubuntu系统可以从网上免费下载,python2.7环境以及libsvm、pydicom、opencv模块都是开源的资源,硬件只需要一台普通笔记本电脑,都易于获取,所需要的开发工具spyder也可以免费下载,无需其他特殊付费软件,开发成本低,简单易实现,从经济角度来看开发该程序经济具有可行性。
    2.1.2 技术可行性在医疗图像预分类平台中,医疗DICOM格式标准图像,在python中有对应的pydicom模块能够对其进行读取、分析、写入,可以对隐私信息进行读取,简单易使用;而opencv模块安装之后,使用简单,读取普通图片,只需要简单的调用函数就可以实现功能,较难的部分也已封装,简单的调用即可;台湾林智仁教授开发的libsvm第三方模块相比于普通的SVM来说,大大减少了难度,操作性强。因此,从技术角度来说,开发医疗图像预分类平台是具有可行性的。
    2.1.3 运行可行性运行性是整体组织结构产生的结果。该程序分为四个文件,但只需要在主函数中调用main函数,并输入相应的路径就可以与运行,只需安装好python2.7的开发环境,并将pydicom、opencv和libsvm模块安装在python中即可。因此,从运行角度来看,开发医疗图像预分类平台是可行的。
    2.1.4 操作可行性开发所采用的工具是spyder,开发出的程序可以在任何配置好环境了的计算机上运行,以console的形式展现运行过程及运行结果,将结果保存在固定位置,以显示器未输出,用户不需要关心运行过程,只看运行结果以及保存后的新图像文件,简单易使用。所以,该程序在操作上具有可行性。
    2.3 小结本章对医疗图像预分类平台分析需求,并从经济、技术、运行、操作等方面说明了该平台的可行性。
    3 医疗图像预分类中支持向量机的应用医疗图像预分类平台核心是分类器,而分类器算法为支持向量机算法,配合python读取dicom格式图像文件及opencv模块实现图像的去噪,形成医疗图像预分类平台。本章旨在说明支持向量机算法应用及配套应用。
    3.1 支持向量机的介绍基于深度学习的医疗图像预分类平台的核心是分类器,分类器的核心算法是支持向量机算法。通俗来讲,它是一种二类分类模型,基本模型定义为特征空间上的间隔最大的线性分类器,即支持向量机的学习策略便是间隔最大化,最终转化为凸二次规划问题的求解。
    对于线性分类问题,线性分类支持向量机是一种非常有效的方法。但是,大多数情况下分类问题是非线性的,这是可以使用非线性支持向量机,主要特点是引入了核技巧。非线性问题往往不好求解,如果用解线性分类的问题的方法来解决,就比较容易了。事实上,确实是这样的,进行非线性变换时分为两步:首先使用一个变化将原空间中的数据映射到新的空间;然后在新空间中用线性分类学习方法从训练数据中学习分类模型。这就是核技巧。
    核技巧的想法是在学习与预测中只定义核函数K(x, z),而不显示的定义映射函数。常用的核函数有多项式核函数、高斯核函数、字符串核函数等,适用于不同的场景。
    支持向量机原理图如图3-1,支持向量机工作流程图如图3-2。

    3.2 医疗图像标准dicom协议简介基于深度学习的医疗图像预分类平台的基础数据保存在DICOM格式医疗图像文件中,因此首先需要对DICOM格式医疗图像文件进行读取解析。医学数字成像和通信,缩写为DICOM,是医学图像和相关信息的国际标准。它定义了质量能满足临床需要的可用于数据交换的医学图像格式,被广泛用于放射医疗。DICOM标准的推出与实现,推动了远程放射学系统、图像管理与通信系统的研究与发展。在我国,DICOM是唯一被接受的医疗影像国际规范。在DICOM中,使用了对应的资料结构信息进行详述,定义了Patient,Study,Series,Image等4层来存储信息。DICOM将每个影像文件包裹成一个物件IOD,而一个IOD可以被分为像素数据和影像资料,前者通过描述图像中的每个点的值来组合一个医学图像,后者则是包含了病人资料信息。DICOM存储格式如图3-3。

    DICOM3.0支持在网络环境中使用标准的网络协议的操作,如OSI和TCP/IP。DICOM是定义在ISO/OSI七层结构中的最高三层,底层部分同样符合TCP/IP结构。
    3.3 python中处理图像模块简介基于深度学习的医疗图像预分类平台首先需要获取DICOM格式图像数据,之后需要对图像数据进行去噪截取、灰度转化、二值化处理、格式转化、分类器分类等过程,最后生成新的医疗图像。使用的python开发语言环境容易安装,python是比较好用和强大的语言,python的强大之处主要体现在它的第三方模块的多样性,在处理图像方面,有多个模块可以支持。在处理DICOM格式医疗图像方面,本文课题设计主要使用pydicom模块;在处理图像方面,本文主要采用了opencv模块;在图像分类方面,本文主要采用了libsvm模块。下文中,将展开对这几个模块的详细说明。
    3.1.1 pydicom模块简介pydicom是一个处理DICOM文件的python软件包,它可以通过非常容易的“Pythonic”方式对DICOM格式进行提取和修改,修改后的数据还可以生成新的DICOM文件。Pydicom是一个纯python语言的包,只要安装了Numpy模块,就可以在Python解释器下任何平台上运行。但是pydicom有其局限性,局限性在于无法处理压缩像素图像以及无法处理分帧动画图像。虽然pydicom有着一些局限性,但依然是非常有用的python模块。
    Python中的pydicom模块在操作性方面非常简单。首先需要导入pydicom模块,由于pydicom0.9.9之前都是dicom,1.0之后为pydicom,而这里所用的是dicom。dicom读取文件后的格式如图3-4。

    pydicom模块与其他一些图像显示模块配合起来,将会达到更好的展示效果,诸如matplotlib、PIL、pylab等一些模块。有时候还需要将修改后的dicom数据保存成新的文件,或者写入新的文件,从操作性方面来说,不具有难度,简单易使用。
    总体来说,纯python编写的pydicom模块大大减少了开发者的时间和精力,除了其局限性,在其余方面,pydicom是一个处理dicom格式文件的一个非常不错的模块。
    3.1.2 opencv模块说明opencv如今已经发布到了3.2的版本,但由于本文课题设计使用的是python2.7,网上的opencv2的资料相对多一些,因此,这里使用的是opencv2的版本。Opencv是英特尔开源计算机视觉库,由一系列C语言的函数和少量的C++的类构成,实现了图像处理的计算机视觉方面的很多通用的算法,拥有包括300多个C函数的跨平台的中、高层API,而且opencv不依赖于外部库,这就意味着,安装简单,不用考虑依赖关系。Opencv支持性较好,支持C++、C、python和java等语言,在操作系统方面,支持windows、linux、Mac OS、iOS和Android。
    opencv目前只支持bmp、jpg、png、tiff等常用格式的读取,首先读取图像,创建一个窗口,在此窗口中显示图像,最后释放窗口。对于保存图像,操作非常简单,直接使用cv2.imwrite即可。
    过滤是图像处理中的基本任务,可以移除噪音、提取感兴趣的可视特征、允许图像重采样等等。由于课题设计中要处理图像的噪音,这里介绍初级滤波操作,可以使用低通滤波器、高斯模糊、中值滤波等。低通滤波目标在于降低图像的变化率,是线性的;高斯模糊通过分配权重来重新计算周围点的值;中值滤波是非线性过滤器,在处理椒盐现象有比较大的作用。代码展示如下:
    result = cv2.blur(img, (5,5)) # 低通滤波gaussianResult = cv2.GaussianBlur(img, (5,5), 1.5) # 高斯模糊medianResult = cv2.medianBlur(img, 5) # 中值滤波
    Sobel算子也是一种过滤器,优势在于它是带有方向的,使用的原理是极值处就是边缘。图像处理中轮廓检测业务也经常用到,使用cv2.findContours()函数可以很好的获得图像的轮廓。
    3.1.3 libsvm模块说明libsvm是台湾林智仁教授与2001年开发的支持向量机库,对数据进行分类或者回归非常方便,支持java、python、C、C++等开发语言。在python中使用,需要首先导入libsvm的库:
    from svmutil import *y, x = svm_read_problem(’train1.txt’) # 读入训练数据yt, xt = svm_read_problem(’test1.txt’) # 训练测试数据m = svm_train(y, x) # 训练svm_predict(yt, xt, m) # 测试
    上述代码中同样给出了libsvm的基本操作,包括读取训练数据、训练测试数据、训练操作以及测试操作,这些操作基本都是直接调用libsvm的接口。
    在libsvm的使用中,参数的选择依然非常重要,在训练和预测过程中,需要一系列参数来调整控制。
    对于libsvm运行后的结果,同样要进行分析。#iter为迭代次数, nu 与前面的操作参数-n nu相同, obj为SVM文件转换为的二次规划求解得到的最小值, rho 为判决函数的常数项b, nSV 为支持向量个数, nBSV为边界上的支持向量个数,Total nSV为支持向量总个数。Libsvm运行后结果格式如图3-5。

    总体来说,libsvm用法简单,操作方便,只需要调用接口就可以使用,对参数的调整是libsvm模块的重中之重,相比于原来的SVM,大大减少了其复杂度。
    3.4 小结本章对支持向量机进行了说明,并对医疗图像标准dicom协议以及python处理图像的一些模块进行了说明,主要对pydicom模块、opencv模块以及libsvm模块进行了说明。
    4 医疗图像预分类平台开发的设计本章旨在说明医疗图像预分类平台开发过程中的设计,以及各功能模块的说明及代码展示。
    4.1 模块结构图本医疗图像预分类平台是属于非桌面软件,是属于后台程序,直接将数据存储在文件夹中。在设计程序时,安装从前到后的顺序将数据进行传输。首先进行设计的是dicom格式医疗图像的读取,将数据读取之后转化成可用格式数据,再将图像二值化后截取有用部分,去除噪音,之后将数据转化成libsvm的输入格式进行分类,最后判断分类后的图像是否是可用图像,如果是,则将分类后的图像保存成PNG格式图像,否则将其舍弃。流程图如图4-1。

    4.2 流程关系图本次课题设计中程序的流程总体来说是非常简单的,基本上是两条线性结构,训练数据线性结构与预测数据线性结构形成一个十字交叉型的结构,不用考虑太多内容,需要考虑的是性能问题。本程序中的核心是libsvm的分类器对图像进行分类。
    基本的流程是,首先将用于训练模型的dicom格式文件循环读取完毕,然后对数据进行格式转换,转换成libsvm分类器可用的格式,然后进行分类器的训练,将训练后的模型保存在文件中,这是第一条线性结构。然后读取用于预测的dicom格式文件循环读取,对图像进行去噪、截取等操作,将返回的数据进行转换,转化成libsvm分类器所用格式,直接调用保存好的模型,输入测试数据进行分类,最后判断分类后的图片是否是有用图片,如果是则保存图片,否则舍弃。
    上述为主要的两条流程,是一个十字形的结构,按照先训练后预测的思路运行程序,在进行去噪过程中是先进行灰度转化,然后进行二值化处理,最后截取图像形成一个新的图像。
    由于处理dicom格式的文件,在进行去噪处理和格式转化的过程中需要花费一定的时间,因此训练一次将花费大量时间,因此,本程序设计中编写了几个工具函数,存储训练后的模型至文件中以及从文件中读取模型等。流程关系图如图4-2。

    4.3 功能设计及分析由于本平台是对dicom医疗图像进行预分类的平台,处理的数据的基础是dicom格式文件,本平台主要是对颈动脉血管图像进行截取、处理、分类操作,由于使用的图像数据中既有颈动脉血管的横截面类型,也有纵截面类型,因此在分类器分类的类型中分类的颈动脉血管的横截面和纵截面类型都属于正确分类,下面将展示颈动脉血管图像,如图4-3。

    4.3.1 读取dicom格式文件并使用opencv去噪医疗图像预分类平台是基于深度学习的对dicom格式医疗图像进行分类的平台,由于医疗图像的格式文件基本上是存储成dicom格式的文件,因此本程序中需要有读取dicom格式文件数据的功能。本段程序中根据传入的文件路径,使用python中的pydicom第三方模块对dicom格式图像文件进行读取。本段代码中的读取dicom格式图像文件部分调用的是pydicom模块中的read_file函数接口,获取到一定格式的数据并存在dicom_data变量中。
    将获取到的数据通过DicomtoRGB函数转化成RGB图像数据并存入image变量中,与此同时调用loadFileInformation函数获取隐私信息,之后调用opencv模块中的cvtColor函数将image进行灰度转化,即将图像RGB图像转化为灰度图。
    将灰度图数据经过带有方向的Sobel算子过滤器过滤,之后转回uint8,再使用低通滤波将图像平滑化处理,处理后的结果保存在blurred变量中。将blurred变量调用opencv中的threshold函数进行二值化处理,调用opencv中的getStructuringElement函数定义矩形结构元素,调用opencv中的morphologyEx函数设定闭运算,然后进行形态学上的腐蚀操作和膨胀操作对图像进行处理,按照先腐蚀后膨胀的顺序进行操作最终检测图像中所需部分的轮廓,将轮廓进行绘制,之后进行截取,形成新的图像数据并返回。
    本段代码功能的实现总共分为三个函数,分别是DicomtoRGB函数、yaxpb函数以及picSplitResize_dicom函数,yaxpb函数的作用是实现线性变换,DicomtoRGB函数的作用是将dicom格式转化为RGB格式,picSplitResize_dicom函数的作用是对图像进行去噪截取转化处理。
    本段代码主要的作用就是对dicom格式图像进行读取、隐私信息提取和去噪截取操作,会返回截取后的图像数据。结果对比图如图4-4。
    关键代码:
    def DicomtoRGB(self, dicomfile, bt, wt): '''将dicom图片文件转换成RGB''' #创建一个黑色空白图片 image = np.zeros((dicomfile.Rows, dicomfile.Columns, 3), np.uint8) #获取图片的宽度和高度 i=0 j=0 while i<dicomfile.Rows: j=0 while j<dicomfile.Columns: #线性变换 color = self.yaxpb(dicomfile.pixel_array[i][j],bt,wt) #同样的R,G,B的值获取灰度标度 image[i][j] = (color,color,color) j=j+1 i=i+1 return image def yaxpb(self, pxvalue, bt, wt): '''线性变换''' if pxvalue < bt: y=0 elif pxvalue > wt: y=255 else: y=pxvalue*255/(wt-bt)-255*bt/(wt-bt) return y def picSplitResize_dicom(self, path): '''截取图片'''#image = cv2.imread(path) dicom_data = dicom.read_file(path)image=self.DicomtoRGB(dicom_data,bt=0,wt=2048)gim = GetInfomation()info = gim.loadFileInformation(dicom_data) #获取隐私信息gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY) #灰度转化#cv2.imshow('0', gray) #带有方向的过滤器gradX = cv2.Sobel(gray, ddepth=cv2.cv.CV_32F, dx=1, dy=0, ksize=-1)gradY = cv2.Sobel(gray, ddepth=cv2.cv.CV_32F, dx=0, dy=1, ksize=-1)gradient = cv2.subtract(gradX, gradY)gradient = cv2.convertScaleAbs(gradient) #转回uint8blurred = cv2.blur(gradient,(225,30)) #用低通滤波来平滑图像(_, thresh) = cv2.threshold(blurred, 90, 255, cv2.THRESH_OTSU+cv2.THRESH_BINARY)#二值化kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (25,77)) #OpenCV定义的结构元素 closed = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel) #闭运算closed = cv2.erode(closed, None, iterations=4) #腐蚀图像closed = cv2.dilate(closed, None, iterations=4) #膨胀图像(cnts, _) = cv2.findContours(closed.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) #检测物体轮廓c = sorted(cnts, key=cv2.contourArea, reverse=True)[0]rect = cv2.minAreaRect(c)box = np.int0(cv2.cv.BoxPoints(rect))cv2.drawContours(image, [box], -1, (0, 255, 0), 3) #绘制轮廓Xs = [i[0] for i in box]Ys = [i[1] for i in box]Xs.remove(min(Xs))Xs.remove(max(Xs))Ys.remove(min(Ys))Ys.remove(max(Ys))x1 = min(Xs)x2 = max(Xs)y1 = min(Ys)y2 = max(Ys)hight = y2 - y1width = x2 - x1cropImg = image[y1:y1+hight, x1:x1+width]#cv2.imshow('test', cropImg)#cv2.waitKey(0) return cropImg
    下面将展示上述代码运行前后的结果对比图如图4-4所示,左边图像为原始图像,即直接从dicom格式文件中读取到的图像,右边图像为去噪后图像,运行本段代码进行灰度转化、二值化处理并进行截取后的图像。

    4.3.2 读取隐私信息医疗图像预分类平台是对dicom格式医疗图像的读取解析以及获得正确分类的平台,由于诊断病情需要病人信息,而病人信息在dicom格式医疗图像中存储,因此解读dicom格式文件获取病人隐私信息。
    在这段代码中,主要功能是获取病人隐私信息并存储在字典中返回。由于医疗图像是以dicom格式进行存储的,并且医疗图像中必须要存储病人的隐私信息和病情信息,因此在进行读取dicom格式文件时,需要同时将隐私信息读取进入程序,之后根据需求将病人隐私信息进行相应的操作。在本段代码中使用python的pydicom第三方模块读取dicom格式文件,主要使用read_file函数,并将数据存储在变量中。声明GetInfomation的对象,将数据传入对象中,并调用loadFileInformation函数。在该函数中,将dicom格式图像数据的PatientID、PatientName、PatientBirthDate、PatientSex、StudyID、StudyData、StudyTime、InstitutionName、Manufacturer及NumberOfFrames属性中的数据保存在information字典中,并返回字典值。
    总体来讲,本段代码实现了读取病人隐私信息的功能,主要获取病人号(PatientID)、病人姓名(PatientName)、病人出生日期(PatientBirthDate)、病人性别(PatientSex)、StudyID、StudyDate、InstitutionName、Manufacturer、NumberOfFrames等方面。
    关键代码:
    class GetInfomation(): def loadFileInformation(ds): '''获取隐私信息''' information = {} information['PatientID'] = ds.PatientID information['PatientName'] = ds.PatientName information['PatientBirthDate'] = ds.PatientBirthDate information['PatientSex'] = ds.PatientSex information['StudyID'] = ds.StudyID information['StudyDate'] = ds.StudyDate information['StudyTime'] = ds.StudyTime information['InstitutionName'] = ds.InstitutionName information['Manufacturer'] = ds.Manufacturer information['NumberOfFrames'] = ds.NumberOfFrames return information
    4.3.3 读取训练数据医疗图像预分类平台本质上来讲,是机器学习的具体应用,是将医疗图像进行预分类的平台,获得正确分类的图像,而在机器学习中需要读取数据后进行后续操作,并且需要大量的数据以用于训练模型。
    本次课题设计中使用的是机器学习中支持向量机的有监督学习算法,因此需要首先将所有训练数据读入内存中,机器学习进行训练时需要成百上千的数据,有甚者,需要几十万条数据才能进行训练,如果训练数据太少容易造成欠拟合现象,造成分类器不能正确分类的现象。
    由于本次实验测试时使用的是png和jpg格式的图像,因此本程序设计中设计有两个函数分别是对dicom格式图像文件训练数据的读取和对普通格式图像文件的读取,首先将展示对dicom格式图像文件的读取代码。
    在读取dicom格式图像文件的函数中,需要传入dicom格式图像文件所在的文件夹路径以及文件后缀名,将文件夹中所有文件的绝对路径存储在path_list_dcm_test列表中,然后将dicom文件路径存储在path_list_dcm列表中,最终调用picSplitResize函数以及resize函数将数据转化为可用于分类的格式,将每张图像的类型标记及图像数据分别存在train_labels和train_images列表中,并将值返回。
    关键代码:
    def traindata_dicom(path_s, suffix): train_images = [] train_labels = [] path_list = [] path_list_dcm_test = [] path_list_dcm = [] dt2d = DicomTo2Data() while os.path.isdir(path_s): for pa in os.listdir(path_s): if os.path.isdir(path_s+'/'+pa): path_list.append(path_s+'/'+pa) path_list_dcm_test.append(path_s+'/'+pa) if len(path_list) != 0: path_s = path_list[0] del path_list[0] else: path_s = '' for p in path_list_dcm_test: for info in os.listdir(p): if info[-4:] == '.dcm' or info[-4:] == '.DIC': path_list_dcm.append(p) break for path in path_list_dcm: for files in glob.glob(path + '/*.' + suffix): filepath,filename = os.path.split(files) if filename[-6:-4] != '-1': train_labels.append(1) else: train_labels.append(-1) pic = dt2d.picSplitResize(filepath + '/' + filename) picNew = resize(pic,(30,30)) train_images.append(picNew[0]) return train_images,train_labels
    上述代码是对dicom格式图像文件训练数据的循环读取,并将训练数据调用picSplitResize函数进行灰度转化、二值化处理和格式转化,之后调用resize函数将转化后的数据进行归一化处理,最后返回正确格式的训练数据以及训练数据标签。由于在测试过程中的输入数据用png或jpg格式进行代替,能减少运行时间,下面将展示对普通png和jpg格式图像文件训练数据的读取。
    对于png或jpg图像文件的的读取函数,只需要传入文件夹路径,由于传入的文件夹路径下存在三个文件夹,分别是错误的分类、颈动脉血管横截面及颈动脉血管纵截面,标记分别为-1、1、2。同样调用picSplitResize函数以及resize函数,最终将图像数据和图像标记存入train_images和train_labels列表中并返回值。
    关键代码:
    def traindata(path_s): train_images = [] train_labels = [] path_list = [] dt2d = DicomTo2Data() for pa in os.listdir(path_s): path_list.append(path_s+'/'+pa) for path in path_list: for files in glob.glob(path + '/*'): filepath,filename = os.path.split(files) if path.split('/')[-1] == 'negative': train_labels.append(-1) elif path.split('/')[-1] == 'positive': train_labels.append(1) elif path.split('/')[-1] == 'us_images': train_labels.append(2) pic = dt2d.picSplitResize(filepath + '/' + filename) picNew = resize(pic,(30,30)) train_images.append(picNew[0]) return train_images,train_labels
    上述两段代码都有两个返回值,分别是train_images和train_labels列表,所代表的内容分别是训练图像数据和训练图像数据标签。
    4.3.4 归一化图片医疗图像预分类平台是首先对医疗图像进行一系列操作后,最终进行分类的平台。在对图像使用支持向量机分类器进行分类时,数据量较大,抵抗几何变换的攻击较弱,因此使用归一化处理,使图像能抵抗几何变换的攻击,同样可以找出图像中的那些不变量,将图像归为一个系列。
    在本段程序中,循环对图像数组中的数据转化,首先调用Image模块中的fromarray函数将图像转化为Image对象,之后通过调用size接口获取到Image的大小,将图像放置在中间位置。通过调用Image模块中的new函数创建一张背景为黑色的图片,之后通过调用point函数加强黑白色彩,调用numpy模块中的array函数将数据转化为一维化,通过[float(x)/255 for x in imgResizeArray]代码将0-255之间的色彩数值转化为0-1之间的数值,最终图像数据被添加到picNew列表中并返回。
    这段程序主要实现的是将图像进行归一化处理的功能,方便在后续的分类器中对图像进行分类操作。
    关键代码:
    def resize(picArray,size): '''调整图片大小,先归一化图片,并把原图片放在中间,返回的数据格式 [[],[],.....]''' picNew = [] for i in range(len(picArray)): imgPIL = Image.fromarray(picArray[i]) h,w = imgPIL.size newH = w//2 - h//2 #把图片放在中间 imgEmpty = Image.new('L',(w,w),0) #创建一张背景为黑色的图片 imgEmpty.paste(imgPIL,(newH,0)) imgResize = imgEmpty.resize(size,Image.ANTIALIAS) imgResize0255 = imgResize.point(lambda x: 255 if x > 10 else 0) #0是黑,255是白,黑白加强 imgResizeArray = np.array(imgResize0255).flatten().tolist() #转换为一维 imgResizeArraySmaller = [float(x)/255 for x in imgResizeArray] #把0-255转成0-1 picNew.append(imgResizeArraySmaller) #imgResize0255.show() #imgResize0255.save('//home//pandas//workspace//gradution//' + str(i) + '.jpg') return picNew
    4.3.5 使用opencv对图像进行处理医疗图像预分类平台是对dicom格式图像进行读取解析分类的一个平台,核心是使用支持向量机算法对图像进行分类,使用python中的libsvm 第三方模块,由于libsvm输入格式有一定的要求,因此需要将图像数据转化为可输入的格式。
    本段程序将图像数据转化为可用于分类器分类的数据输入格式,picSplitResize函数传入参数为image图像数据,首先调用opencv模块中的cvtColor函数进行灰度转化,将转化后的结果存储在gray变量中,之后调用opencv模块中的dilate和erode函数先膨胀后腐蚀,再进行腐蚀后膨胀,处理后的数据存储在dilated变量中。将dilated变量通过opencv中的medianBlur函数进行细化,然后调用opencv模块的findContours函数进行二值化处理并查找图像数据的轮廓。最后通过调用一个循环将数据转化为可用于分类器分类的输入格式,在循环中首先通过opencv中的formarray函数将array转化成cvmat,然后通过opencv中的GetImage函数将cvmat转化为iplimage,最终将iplimage转换成cvmat,再将cvmat转换成数组。
    这段程序的主要功能在于格式转化,主要使用python中的opencv模块处理医疗图像,下面将本段程序的代码展示出来。
    关键代码:
    def picSplitResize(self, image): '''分割影片尺寸大小''' #image = cv2.imread(path) #dicom_data = dicom.read_file(path) #image=self.DicomtoRGB(dicom_data,bt=0,wt=2048) #灰度转化 gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY) #ret , bin = cv2.threshold(gray, 100,255, cv2.THRESH_BINARY) #cv2.imshow('2',gray) #膨胀后腐蚀 dilated = cv2.dilate(gray, cv2.getStructuringElement(cv2.MORPH_RECT,(2, 2))) eroded = cv2.erode(dilated, cv2.getStructuringElement(cv2.MORPH_RECT,(2, 2))) #腐蚀后膨胀 eroded = cv2.erode(eroded, cv2.getStructuringElement(cv2.MORPH_RECT,(2, 2))) dilated = cv2.dilate(eroded, cv2.getStructuringElement(cv2.MORPH_RECT,(2, 2))) #细化 median = cv2.medianBlur(dilated, 3) median1 = cv2.medianBlur(dilated, 3) #轮廓查找,查找前必须转换成黑底白字 contours, heirs = cv2.findContours(median1,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE) #i = 0 pic = [] dictPic = {} for tours in contours: #rc[0] 表示图像左上角的纵坐标,rc[1] 表示图像左上角的横坐标,rc[2] 表示图像的宽度,rc[3] 表示图像的高度, rc = cv2.boundingRect(tours) #array转换成cvmat image1M = cv.fromarray(median) #cvmat转换成iplimage image1Ip = cv.GetImage(image1M) cv.SetImageROI(image1Ip,rc) imageCopy = cv.CreateImage((rc[2], rc[3]),cv2.IPL_DEPTH_8U, 1) cv.Copy(image1Ip,imageCopy) cv.ResetImageROI(image1Ip) #将iplimage转换成cvmat,然后将cvmat转换成数组,将图像左上角的纵坐标和图像的数组元素放到字典里 dictPic[rc[0]] = np.asarray(cv.GetMat(imageCopy)) pic.append(np.asarray(cv.GetMat(imageCopy))) #i = i+1 sortedNum = sorted(dictPic.keys()) for i in range(len(sortedNum)): pic[i] = dictPic[sortedNum[i]] #cv2.waitKey(0) return pic
    4.3.6 使用libsvm进行训练并进行预测分类医疗图像预分类平台是将dicom格式医疗图像读取解析之后,再通过机器学习中的支持向量机将医疗图像进行分类,而该平台中最核心的部分就是支持向量机算法分类器分类部分。
    下面这段程序是所有代码程序中最重要、最核心的部分,其余部分代码都是这段代码的辅助。本次课题设计的核心其实就是分类,找出正确的医疗图像并进行去噪处理,在这段代码中,主要使用了libsvm的模块来实现支持向量机的分类操作。首先将训练数据、训练标签以及参数确定下来,通过libsvm函数中的svm_problem函数将训练标签和训练数据读入分类器,通过svm_parameter函数将参数确定,之后通过调用libsvm模块中的svm_train函数训练模型。之后先确定预测数据的标签,然后调用svm_predict函数对未知的图像数据进行预测,并将预测结果值返回
    在这段函数代码中,参数需要训练数据、训练数据标签以及预测数据,首先要进行训练,训练出模型,然后使用训练好的模型对未知图像数据进行预测分类,代码如下。
    关键代码:
    def predictPIC(train_images, train_labels, picdata): '''创建LibSVM分类器,返回值为识别出的内容''' prob = svm_problem(train_labels,train_images) param = svm_parameter('-s 0 -t 2 -c 1.2 -g 2.8') prob = svm_train(prob,param) labels = [1] * len(picdata) flag = svm_predict(labels,picdata,prob) return flag
    下面将展示运行本段代码之后的结果图,本段代码是libsvm分类器对图像进行分类的的最后结果图,运行一个测试数据的分类结果图。#iter为迭代次数, nu 与前面的操作参数-n nu相同,obj为SVM文件转换为的二次规划求解得到的最小值, rho 为判决函数的常数项b, nSV 为支持向量个数, nBSV为边界上的支持向量个数, Total nSV为支持向量总个数分类器分类结果图如图4-5。

    4.3.7 保存新图像医疗图像预分类平台最终的结果需要生成经过去噪处理的新医疗图像。
    使用支持向量机分类器对预测图像进行分类后,还需要对图像进行最后一步操作,即将数据保存成png格式图像以便于后续操作。主要调用opencv模块的imwrite函数将最终分类的图像数据保存成png格式的图像文件,生成的新图像文件命名与原始图像文件命名相同,但保存在固定的文件位置。现将代码展示如下。
    关键代码:
    def makePNG(self, image_info, file_name): res = cv2.resize(image_info, (255,255),interpolation = cv2.INTER_LINEAR) name = file_name.split('/')[-1][:-4]cv2.imwrite("/home/pandas/workspace/python/graduation/DICOM_IMAGE/error/"+name+'.png', res, [int(cv2.IMWRITE_PNG_COMPRESSION), 9])
    4.4 医疗图像预分类平台开发方法医疗图像预分类平台开发使用的是面向对象和结构化开发混合的开发方法,本次开发由于没有使用可视化开发技术,因此下面将不会对可视化开发技术进行介绍。
    面向对象开发的方式是将真实的问题转化映射,映射到抽象对象空间,实际上,它是将现实世界中的事物进行模拟,主要是以大量的数据为中心,因而,面向对象开发方式较为稳定,程序中代码的重用性和可维护性比较好。
    对于结构化开发方式来说,主要是按照功能进行开发,将功能进行分解开发,从上而下,最终实现需求的功能。以功能的划分为开发基础。总共分为以下几个阶段:划分功能范围、功能分析、功能设计、功能实现以及功能支持。
    本程序中代码量相对比较少,因此主要是以类和函数混合的方式实现,从扩展性方面来讲,相对较好。
    4.5 定义规范作为一名开发者,需要有一定的开发规范,从而实习代码的可读性和简洁性。这不仅是了方便自己,更是为了培养出团队合作的基础,从此方面来讲,定义代码规范是非常重要的一点规则。
    4.5.1 代码注释规范1.函数注释
    由于函数名的命名就是函数具体作用信息的英文单词组合,因此只有部分函数有注释信息,注释信息是使用’’’’’’的注释方式来注明函数的具体作用,如下:
    def makeOnePicture(file_name, train_images, train_labels): '''调用predictPIC函数进行预测并截取图片''' #info = loadFileInformation(dicom_file) dt2d = DicomTo2Data() image = dt2d.picSplitResize_dicom(file_name) pic = dt2d.picSplitResize(image) picdata = resize(pic,(30,30)) result = predictPIC(train_images, train_labels, picdata) if result[0][0] == 1.0 or result[0][0] == 2.0: dt2d.makePNG(image, file_name)
    2.类注释
    类名的命名方式和函数名称命名的方式基本类似,因此也是部分类有注释信息,主要是说明类的作用信息,大部分类没有注释说明,主要是通过类中的属性函数进行注释。
    3.语句注释
    语句注释中有三种:第一种是对用于的测试语句的注释,在代码中可忽略不计;第二种是在代码的后面空四个空格然后使用“#”进行注释,用于说明该行代码中语句的作用;第三种是单独一行使用“#”进行注释,作用范围直至空行或下一个注释行之间,用于说明从注释行到空行之间所有代码的作用,示例如下:
    def resize(picArray,size): '''调整图片大小,先归一化图片,并把原图片放在中间,返回的数据格式 [[],[],.....]''' picNew = [] for i in range(len(picArray)): imgPIL = Image.fromarray(picArray[i]) h,w = imgPIL.size newH = w//2 - h//2 #把图片放在中间 imgEmpty = Image.new('L',(w,w),0) #创建一张背景为黑色的图片 imgEmpty.paste(imgPIL,(newH,0)) imgResize = imgEmpty.resize(size,Image.ANTIALIAS) imgResize0255 = imgResize.point(lambda x: 255 if x > 10 else 0) #0是黑,255是白,黑白加强 imgResizeArray = np.array(imgResize0255).flatten().tolist() #转换为一维 imgResizeArraySmaller = [float(x)/255 for x in imgResizeArray] #把0-255转成0-1 picNew.append(imgResizeArraySmaller) #imgResize0255.show() #imgResize0255.save('//home//pandas//workspace//gradution//' + str(i) + '.jpg') return picNew
    4.5.2 代码命名规范对于类名采用驼峰命名法,英文,有语义单词首字母全部需要大写,剩余有语义单词小写,从类作用方面命名,如:GetInfomation,意义是获取信息,即获取病人隐私信息。
    对于函数命名采用驼峰命名法或者下划线命名法。驼峰命名法首个有语义单词首字母小写,其余有语义单词首字母大写,其余字母小写。命名法如:loadFileInformation、traindata_dicom。
    对于变量,主要使用下划线命名法,从变量作用方面进行命名,如:train_images。
    4.6 小结本章主要对医疗图像预分类平台的设计进行了说明,首先展示了模块结构图和流程关系图,然后对代码中的每一块功能进行说明,之后说明了开发的方法,并说明了代码定义规范。
    5 医疗图像预分类平台开发测试与调试本章旨在说明医疗图像预分类平台开发中测试与调试过程,分别对程序调试和工具测试以及测试数据进行说明。
    5.1 程序调试在进行软件程序开发的过程中,难免会遇到各种各样的问题,不仅仅是常见的开发环境问题和语法问题,还有许多其它问题在实际开发过程中。在开发该程序中,采用模块化、结构化的开发方式,按照功能进行开发,同样按照功能进行测试,单独测试,便于检查出现问题时问题的来源。
    在测试调试过程中,主要使用Spyder软件进行程序的开发和测试过程,使用该软件中的错误语法提示信息和单元测试运行后的结果以及打印语句对错误进行定位和显示。
    5.2 工具的测试5.2.1 测试的意义及目的软件测试的目的,从本质上讲,是为了程序的有效可靠可运行性,比较软件最初的需求和在真实运行环境下的功能的差别,从而对差异之处进行一定程度的修正,以达到预期的功能和结果。
    5.2.2 测试步骤测试是有一定的方式、步骤以及技巧的。这段程序中不涉及与其他机器之间的操作,只是本机Ubuntu系统中的代码的运行,因此,在测试程序中只运用了手工测试、单元测试和系统测试的测试方法对系统的功能进行测试,以追求稳定可靠的程序。
    首先要进行单元测试,对源代码中的每一个功能单元进行测试,基本要达到覆盖每一个类以及函数,最终组合起来,检查各个功能部分是否正确地实现了功能模块。
    系统测试把经过确认的功能单元进行组合,纳入到实际运行的环境中,即Ubuntu系统中的python2.7开发环境中进行运行,将整个程序统一在系统中整体运行,查看结果的一种测试。
    5.3 测试数据在每一个功能单元完成后都会进行相应的分模块功能测试以确保该模块的正常运行,也同样是为了方便后续对功能进行整合。在对每一个功能单元进行测试时选择训练数据中的一部分进行测试,如在测试去噪功能、格式转化功能等,而对分类器进行测试时,需要使用的是所有训练数据以及部分预测数据等,在对dicom文件读取功能进行测试时,使用的是部分dicom数据。通过选择某些具有代表性的数据进行测试可以测试程序的健壮性,确保程序的良好运行。下面将本程序中选择主要功能所做的测试,并展示所出现的问题和解决方法,还有最终效果。



    功能模块
    测试方法
    测试数据
    测试出现过的问题
    解决方法
    结果




    读取dicom文件、去噪
    使用一个dicom文件作为输入数据,
    dicom文件
    不能处理压缩过dicom文件
    使用未压缩的dicom图像文件
    可以读取并去噪


    读取隐私信息
    手动测试
    dicom文件
    有些隐私信息没有
    读取关键隐私信息即可
    正常


    读取训练数据
    输入路径循环读取数据
    所有训练数据


    正常


    归一化图片
    手动测试
    一个图像数据


    正常


    opencv处理图像
    对图像数据进行处理
    图像数据


    正常


    libsvm训练预测分类
    训练数据,预测数据
    训练、预测数据


    正常


    保存新图像
    分类后图像数据保存在固定位置
    图像数据


    正常



    5.4 小结本章主要说明了医疗图像预分类平台的测试和调试过程,主要从程序的调试、工具的测试以及测试数据方面进行了说明。
    6 结论医疗图像预分类平台采用python技术进行开发,使用了pydicom模块读取dicom格式图像文件,opencv模块对图像进行去噪和格式转换以及libsvm模块进行图像分类处理,最后保存成新图像,使用企业级规范开发。本程序代码已推送到github上,但是并未书写说明文档,仅仅是在代码中有注释。
    本次基于深度学习的医疗图像预分类平台的输入数据是DICOM格式医疗图像文件,该平台对DICOM格式医疗图像读取后,进行去噪处理并进行截取,形成新的图像文件,然后将新的医疗图像文件进行格式转化,转化成可用于支持向量机输入的格式,之后将新格式的医疗图像数据通过支持向量机分类器进行分类,主要调用libsvm模块。最后对分类正确的图像数据进行保存操作,保存成PNG格式图像。以上就是该医疗图像预分类平台的功能作用。
    参考文献[1] Andrew Ng.Machine Learning[OL].2017.3 https://www.coursera.org/learn/machine-learning
    [2] 李航.统计学习方法[M]. 北京:清华大学出版社, 2012.3
    [3] TOBY SEGARAN.集体智慧编程[M]. 北京:电子工业出版社,2009.1
    [4] [美]\ 哈林顿著 李锐等 译.机器学习实战[M]. 北京:人民邮电出版社,2013.6
    [5] 林意,廖琴枝.基于无损水印的DICOM文件头信息篡改检测[J].计算机工程,2016:151-155,162.
    [6] 龙华飞,唐月华,陈泓伶.PACS系统中DICOM医学图像格式解析[J].中国数字医学,2014: 29-31.
    [7] 梁卫星,陈平平,张戈,徐洋洋.DICOM文件剖析及读取设计[J].现代计算机:普及版,2014:72-75.
    [8] 王云英,阎满富.C-支持向量机及其改进[J].唐山师范学院学报,2012.
    [9] 张立,王渊民.基于核函数的非线性支持向量机[J].科技展望,2017.
    [10] LAROBINA M,MURINO L.Medical image file format FAQ[J].J Digit Imaging, 2014:200-206.
    [11] Richard Curran,Nel Wognum,Milton Borsato,Josip Stjepandi&cacute, Wim J. C. Verhagen, Rodrigo Meira de Andrade,Anderson Luis Szejka,Osiris Canciglieri Junior.A Gingival Mucosa Geometric Modelling to Support Dental Pros thesis Design[M].IOS Press:2015.
    [12] William Cheng-Chung Chu,Han-Chieh Chao, Stephen Jenn-Hwa Yang, Yu-Ann Chen,Pau-Choo Chung.Enhanced Local Support Vector Machine With Fast Cross-Validation Capability[M].IOS Press:2015.
    [13] KhazendarS, Sayasneh A,Alassam H,et al.Automated characterisation of ultrasound images of ovarian tumours: the diagnostic accuracy of a support vector machine and image processing with a local binary pattern operator[J].2015:7-15.
    [14] Ma Y,Wang L,Li L.A Parallel and Convergent Support Vector Machine Based on Map Reduce[M]//Computer Engineering and Networking.Springer International Publishing,2014:585-592.
    [15] Sharifi M A, Souri A H.A hybrid LS-HE and LS-SVM model to predict time series of precipitable water vapor derived from GPS measurements[J].Arabian Journal of Geosciences,2015:7257-7272.
    1 评论 10 下载 2018-10-03 22:33:13 下载需要14点积分
  • 基于Python与Node.js实现的医疗图像库在线存储与检索平台网站

    摘 要图像数据相对于一般的文本数据来说管理起来更具有复杂性。传统的图像存储方式有两种,一是直接将图像存入数据库,二是将图像存放在文件系统,而将路径存放在数据库,前一种基于“大字段数据最好不要存放在数据库中”这种规则一般不被使用,常用的是后一种,而这种方式也有明显的性能劣势,原因在于访问图像时要两次访问IO,这在高并发访问中很难满足需要。
    为了对Dicom格式实现高效的管理以及为项目其他模块提供便捷的服务,特设计与实现了一系列对医疗图像、病人隐私信息、深度学习框架操作的接口。
    Mongodb是一个非关系型数据库亦可以称作文档型数据库,因为其存储的节本单位是文档等同于关系型数据库中表的一行。该数据库考虑了图像的存储,提供了Gridfs这种存储方式,来满足大量图像管理的需要。
    关键字:Dicom,Mongodb,Gridfs
    ABSTRACTThe image data with respect to the general text data management is more complicated. There are two kinds of image storage in the traditional way, one is the image stored in the database, the two image is stored in the file system, and the path stored in the database, which is based on the former “characters of data stored in the database is best not the rule the general is not being used, is commonly used after a while, this approach also has disadvantage performance obviously, because access to the image two times to visit IO, in this highly concurrent access is difficult to meet the needs.
    In order to realize the efficient management of the Dicom format and provide convenient service for the project design and implementation of other modules, especially a series of medical images, patient privacy information, deep learning framework interface.
    Mongodb is a non relational database can also be called the document database, because its storage cost is equivalent to a unit of the document in a relational database table. The database is considered image storage, Gridfs provides this storage method to meet the need to manage a large number of image.
    Key words: Dicom, Mongodb, Gridfs
    1 绪论1.1 研究背景及意义1.1.1 研究背景图像作为数据的一种,和其他形式的数据一样必要的时候要被保存起来,以备后用。目前存储图像的方式主要有两种,其一,将图像保存在文件系统,然后将路径存放在数据库,其二,将图像直接存放在数据库中。一般都会采用第一种,因为其二大字段数据会导致数据库压力增大、备份困难、查询速度变慢等问题。第一种存储方式也存在性能,因为会访问IO两次,这在高并发的情况下读写速度会异常的慢。面对高并发医疗图像读写这种局面,迫切需要一种更为优化的方式来满足性能的需要。
    1.1.2 研究意义随着时代的迁徙,医疗图像在医生的诊断过程中已经成为不可或缺的一部分,并且也是诊断中最重要的一环之一,医生通过肉眼观察医疗图像来判断病人的健康情况,然后做出治疗。不过在这种什么都讲究效率的时代,人工观察图像已经不能满足需要,并且这种方法也存在很多的缺点,比如准确性问题。因此,我们研究并设计了一款自动识别医疗图系统,而我的工作就是为其他模块提供数据支持。众所周知,在一个项目中往往IO是影响性能的主要因素,而性能也是所有用户最在乎的一点,所以我的模块可以说是在整个项目中举足轻重。
    1.2 技术栈的选择整个项目主要采用node.js、python两种语言来满足不同模块的需要。而我负责的这部分主要主要是想其他模块提供数据支持,因此就需要分别采用这两种语言来开发各个接口集合。
    再数据库选择这块,可以有多种选择,如MySQL、oracle等关系型数据库以及mongodb这种非关系型数据库,不过最终选择了mongodb数据库,具体有如下原因:

    高性能、易部署、易使用。
    提供Gridfs存储格式来支持文件存储,以这种存储格式存放图像相比其他方式性能更为优越,这也是本系统所追求的。
    其他模块有采用node.js 开发,而mongodb对js有很好的支持,node.js本身就是js,所以两者的契合度较高。

    综上所述,整个系统采用node.js、python、mongodb技术栈。
    1.3 论文的主要工作内容为了实现该系统,首先得从数据库入手,设计并创建数据库是所有工作的前提。因为其他部分都是基于数据库的接口,没有事先设计好的数据库,后面的工作是无法进行的。
    接着就是编写node.js部分的代码了。在这大的一步里,先要进行配置编写这一小步,因为接口的编写依赖配置信息。完了之后进行图像接口的编写,最后在进行xml接口的编写,后两步其实区分先后次序没有多大的意义。最后就是编写mocha测试代码,进行代码的调试。
    接着是python部分代码的编写,至于先编写node.js还是python,这都无所谓,这两部分并没有先后次序。Python部分代码编写的时候先进性配置的编写,然后进行图像接口的编写,最后进行病人隐私信息接口的编写。
    数据库的设计以及两种语言借口的设计与编写都在第四章详细介绍了。
    1.4 本章小结本章首先介绍了系统的背景,包含传统的图像存储方法、高并发环境下图像访问将会变慢等相关论述。接着详细说明了系统的意义,提到系统的实现有助于缓解高并发环境下图像访问速度变慢的问题。再者介绍了系统实现的技术栈,提到mongodb、python、node.js三种。最后介绍了论文的主要工作内容,说明系统的设计与实现的顺序。
    2 相关技术分析2.1 MongoDB数据库2.1.1 数据库简介MongoDB是一个基于分布式文件存储的数据库。由C++语言编写。旨在为WEB应用提供可扩展的高性能数据存储解决方案。
    MongoDB是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。他支持的数据结构非常松散,是类似json的bson格式,因此可以存储比较复杂的数据类型。Mongo最大的特点是他支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。
    2.1.2 BSON存储格式BSON是一种类json的一种二进制形式的存储格式,简称Binary JSON,它和JSON一样,支持内嵌的文档对象和数组对象,但是BSON有JSON没有的一些数据类型,如Date和BinData类型。
    BSON可以做为网络数据交换的一种存储形式,这个有点类似于Google的Protocol Buffer,但是BSON是一种schema-less的存储形式,它的优点是灵活性高,但它的缺点是空间利用率不是很理想,
    BSON有三个特点:轻量性、可遍历性、高效性
    {“hello”:”world”} 这是一个BSON的例子,其中”hello”是key name,它一般是cstring类型,字节表示是cstring::= (byte*) “/x00” ,其中*表示零个或多个byte字节,/x00表示结束符;后面的”world”是value值,它的类型一般是string, double, array, binary data等类型。
    2.1.3 Gridfs存储格式数据库支持以BSON格式保存二进制对象。 但是MongoDB中BSON对象最大不能超过16MB。 GridFS 规范提供了一种透明的机制,可以将一个大文件分割成为多个较小的文档。这将容许我们有效的保存大的文件对象,特别对于那些巨大的文件,比如视频。

    GridFS 用于存储和恢复那些超过16M(BSON文件限制)的文件(如:图片、音频、视频等)。
    GridFS 也是文件存储的一种方式,但是它是存储在MonoDB的集合中。
    GridFS 可以更好的存储大于16M的文件。
    GridFS 会将大文件对象分割成多个小的chunk(文件片段),一般为256k/个,每个chunk将作为MongoDB的一个文档(document)被存储在chunks集合中。GridFS 用两个集合来存储一个文件:fs.files与fs.chunks。每个文件的实际内容被存在chunks(二进制数据)中,和文件有关的meta数据(filename,content_type,还有用户自定义的属性)将会被存在files集合中。

    2.2 传统图像存储方法介绍图像作为数据的一种也需要被存储起来以备后面的访问,目前业界存储图像有一下两种做法:

    把图像直接以二进制形式存储在数据库中,一般数据库会提供一个二进制字段来存储二进制数据,比如mysql中有blob字段,oracle数据库中是blob或bfile类型。这种方法缺点,一方面增加了数据负担,二方面代码处理也比较复杂。
    图像存储在磁盘上,数据库字段中保存的时图片的路径。

    互联网环境中,大访问量,数据库性能很重要。一般在数据库存储图片的做法比较少,更多的是将图片路径存储在数据库中,展示图片的时候只需要连接磁盘路径把图片载入进来即可。
    2.3 性能对比分析2.4 node.js对MongoDB的支持在npm上有很多用于node.js对MongoDB支持的包,如mongodb、mongoose等。本系统选用了官方驱动mongodb包,该包提供了很多对mongodb操作的接口,具体的接口说明可以查询官方提供的手册。而为了支持gridfs存储格式,特选用了gridfs包,这也可以在npm上找到,该包提供的接口集可以很方便的实现图片的读写。
    2.5 python对MongoDB的支持本系统选用了pymongo包来作为python访问mongodb的驱动,当然还有其他类型的驱动,不过这个是最常用的一个,所以就选择了这个。为了对gridfs存储格式的支持,系统选用了gridfs这个包来满足mongodb存储和访问图像。
    2.6 加密介绍本系统为了病人隐私信息的安全性,特采用AES加密标准对该数据进行了加密。
    AES中文全称是高级加密标准,是美国联邦政府采用的一种区块加密标准。这个标准用来替代原先的DES,已经被多方分析且广为全世界所使用。经过五年的甄选流程,高级加密标准由美国国家标准与技术研究院(NIST)于2001年11月26日发布于FIPSPUB 197,并在2002年5月26日成为有效的标准。2006年,高级加密标准已然成为对称密钥加密中最流行的算法之一。
    AES只是个基本算法,实现AES有若干模式。其中的CBC模式因为其安全性而被TLS(就是https的加密标准)和IPSec(win采用的)作为技术标准。简单地说,CBC使用密码和salt(起扰乱作用)按固定算法(md5)产生key和iv。然后用key和iv(初始向量,加密第一块明文)加密(明文)和解密(密文)。
    系统为了支持AES特选用了Crypto.Cipher包来进行加密解密等操作。
    2.7 本章小结本章首先介绍了mongodb数据库,其中重点说明两种存储格式,分别是BSON和GridFS。前一种是一般的数据存储方式,数据的大小不能超过4M,后者是为了存储大于4M的文件而设计的。接着粗略的介绍了传统有哪些图像存储方法,并且对比说明这些方法优缺点。接着通过数据展示了三种方式随着线程的数量变化的变化情况,最终得出本系统所采用的方式适合高并发的情况下的结论。后面又介绍了python以及node.js为了访问mongodb所使用的驱动。最后详细的介绍了系统所采用的AES加密方式。
    3 系统需求分析3.1 系统可行性分析对于每个软件项目中的问题并不都有明显的解决办法,有时候很难在预定的时间和费用之内解决这些问题,也无法用现有的技术圆满完成。如果贸然去开发,会造成时间、人力、资源和经费的不必要浪费,或者还会带来一些意料不到的问题。所以本系统在开发之前分别在经济可行、技术可行性、运行可行性、操作可行性几个方面进行了分析。
    3.1.1 经济可行性开发该软件所需的相关资料可以通过文献资料和网络进行调查采集,所需的软件系统、硬件平台等都易于获得,无需特殊工具,开发成本低,简单易实现,从经济角度来看,开发该软件经济可行。
    3.1.2 技术可行性系统涉及的任务主要有各种图像存储方案性能对比分析、数据库设计、接口设计与实现。
    性能分析方面我准备采用C++编写,实现各种存储方案的接口,然后线程的个数为参数,时间为结果,并且将结果通过图形化界面展示出来,所涉及的东西目前都是我自身具备的,所以在这方面可行。
    数据库设计方面,数据库用到的是mongodb,虽然之前并没接触过,不过网上有大量的资料和书籍以及用到的知识知识mongodb中的基础部分,而且数据库设计大学里已经学过,所以在这方面也技术可行。
    接口设计与实现,实现主要采用node.js、python两种语言,这两种语言之前并没有接触过,不过我有大量的时间去学习,所以在这方面也技术可行。
    3.1.3 运行可行性该系统是通过python、node.js、mongodb开发而成,如果要运行只要具备python运行环境、node.js执行环境、mongodb数据库即可。所以在运行上可行的。
    3.1.4 操作可行性由于该系统不是被用户直接操作的,系统所面向的用户是其他模块的开发人员,而其他模块的程序开发人员,都具备扎实的node.js、python知识,所以在操作上是绝对可行的。
    综上所述,该系统在经济、技术、运行、操作上完全可行。
    3.2 系统需求分析该系统身为医疗图像识别系统中的一个模块,主要是为其他模块提供数据支持,也就是说,其他模块通过此模块进行数据读写。通过各种需求获取手段包括向用户反复咨询、咨询老师、跟其他成员讨论,最终得出系统的需求如下:
    功能需求:

    系统可以分别满足node.js、python两种语言的接口需要。
    针对python可满足图像以及隐私信息增、删、改、查的需要。
    针对node.js可满足图像增、删、改以及xml的增、删、改、查的需要

    性能需求:

    响应时间不超过1秒
    支持2万用户同时操作数据库,保证性能不受影响

    安全性需求:

    系统对病人隐私数据进行加密
    系统自动进行数据备份,防止数据丢失造成危机
    本系统应该能够记录系统运行时所发生的所有错误,这些错误记录便于查找错误的原因

    可靠性和可用性需求:

    系统提供的接口尽可能满足方便其他模块的操作
    系统具有一定的容错和抗干扰能力,在非硬件故障和非通讯故障时,系统能够保证正常运行,并有足够的提示信息帮助用户有效正确的完成任务

    3.3 本章小结可行性分析是一个从经济、技术等多方面进行评估的过程,可行性分析的结果决定这个项目是否值得去做。在正式开发一个项目之前,都应该或者说必须对项目进行可行性分析。本章一开始先从经济可行性、技术可行性、运行可行性、操作可行性四个方面对系统进行了可行分析,结论是该项目可行。可行性分析之后就是需求,需求分析是一个反复确定用户需求的过程,只有完整、准确地对项目进行了需求分析才能做出让用户满意的作品,本章主要从功能、性能、安全性可靠性和可用性等几个方面调查用户的需求。
    4 系统设计4.1 模块结构系统由于要分别向node.js开发者以及python开发者提供数据访问接口,所以把系统根据语言的不同分为两个部分分别讨论,虽然分开了,但是很多方面还有很相似的,以下是两部分模块结构设计。
    4.1.1 node.js模块结构我们团队中有一人是开发一个平台用来跑深度学习框架,然后对比这些框架的性能,而我这个模块就是为他服务的,而他这一块需要将框架配置信息、原始图像保存进数据库以备后用,具体需要哪些操作,以下的模块图有说明。语言方面我们经过协商,最终选择了node.js。下面说道的系统配置模块,是为了便于系统某系内容的变化而设计的,如数据库的名以及字段的名字等,如果发生改变只需在修改这个文件即可,避免了全面的改动。

    4.1.2 python模块结构团队中其他人都是用python写的,其中一人负责对原始图像的处理以及分析以提取出病人隐私数据和满足深度学习的需要。另外一人是用处理好的图像进行深度学习,训练模型便于系统对图像的识别。经过分析,设计了图像管理模块用于管理各种图像以及病人隐私信息管理模块用于管理提取的病人隐私数据。而至于AES加密模块为了对病人隐私数据进行加密,防止数据泄露,造成不必要的损失。剩下的系统配置模块同上。

    4.2 功能设计与分析系统主要由两部分够成,其一是python接口集合,其二是node.js接口集合。下面将分别介绍这两部分的设计。
    1.node.js接口集合
    node.js接口集合操作的对象有Dicom医疗图像、深度学习框架配置这两个,所以根据这一点,将这一部分主要分为两部分,其次,为了减少因系统部署的环境的改变而做大量的修改,特将系统配置提取出来作为第三个模块,因此系统总的分为三个模块,以下将详细介绍这三个模块的设计。
    2.Dicom图像管理模块
    这个模块主要是为其他模块提供Dicom医疗图像操作的接口,根据需要接口的类型被设置为三种,分别是增、删、查等。
    “增”就是将图像存放到数据库,考虑到方便用户、性能两方面,增加操作主要由insertOneImage、insertImages两接口负责。这两个接口的区别是前者一次只能插入一张图像,后者可以一次插入多张图像,后者并不是前者的多次重复调用,如果这样,后者就没有单独设计的必要了,插入操作开始先要进行数据库连接,结束后要释放链接,考虑到多次插入仅需要一次连接以及一次释放,据此设计出了insertImages这个接口。不过两者在大体上结构上是相同的,流程图如下:

    主要代码说明:

    将图像存放进数据库
    // 创建用于Gridfs格式的对象进行Gridfs操作var gfs = grid(db, mongo); var obj = {};obj.filename = filename;obj.processed_times = 0;// 将图像从文件系统转储到数据库gfs.fromFile(obj, path + filename, function(err, file){ if (null != err){ error(); return; } else{ sucess(); // 关闭打开的数据库连接 db.close(); } });

    判断图像是否存在
    fs.exists(path + filename, function(exist){ if (exist){ // 如果存在 } else{ //如果不存在 }});

    连接数据库
    var db = new mongo.Db(config.DB, new mongo.Server(config.HOST, config.PORT, {})); // 异步打开 db.open(function(err, db){ if (err != null){ error(); return; } … }
    “删”就是将满足条件的图像从数据库中去除,满足条件的可能不止一个,所以删的时候可能存在多条被删除,这一块接口同样也是这么设计,而不是只删除满足条件的第一条。由于包Gridfs没有直接删除多条的接口,只有一个根据图像的id或者filename一次只删除一条的接口,所以这一块设计的时候,是先将满足条件的图像的id都查询出来,放在一个数组,然后一条一条的调用Gridfs提供的接口去删除。删除操作由imgDelete接口负责, 接口的程序流程图如下。

    主要代码说明:

    连接到指定名称的集合
    db.collection("fs.files", {strict: true}, function (err, collection) { if (err != null) { console.log(err); db.close(); return; } …});

    删除指定条件的图像
    gfs.remove(obj, function (err) { if (null != err) { error(); console.error(err); } else { // 如果成功删除数组中的最后一张图片,则该接口执行成功,并关闭数据库 if (i == length - 1){ success(); db.close(); } }});
    “查”就是将数据库中符合条件的图像取出来,在这里会将取出的结果存储在配置文件指定的目录。查询操作是由imgFind接口负责,该接口可以一次性查询出多张图像,在进行readFile操作之前,先根据参数doc查询出所有的图像信息,然后根据id一个一个的转储数据库中的图像到指定的目录,之所以这样进行,在于readFile一次只能查询一张图像,再者为了在目中情况下提醒用户没有满足条件的图像。流程图如下。

    主要代码:

    从mongodb数据库查询出图像并存放带指定目录
    var id = docs[i]["_id"];var obj = {};obj._id = id;(function(i){ // 根据id,查找图像,并写到目录 gfs.readFile(obj, function (err, data) { if (err != null){ error(); } else{// 写到目录 fs.writeFile(config.IMAGE.FIND_RESULT_PATH + id + config.IMAGE.EXT_NAME, data, {flag: 'w'}, function (err) { if (null != err) { error(); console.error(err); } else { // 如果最后一个图像查询成功,则整个接口才成功 if (i == length - 1){ success(); db.close(); } } }); } });})(i);//循环加异步所以这块使用闭包函数
    系统配置模块
    该模块主要为其他模块提供配置信息,包括数据库名称、主机地址、端口、查询结果存储位置等。这些信息可能会因为某些原因要进行修改,这里为了便于修改,特将此部分单独分离出来作为一个模块。
    主要代码:
    module.exports = { "DB" : "foobar","HOST" : "localhost","PORT" : 27017,"XML" : {"COLLECTION_NAME" : "xml_collection", "FRIST_COL_NAME" : "framename", "SECOND_COL_NAME" : "xml_content", "FIND_RESULT_PATH" : "./", "EXT_NAME" : ".xml"},"IMAGE" : {"FIND_RESULT_PATH" :"C:\\Users\\", "EXT_NAME" : ".jpg"}}
    深度学习框架配置文件管理模块
    系统采用多个深度学习框架,需要对这几个框架进行对比,根据需要,需要将这些框架的配置文件存放到数据库中,由于框架配置文件的大小没有超过4M,所以还是采用BSON的存储格式,直接将内容以字符串的形式存放在数据库中,考虑到其他模块的需要,特设计增、删、该、查几种接口形式。
    “增”这个操作,这一块由xmlInsert接口负责,该接口在对数据库进行操作之前首先对文件是否存在进行判断,接着连接数据库,并判断对应的集合是否存在,如果不存在则创建,接下来从文件系统读取文件的数据,插进数据库当中,最后释放连接。该接口对应的流程图如下。

    主要代码:

    读取文件内容
    // 根据路径和文件名从文件系统读取内容fs.readFile(path + filename, function(err, data){ if (null != err){ error(); db.close(); return; }

    将数据插入到数据库
    // 将读取内容转换成字符串var xmlContent = data.toString();// 组织文档var doc = {};doc[config.XML.FRIST_COL_NAME] = framename; doc[config.XML.SECOND_COL_NAME] = xmlContent;// 插入到数据库result.insertOne(doc, function(err, r){if(err != null){ error(); db.close(); return; } else{ success(); db.close(); return; } });
    “删”就是根据框架名称删除数据库中对应的文档,删除操作这里是由xmlDelete接口负责,该接口的实现原理是读取用户要删除的框架名称,然后根据名称调用驱动中提供的deleteOne接口删除,不过在删除之前,进行了查询操作,目的是判断要删除的内容是否存在,对于deleteOne接口,如果不存在不会有什么反应,所以对于用户来讲,就无法了解是否已经删除,而增加查询操作,就是为了在对应内容不存在的时候,提示用户。流程图如下。

    主要代码:

    根据条件删除
    // 组织删除的条件var query = {};query[config.XML.FRIST_COL_NAME] = framename;// 根据条件删除collection.deleteOne(query, {}, function(err, result){ if (null != err){ error(); db.close(); return; } else{ success(); db.close(); return; } });
    “查”就是根据框架名称查询出对应的框架配置内容,这里根据需要提供了xmlFind接口来负责这个操作,xmlFind查询的结果会自动写入到特定的目录当中。接口流程图如下。

    主要代码:

    查询并写入到指定目录
    var query = {};query[config.XML.FRIST_COL_NAME] = framename; // 根据query查询collection.findOne(query, {}, function(err, doc){ if (null != err || doc == null){ error(); db.close(); return; } //写入到指定目录 fs.writeFile(config.XML.FIND_RESULT_PATH +framename+config.XML.EXT_NAME, doc[config.XML.SECOND_COL_NAME], {flag: 'w'}, function (err) { if (err) { error(); db.close(); return; } else { success(); db.close(); return; } }); });
    2.python接口集合
    Python接口集合管理的对象有Dicom医疗图像以及病人隐私信息,根据这一点首先将系统分为两部分,再者为了保证系统的环境发生改变时不用大面积的去修改,特增加第三个模块配置模块,最后为了保证病人信息的安全,增加了AES加密模块。综上,系统总共分为四个模块,以下将详细介绍这四个模块的设计。
    Dicom图像管理模块
    这个模块跟node.js部分基本相同,不过还有些差别,差别在于管理的图像在原来原始图像的基础增加了处理过得图像,所以在接口的设计方面也会发生改变。根据需要这部分接口被设置为三个类型:增、删、查,下面将详细介绍这每种接口的设计。
    “增”即向数据库中添加图像,在这里并没有设计多个接口,而仅有InsertImage一个接口负责。其接受一个参数,这个参数为Image的对象,这个对象里封装着图像的相关信息,包括图像的名称、路径、处理次数、原始图像id等。至于为什么采用一个接口,而不像node.js部分设计多个接口而适应各种情况,因为在这里并没有出现node.js部分的问题,调用这一个接口多次插入不会影向插入的性能的,这得感谢pymongo包的底层设计,它会在适当的时候创建连接以及在适当的时候释放连接,上层不需要做这些事情。流程图如下。

    主要代码:

    读取图像数据并存放到数据库
    img = open(image.GetPath() + image.GetFilename(), 'rb')data = img.read()# 存放数据到mongodb数据库gfs.put(data,filename=image.GetFilename(),process_times=image.GetProcessTimes(),origin_fileid=image.GetOriginFileid())
    “删”即从数据库删除满足指定条件的图像,该操作由DeleteImage接口负责,DeleteImage为了保证当数据库中没有符合条件的图像时给予提示,所以在执行删除操作之前先进行了查询操作,如果没有符合条件的则抛出查询结果为空的异常。流程图如下。

    主要代码:

    查询并删除
    fileList = files.find(dict) #统计个数len = 0#根据查询图像的id,分别删除for file in fileList: len = len + 1 id = file['_id'] gfs.delete(ObjectId(id))# 如果没有查询到结果,就抛出异常if len == 0: raise NoResultException("查询结果为空")
    “查”即从数据库中查询出满足条件的图像,查询操作由FindImage接口负责,它接受一个字典类型的参数,用来表示查询条件,最后将查询的结果存放到指定的目录。查询成功返回True,查询失败返回False并抛出对应的异常,异常这块可以通过配置文件打开或关闭,需要用户自己设置。流程图如下。

    主要代码:

    查找满足条件的图像并写入目录
    # 查找满足条件的图像files = self.__db['fs.files']fileList = files.find(dict)# 用来存放fileList的长度len = 0# 分别取出图像数据并写入指定目录for file in fileList:len = len + 1id = file['_id']data = gfs.get(ObjectId(id))f=open(configs['IMAGE']['FIND_RESULT_PATH'] + str(id) + configs['IMAGE']['EXT_NAME'], 'wb')f.write(data.read())f.close()# 如果fileList没有元素,则抛出异常if len == 0: raise NoResultException("查询结果为空")
    系统配置模块
    系统配置模块如果node.js部分,主要包含系统数据库、字段、服务器地址等信息,方便以后修改。代码展示如下。
    configs = { 'DB' : { 'NAME' : 'foobar', 'PORT' : 27017, 'HOST' : 'localhost' }, 'PATIENT_COLLECTION_NAME' : 'patientInfo', 'IMAGE' : { 'FIND_RESULT_PATH' : './', 'EXT_NAME' : '.jpg' }, 'AEX' : { #'MODE' : 'CBC', 'KEY' : 'yangke' }, 'SHOWEXCEPTION' : True}
    AES加密模块
    AES加密模块主要负责病人隐私信息的加密和解密,AES只是个基本算法,实现AES有若干模式,这里采用CBC模式。Python通过Crypto.Cipher这个包实现AES加密模块代码的编写。
    主要代码:

    加密
    def encrypt(self, text):cryptor = AES.new(self.key, self.mode, self.key)length = 16count = len(text)add = length - (count % length)text = text + ('\0' * add) self.ciphertext = cryptor.encrypt(text) return b2a_hex(self.ciphertext)

    解密
    def decrypt(self, text):cryptor = AES.new(self.key, self.mode, self.key)plain_text = cryptor.decrypt(a2b_hex(text))return plain_text.rstrip('\0')
    病人隐私信息管理模块
    病人隐私数据来源于从医疗图像中提取,这些信息需要存放进数据库来进一步管理,根据需要这里提供增、删、改、查四中操作。至于这些数据怎么在数据库中存储,后面会有相应的数据库设计部分来解释。下面分别针对这四种操作详述他们的设计原理。
    “增”即向数据库中添加隐私数据,该操作InsertPatientInfo接口负责,该接口主要调用pymongo包中的insert接口实现。流程图如下。

    主要代码:

    连接集合并插入数据
    try: #如果集合不存在,插入第一文档时会自动生成 collection = self.__db[configs['PATIENT_COLLECTION_NAME']] # 插入 collection.insert(patientInfo) return True except Exception as e: if configs['SHOWEXCEPTION']: # 打印异常信息 print(e) return False
    “删”即从数据库中删除符合条件的病人隐私数据,删除操作由DeletePatientInfo接口负责,该接口接受一个字典类型的参数,用来传递要删除数据的条件,再删除之前首先判断是否存在这样的数据,如果不存在的时候提示用户,避免什么都不做。最后调用remove接口删除。流程图如下。

    主要代码:

    查找并删除病人隐私信息
    res = collection.find(dict) # res的长度len = 0for r in res: len = len + 1# 如果查找结果为空,则抛出异常if len == 0: raise NoResultException("查询结果为空") collection.remove(dict)
    “改”即将符合条件的病人隐私数据某些属性值改成其他的,修改这一操作由UpdataPatientInfo接口负责。该接口接受两个参数,分别是dict1,表示条件,dict2表示更新的目标。接口的更新操作主要有pymongo中update接口实现,流程图如下。

    主要代码:

    查询并更新病人隐私信息
    # 返回指定集合collection =self.__db[configs['PATIENT_COLLECTION_NAME']]# 查找res = collection.find(dict1)# res的长度len = 0for r in res: len = len + 1# 如果查找结果为空,则抛出异常if len == 0: raise NoResultException("查询结果为空") tmp = {}tmp['$set'] = dict2# 更新collection.update(dict1, tmp)
    “查”即从数据库中查询符合条件的病人隐私数据,查询操作由FindPatientInfo接口负责,该接口接受一个参数,用来存放查询的条件,如果查询结果为空会抛出异常,当然,用户可以通过设置配置文件来关闭或者打开异常,FindPatientInfo主要是调用pymongo包中find接口实现,流程图如下。

    主要代码:

    根据条件查询
    # 返回指定的集合collection=self.__db[configs['PATIENT_COLLECTION_NAME']]# 查找res = collection.find(dict) # res的长度len = 0for r in res:len = len + 1# 如果查找结果为空,则抛出异常if len == 0:raise NoResultException("查询结果为空")return res
    4.3 系统开发方法目前使用最广泛的软件工程方法学,包括传统方法学和面向对象方法学。
    传统方法学
    传统方法学又称生命周期方法学,或结构化方法学。传统方法学把软件生命周期的全过程依次划分为若干个阶段,然后顺序地完成各个阶段的任务。它采用结构化技术(结构化分析、结构化设计和结构化实现)来完成软件开发的各项任务。
    在这个过程中,软件文档是通信的工具,他们清楚、准确地说明了到这个时候为止,关于该项工作已经知道了什么,同事奠定了下一步的基础。
    面向对象方法学
    客观世界是由对象构成的,对象是一个属性和数据操作的封装体。数据和对数据的处理原本密切相关的。
    传统方法学把数据对数据的操作人为地分离成两个独立的部分,要么面向数据,要么面向对数据的操作。而面向对象方法是把数据和对数据的操作紧密的结合起来的方法。
    本系统开发采用的是传统的方法学即结构化开发方法。原因在于这个开发方法更接近人的常规思维。
    4.4 定义规范代码规范在程序开发过程中是很必要的,一般一个项目都是多个人开发,如果代码不遵循一定的规范,这将给后面阅读和维护你代码的开发人员造成极大的困难,从而浪费不必要的时间。本系统虽然从头到尾都是自己一人开发,但还是要遵循严格代码规范。因为平时开发中注意这些,以后就不会犯这种错误。
    4.4.1 代码注释规范1.源文件注释
    所有头文件开头都要加注释,写明文件创建时间、作者、用途、概述等。格式如下所示:
    /* * 作者:XXX * 功能说明:XXXXXXXXXXXXXXXXX*/… 作者:XXX 功能说明:XXXXXXXXXXXXXXXX…
    2.函数注释
    所有函数一定要注明函数的作用、参数的作用、返回值的作用等。格式如下所示:
    /* 函数的作用:XXXX * @param:XXX * @return:XXX */…函数的作用:XXXX@param:XXX@return:XXX…
    3.常量变量注释
    所有的常量和变量,无论全局或者局部只要在代码中起关键作用的必须都加上注释。格式如下所示:
    // 变量或常量作用或意义# 变量或常亮作用或意义
    4.4.2 命名规范采用驼峰命名法。

    类命名:英文,单词首字母大写,剩余字母小写。在意义上表达该类的作用。函数命名:英文,首字母大写,剩余字母小写。在意义上表达该函数的作用,如:InsertImage,是插入图像。变量命名:英文,首字母小写,在意义上表达该变量保存值得类型。文件命名:英文,首字母小写,在意义表达该文件的类型以及作用。
    4.4 本章小结本章首先从整体的角度分别介绍node.js、python的模块结构,接着就是本章最核心的部分“功能设计与分析”,针对这两部分的每个模块都进行了分析与介绍。对于每个模块中的接口的设计与流程以及主要代码都进行了说明。最后介绍了系统所定义的规范,来约束系统的设计与代码的编写。
    5 系统测试5.1 程序调试软件完成设计之后就进入了用语言实现的阶段,这一阶段主要是靠程序员手工编写,过程中难免会因为各种原因出现语法或者语义上的错误,出现了错误就需要去修正,这个修正的过程称为调试,调试的方法有很多,如观察错误提示、打断点单步执行、肉眼观察等。本系统在调试过程中采用的时候第一种,根据错误提示修正错误。在我看来,每种调试方法各有优缺点,在特定的场合某种方法可能比较优秀。不过由于对语言的不熟悉,所以全程采用一种固定的方法调试。
    5.2 工具的测试5.2.1 测试的目的及意义测试的目的及意义主要有一下几点:

    验证软件是否满足软件开发合同或项目开发计划、系统/子系统设计文档、软件需求规格说明书、软件设计说明书和软件产品说明等规定的软件质量要求。通过测试,发现软件缺陷。为软件产品的质量测量和评价提供依据。
    5.2.2 测试框架1.node.js代码的测试框架
    这部分测试选用的mocha框架。Mocha是一款功能丰富的javascript单元测试框架,它既可以运行在node.js环境中,也可以运行在浏览器环境中。Javascripte是一门单线程语言,最显著的特点就是有很多异步执行。同步代码的测试比较简单,直接判断函数的返回值是否符合预期就行了,而异步的函数,就需要测试框架支持回调了、promise或其他的方式来判断测试结果的正确性。Mocha可以良好的支持javascripte异步单元测试。Mocha会串行地执行我们编写的测试用例,可以在将未捕获异常指向对应用例的同时,保证输出灵活准确的测试结果报告。
    2.python代码的测试框架
    这部分测试选用的是unittest框架,unittest是python内置的标准类库,是其他测试框架、测试工具的基础。对于这部分的单元测试,每个函数作为一个模块,分别测试,根据需要每个接口设计三个测试用例,两正一反来测试接口的正确性。测试代码根据接口管理的对象分为两部分代码来编写。
    5.2.3 测试步骤虽然该系统规模比较小,模块比较少,不过为了熟悉软件测试方法,以及如果系统某些地方发生错误能及很快很方便的定位,系统根据需要在测试阶段是按照单元测试、集成测试、系统测试、验收测试这个顺序进行的。
    首先是单元测试是系统内部测试,该阶段主要是测试各个模块是否满足当初的设计。例如加密模块,主要测试该模块是否能正常加密解密而不出错。
    接着是集成测试,集成测试主要测试模块间以及模块与现有系统接口之间是否能正常协作。
    然后是系统测试,系统测试阶段将所有的模块拼接在一起,根据最初的设计通过软件测试方法验证整个系统是否满足需要。
    最后是验收测试,本系统主要是为其他模块提供数据支持,验收测试的时候主要是其他成员检查当前系统是否满足自己模块的需要。
    5.3 测试用例设计测试的对象分为两部分,一个是python部分,一个是node.js部分,针对这两部分分别设计测试用例,不过由于两部分操作的相似性,测试用例也基本没什么差别。在每个部分项目代码下面有一个test目录,里面存放着该部分的测试代码以及测试所用的数据。
    Node.js部分:

    代码展示:
    // 测试image操作函数的正确性describe("image", function(){ // 测试insertOneImage接口的正确性 describe("#insertOneImage", function(){ // 给出合法的路径和文件名来测试函数的正确性it("insert successfully when path and filename is right", function(done){ mongodb.insertOneImage(__dirname+"\\","1.jpg",function(){},function(){done();}); mongodb.insertOneImage(__dirname + "\\", "2.jpg",function(){}, function(){done();}); }); // 给出不合法的路径或文件名来测试函数的正确性 it("insert failly when path or filename is wrong", function(done){ mongodb.insertOneImage(__dirname + "\\", "4.jpg",function(){ done(); }, function(){}); }); }); // 测试insertImage接口的正确性 describe("#insertImages", function(){ // 所有的路径和文件名都合法来测试接口的正确性 it("insert successfully when path and filename of imags is right", function(done){ var images = new Array(3); images[0] = {"path":__dirname + "\\", "filename" : "1.jpg"}; images[1] = {"path" : __dirname + "\\", "filename" : "2.jpg"}; images[2] = {"path" : __dirname + "\\", "filename" : "3.jpg"}; mongodb.insertImages(images, function(){}, function(){ done(); }); }) });// 存在有路径或文件名不合法来测试接口的正确性 it("insert failly when path and filename of imags is false", function(done){ var images = new Array(3); images[0] = {"path" : __dirname + "\\", "filename" : "1.jpg"}; images[1] = {"path" : __dirname + "\\", "filename" : "4.jpg"}; images[2] = {"path" : __dirname + "\\", "filename" : "3.jpg"}; mongodb.insertImages(images,function(){done();}, function(){});});// 测试imgDelete接口的正确性describe("#imgDelete", function(){ //当给出的条件合法的时候,判断接口的正确性 it("remove successfully when condition is legal", function(done){ mongodb.imgDelete({"filename":"1.jpg"}, function(){}, function(){done();}); });// 当给出的条件不合法的时候,如,不存在这样的属性,// 判断接口的正确性it("remove successfully when condition is legal", function(done){ mongodb.imgDelete({"filename":"10.jpg"}, function(){done();}, function(){}); });}); // 测试imgDelete接口的正确性describe("#imgFind", function(){ //当给出的条件合法的时候,判断接口的正确性 it("find successfully when condition is legal", function(done){ mongodb.imgFind({"filename":"1.jpg"},function(){}, function(){done();}); }); // 当给出的条件不合法的时候,如,不存在这样的属性,// 判断接口的正确性 it("find successfully when condition is legal", function(done){ mongodb.imgFind({"filename":"10.jpg"}, function(){done();}, function(){}); }); });})// 测试xml各接口正确性describe("xml", function(){ // 测试xmlInsert接口,判断其是否正确 describe("#xmlInsert", function(){ //当参数合法时测试接口是否插入成功 it("insert successfully when param is legal", function(done){ mongodb.xmlInsert(__dirname+"\\","test.xml","abc", function(){}, function(){done();});}); // 当参数不合法时测试接口是否插入失败 it("insert failly when param is not legal", function(done){ mongodb.xmlInsert(__dirname+"\\","test.txt","abc", function(){done();}, function(){}); }); }); // 测试xmlDelete接口的正确性 describe("#xmlDelete", function(){ //当参数合法时测试接口是否删除成功 it("delete successfully when param is legal", function(done){ mongodb.xmlDelete("abc", function(){}, function(){done();}); }); // 当参数不合法时测试接口是否删除失败 it("delete failly when param is not legal", function(done){ mongodb.xmlDelete("abcd",function(){done();},function(){}); }); }); //测试xmlFind接口的正确性 describe("#xmlFind", function(){ //当参数合法时测试接口是否查询成功 it("find successfully when param is legal", function(done){ mongodb.xmlFind("abc", function(){}, function(){done();}); }); // 当参数不合法时测试接口是否查询失败 it("find failly when param is not legal", function(done){ mongodb.xmlFind("abcd", function(){done();}, function(){}); }); }); // 测试xmlUpdate接口的正确性 describe("#xmlUpdate ", function(){ //当参数合法时测试接口是否更新成功 it("update successfully when param is legal", function(done){ mongodb.xmlUpdate("abc",__dirname+"\\","test1.xml", function(){}, function(){done();}); }); // 当参数不合法时测试接口是否更新失败 it("update failly when param is not legal", function(done){ mongodb.xmlUpdate("abcd",__dirname+"\\","test1.xml", function(){done();}, function(){}); mongodb.xmlUpdate("abc", __dirname + "\\", "test2.xml", function(){done();}, function(){}); }); });});
    Python部分:

    代码展示:
    # 图像测试类,继承unittest.TestCase类,用于对图像接口的测试class ImageTest(unittest.TestCase): # unittest里特殊函数,每个测试在运行前都会执行 def setUp(self): # 创建Mongodb对象,以备其成员函数的调用 self.mongodb=Mongodb(configs['DB']['HOST'], configs['DB']['PORT'], configs['DB']['NAME']) # unittest里的特殊函数,每个测试在运行前都会执行 def tearDown(self): self.mongodb = None ''' 函数功能:测试InsertImage接口的正确性 无参数,无返回值 '''def test_InsertImage(self): # 给出合法的路径 self.assertEqual(self.mongodb.InsertImage(Image(sys.path[0] + "\\", "1.jpg", 0, "001")), True) # 给出合法的路径 self.assertEqual(self.mongodb.InsertImage(Image(sys.path[0] + "\\", "2.jpg", 0, "001")), True) # 给出不合法的路径 self.assertEqual(self.mongodb.InsertImage(Image(sys.path[0] + "\\", "5.jpg", 0, "001")), False) ''' 函数功能:测试DeleteImage接口的正确性 无参数,无返回值 ''' def test_DeleteImage(self): # 条件合法即存在这样的图像满足条件 self.assertEqual(self.mongodb.DeleteImage({"filename":"1.jpg"}), True) # 条件合法 self.assertEqual(self.mongodb.DeleteImage({"filename":"2.jpg"}), True) # 条件不合法 self.assertEqual(self.mongodb.DeleteImage({"filename":"5.jpg"}), False) ''' 函数功能:测试FindImage接口的正确性 无参数,无返回值 ''' def test_FindImage(self): # 条件合法 self.assertEqual(self.mongodb.FindImage({"filename":"1.jpg"}), True) # 条件合法 self.assertEqual(self.mongodb.FindImage({"filename":"2.jpg"}), True) #条件不合法 self.assertEqual(self.mongodb.FindImage({"filename":"5.jpg"}), False) ''' 函数功能:测试FindImageById接口的正确性 无参数,无返回值 '''def test_FindImageById(self): # id合法 self.assertEqual(self.mongodb.FindImageById("5930da46d3a3660838899992"), True) # id合法 self.assertEqual(self.mongodb.FindImageById("5930dad1d3a36621846fdc6f"), True) # id不合法 self.assertEqual(self.mongodb.FindImageById("5930dad1d3a36321846fdc6f"), False)# 图像测试类,继承unittest.TestCase类,# 用于对病人隐私信息接口的测试class PatientInfoTest(unittest.TestCase): # unittest里特殊函数,每个测试在运行前都会执行 def setUp(self): # 创建Mongodb对象,以备其成员函数的调用 self.mongodb=Mongodb(configs['DB']['HOST'], configs['DB']['PORT'], configs['DB']['NAME']) # unittest里的特殊函数,每个测试在运行前都会执行 def tearDown(self): self.mongodb = None ''' 函数功能:测试InsertPatientInfo接口的正确性 无参数,无返回值 ''' def test_InsertPatientInfo(self): # 给合法的病人隐私信息以及图像id self.assertEqual(self.mongodb.InsertPatientInfo("001", Patient({"patientId": "001"})), True) # 给合法的病人隐私信息以及图像id self.assertEqual(self.mongodb.InsertPatientInfo("002", Patient({"patientId": "002"})), True) ''' 函数功能:测试InsertPatientInfo接口的正确性 无参数,无返回值 ''' def test_FindPatientInfo(self): # 存在满足条件的病人隐私信息 self.assertNotEqual(self.mongodb.FindPatientInfo({"patientId":"001"}), False) # 存在满足条件的病人隐私信息 self.assertNotEqual(self.mongodb.FindPatientInfo({"patientId":"002"}), False) # 不存在 self.assertEqual(self.mongodb.FindPatientInfo({"patientId":"005"}), False) ''' 函数功能:测试InsertPatientInfo接口的正确性 无参数,无返回值 ''' def test_UpdataPatientInfo(self): # 存在满足条件的病人隐私信息 self.assertEqual(self.mongodb.UpdataPatientInfo({"patientId":"001"}, {"patientName":"zhangsan"}), True) # 存在满足条件的病人隐私信息 self.assertEqual(self.mongodb.UpdataPatientInfo({"patientId":"002"}, {"patientName":"lisi"}), True) # 不存在 self.assertEqual(self.mongodb.UpdataPatientInfo({"patientId":"006"},{"patientName":"wangwu"}), False) ''' 函数功能:测试InsertPatientInfo接口的正确性 无参数,无返回值 ''' def test_DeletePatientInfo(self): # 存在满足条件的病人隐私信息 self.assertEqual(self.mongodb.DeletePatientInfo({"patientId":"001"}), True) # 存在满足条件的病人隐私信息 self.assertEqual(self.mongodb.DeletePatientInfo({"patientId":"002"}), True) # 不存在 self.assertEqual(self.mongodb.DeletePatientInfo({"patientId":"007"}), False)
    5.4 测试数据在每一个功能完成之后都会进行分模块的功能测试,以确保这个功能模块的正常运行,同时也是为了方便之后功能的整合。通过选择某些具有代表性的数据和某些边界数据来测试可以很好的测试程序的健壮性,以保证程序的良好运行。下表是本系统中选择某些主要功能所做的测试以及出现的问题和解决方案,以及应用此解决方案后产生的效果。



    功能模块
    测试数据
    测试出现过的问题
    解决方法
    结果




    插入模块
    合法和非法的图像或者xml的路径
    插入失败
    根据错误和异常修改代码
    接口正常


    查找模块
    合法和非法的查询条件


    接口正常


    删除模块
    合法和非法的删除条件
    删除失败
    根据错误和异常修改代码
    接口正常


    更新模块
    合法和非法的被更新的限制条件以及更新目标
    抛出异常
    根据错误和异常修改代码
    接口正常



    5.5 本章小结本章详细介绍了系统的测试,首先从调试的方法以及意义的角度介绍了程序的调试。然后介绍了测试的目的以及意义,紧接着又介绍了mocha、unittest两种单元测试框架。后面又具体详述了系统的基于这两种框架测试代码的编写,以及系统所采用的测试数据。
    6 结 论“医疗图像库在线存储与检索平台的设计与实现”作为我的毕业设计题目即将接近尾声,伴随它的也有我的大学生涯,在这里将它比作大学,我觉得挺恰当的,因为,他们都给予了我很多的知识。当然,这仅仅知识一方面,还有很多的相似性,剩下的自己体会。
    这个题目,最初理解是做一个可以提供查询、上传操作类似于网站那样的平台。其实不然,简单来说,其实是要实现一个集合,一个可以提供其他模块增、删、改、查操作的接口集合。
    这个系统在开发过程中采用了pymongo、mongodb、gridfs等几种包,这些包都是python、node.js官方所提供的包,其实还有其他的包可供使用,之所以选择了前者,是因为前者有完整的文档资料以及用户使用案例,这些都在我开发过程中起着很大的作用。
    该系统根据语言的不同被分为两个部分,分别进行开发,两部分的模块划分基本是一样的。主要包含图像管理、系统配置模块、框架配置文件管理模块、病人隐私信息管理模块以及加密模块。在实现上图像管理模块花了比较长的时间,特别是用node.js这样的异步语言去实现。
    整个系统在开发过程中都是我一个人负责的,包括最初的需求调查以及后面的设计、实现、测试。系统所采用的技术没有一样是之前接触过的,准确点来说node.js因为采用的是js语法还稍微能熟悉点,不过也就一点。所以最大的收获就是提高了自己学习陌生的东西开发系统的能力,这一点在程序员这个行业是很有必要的。
    毕业设计这次机会是好的,学校基于了充足的时间去做这个东西,本来想着做的更好,但由于特殊的原因耽误了很多的时间,在这一点上是比较遗憾的。以后,针对每次开发或者写代码我都会加倍努力,原因只有一点,要对的起自己的兴趣。
    7 参考文献[1] 王金龙, 宋斌, 丁锐. Node. js: 一种新的 Web 应用构建技术[J]. 现代电子技术,2015, 38(6): 70-73.
    [2] 谢华成, 马学文. MongoDB 数据库下文件型数据存储研究[J]. 软件, 2015 (11):12-14.
    [3] 牛倩. MongoDB 数据库中自动分片技术应用研究[J]. 数字技术与应用, 2016(6):112-112.
    [4] 李鹏. Node. js 及 MongoDb 应用研究[J]. 天津科技, 2015 (2015年 06): 34-36,39.
    [5] Åkesson, Anders, and KennethLewenhagen. “Node. js in Open Source projects on Github: A literaturestudy and exploratory case study.” (2015).
    [6] 骆文亮. Node. js 服务器技术初探[J]. 无线互联科技, 2014,3: 178.
    [7] 高飞, 何利力, and 高金标. “基于 Node. JS 内存缓存的 Web 服务性能研究.”工业控制计算机 11 (2015): 047.
    [8] 王金龙, 宋斌, and 丁锐. “Node. js: 一种新的 Web 应用构建技术.” 现代电子技术 38.6 (2015): 70-73.
    1 评论 3 下载 2018-10-03 22:17:10 下载需要12点积分
  • 基于Node.js的医药搜索平台网站设计与实现

    摘 要随着科技的快速发展, 越来越多的医药公司积攒了大量的医药文档。这些文档资源如何高效、快速的被管理员管理,以及被用户检索,如何统一的实现资源管理与资源开放,成为了当下医药公司待解决的问题。
    普通的、零散的、单一的文档管理方式已经不能满足企业的要求,企业需要的是综合、协同、集成化的文档搜索解决方案。构建基于Node.js的医药搜索平台,实现对文档的添加、删除、编辑等常见的操作,以及对大规模数据的快速检索匹配,用户对文档的查看权限等功能。为了解决如上问题,需要使用文档管理系统来对文档进行管理,如果文档是面向用户的还需要添加用户管理系统,权限系统等相应的辅助系统。
    建立文档管理系统的目的就是要实现对文档的集中存储和管理,从而可以很好地保证文档的存储安全,提高文档的安全访问级别,很好地实现文档的分发、查询、共享,提高企业的文档管理和使用效率。在Internet环境下,我们设计的新型文档管理系统的体系结构采用B/S结构。本地用户可以通过企业内部网络直接进入文档管理系统,当然也可以进入企业的其他的业务系统。移动办公用户以及企业的客户可以通过门户站点访问到本系统,分支机构以及企业的合作伙伴可以通过Web服务方式建立与系统的连接。
    本文最终实现一个基于Node.js的医药文档搜索平台,其中包括用户管理模块、文档管理模块、系统设置模块等多个子模块。并利用Sphinx和MYSQL实现了具有分词功能的多语言文档搜索引擎。前端利用Angular实现了Single Page Application。本系统现在已经在阿里云服务器上部署应用,并上线。
    关键词:Node.js,Sphinx,Angular.js,ORM
    ABSTRACTWith the rapid development of technology, more and more pharmaceutical companies accumulate a large number of medical documents. These documents how resource efficient, rapid management by an administrator, and retrieved by the user, how to achieve a unified resource management and resource opened as the current problems to be solved pharmaceutical companies.
    Ordinary,fragmented, single document management methods can not meet the requirements of enterprises, enterprises need a comprehensive, coordinated, integrated document search solutions. Construction Node.js based medical search platform, to add to the document, delete, edit and other common operations, and rapid retrieval of large-scale data matching the user permission to view the document on Zen and other functions. In order to solve the above problem, we need to use document management system to manage the document, if the document is a user-oriented management systems also need to add a user, system privileges and other appropriate assistance systems.
    The purpose of establishing the document management system is to achieve the centralized document storage and management, which can ensure the safe storage of documents, improve security access level of the document, to achieve a good distribution of the document, query, share, and improving document management and efficiency. In the Internet environment, the architecture we designed a new document management system using B / S structure. Local users can go directly through the corporate intranet document management system, of course, can also enter other business systems business. Mobile office users and corporate customers can access the system through the portal, branch offices and business partner scan establish a connection with the system through a Web service.
    In this paper, the ultimate realization of a medical document search based Node.js platform,including user management module, document management module, system settings module multiple sub-modules. And using Sphinx and have realized the word MYSQL function multi-language document search engine. Angular use front-end to achieve a Single Page Application. The system is now deployed on the application server Ali cloud and on-line.
    Key words: Node.js, Sphinx, Angular.js, ORM
    1 绪论1.1 课题来源及研究背景本课题源于实际生产,目的是为西安泰科迈医药科技开发医药文档管理系统来优化现有的文档管理系统。
    原有的文档管理方式为手工编辑Excel,利用目录来对文档进行分类和查询。当需要修改一个文档的时候,需根据目录结构进行检索,逐目录的查找文档,然后编辑后保存。
    但这样的查询有一定的局限性,如只能根据文件的名称进行字典排序,或者根据文档的关键词来进行分类。
    无论哪种的方法都不适合大量的文档管理,如当文档的名称被修改后,需要根据名称进行重新的排序,或者一个文档的关键词被修改后,需要根据新的关键词来重新分类,或者当一个文档有多个关键词的时候,分类方式变的极其复杂。
    基于上述的缺点和不足,我们将实现一个医药文档搜索和管理平台。利用程序来实现常见的文档操作,比如文档的上传、下载、修改、搜索、查看等,因为文档是面向用户的,用户分为游客、普通用户、管理员,每个用户对文档都有不同的操作权限,所以我们还需要实现角色权限系统。
    1.2 技术栈的选择系统需要分别实现前端和后端。
    在后端框架上,我们有很多选择,比如PHP下的Yii、ThinkPhP等框架都很流行,Java下也有 Spring、Play 等框架,还有近两年比较火的Node.js。在本系统中我选择了使用Node.js来搭建后端系统,具体有如下原因。

    由于常见的操作,均为I/O 型操作,所以使用异步非阻塞I/O模型和事件编程可以提高系统的吞吐量。相比于PHP或者Java,Node.js天生就支持异步非阻塞I/O模型和事件编程。所以这里Node.js 更加合适。本系统将运行在单核服务器上,考虑到系统的资源有限,由于Node.js为单线程的,所以很适合在单核CUP上跑,相比于PHP或者Java, Node.js由于不需要频繁的线程切换,加上内置的Event Loop机制[1],所以Node.js在此环境下更加快。考虑到是一个人开发前后端,使用JS全栈开发效率更高。
    前端的开发上,我选择使用Angular.js 来进行SAP开发,由于我们开发的是医药文档搜素和管理平台,并不需要支持搜素引擎友好,并且前端逻辑页面逻辑比较复杂,所以选择Angular.js来进行单页面Webapp 开发。
    总上所述我最终选择了使用Mean栈来进行开发,Mean栈包括MySQL、Express、Angular.js 、Node.js。
    1.3 论文主要工作内容为了实现本系统,我们首先的从实现数据层开始,提供良好的数据层接口,可以保证我们后期实现Model层时更加的快速和稳定。
    接下来我们要基于数据层实现用户管理模块和角色权限模块的接口,因为网站是面向用户的,所以基本上所有功能都依赖于用户。将用户模块和角色权限模块放在第二步实现,会保证我们后期的其他模块实现起来更加方便。
    然后我们就可以建立文档模块,文档模块主要有文档的上传,修改,删除,搜索等功能。在第四章中,我们会详细介绍到如何将原始数据Excel解析并存储到MySQL中,以及如何从MySQL中将数据还原到Excel中。还有文档项权限的设置等技术。
    文档的搜索,我是基于Sphinx进行开发的,所以在第五章我会详细的讲解如何配置和使用Sphinx来提供多语言全文搜索功能。
    当我们将后端服务器全部构建完毕后,我们便可以编写前端代码。前端我是基于Angular.js 构建的SPA 应用。我会在第6章介绍利用Angular实现本系统前端界面的核心代码。
    2 系统数据层的设计与实现2.1 基于ORM实现的ActiveRecord在MVC的开发模式中,我们通常需要一个model层用于对数据的抽象。
    ActiveRecord是一种领域模型模式,特定是一个模型对应关系型数据中的一个表,而模型类的一个实例对应表中的一个记录。在数据库中,不同的表之间往往通过外键来关联。ActiveRecord中通过对象的关联和聚集来实现这种关系映射。
    这样做的好处是我们可以将数据抽象为对象。从而更加直观和方便的进行数据操作,也方便后期的维护。
    在node的中Express框架中并没有实现数据层,所以我么需要借助其它工具来实现本系统中的ActiveRecord。
    2.2 用Bookshelf.js来构建数据层基类Bookshelf.js[4]是Node.js中的一个ORM框架。建立在 KnexSql生成器上。同时实现了Promise以及传统的Callback调用方式。并支持transaction、一对一关系、一对多关系、多对多等数据映射关系。它可以很好的与PostgreSQL、MySQL、以及SQLite工作。
    Bookshelf.js基于knex.js开发,所以关于数据库的链接需要使用Knex来进行操作。
    Knex 数据库的链接代码如下:
    var knex = require(‘knex’)({ client: ‘mysql’, // 数据库类型 connection: config.mysqldb // mysqldb中包含数据库的地址,帐号和密码等信息});
    接着在Bookshelf的初始化中加入之前创建的Knex对象便可完成基类ActiveRecord的创建, 实例代码如下:
    varmarkBookshelf = require(‘bookshelf’)(kenx);markBookshelf.Model= markBookshelf.Model.extend({ // 在此添加私有方法}, { // 在此添加静态方法});
    由代码我们可以看出,我们将之前创建好的knex对象交由bookshelf工厂函数,生产一个markBookshelf基类,之后我们可以通过使用markBookshelf.Model的extend方法扩展Model。extend方法接受两个参数,第一个参数对象中的所有方法和值会变为Model的私有方法,第二个参数中的所有对象和值会变成Model中的静态方法。之后我们在建立更多得model的时候只需要继承markBookShelf基类便可以。
    2.3 示例:构建用户 Model目前系统数据层的models如图2-1所示:

    有了上面的markBookshelf基类后,我们便很容易生成一个数据模型,我们这里将建立一个user model,代码如下所示:
    var User = markBookshelf.Model.extend({ table: ‘User’, // 和数据库中的user表名相对应 initialize: function() { /* 实例初始化的时候会调用 */ }, toJson: function() { /* 序列化对象 */ } // 我们可以根据需求天假更多的私有方法或静态方法});var user = new User({id: 1});user.set({username: ‘markstock’});user.save();
    我们可以很简单的就创建一个user model,后续对user表的操作不必在写SQL语句进行操作,只需要实例化一个User对象便可完成所需的操作。
    3 角色权限系统与用户管理模块的实现3.1 基于角色的访问控制基于角色的访问控制[6] (RBAC, Role-Based Access Control) 有效的克服了传统访问控制技术中的不足之处,是当今广泛流行的访问控制技术模型之一。
    在角色访问控制中引入了角色这一个概念[7]。它的基本思想是将访问的权限分给不同的角色,在将角色划分给不同的用户。用户的每个操作的权限检查,其实是通过用户所拥有的角色是否拥有该权限来确定的。角色的不同,访问权限也不同。这样整个访问控制工作分为两个部分,即访问权限与角色的关联,角色与用户的关联。从而实现了用户与访问权限的逻辑分离。
    在角色访问控制系统中,每个用户的权限都不可自主定义,权限仅受限于用户所拥有的角色,一个用户只能同一时间拥有一个角色。拥有同一角色的用户拥有同样的权限。正因为角色访问控制系统有这样的限制,所以它不适合用来设计复杂的权限系统。
    3.2 权限控制权限控制可以简单的描述为WWH操作[8],即“Who 对 What 进行了How的操作”的逻辑表达式是否为真的检验。它的基本任务是防止一个合法用户对系统资源的非法访问和使用。它可以约束一个用户在系统中可进行的操作。在本系统中,我们将实现一个canI的函数来对WWH进行检验。如果用户没有权限便返回422提示用户没有权限进行当前的操作。
    3.3 用户动态权限数据表的设计
    如图3-1所示,要构建角色权限访问控制,我们至少需要5个表。
    Table permission 我们用来定义所有的权限,它包括权限的类型和具体的权限。部分数据如下表所示:

    如其中的第一行所示,user为what,browse为how,即验证用户是否有权对user进行browse操作。
    Table roles 用来定义我们的角色, 本系统目前有三个角色。

    因为permissions和roles 为n对n的关系,roles和users为n对1的关系,所有我们需要permissions_roles 和roles_users两个中间表来纪录关系。
    3.4 用户权限动态分配功能实现因为我们是使用ActiveRecored来做数据库的表和对象的映射,所以我们可以将角色权限系统中的表根据我们在第一章创建的ActiveRecord基类来创建模型类。我们可以使用如下代码创建Permission model。
    var Permission = markBookshelf.Model.extend({ tableName: ‘permission’, roles: function roles() { return this.belongsToMany(‘Role’); }});
    如上述代码所示,我们创建了一个Permission类,在bookshelf.js中,当有Many to Many 或者 One to Many的表间关系时[10],我们并不需要生成中间表的类,比如roles_users表和permissions_roles表,我们只需在Permission中指出需要关联的表,比如上述代码有一个roles方法,其中调用了belongsToMany(‘Role’)说明Permission表和Roles表是多对多的关系。
    根据如上我们可以创建Roles类。因为roles表和permission表为多对多的关系,roles表和users表为多对一的关系。所以我们需要在Role的模型中加入users和permissions方法。
    var Role = markBookshelf.Model.extend({ tableName: ‘roles’, users: function users() { return this.belongsToMany(‘User’); }, permissions: function permissions() { return this.belongsToMany(‘Permission’) }});
    在系统启动的时候我们需要将所有的permission从数据库查出,并加载进内存,这样每次查询权限的时候我们便不需要查询数据库,只需要从内存中读出permission信息便可以,实现代码如下。
    var actionMap = {};var init = function() { return models.Permission.findAll().then(function(perms) { _.each(perms.models, function(perms) { var actionType = perm.get(‘action_type’), objectType = perm.get(‘object_type’); actionMap[actionType] = actionsMap[actionType] || []; }); });};
    models.Permission.findAll为查找出所有permission表中的数据,之后的then为promise风格的异步调用,当findAll成功调用后,调用then中的回调函数。
    每次需要检查用户是否有相应的操作权限时,我们需要根据用户的user_id来查找其的role然后根据role来判断其是否有相应的操作权限,实现的伪代码如下所示。
    canI(obj_type, action_type, user_id) { var user = Get user from User Class with user_id; var role = user.getRole(); return check if user with role can do this action_type;}
    3.5 用户管理模块的实现
    有了前面的数据模型和方法我们可以很方便的创建出用户的增删改除等功能。这里以实现后台管理员查看用户列表为例。管理员可以在后台管理页面中查看所有的用户列表,并以分页的形式展现结果。
    如下函数将会根据提供的搜素请求获取用户的分页数据信息。
    function doQuery(options) { models.User.findPage(options); }
    options中包含了当前页page,以及每页显示的数量num。查找的原理是基于如下的SQL语句(伪代码)的来实现的:
    SELECT * FROM user limit page * (num - 1), num;
    用户管理界面如图3-4和图3-5所示。

    如图为后台管理页面中的用户管理页面,管理员可以根据用户的邮箱、昵称、或者角色查询用户,管理员可以修改其他用户的信息,或者删除其他非管理人员。
    对于前台页面在实现了用户模块后,我们可以实现登陆,注册,用户账号设置等功能。



    用户登陆窗口
    用户注册窗口









    4 文档的存储与文档项权限的设计4.1 文档管理功能描述原有的文档是通过Excel进行存储,通过目录来进行分类。因为要建立搜索功能,我们取消分类的功能,用户可以通过关键词来搜索到相关的文档。
    管理员可以通过后台来编辑文档、查看文档、搜索文档、以及重新上传文档。
    对于文档的搜索功能,我们需要实现可以进行中文搜索,英文搜索,以及中英文分词搜索的功能,而且我们还需要可以查出所有文档的接口来供管理员使用。对于搜索的结果我们要做分页处理,用户可以选择每页显示20项或者40项结果,默认以20条方式呈现。
    每个文档的每一项我们称为一个文档项。文档项有多种存储类型,具体如下所示:
    Image
    此类型用来标记当前项目用来存储图片,每个文档都可以存储多张药品图片,来供展示。管理员需要可以上传和删除图片。
    Text
    纯文本类型,如药品的介绍、规格、生成常见等都是纯文本存储。
    Text + En
    对于药品的通用名、商品名均有中英文两种表示方式,所以此项用来分别存储中文和英文信息。
    Download
    有些项目可以用来存储其它文档资料,比如PDF,或者word等格式的文件来供用户下载,所以此项用来存储其它文件。
    Download + Text
    和Download项类似,都可以用来存储其它文件,但是此项还可以加上下载说明等额外的信息。
    每个文档项都有一定的查看权限,只有满足权限的用户才可以产看或下载,所以我们必须要存储每个文档项的权限,并且管理员可以更改。
    目前文档项的权限分为如下三类:

    游客可以查看或下载通用户可以查看或下载VIP用户可以查看或下载
    4.2 面向文档动态权限的数据格式数据均需要从用户上传的Excel文档进行解析,所以为了解析方便,我们对原始文档的格式进行了定义,数据格式如图4-1所示。

    对于Text 和 Text + En的类型其值直接填写进中文值和英文值中即可。
    对于Image 和 Download类型只填写名称就好,后续的文件通过后台管理平台在进行添加。
    Download + Text中的Text部分填写进中文值就好。
    因为每个文档项目均有查看和下载权限,目前有四种用户类型Guest、User、
    VIP、Administrator。所以我们赋予每中用户一定的权限值,如下所示:
    Guest = 1; User = 10; VIP = 20; Administrator = 40;每个角色只能查看或下载小于其值的文档项,比如当前用户为VIP, 则它可以查看权限值等于1、10、20的文档项。
    对于数据库中的存储格式,我们选择利用JSON字符串进行存储,针对每种不同的文档项,都有不同的JSON数据格式。
    Image格式
    我们将图片的具体存储地址,存储在image数组中,每个图片用逗号分隔。当我们需要添加新的图片的时候,只要将图片上传到服务器上,然后将地址插入进image数组中便可。
    { “name”: “产品图片”, “image”: [], “value”: “”}
    Text
    对于Text类型的数据,我们只需要将值存进value中即可。
    { “name” : “化学名”, “role: “1”, “value”: “ 2,6-二甲基-4-( 2-硝基苯基)-1,4-二氢-3,5-吡啶二甲酸二甲酯”}
    }
    Text+ En
    对于 Text + En类型的值,我们分别存储为zhValue 和 enValue。
    { “role”: “1”, “name”: “商品名(中英文)”, “zhValue”: “肠虫清”, “enValue”: “Eskazole”}
    Download
    { “file”: { “path”: ‘path/to/the/file/on/server”, “filename”: “file name”, “fid”: “the identify of the file” }, “role”: “1” “name”: “质量标准JP”}
    对于文件的数据格式,我们将文件的存储路径存储在file.path中,fid(唯一文件标识符)用来做下载验证,避免用户非法批量下载。
    4.3 文档动态权限数据表的设计为了实现文档项可扩展的文档管理系统,我们需要两张表,一个是文档项表 Attributes , 一个是Documents表。
    因为每个文档的文档表项都大致相同,所以文档项表用来纪录所有类型的文档项。文档项表的用途,只在解析原始文档和生成新文档的时候用来保证信息的一致性。
    文档项表的设计如图4-2所示:

    attr_name 为文档项的名称,每个原始文档的每一项的名称都得与attr_name相对应,才可以被识别,否则会被忽略。Alias与documents表中得文档项别名一一对应,type为当前文档项得类型。
    部分文档项表数据如图4-3所示:

    documents表比较简单,只包括id和各个文档项得值,部分column如图4-4所示:

    4.4 功能设计及分析4.4.1 元文档的上传与解析搭建文档管理系统的第一步就是上传文档,解析原始excel数据,存储进数据库。为了更加方便的解析excel文档,我们使用node-excel插件,它会将excel解析为JSON对象。
    var filepath = req.files.file.path;try { obj = nodexlsx.parse(filepath);} catch { console.error(e);}
    我们只需将上传文件的路径地址传送给nodexlsx的parse方法,便可获取到json对象,如果失败则上传的文件无法被识别,可能不是excel文件。
    解析出原始数据,我们便可以去格式化数据,然后存储进数据库了。
    _.each(xlsxData, function(rowData) { if (rowData[0] === attr.get(‘attr_name’)) { if (attr.get(‘type’) === ‘text’ || attr.get(‘type’) === ‘download+text’) { obj.value = rowData[1]; } else if (attr.get(‘type’) === ‘text+en’) { obj.zhValue = rowData[1]; obj.enValue = rowData[2] } } insertData[attr.get(‘alias’)] = JSON.stringify(obj);});
    如代码所示,其中的xlsxData及为解析出的原始excel数据,attr为一个文档项,我们一次遍历所有的原始数据去匹配文档项,然后格式化数据。
    当所有的文档项都处理完后,我们既得到格式化后的数据,然后只要调用Document类的add方法就可以存储进数据库了。
    return models.IDocument.add(insertData, options);

    当文件的大小超过了5Mb的时候就会提升异常文件,当文件的格式不正确,无法正常解析的时候就会提示错误的格式。
    4.4.2 元文档的编辑当我们需要编辑文档的时候,我们需要先将文档项中的JSON字符串解析为对象,然后修改对象中的值,在将对象stringify化存储起来。其核心代码如下:
    attrs = attrs.models;_.each(attrs, function(attr) { if (options.data.idocuments[0][attr.get(‘alias’)]) options.data.idocuments[0][attr.get(‘alias’)] = JSON.stringify(options.data.idocuments[0][attr.get(‘alias’)]);});
    我们将修改后的对象重新stringify化后便可存储。

    4.4.3 Excel格式元文档的下载文档的下载可以看成为上传的逆过程,上传是从excel将数据解析到数据库中,下载则是将数据库中的数据解析为excel文档。因为我们在将excel中的数据解析到数据库中的时候,只解析了类型为Text 和 Text + En类型的值,但是在将数据库中的数据解析到Excel中时,我们必须要考虑Download 和 Image以及Download + Text类型的值。
    因为Excel中不能存储文件,以及node-excel不能将图片添加进excel文件中,我们将文件和图片的地址添加进Excel中。
    首先我们从数据库中获取所有的数据,并解析为数组,代码如下:
    _.each(iattributes, function(iattribute) {try { // 尝试去解析 json stringidocument.set(iattribute.get(‘alias’), JSON.parse(idocument.get(iattribute.get(‘alias’)))); if (iattribute.get(‘type’) === ‘text’) { // 填充text类型的数据 } else if (iattribute.get(‘type’) === ‘text+en’) { // 填充text + en类型的数据 } else if (iattribute.get(‘type’) === ‘download’) { // 填充download类型的数据 } else if (iattribute.get(‘type’) === ‘download+text’) { // 填充download+text类型的数据 } else if (iattribute.get(‘type’) === ‘image’) { // 填充image类型的数据 }} catch (e) { xlsxData.push([iattribute.get(‘attr_name’), ‘’]);}});
    当我们解析出数据xlsxData时,我们便可以生成excel文件,但是在发送给浏览器的时候,我们必须要设置response header, 以及将生成的excel转变为字节流传送给浏览器。具体代码如下所示:
    var wb = new Workbook(), ws = sheet_from_array_of_array(xlsxData), ws_name = ‘SheetJS’;wb.SheetName.push(ws_name);wb.Sheets[ws_name] = ws;// 生成excelvar wbout = nodeXlsx.write(wb, {bookType: ‘xlsx’,bookSST: true,type: ‘binary});// 设置 excel文件 response headerres.setHeader(‘Content-disposition’, ‘attachment; filename=’ + filename);res.setHeader(‘Content-type’,‘application/vnd.openxmlformats-officedocument.spredsheetml.sheet’);// 将文档数据转换为字节流传给浏览器res.send(new Buffer(s2ab(wbout)));
    5 基于Sphinx的检索子系统5.1 使用Sphinx和MySQL实现多国语言全文搜索如过只是依赖MySQL的like语句来实现全文搜索,那么局限性将很大,首先不能实现分词功能,比如我们以阿莫西林为关键词来进行搜索,如果使用SQL语句我们可能就要写成like ‘%阿莫西林%’ 这样。但这样我们只能查到是否有阿莫西林整个词出现在内容中,而不能匹配到“阿”,“阿莫”,“阿莫西”等词的匹配情况。
    可能“阿”,“阿莫”,“阿莫西”这样的词并不会匹配到任何结果,看不出分词的作用性。但如果用户想根据两个关键词来搜索文档,那么分词的作用性就会体现出来。
    分词系统首先会对输入进行分词,然后根据每个分词在文档中的匹配情况,计算匹配权值,结果会根据权值的大小排序进行返回。
    Sphinx是一个基于SQL的全文搜索引擎。它会从MySQL数据库或者PostgreSQL中读取数据,然后建立索引。
    首先我们要配置数据源具体代码如下:
    source medical { type = mysql sql_host = localhost sql_user = mysql-user sql_pass = mysql-user-password sql_db = med_dev sql_port = 3306 sql_query = SELECT id, kxm, spm, type, cpsy, jx, gg, sccj ,yyycg FROM documents sql_query_pre = SET NAMES utf8}
    我们在配置信息中配置了MySQL的链接信息,以及获取数据的SQL语句。在启动Sphinx后,sphinx会自动链接MySQL获取数据,然后建立索引。
    建立索引的代码如下,我们设置了最小分词长度为1,然后支持中文分词。
    Index medical { source = medical path = /var/lib/sphinxsearch/data/medical ngram_len = 1 ngram_chars = U+4E00...U+9FBB, U+3400..U+4D85 …….}
    要运行Sphinx需要两步,第一步是生成索引,第二部是启动sphinx服务器。

    建立索引:/usr/local/sphinx/bin/indexer–config /usr/local/sphinx/etc/sphinx.conf 启动服务:/usr/local/sphinx/bin/sphinx start
    5.2 文档搜索接口的实现Sphinx实现了node API 插件sphinxapi16],所以我们可以很轻松的使用node.js访问sphinx服务器。
    要连接Sphinx服务器我们首先要创建Sphinx客户端实例,具体代码如下:
    var SphinxClient = require(‘sphinxapi’), config = require(‘../config’), cl;cl = new ShpinxClient();cl.setServer(config.sphinx.host, config.sphinx.port);cl.setMatchMode(SphinxClient.SPH_MATCH_EXTENED2);
    我们使用SphinxClient创建了一个客户端实例,然后设置了sphinx服务器的地址和端口。有了创建好的实例,我们便可以通过指定的索引来进行搜索了。
    Sphinx.QueryAsync(keyword, ‘medical’).then(function(ret) { … });
    QueryAsync接收两参数,第一个为要搜索的关键词,第二个为要查找的索引,如果查找成功会调用then中的回调函数。ret中会有两个主要值,一个是ret.total_found 为查找到的总结果数,一个是ret.matches 为匹配到的结果集。
    但我们从sphinx中查找出的只是匹配的id集合,我们还需要通过id集合来从MySQL中获取真正的数据,其核心代码如下。
    _.forEach(ret.matches, function(match) { ids.unshift(‘id’ + match.id);});if (ids.length) { options.filter = ids.join(); return models.IDocument.findPage(options);} else { return Promise.resolve({ documents: [] });}
    首先我们从sphinx中查处的结果中获取所有的id,然后将id拼接为字符串如“id1id2 id3 …”, 如果ret_matches为空则直接返回空数组。
    5.3 前台搜索结果分页显示有了前面设计的接口我们便可以实现搜索RESTFUL API,如图5-1所示

    如图5-1所示我们通过浏览器发送GET请求,RequestUrl为/api/documents。为了可以获取搜索结果,我们需要添加几个请求参数。

    如图5-2所示,其中keyword为我们要搜索的关键词,因为关键词可能包含非法字符,所有在我们发送请求前,我们应该利用encodeUrlComponent函数对参数进行解析,limit为每页显示的结果数,系统默认为20个,可选数量为40个。page为当前的页数。
    访问如上的API我们便可以获取搜索结果,服务端会返回如下结果。

    其中documents为所搜索到的文档结果,meta为搜索结果附加数据,其中包括当前关键词可搜索到的结果的总数total,以及当前的页数,以及每页显示结果数。有了这些数据我们便可以编写前台分页视图。
    在前台页面中,我们只需在输入框中输入关键词,并选择当前页数便可以显示所有搜索结果,效果图如图5-4和图5-5所示。

    6 基于Angular.js实现前台SPA6.1 文档检索平台的 UX 与 SPA所谓单页面应用,指的是在一个页面上集成多种功能,甚至整个系统就只有一个页面,所有的业务功能都是它的子模块,通过特定的方式挂接到主界面上。它是AJAX技术的进一步升华,把AJAX的无刷新机制发挥到极致,因此能造就与桌面程序媲美的流畅用户体验[16]。但单页面应用也有缺点,那就是搜索引擎不友好性。搜索引擎利用爬虫来爬取页面,但是并不能解析页面中的JS代码。单页面中的页面渲染,数据的获取,以及路由全交由前端JS代码来执行,所以搜索引擎并不能收藏到网页的所有页面。
    但本系统是面向内部人士进行开放,所以并不需要支持搜索引擎友好性,所以才用SPA可以有效提高UX。
    6.2 初始化 Angular 项目Angular 有着诸多特性,最为核心的是:MVVM、模块化、自动化双向数据绑定、语义化标签、依赖注入等等。
    要使用Angular 我们必须先初始化项目,网站的前端分为两个部分一个是front端,供普通用户使用;一个是admin端,供管理员使用;
    因为Angular.js为模块化开发,所以我们设定根模块为“Medical”,front端和admin端分别继承根模块进行开发,根据路由的地址不同,加载不同的JS文件,从而有效的减少了资源的传输。路由代码如下所示:
    // 加载Admin页面app.route(‘/medi_admin_panel/*’).get(core.rednerAadminIndex);// 加载Front页面app.route(‘/*’).get(core.renderIndex);
    对于子模块的划分,如图6-1所示:

    接下来我们主要以front端为主来讲解如何使用Angular.js。Angular.js一大特点就是依赖注入,依赖注入就是一个模块要使用另外一个模块功能的时候,只要在注册的时候,将另一个模块注入进来即可。
    Medical模块为我们的根模块,它主要是用来加载一个Angular.js的核心模块,这样其自模块便可以直接使用。
    Medical的配置代码如下所示:
    var applicationModuleName = 'Medical';var applicationModuleVendorDependencies = ['ngResource', 'ngAnimate', 'ngMessages', 'ngSanitize', 'ui.router', 'ui.utils', 'ui.bootstrap', 'ngFileUpload', 'ngImgCrop', 'ngStorage'];// Add a new vertical modulevar registerModule = function(moduleName, dependencies) {// Create angular module angular.module(moduleName, dependencies || []); // Add the module to the AngularJS configuration file angular.module(applicationModuleName).requires.push(moduleName); };
    我们在applicationModuleVenderDependencis中声明了要依赖的模块。然后利用registerModule方法注册根模块即可。
    6.3 SPA路由前置因为我们搭建的是SPA应用,所以我们的路由器要定义在前端,使用angular可以很方便定义路由,比如我们需要定义documents的相关路由,我们创建一个documents.routes模块即可,用documents.routes来管理与documents相关的路由。
    在front端,我们需要定义三个关于documents的路由。

    路由一:/documents/search?*keyword路由二:/documents/view/:document-Id路由三:/documents/not-found
    路由一为当我们搜索一个文档时,我们要提供一个关键词,当有结果返回时,我们就用文档列表展示结果,例如我们搜索阿莫西林就可以访问路由http://drugago.com/documents/search/阿莫西林。当我们需要查看编号为1的文档的具体内容时,我们便可以访问http://drugago.com/documents/view/1。如果一个文档没有找到时,就会跳转到/documents/not-found。
    定于document路由的部分代码如下(我们只定义来搜索的路由,其他路由省略)。
    Angular.module(‘documents.routes’).config([‘$stateProvider’, function($stateProvider) { $stateProvider.state(‘documents-search’, { url: ‘/documents/search?*keyword’, templateUrl: ‘modules/documents/client/views/document-search.client.view.html’, controller: ‘SearchDocumentsController’ }); // 添加更多的路由});
    6.4 Service的实现与数据预加载Angular.js有一个factory函数,我们可以用它来包装resource服务来生成据源Service,当我们需要利用Ajax动态获取数据的时候,我们可以直接调用resource 的中相应的http方法。
    常见的Http方法有Get、Post、Put、Delete等。
    定义资源Service的核心代码如下,这里我们只定义了document的Service。
    angular.module('core').factory('iDocuments', ['$resource',function($resource) {var url = '/api/documents/:id'; return (function() { var defaults = { update: {method: 'PUT'}, create: {method: 'POST'}, query: {method: 'GET', isArray: false}, destory: {method: 'DELETE'}, get: {method:'GET'} }; return $resource(url, {id: '@id'}, defaults); }());}]);
    单页面应用有一个常见的缺点就是当页面加载的时候,是先加载JS文件和模版,然后在去加载数据,但是当数据还没有返回的时候,浏览器就已经改变了路由,此时会显示空白页面,当Ajax数据返回后,内容突然一下全显示出来,这在前端叫闪屏。解决闪屏的方法就是数据预加载,当我们在改变路由之前,先去加载数据,当数据返回后,浏览器才改变路由。从而解决了闪屏的问题,提高了用户友好性。
    比如我们在查看一个文档时候,我们先去预加载文档的内容,然后在做页面跳转。要实现数据预加载我们需要在定义路由的时候添加resolve方法。
    .state('admin.documents-view', {url:'/view/:documentId',templateUrl: 'modules/documents/client/views/admin/documents-view.client.view.html',controller: 'ViewDocumentsController',resolve: {// 预先加载数据 idocumentResolve: ['$stateParams', 'iDocuments', '$q', function($stateParams, iDocuments, $q) { var defer = $q.defer(); iDocuments.get({ id: $stateParams.documentId}, function(data) { defer.resolve(data); },function(err) { defer.reject(err); }); return defer.promise; }]},})
    在resolve方法中,我们调用了Document Service的get方法获取文档数据,然后直接返回promise对象。
    7 结 论Javascript是一个事件驱动语言,Node利用了这个优点,编写出可扩展性高的服务器。Node采用了一个称为“事件循环(event loop)”的架构,使得编写可扩展性高的服务器变得既容易又安全。提高服务器性能的技巧有多种多样。Node选择了一种既能提高性能,又能减低开发复杂度的架构。这是一个非常重要的特性。并发编程通常很复杂且布满地雷。Node绕过了这些,但仍提供很好的性能。
    基于Node.js的医药搜索平台设计与实现采用MEAN栈开发,使用了很多Node.js下的库,比如使用Gulp基于流自动化构建工具,使用Express框架构建web服务端,使用Bookshelf实现数据层ActiveRecord,使用Bluebird.js 优化异步流等。
    本系统大致可以划分为如下几个模块,角色权限模块、邮件模块,系统设置模块,用户管理模块,文档管理模块,搜索模块。其中角色权限模块、邮件模块、用户管理模块和系统设置模块都是一般系统中常见的模块,为了实现各个模块以及使各个模块可以很好的协同工作,在代码编写的过程中参考了很多资料,以及学习了很多编程技巧,比如更加的熟练函数式编程,以及更加的熟练Promise异步编程。同时使自己更加的了解Node.js以及web开发。
    在整个毕业设计的过程中,从最初的系统模型设计,代码实现,前台页面的设计均有一人完成。整个过程中总结了一大堆理论并转化为自己的知识,不断的学习使自己不断的接近的自己的目标--成为一名全栈工程师。虽然在系统实现的过程中,遇到很多的困难,比如底层数据层的实现,前台页面的设计,角色权限系统的设计和代码的实现,excel的解析和生成,sphinx的安装和配置,如何优化系统性能等。虽然遇到很多问题,但都自己通过查资料一个个解决。通过一个多月的不断学习和编码,最终完成了本系统的开发,并上线使用,可以访问http://www.drugago.com查看。
    通过这次毕业设计,我深深的体会到了一个完整的系统从前到后搭建起来是有多么的不易,在解决的一个又一个问题之后,对自己的技术也更加的自信,使自己有能力面对毕业后的工作。
    大学,将要在毕业设计中结束,心中多的是无尽的怀念和十分的舍不得。
    参考文献[1] https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop . Concurrency modeland Event Loop
    [2] http://meanjs.org/ . Open-SourceFull-Stack Solution For MEAN Applications
    [3]Marshall K, Pytel C, Yurek J. Introducing active record[J]. Pro Active Record:Databases with Ruby and Rails, 2007: 1-24.
    [4] http://bookshelfjs.org/ . bookshelf.js
    [5] http://knexjs.org/#Builder .A SQL Query Builder for Javascript
    [6] Ferraiolo D, Cugini J,Kuhn D R. Role-based access control (RBAC): Features andmotivations[C]//Proceedings of 11th annual computer security applicationconference. 1995: 241-48.
    [7] Halpin T. Object-role modeling (ORM/NIAM)[M]//Handbook onarchitectures of information systems. Springer Berlin Heidelberg, 1998: 81-103.
    [8]Halpin T. Object rolemodeling: An overview[J]. white paper,(online at www. orm. net).gadamowmebulia, 2001, 20: 2007.
    [9] Keet C M. Part-wholerelations in object-role models[C]//On the Move to Meaningful Internet Systems2006: OTM 2006 Workshops. Springer Berlin Heidelberg, 2006: 1118-1127.
    [10] http://bookshelfjs.org/#associations .booshelf associations
    [11] Cantelon M, Harter M,Holowaychuk T J, et al. Node. js in Action[M]. Manning, 2014.
    [12] Tilkov S, Vinoski S.Node. js: Using javascript to build high-performance network programs[J]. IEEEInternet Computing, 2010, 14(6): 80.
    [13] https://www.npmjs.com/package/node-excel . Simple data set export to Excel xlsx file
    [14] Suchal J, Návrat P.Full text search engine as scalable k-nearest neighbor recommendationsystem[M]//Artificial Intelligence in Theory and Practice III. Springer BerlinHeidelberg, 2010: 165-173.
    [15] Aksyonoff A.Introduction to Search with Sphinx: From installation to relevance tuning[M].” O’Reilly Media, Inc.”, 2011.
    [16] https://www.npmjs.com/package/sphinxapi.SphinxSearch Client for NodeJS
    [17] Mikowski M S, Powell J C.Single Page Web Applications[J]. B and W, 2013.
    [18] http://www.apjs.net/ .angular.js 中文网
    [19] 许会元,何利力. NodeJS的异步非阻塞NodeJS研究[J]. 工业控制计算机,2015,03:127-129.
    [20] 杨伟超,刘阳,李淑霞. 基于搜索引擎的一站式检索平台设计与实现[J].计算机与现代化,2012,11:220-222.
    [21] 王金龙,宋斌,丁锐. Node.js:一种新的Web应用构建技术[J].现代电子技术,2015,06:70-73.
    [22] Pasquali S. Mastering Node. js[M]. Packt Publishing Ltd, 2013.
    [23] Tilkov S,Vinoski S. Node. js: Using javascript to build high-performance network programs[J]. IEEE Internet Computing, 2010, 14(6): 80.
    [24] Ihrig C J.Pro Node. js for developers[M]. Apress, 2013.
    [25] Mardan A.Publishing Node. js Modules and Contributing to Open Source[M]//Practical Node.js. Apress, 2014: 261-267.
    [26] Yu-yang L I U Q P, Zi-cheng P. The Design and Implementation of BuildingWebsite Internal Search Engine Based on Sphinx [J][J]. MicrocomputerInformation, 2010, 15: 050.
    [27] 姚立.IBM云计算平台下NodeJS应用支持环境的设计与实现[D].哈尔滨工业大学,2013.
    [28] 袁婷.RESTful Web服务的形式化建模与分析[D].华东师范大学,2015.
    [29] 王金龙,宋斌,and 丁锐.”Node. js: 一种新的 Web 应用构建技术.” 现代电子技术 38.6 (2015): 70-73.
    [30] 高飞,何利力,and 高金标.”基于Node. JS 内存缓存的 Web 服务性能研究.”工业控制计算机 11 (2015): 047.
    1 评论 8 下载 2018-10-03 22:09:09 下载需要12点积分
  • 基于Node.js中间层的微信图书借阅平台网站的设计与实现

    1 引言步入信息时代以来,互联网给人们的生活带来了翻天覆地的变化,互联网也不再简单地仅仅通过提供便利快捷的资讯服务来丰富我们的生活。互联网的出现打破了许多传统行业垄断的格局,互联网以其接入面广、信息即使、人人可参与等等性质,迅速融入到了人们的生活中,并且已经成为了整个社会不可缺少的一部分。大约5年以前的互联网是属于PC的时代,那时人们的网络生活包括逛贴吧、看资讯、写博客、下载多媒体文件以及在线网购等等。互联网是一个时刻都在发展的行业,其发展速度之快是大部分传统领域望尘莫及的,一方面是因为互联网虽然已有近30年的念头,但相较于传统行业,这个年份几乎还是跟刚出生的婴儿差不多。因此互联网有极为广阔的发展空间,另一方面由于互联网本身就技术性质而言,属于一门科学领域,大量国内外的高等技术人才都在不断完善互联网领域的科学根基。从小到微机硬件性能一代又一代地提升,再到大型路由交换网络环境的接入覆盖范围越来越广、越来越快,再到现如今机器学习、人工智能、分布式计算、分布式存储的铺开,互联网领域逐渐形成了一个包含计算机科学、软件工程等等领域的超大集合。以前人无法做到的事情已经逐渐慢慢在通过机器来实现。对于互联网领域而言,只有针对用户有意义的应用才是有价值的。随着互联网的发展和移动设备的普及,目前移动端市场正在以突飞猛进的速度渗透到用户群当中去。人们通过随手可得的智能手机、智能平板,甚至电子阅读器等等诸多设备都可以便捷的通过移动4G网络随时接入互联网。移动4G和公共Wifi网络的速度提升和逐步普及为移动互联网深入人们的生活打下了良好的开端。越来越多的人开始使用移动端设备接入互联网丰富日常生活。从在线购物、在线订餐,订房、订票,到日常信息的获取和交流。
    移动互联网是传统互联网的一个分支,但抛开其在移动端运行这一点而言,移动互联网跟传统互联网没有任何区别。然而仅仅是在移动端运行这一点而言,就足以体现出移动互联网与传统互联网的巨大区别。首先考虑用户的使用场景,一般而言,传统互联网的接入方式一般是PC浏览器,用户端座位上,面对着电脑屏幕获取互联网资讯。而移动互联网的用户没有这么多时间,他们可能是上班的路上拿出了手机,可能是吃饭时想发一张自拍,最常见的是与好友们的即使通讯,分享自己某一刻的心情等等。这些事情的共同点是操作时间短,使用地点自由。移动互联网因为其便捷的接入性完全满座以上需求。用户在使用智能设备访问移动互联网是没有端坐在桌面上通过PC浏览器访问桌面网页时那么有耐心,移动互联网让用户们得以随时随地享受快速的服务。
    然而重新提到移动互联网和传统互联网的共同点时,就又要回归到技术层面上,那就是虽然互联网的接入模式在变,但是基于的技术架构仍然没有任何变化,当用户在移动设备上访问互联网时,和在PC上通过浏览器访问后端服务没有任何差别。因此此前在传统互联网领域所没有克服的技术问题也就顺势带到了移动互联网开发领域。这十分让人苦恼,在之前,由于传统互联网的接入方式通常是宽带接入,网络稳定性和速度有保证,于是开发者们可以牺牲一些新能和速度以换取用户的良好体验。然而当面临移动互联网时,开发者们要面临移动端网络环境不稳定、速度有延迟等无法避免的事情。又由于移动端通常不具备PC端良好的处理性能和存储空间的优势,因此移动端开发时需要对性能有更高的要求。除此之外,还有一个难以调和的矛盾,就是移动端涌入我们的生活了,对于用户而言是好事,他们可以通过更多种途径获取更多种便捷的服务。但是服务和业务规模的扩增对开发者而言却是让人头疼的难事,因为传统互联网一直没能很好地解决前后端耦合的问题。前端由于接入设备种类的增加以及业务规模地愈发复杂化,导致我们强烈需要把前端从后端中解耦出来,让前后端不近从运行平台上解开,更重要的是从代码设计和架构上实现完全的解耦。
    在这些基础上,前人为了实现前后端解耦总结了许多实践方法,但每解决一个问题往往会引入新的问题。本课题就是试图引入一种新的模式,从而从根本上实现前后端的彻底解耦。通过微信公众号开发实现一个图书借阅平台的真实例子,从而验证所提出的新模式的可行性与架构方法。
    1.1 项目研究背景Web软件研发这一行业,从互联网诞生的那一刻开始诞生,开发模式也在不断演进。从最初既做设计图又要写代码,不分前后端端一人通吃的蛮荒时代,到现在逐渐分工明确各司其职的现代化开发时代,Web行业正在以历史上最快的速度向前发展。在这个历史进程中,Web研发模式经历了如下阶段。
    最初期可称为Web初期,那时不区分前后端工程师,对于大部分公司而言,所懂得技术的人才并不多,然而当初的业务成都也没有现在这么复杂。通常所有工作由一个小团队即可完成,HTML内容直接由JSP或者PHP等后端语言直接输出,交给浏览器来呈现,所有关于页面的展示逻辑都由浏览器来负责。浏览器端也很少参与业务逻辑,内容完全由服务器端程序来决定。这种模式的方便之处就是业务逻辑十分简单,业务核心依赖服务器端,代码结构简单清晰,利于调试和编写。然而一旦随着业务规模的增长,过多参杂了View层逻辑的服务器端代码就会不断增长,不单影响性能、效率,还严重制约程序开发的进度。服务器端的复杂度越来愈大,为了解决问题,单纯靠填补服务器端开发人员的数量也难以应对庞大的服务器端代码。
    这期间遇到的一些典型问题令许多人深有感触,随着服务逐渐增长,API调用关系变复杂,服务器端程序部署成为问题。后端人员对代码做出修改后上线需要进行上线部署,而前端人员对页面的细微调整也需要与后端人员进行沟通,提交代码给后端进行处理,完成本地开发,代码合并,后端部署等诸多环节。这导致前端的任务严重依赖于后端,没有后端的前端程序完全无法独立运行,制约着前后端之间开发进度的衔接。
    为了解决这种混乱不堪的开发状况,诞生了一种耦合式的前后端解决方案:即前端人员书写完页面代码之后,交给后端人员处理与数据库的对接,让前端人员给后端人员打下手。并且严格按照这种开发流程来执行。虽然这种方式解开了前后端人员之间的工作任务,但是并没有解开前后端之间耦合的代码。
    到了Web2.0时期,浏览器所呈现的内容愈发丰富,各种Web应用层出不穷,开始强调客户端丰富的用户交互。此时再把前后端耦合在一起显然不合适了。所以为了降低复杂度,开始在Web后端领域实践MVC开发模式。这一阶段涌现出了一些非常优秀的后端框架,比如RubyonRails、JavaSpring、Django等等。后端MVC强调模型、视图和控制器职责的分离,而一直以来前端的工作都是与View层紧密相连的,所以后端可以完全专注于Model和Controller的开发工作,而前端则专注于View层页面的开发。当前端编写完页面模板之后,交给后端去渲染Model层的数据。虽然前后端各自的关注点分离了,但是依然没有解决代码耦合的问题。对于后端而言,Controller层和Model层纠缠不清,Model层又和View层纠缠不清。虽然后端MVC是一种非常优秀的开发模式,但是前后端耦合这些根源问题依然没有解决。
    随着Google推动Ajax技术的发展,现在几乎所有网站都有Ajax的身影,异步的JavaScript和XML技术给Web开发模式提供了全新的思路。服务器端单纯以服务的模式提供数据,View层完全独立于后端的服务,前端所需的数据都通过Ajax异步请求向服务器端获取,页面内容的渲染工作全部由前端完成。这种模式下,前端与后端实现了完全的分离,交互的关键点就是Ajax接口,但是此时,服务器端的业务的虽然降低了,但是前度的复杂度却增加了。一切总归是平衡的,后端所减轻的负担,现在全部施加到了前端上。这个阶段,是前端工作人员最痛苦的时期,JavaScript走向了之前JSP和PHP走过的路,前端代码陡增,前端尝试引入框架来解决代码逻辑问题,但前端的压力依然很大。
    到了最近一个阶段,也是目前业界正在广泛使用的一个模式,即在前端也部署MVC甚至MVVM这种大型软件的构建模式。这一阶段前后端之间的代码耦合已经降低到最低了,几乎解除了Web开发之间的代码耦合问题。后端专注于业务逻辑的开发,通过RESTful接口输出数据,前端只要准从设计模式的标准,也可以应对复杂的交互逻辑了。但是方便的背后,也带来了一些其他问题。比如全部异步的请求模式给前端编码带来难度,全部通过请求接口来渲染数据对搜索引擎也十分不友好,不方便爬取页面数据。而且对于移动设备而言,尤其是手机,在网络状况不好的情况下,前端页面需要等待网络请求返回到结果才能呈现。对于前端SPA应用,最重要的路由问题难以和后端调谐,长此以往路由都是交给后端处理的,但是现实的情况是前端对路由的依赖更加严重。
    展望未来,业界正在积极寻找一种新的解决方案,一个新的模式。既能解除Web开发面临的耦合问题,又能让前后端关注自己的职责,并且彼此的功能划分地更加细致。让前端可以脱离于后端,让后端可以专注于业务服务。随着Node.js技术的发展,再一次为Web研发模式带来了新的思路。Node.js与前端共享开发语言,可以与前端实现高度的代码重用,也赋予了JavaScript在服务器端执行的能力。这种新的模式就是在传统后端和前端之间,引入一个Node.js中间层来调和矛盾和彼此的缺失。以往所面临的问题,随着Node.js的引入都可以得到解决,所带来的代价便是需要前端工程师对后端有更深入的理解,需要他们设计更多后端的知识,已经引入一个新层之后的通信效率略微下降。更多的制约可能是面临新的技术解决方案时的担忧,缺乏对新技术的实践经验,无法短期内改善大量的历史代码。
    1.2 项目研究内容本课题所要研究的是长久以来Web研发领域前、后端耦合的问题,进而提出一个全新的解决方案,并通过开发一个基于微信的图书借阅系统展示新方案的可行性。在此以前,我们所面临的耦合问题的广义层面含义包含很多中,下面逐一分析这些问题的具体内容。
    从代码的耦合来看,Web研发领域在初始时期由于代码规模较小,并不注重代码分离。导致出现了不区分前后端的混合式编程模式。比如页面数据从数据库获取之后直接写入到页面当中输出给浏览器端展示。衍生出的问题包括代码糅杂和混乱不堪,难以区分服务端代码和客户端代码,不容易做分离,代码的灵活性不强,不利于代码复用和拓展。业界要想实现中到大型规模的软件产品,与团队协作的重要性不可分。需要由不同的团队负责不同的模块代码开发,所以解耦的第一步就是要解开代码层面的耦合,以适应大规模的代码量。
    从业务的耦合来看,以电商网站为例,用户完成一个完整的购物流程包括浏览商品,搜索商品,购物车下单,支付,等待收货,完成评价。既然作为一个整体的流程,那么每个环节之间必然会出现耦合,比如搜索功能依赖展示功能呈现结果,支付功能依赖购物车功能计算总价,评价系统依赖用户信息系统。安装业务的属性来分,展示功能属于浏览器端,搜索和支付属于服务器端,但脱离前、后任何一端,任何功能都无法完成。传统Web研发领域由于前后端耦合的问题,导致前后端的职责并没有很好地区分开来,虽然从业务职责上各个模块是分离的,但是前后端确实耦合的,这就给开发带来了难度,依旧不能解决随着业务规模扩展带来的开发压力。
    从服务的耦合来看,随着目前移动互联网的火热,用户接入互联网的方式不再局限于电脑。使用安卓智能手机、苹果智能手机、各类平板电脑、所占据的用户比例甚至已经超过了传统使用PC浏览器的上网比例。这又给Web研发提出了新的要求,如果统一提供跨平台的服务。传统Web研发由于后端服务与前端服务耦合,不方便做统一地跨平台支持。同样的,随着用户设备多样性的增加,需要提供越来越多的服务需求,开发压力也将陡增。
    本课题通过研究Web领域历史以来的演变过程中所诞生的各种技术的优缺,通过探索新的模式,提出新的方案来解决耦合这个历史问题。以达到让前后端的代码更分离、职责更清晰、服务更统一、分工更合理高效的目的。
    要解决问题需要首先分析问题出现的原因,传统Web开发带来的制约源于后端没有纯粹地负责数据处理和存取功能,而是过度耦合了前端呈现和用户交互。后端应该不区分端,只负责提供统一地服务,然后各个端之间负责利用后端提供的服务接口完成与用户的交互和业务逻辑。这样就找到了解决问题的方法。
    所尝试探索的途径是通过引入Node.js中间层技术,在Web服务器上同时部署服务器程序和Node.js中间层,当用户请求前端页面时,前端首先访问Node.js中间层进行页面关键数据的渲染,当后续需要数据时即可跳过Node.js中间层直接请求后端。当用户进行页面跳转时,使用Node.js中间层和前端同步页面路由,对于用户的状态管理等与业务逻辑不是强相关的服务也由Node.js来提供。依然会依赖Ajax技术来处理大量的数据依赖,除了引入Node.js之外,其他的前后端技术依然保持不变,变的是它们之间的组合关系。本课题所要实现的,也正是这样一种合理利用Node.js中间层解决前后端耦合的研发模式。
    1.3 项目研究意义就软件工程领域而言,近些年发展最快的莫过于Web开发。绝大部分互联网公司采用Node.js、PHP、Python、JavaWeb、Ruby、Go、HTML/CSS/JavaScript等等技术为用户提供丰富便捷的互联网服务。这些服务涵盖我们日常生活的方方面面,从衣食住行到社交通讯,到处都需要Web开发提供服务程序。对于一个完整的应用声明周期而言,从开发到测试再到上线再到版本迭代,这期间会不停地伴随着功能增加和业务规模的扩增,因此Web开发对程序设计的要求需要具备灵活的拓展性。程序等于数据结构加算法,这是一条基本常理,无论多么复杂的程序,归根结底就是在做这样两件东西。相比之下,软件架构的设计也离不开另一个基本的常理,那就是高内聚低耦合。无论是命令式编程的独立函数还是面向对象式编程的设计模式,无论是MVC还是MVVM架构,在设计软件架构的时候,都要时刻考虑这两个基本原则。因为只有软件的模块、类、功能、服务彼此之间尽量独立,尽量不要依赖别的部分,自己内部的状态尽量保持封闭这些都能做到时,才能保证模块可以随意增删以及单独测试等等。研究耦合这个问题,就像研究数据结构与算法对于程序设计的意义一样,是Web应用程序架构的基础。对于Web开发这种功能快速变化,版本快速迭代的模式而言,如何实现高度的解耦是功能业务能否快速拓展以及保证前后端开发人员各司其职高效配合的重要基石。
    研究Web研发新模式的意义,不是一味地标新立异,而是真正着眼于当下Web开发领域所面临的开发效率低效这个切实问题。契合当今前、后端独立发展,开发人员职责合理分配,让团队之间的协作更加高效,并让产品的开发速度,软件服务的可拓展性、灵活性、独立性提升的需求,解放生产力。
    2 技术选型与开发环境本微信图书借阅平台是一个完整的B/S架构的实现,在服务器端通过Nginx监听用户请求并分发请求给StrongLoop进程管理器,StrongLoop创建多个slave进程维护Node.js实例来处理用户请求,Node.js作为中间层可以构建在任何其他语言的后端之上,通过基于RESTful规则设计的接口可以建立Node.js中间层与其他后端语言的数据通信。数据库采用MySQL、通过Jade模板引擎实现服务端HTML预渲染、在客户端通过微信内置的Blink内核的WebView作为与用户交互的入口、UI效果展示采用Bootstrapcss界面库快速制作移动端友好的响应式界面。
    2.1 技术选型2.1.1 Node.js介绍Node.js是一个基于事件驱动的JavaScript异步运行环境,从一开始设计之初就是针对互联网这种节点灵活、易于扩展、高性能的场景而生的。Node.js处理并发的性能非常好,下面有个简单的实例,展示Node.js如何用简短的代码实现一个HTTP服务器,并且可以承受极高的并发请求量。当有HTTP请求到达服务器端时,Node.js迅速处理调度事件循环处理请求,当没有HTTP请求到达服务器是,Node.js就自动挂起进程,节约CPU和内存资源。
    示例:用Node.js创建一个HTTP服务器
    consthttp=require('http');consthostname='127.0.0.1';constport=3000;constserver=http.createServer((req,res)=>{res.statusCode=200;res.setHeader('Content-Type','text/plain');res.end('HelloWorld\n');});server.listen(port,hostname,()=>{console.log(`Serverrunningathttp://${hostname}:${port}/`);});
    Node.js在设计上受到了RubyEventMachine和PythonTwisted的影响,但对于事件模型的处理上Node.js更进一步,Node.js不依赖第三方库,在运行时自动创建一个事件循环处理任务。一般在别的时间循环系统当中,需要在代码起始执行的地方显式地通过阻塞方法开启一个事件循环,然而这些在Node.js当中都是不需要的,Node.js在执行代码文件的伊始就自动开启了事件循环。这种模式跟浏览器端JavaScript脚本的执行非常类似,异步操作都是浏览器在幕后完成的。
    HTTP是Node.js中非常重要的一部分,因为Node.js的初衷就是针对互联网上大规模可灵活拓展的HTTP服务而设计的。因此Node.js作为提供HTTP服务的基础服务层十分适合。
    2.1.2 异步编程介绍如上所述,可以看出Node.js的异步处理模型与常见的基于系统线程实现并发处理的模型有很大反差。基于线程的网络模型唯一的优势就在于用最低的资源开销实现并发处理,但是多线程编程非常不利于控制和管理,相比之下,单线程的编程模式就简单直接多了。虽然Node.js引入的是异步编程,但目前已经有良好的异步编程解决方案,比如Async/await+Promise的解决方案使得异步编程可以像同步编程一般方便直观。反观多线程编程,多个线程之间的任务如果是非相关的,那么影响倒不是很大,然而一旦多个线程依赖同一个数据源,就会导致资源争夺而无法合理分配的情况,需要引入锁机制来防止死锁。而且多线程编程还有一个致命问题在于一旦其中一个线程出现异常,但是这个异常没有被正确处理(这十分常见),就会导致主线程崩溃的情况,最终导致所有线程崩溃。所以为了提升多线程程序的鲁棒性,必须依赖经验丰富的程序员通过巧妙的机制来合理处理资源争夺、数据不同步、异常崩溃等问题。仅仅是为了节省资源而引入多线程编程,在大多数情况下并没有这个必要。
    示例:Node.jsAsync/await+Promise解决方案
    function_fetchbooks(){ returnnewpromise((resolve,reject)=>{ //ajaxoperation if(ajax.success){ resolve(ajax.response); }else{ reject(ajax.error); } });}asyncfunctionfetchbooks(){ constbooks=await_fetchbooks(); console.log(books); returnbooks;}
    而Node.js的开发者几乎不需要考虑进程死锁的问题,因为单线程的执行环境中并没有锁这个概念。而且Node.js中几乎所有函数都是异步I/O函数,因此Node.js的主进程永远不会阻塞,又因为不会发生阻塞,所以扩展系统业务的规模也十分方便。虽然Node.js被设计成不依赖线程编程,但并不意味着Node.js不能发挥CPU多核心的性能。通过child_process.fork()API可以灵活地复制出子进程,而且它们彼此之间的通讯也经过了特殊设计,非常方便。基于内置的cluster模块,可以在本地的多进程之间共享sockets,充分发挥多核心的优势。
    因为随着计算机的普及和计算机硬件行业的发展,服务器上所采用的硬件在处理能力上正在发生翻天覆地的变化,绝大多数公司在考虑后端技术选型和服务器硬件配置时,都是首先考虑技术的复杂度,服务器能承受的并发压力,未来产品迭代的速度等等。除非是顶尖的互联网软件行业对性能要求极高的公司,对于大部分公司而言,Node.js所提供的这种简洁轻快、高效方便的优势都会对快速产品迭代开发带来极大的优势。
    2.1.3 阻塞和非阻塞介绍阻塞是指当执行JavaScript代码时,Node.js进程必须等待非JavaScript操作结束,之所以发生这种事情是因为事件循环在阻塞操作发生时无法继续执行。比如说读取一个大文件,当Node.js执行读取文件的代码时,必须等待操作系统将整个文件读取完毕,否则Node.js的事件循环将无法继续工作。因此在Node.js中面临CPU密集型任务时,JavaScript通常会表现出很差的性能,但是在处理I/O密集型任务时,则表现出优异的性能。Node.js标准库中基于libuv的同步方法一般都调用阻塞操作,而异步方则调用非阻塞操作并且接收回调函数用来处理异步的结果。通常Node.js会提供两个版本的函数,默认是异步方法,方法名后面加上sync的是同步方法。
    示例:Node.js阻塞方法
    constfs=require('fs');constdata=fs.readFileSync('/file.md');//blockshere
    示例:Node.js非阻塞方法
    constfs=require('fs');fs.readFile('/file.md',(err,data)=>{ if(err)throwerr;});
    在上面的阻塞方法示例中,Node.js会等待文件全部读取完毕才能继续往下执行,这将导致Node.js进程阻塞无法继续执行,等待非JavaScript操作也就是操作系统读取完毕后,方可继续执行。而在非阻塞方法的示例中,当执行readFile之后程序会立即结束,继续往下执行,但是读取文件的任务并没有终止而是放在了事件循环中去做,当文件读取完毕后回调函数会被出发。
    2.1.4 MySQL数据库介绍MySQL从最初开始就被设计为一款支持SQL查询语言的关系型数据库管理系统,最初MySQL以开源的形式向外界发放。它的兼容性非常好,支持Linux系统、Windows系统、Unix系统。由于其广泛的支持度,MySQL被用于各种类型的应用程序的开发之中,当然MySQL的最大用途还是作为在线Web应用的数据库,也就是我们所常用的网络应用数据库服务器。MySQL不光以其强大的兼容性风靡各大平台,其中广为熟知的LAMP一站式打包服务更是为MySQL在Linux上的统治地位奠定了强有力的基础。配合Linux内核操作系统、Apache应用服务器、MySQL数据库服务器、PHP(Python或Perl)网络开发语言,无数中小企业通过这些技术向用户提供各种丰富的网络服务。
    虽然MySQL最初以开源的方式向外界授权,后来被瑞典MySQLAB公司收购,后者2008年又被甲骨文公司收购,甲骨文也因此获得了MySQL的版权,收购MySQL之后的甲骨文公司宣称MySQL面向普通用户将永远免费,只有企业用户需要获得授权才能使用。也正因此导致MySQL发展出继续开源的MariaDB分支,MariaDB保持跟MySQL完全的兼容性,但仍会以开源的性质继续发布。
    MySQL的特点:

    MySQL是一款完善的关系型数据库管理系统。MySQL基于Client/Server架构,只需要一个Server端,就可以支持大量的Client端接入。对于集群式应用开发,只需要部署一台MySQLServer服务器即可。相比于Oracle、DB2、SQLServer等商业服务器而言,MySQL对个人用户完全免费,也正因为如此吸引了大量的用户。MySQL性能优异,安全性、跨平台兼容性都非常优异。MySQL对SQL的支持度很好,并且支持SQL的最新标准。MySQL功能完善,支持外键约束、索引、子查询、视图、过程、触发器、Unicode字符、全文搜索等实用功能。MySQL支持事务回滚,数据操作失败时根据数据库读写日志恢复到出错前的状态。MySQL社区用户活跃,社区对于一款开源软件的影响将起到主导作用。
    MySQL操作:
    MySQL可以在Windows、Linux和Unix下很好地工作,推荐在Linux命令行中安装MySQL,过程快捷便利。以Ubuntu操作系统为例,执行如下命令即可安装MySQL服务器端和客户端:
    $sudo apt-get install mysql
    执行如下命令即可进入MySQL数据库进行数据库操作:
    $mysql -u root -pmysql> CREATETABLEmytable;
    除了命令行方式以外,MySQL支持多种语言的API接入,MySQL服务端默认监听服务器的3306端口,等待来自外界的TCP连接,常见的Web开发语言通过各自版本的MySQL连接驱动即可成功连接MySQL数据库执行数据库操作。
    2.1.5 Nginx服务器介绍Nginx是一个高性能的Web和反向代理服务器,它具有有很多非常优越的特性:

    作为Web服务器:相比Apache,Nginx使用更少的资源,支持更多的并发连接,体现更高的效率,这点使Nginx尤其受到虚拟主机提供商的欢迎。能够支持高达50,000个并发连接数的响应,Nginx支持epoll、kqueue等多种开发模型.作为负载均衡服务器:Nginx既可以在内部直接支持Rails和PHP,也可以支持作为HTTP代理服务器对外进行服务。Nginx用C编写,不论是系统资源开销还是CPU使用效率都比Apache等其他服务器要好很多。作为邮件代理服务器:Nginx同时也是一个非常优秀的邮件代理服务器。
    Nginx安装非常的简单,配置文件非常简洁(还能够支持perl语法),Bugs非常少的服务器:Nginx启动特别容易,并且几乎可以做到7*24不间断运行,即使运行数个月也不需要重新启动。还能够在不间断服务的情况下进行软件版本的升级。
    2.1.6 StrongLoop进程管理器介绍StongLoop是Node.js集群工作环境下最完善的运行时管理控制和监控程序,StrongLoop在吸取了Node.js社区一些现有的进程管理器(forever、pm2)的基础上又扩增了独有的功能,StrongLoop支持的功能如下:

    图形化操作界面;集成nginx负载均衡;支持远程管理;自动化多站点部署;集成运行监控和性能评估;多机器、多进程分布式实时控制。
    2.1.7 微信公众号平台介绍微信公众号是腾讯公司所推出的即时聊天工具“微信”中,面向非普通用户即具备推广能力的个人用户和企业用户所推出的推广平台。通过开放授权和实名审查,吸引有开发和推广能力的人员使用。普通用户可以关注微信公号,获取资讯和服务。微信公众号的本质是提供一个第三方平台打通内容推广者与大众用户的桥梁。
    申请个人公众订阅号和商业公众企业号的用户,首先需要首先到微信官网注册账号,个人公众订阅号用户需要提供证件申请实名认证,商业公众企业号用户需要提供商业资质证明。等待申请结果成功后,用户们便可以通过微信搜索功能搜索公众号名字,查看详情后关注该微信公众号。开发者在微信后台配置微信与自己的服务器之间的连接,从而打通从用户到微信服务器再到个人服务器之间的消息传递,并将处理后的结果回传给微信服务器,最终转发到用户的微信客户端上。开发者有两种方式与用户进行交互,一是通过聊天信息和推送小时与用户进行信息交互,二是通过微信公号底部的自定义菜单栏引导用户触发指定的功能,与用户完成交互。微信提供OAuth2.0认证机制,帮助开发者在经过用户的授权之后获取用户的一些非隐私性个人信息,比如用户id、头像、昵称、地域等等信息。帮助开发者给用户提供更加精准、有效的服务。
    3 需求分析自从移动互联网兴起以来,开发人员面临了更大的挑战,要求也更加严格。传统的仅基于PC浏览器平台的Web应用模式无法满足现如今各大移动终端平台的需求。面临更多的平台,需要提供针对不同平台的服务来满足用户需求,Web应用的定制性越来越强,代码规模越来越大。为了满足新的业务需求,提升开发效率,节约开发成本。需要将前、后端开发的人员独立开来,让彼此各司其职。在这个需求下,我们需要达到如下几点:对于后端而言,仅负责业务逻辑和数据接口的开发,一套服务和接口的独立性尽量高,从而可以被多套业务重用。而对于前端而言,负责保持用户的状态,为用户展示界面效果,与用户进行逻辑交互,一套界面组件的独立性尽量高,从而可以被多套业务重用。
    3.1 技术需求其实Web开发解耦这个话题的讨论从若干年前就早已开始了,只是随着Web研发一个阶段一个阶段的推进,业界虽然探索出了一些比较实用的方案,但是没有从根本上解决问题。另一方面因为Web领域的受众用户基础量太大,一旦采用新模式重构旧代码,会导致伤筋动骨,之前大量的业务和逻辑需要重写。所有很多时候,历史问题比技术问题更难解决。
    当我们讨论前、后端解耦,我们在讨论什么?对于前、后端解耦,大家对于其认知不尽相同。对于大部分初步接触解耦的人而言,比较认同的解耦模型是SPA(Single Page Application)模式,这种模式的技术特点是,后端处理所有的业务逻辑和数据,但不处理任何与视图界面和用户交互方面的问题。后端仅通过RESTFul、SOAP或普通的HTTP请求对前端发出的请求给予响应。到了用户这一端都是前端在进行处理,除了页面上硬编码的HTML结构,所有的动态数据都通过接口请求后端获得,然后动态渲染视图展示给用户,所有与用户交互的操作都有前端完成。这种架构是一种非常彻底的解耦模型,但是它太过于彻底了,以至于前后端之间充斥着大量的接口请求操作,以完成视图界面的创建。对于前端而言,又由于请求都是异步操作,会带来编码上的复杂性。
    除了SPA模式之外,目前业界没有其他更成熟的前、后端解耦方案。在PC浏览器时代,由于物理带宽得到稳定保证,因此浏览器发出的HTTP请求通常可以得到快速响应,而且上网花销比较低。然后随着移动互联网的普及,流量费用是一笔不小的开销。而且移动网络的稳定性并没有传统物理宽带那样稳定,SPA架构的Web应用被这两个问题频繁困扰,不通过请求获取数据,页面上将会呈现大片空白区域,网络速度不稳定,加载的数据不完整,频发发送请求,对用户的流量也是一个比较大的负担。再之,SPA的可用范围比较窄,对于以内容为主的Web应用,比如新闻类和分享类的网站,需要被搜索所收录,SEO对于此类网站而言尤为重要。而SPA应用的网页内容都是通过JavaScript异步请求后端接口获得数据后动态插入到页面当中的,搜索引擎无法执行JavaScript,因此无法收录网站的内容。这将会对企业造成极大损失,因此SPA只能在有限的场景下发挥作用。以上便是我们所熟知的前后端解耦。
    为什么要探索前、后端解耦的新模式?对于目前现有的几种Web研发模式而言,它们都有各自适合的开发场景,没有谁能够完全取代别彼此。这就造成了开发人员为了兼容多套不同的平台业务,需要采用多种方案来实现,学习成本高,维护的代价也大,需要大量的开发人员来维护多套解决方案代码。基于Node中间层的解耦方案,可以实现前、后端解耦架构的大一统,可拓展性、灵活性都非常强。不管是垂直业务深度拓展,还是水平平台种类覆盖方面,Node中间层都能以一个灵巧的胶水的角色拟合它们的差异,实现极高的重用和复用。
    对于现今Web研发的开发效率而言,前、后段人员的分工,团队之间的配合,代码的整合与部署,新产品的迭代,出错后的容灾回滚等等都受到现有开发模式的限制。因为前、后端的工作内容没有分离开,为了解决分分合合的代码问题,将会浪费大量的时间处理非技术性问题。
    另一方面,前端人员所能做的事情一直受到后端的制约,前端所开发出来的应用无法再脱离后端的情况下运行,难以调试、部署。过渡依赖于后端环境提供前端运行环境的支撑。又因为前端需要耦合到后端当中,也就意味着后端参与了很多本应该前端做的事情,为了给后端做出让步,前端不能充分发挥自身的能力,需要把功能做得尽量简单,转而通过后端来实现复杂的逻辑。后端又因为需要处理与用户交互先关的内容而叫苦连天。
    还有十分重要的一点,就Web应用的性能而言,服务器在承受着巨大并发压力的情况下,如果参与了过多关于视图界面相关的工作,会影响CPU的处理能力和内存资源的占用。而将这些工作搬到前端之后,后端的职责更加清晰,仅需要执行纯粹的数据处理的请求处理就可以了。
    以上这些问题是Web研发领域面临的迫切需求,只有进行良好的设计解除前、后端之间的耦合关系,充分发乎前后端开发人员的能力,才能更加高效地推动行业的发展。
    3.2 功能需求微信图书借阅平台提供添加图书、借阅图书、归还图书、浏览图书等功能。用户首次向书架贡献自己的图书时,使用添加图书功能;当用户需要查看当前书架图书信息时,使用浏览图书功能;当用户需要从书架上借阅图书时,使用图书借阅功能;当用户阅读完毕图书时,使用归还图书功能。用户用例图如图3.1所示。

    3.3 系统操作流程微信图书借阅平台的使用流程有三个步骤。第一步,用户关注微信图书借阅平台公众号。第二步,用户使用微信图书借阅平台的各个功能,包括添加图书、浏览图书、借阅图书、归还图书。第三步,扫描图书条形码,完成图书信息读取,并确认图书的添加、借阅、归还等功能。系统流程图如图3.2所示。
    3.3.1 关注微信图书借阅平台目前微信平台使用的广泛度已经家喻户晓,作为即时通讯软件,它还提供了开发平台共开发者使用。因此微信图书借阅平台基于微信,不需要下载任何软件,只要用户关注“微信图书借阅平台”公众号,就可以使用平台的图书相关功能。“微信图书借阅平台”作为一个公众号,可供任何拥有微信号的用户搜索并关注,点击微信界面右上角“+”,在搜索框输入“微信图书借阅平台”,点击进入平台主页,点击关注按钮,便完成了关注过程。
    3.3.2 功能菜单微信图书借阅平台公众号的主界面是由三个一级菜单组成,包括借阅图书、归还图书、管理图书。其中管理图书菜单下还有两个二级菜单,包括添加图书、浏览图书。用户第一次使用本平台可能是要贡献图书的贡献者用户或者是借阅图书的借阅者用户,对于贡献者,点击管理图书菜单下的添加图书,并将图书放入书架;对于需要借阅书籍的用户,点击借阅图书菜单。用户需要借阅书籍时,可以点击浏览图书,查看当前书架上有哪些图书,点击具体的某本图书,查看图书相关信息。
    3.3.3 扫码微信图书借阅平台并没有图书馆手持款图书条形码扫描仪,但是,平台使用微信的“扫一扫”功能,完成对图书条形码的扫描,从而获取实体图书的图书信息。添加图书、借阅图书、归还图书的功能都需要获取图书信息,通过点击功能菜单会自动转到扫一扫界面。
    3.3.4 确认操作微信图书借阅平台的操作流程是关注“微信图书借阅平台”公众号、选择功能菜单、进入扫码、完成操作确认。系统操作流程图如图3.2所示。

    4.系统设计概要设计是建立在需求分析的基础上的,概要设计的主要任务是,通过仔细分析软件规格说明,适当的对软件功能进行分解,从而把软件划分为模块,并设计出能完成预订功能的模块结构。
    4.1概要设计概要设计是软件设计具体实施阶段的第一个环节,通过给出核心功能点和设计要素的概括性描述,指导设计开发的方向。在这个阶段的主要任务一方面是概括地描述出整个软件的整体设计。另一方面是将工程概括的拆分出主要的核心模块描述各个模块大致的实现方案。
    4.1.1 整体设计1.微信公众号自定义菜单
    微信公众号是用户使用该图书分享平台的入口,公众号底部的自定义菜单是引导用户进行功能操作的入口,需要设置简洁直观的入口让用户快速了解菜单对应的功能。
    2.前端交互
    图书分享和借阅应该是一个非常便捷的操作,不应当涉及过多的交互步骤,使得各个功能一触即达。
    3.Node.js中间层
    中间层的职责是前端与后端的桥梁,负责从后端获取数据并渲染出页面或进一步处理数据后传递给前端。
    4.1.2 模块设计1.借阅图书模块
    图书借阅模块实现了图书的借阅操作,读者从书架上拿到图书,通过扫描图书背面的条形码确认借阅完成借书操作,取走图书。
    2.归还图书模块
    图书归还模块实现了图书的归还操作,当读者阅读完毕后,扫描图书条形码后确认图书归还,然后将图书归回原位。
    3.添加图书模块
    图书添加模块实现了图书的入库操作,读者拿出自己的图书,扫描图书条形码后确认图书归还,然后将图书放入书架。
    4.浏览图书模块
    浏览图书模块实现了浏览图书信息的功能,用户可以远程查看书架上的书籍信息,包括书籍名等基本信息、书籍数量、书籍可借阅数量。
    4.2详细设计用户进入微信后,关注公号进入公号界面,通过公众号主页底部的自定义菜单进入相关操作界面。点击“借阅图书”自动弹出二维码让用户扫描书籍自动借阅。点击“归还图书”自动弹出二维码让用户扫面书籍自动归还,点击“添加图书”自动弹出二维码让用户扫描书籍完成添加,点击“浏览图书”跳转到设定的网页浏览当前书库的所有书目。
    4.2.1 微信公众号设置申请个人公众订阅号和商业公众企业号的用户,首先需要首先到微信官网注册账号,个人公众订阅号用户需要提供证件申请实名认证,商业公众企业号用户需要提供商业资质证明。等待申请结果成功后,用户们便可以通过微信搜索功能搜索公众号名字,查看详情后关注该微信公众号。
    在微信后台配置微信与自己的服务器之间的连接,从而打通从用户到微信服务器再到个人服务器之间的消息传递,并将处理后的结果回传给微信服务器,最终转发到用户的微信客户端上。
    微信图书借阅平台通过企业公众号底部的自定义菜单栏,分别引导用户至不同的功能,与用户完成交互。菜单栏分为一级菜单和二级菜单,本平台使用借阅图书、归还图书、管理图书三个一级菜单,管理图书下有添加图书、浏览图书两个二级菜单。
    4.2.2 获取图书信息实现一个图书数据库的工作量非常庞大,设计到市面上所有已有图书的信息采集入库操作。为了便利地获取图书信息,采用豆瓣开放的公共图书API接口实现图书信息查询功能。用户首先打开微信进入公众号界面,通过公众号底部菜单中的“添加图书”菜单,点击后自动转至图书条形码扫描界面,用户扫描图书背面的条形码后,微信将识别出条形码中包含的ISBN号并回传给前端程序。前端程序根据将ISBN回传给Node.js中间层服务器,Node.js向豆瓣开放API服务器发送RESTful请求,附带着图书的ISBN信息,取得图书信息(图书名、图书封面图片链接、图书内容摘要、作者、出版社等)。
    4.2.3 存储图书信息所有图书的信息都通过自豆瓣的开放API接口服务获得,每当采集到一本图书的信息,便会执行一次图书信息的存储任务,将图书信息存入服务器本地数据库中。这样做的原因是,一方面便于再次获取图书信息时,可以快速查询图书结果。另一方面将初次获取后的信息按照关系存入本地数据库有利于信息的统计和变更。
    在取得图书信息后,用户点击确认按钮,图书的书名、ISBN号、作者、出版社、封面、摘要等信息会存储到数据库中,并附加与借阅相关的图书信息,比如图书库存量,借阅次数,当前借阅者的信息等等。
    4.2.4 展示图书信息Node.js中间层在取得图书信息后,根据预先定义的Jade模板渲染出图书界面的HTML内容。微信用户内置的Webview控件在收到HTML内容后,渲染内容,加载图书封面等静态资源,经过CSS样式表处理后渲染出图书展示界面。图书信息的展示工作完全由Node.js中间层负责,当用户在图书页面之间跳转时,均直接请求Node.js中间层服务器,交由Node.js选择指定的Jade模板渲染内容。
    系统功能模块图

    5 数据库设计微信图书借阅系统的正常使用需要有正确的数据库读取操作,数据库设计的优劣与否关系到系统能否运行流畅。微信图书借阅系统的数据库设计主要分为概念结构设计和物理结构设计,概念结构设计主要分析实体关系,以E-R图形象的展示实体关系。物理结构设计将实体关系转化成数据库中表的关系,以及表结构中各个字段的设置。
    5.1 概念结构设计微信图书借阅系统的实体是各个用户,用户之间的地位平等,每个用户都可以贡献图书、借阅图书、查看图书借阅情况。本系统的数据库E-R图如图5.1所示。

    5.2物理结构设计本系统使用的数据库是MySQL数据库,设计的充分考虑了程序处理的要求、应用环境的需求,斟酌数据库表结构和字段设置,考虑数据库各表中的字段类型和长度,尽可能提前预估到可能出现的问题,避免程序设计过程中带来不必要的麻烦。MySQL数据库中,凡字段为varchar或char类型时,需要指定编码方式为UTF-8,本系统对以上两种类型数据,全部设置为UTF_8编码方式。
    微信图书借阅系统共设计了3张表,分别是图书表(Books)、用户表(Users)、借阅表(Borrowing)。
    1.图书表
    图书表存储图书的相关信息,包括字段:图书id、ISBN号、名称、封面、作者、出版社、摘要、总数、可借阅数、贡献者。图书表结构如表5.1所示。



    字段名
    数据类型
    宽度
    是否可空
    说明




    id
    integer


    图书id,主键,自增


    ISBN
    varchar
    255

    ISBN号


    title
    varchar
    255

    名称


    cover
    varchar
    255

    封面


    author
    varchar
    255

    作者


    press
    varchar
    255

    出版社


    excerpt
    text


    摘要


    total_count
    integer


    总数


    avail_count
    integer


    可借阅数


    owner_id
    integer


    贡献者



    2.用户表
    用户表存储系统用户的相关信息,包括字段:用户id、用户标识、用户名。用户表结构如表5.2所示。



    字段名
    数据类型
    宽度
    是否可空
    说明




    id
    integer


    用户id,主键,自增


    uuid
    integer


    用户标识


    name
    integer


    用户名



    3.借阅表
    借阅表存储用户借阅信息,包括字段:借阅id、图书id、用户id、借阅时间。借阅表结构如表5.3所示。



    字段名
    数据类型
    宽度
    是否可空
    说明




    id
    integer


    借阅id,主键,自增


    book_id
    integer


    图书id


    user_id
    integer


    用户id


    borrow_time
    date


    借阅时间



    6 图书借阅平台的实现微信图书借阅平台是一个完整的B/S架构的实现,在服务器端通过Nginx监听用户请求并分发请求给StrongLoop进程管理器,StrongLoop创建多个slave进程维护Node.js实例来处理用户请求,Node.js作为中间层会可以构建在任何其他语言的后端之上,通过基于RESTful规则设计的接口可以建立Node.js中间层与其他后端语言的数据通信。数据库采用MySQL、通过Jade模板引擎实现服务端HTML预渲染、在客户端通过微信内置的Blink内核的WebView作为与用户交互的入口、UI效果展示采用Bootstrap、CSS界面库快速制作移动端友好的响应式界面。
    6.1 配置微信图书借阅平台是一个微信公众号,用户使用之前需要先关注公众号。用户登录微信,点击右上角的添加按钮,点击添加朋友菜单,输入“微信图书借阅平台”,关注公众号。成功关注公众号以后,用户便可以正常使用平台的各项功能。
    6.2 主界面主界面是微信图书借阅平台的功能入口界面,提供借阅图书、归还图书、管理图书等三个一级菜单,其中管理图书分为添加图书、浏览图书两个二级菜单。主界面如图6.1所示。

    6.2添加图书微信图书借阅平台是一个提供用户共享书籍的平台,用户有闲时不看的图书便可以自己的图书添加到公共书架上。在平台上,即点击图书管理下的二级菜单添加图书,便进入“扫一扫”界面。通过扫码图书,平台读取图书ISBN号,获取图书信息,最后跳转到贡献图书确认界面。
    添加图书菜单按钮提供了用户添加图书的功能入口,点击添加图书,便进入扫一扫界面,可收集图书的信息,如图6.2所示。

    获取图书ISBN号后,经过处理,获取图书信息并展示出来,用户可以对图书贡献进行确认,如图6.3所示。

    6.3借阅图书微信图书借阅平台是一个提供用户共享书籍的平台,用户可以通过扫码借阅公共书架上的书籍。在平台上,即点击借阅图书菜单,进入“扫一扫”界面。通过扫码图书,平台获取图书ISBN号,获取图书信息,最后跳转到借阅图书确认界面。
    借阅图书界面显示当前书籍信息(若要凑字,可以多描述一下),图书总量、已借阅、剩余等信息,如果当前书架上有可以借阅的书籍,确认借阅按钮可用,如图6.4所示;否则,确认按钮不可用,如图6.5所示。



    借阅图书(可借)
    借阅图书(不可借)









    6.4归还图书微信图书借阅平台是一个提供用户共享书籍的平台,用户阅读完书籍可以通过扫码图书,归还公共书架上的书籍。在平台上,即点击归还图书菜单,进入“扫一扫”界面。通过扫码图书,平台获取图书ISBN号,获取图书信息,最后跳转到归还图书确认界面。
    归还图书菜单按钮提供了用户归还图书的功能入口,点击归还图书,便进入扫一扫界面,可收集图书的信息。点击确认归还按钮,可以归还图书,如图6.6所示。

    6.5 浏览图书微信图书借阅平台是一个提供用户共享书籍的平台,用户可以添加书籍到公共书架、借阅书籍、归还书籍、浏览书籍。浏览书籍功能菜单直接跳转到书籍列表,展示当前书架上都有哪些书籍,点击书记项进入书籍详情页,查看书籍详细信息。
    在平台上,点击浏览图书菜单,进入图书列表界面,如图6.7所示。通过点击书籍项目,进入详情页。详情页展示书籍详细信息,包括书籍名称、作者、图书ISBN号等书籍信息,还包括书籍当前借阅情况,例如总量、已借阅、剩余,如图6.8所示。



    图书列表
    图书详情









    参考文献[1] NicholasS.Williams.Java Web高级编程.北京:清华大学出版社,2015
    [2] 列旭松,陈文.PHP核心技术与最佳实践.北京:机械工业出版社,2012
    [3]王志刚.PHP5应用实例详解:使用Zend Framework&Smarty构筑真正的MVC模式应用.北京:电子工业出版社,2010
    [4] JezHumble,David Farley.持续交付:发布可靠软件的系统方法.北京:人民邮电出版社,2011
    [5] 王彦平,吴盛峰.网站分析实战.北京:电子工业出版社,2013
    [6] 秦小波.设计模式之禅(第2版).北京:机械工业出版社,2014
    [7] 许勇.Ruby on Rails 程序设计深入剖析与范例应用.北京:清华大学出版社,2013
    [8] CraigWalls.Spring实战(第3版).北京:人民邮电出版社,2013
    [9] Harry J.W.Percival.Python Web开发:测试驱动方法.北京:人民邮电出版社,2015
    [10] MarkSimon.Introduction to Ajax:Client Server Communications on the Web.O’Reilly Media,2016
    [11] 司徒正美.JavaScript 框架设计.北京:人民邮电出版社,2014
    [12] 张欣毅.XML简明教程.北京:清华大学出版社,2009
    [13] the5fire.Backbone.js入门实战:WEB端MVC框架开发单页应用实战.北京:机械工业出版社,2014
    [14] YakovFain,Anton Moiseev.Angular 2 Development with TypeScript.Manning Publications,2016
    [15] LeonardRichardson,Mike Amundsen,Sam Ruby.RESTful Web APIs.O’Reilly Media,2013
    [16] SemmyPurewal.Learning Web AppDevelopment.O’Reilly Media,2014
    [17] EthanBrown.Web Development with Nodeand Express.O’Reilly Media,2014
    1 评论 7 下载 2018-10-02 21:05:13 下载需要14点积分
  • VC++实现的仿QQ和飞秋并支持语音视频白板屏幕共享的即时聊天软件

    第2章 系统分析及开发技术说明2.1 需求分析2.1.1 功能需求分析

    用户端的基本聊天信息发送,这些基本聊天信息包括文本和图片。文本和图片聊天是聊天软件最基础的功能。用户通过输入IP来查找用户,并申请加为好友,在对方同意加为好友后,在线用户列表就会更新用户,把加入的用户添加到用户列表中。这样,两个用户之前就可以实现通信了。在信息传输中,预计利用TCP/IP协议中的UDP协议,这是面向无连接的协议,但发送速度快,用于聊天信息传输用适合。用户端的音视频数据传输,这是本设计的扩展功能。用户可以正常通信后,就可以选择是否进行语音或视频聊天。本功能也将采用UDP协议,UDP协议可能会丢失数据,但对于音视频聊天需要传输大量数据但又允许丢失少量数据的情况下,UDP的快速发送信息的特点就得到很好的体现。用户端文件传输的功能,用户之间可以断点续传文件。在传文件之前,首先创建一信息文件,记录文件传送的一些信息,并根据传输的数据量实时修改。如果没有传完,下次就可以继续打开这个信息文件,接着上次的进度传输。因为文件传输入要求数据不能出错,因此此模块采用TCP协议。用户端之间白板和共享屏幕的功能,这个功能有些类似视频的传输,因些并不是很难,可以在视频传输的功能上加以修改。用户之间后台的连接,每个用户隔指定时间会向用户列表中的每一用户发送消息,查看用户是否在线,如果不在线,就更新用户列表,删除用户。
    2.1.2 数据需求分析
    客户端之间聊天信息。在控件上显示时格式化,更易于用户的查看自己发送或接收到的信息。在线用户列表信息。服务器端存放在适当的空间中,在发送给客户端时,对信息列表进行格式化,便于客户端提取信息。客户端向服务器端发送的确认在线信息。包括客户端刚刚启动时的初始化信息和在使用过程中的确认在线信息。其它程序内部可能需要设计的数据结构体。
    2.1.3 性能需求分析
    可靠性高,能在由于系统问题或其它原因产生错误后,作出相对应处理,比如网络初始化失败、服务器不在线等,可以提示用户安全退出本程序,在出现不可知的错误以后,可以尽量安全的退出程序。在程序的设计过程中,要求能尽可能多的设想到用户使用过程中可能发生的事件,并能在判断事件后做出相应的处理,使程序具有较高的容错性能。宜操作性,程序简单易懂,容易上手使用。设计界面是,简化界面的复杂性,模拟QQ等现有即时通讯工具的界面,使用户能很容易看懂并使用。开发文档易理解,保证以后自己二次开发或他人接手开发时,能够清晰的理解整个系统的设计思路和实现细节。模块化设计此软件的功能,不同的模块实现不同的功能,使得软件易于以后的维护与扩展,在以后可以更好的完善本软件的功能,更方便于在工作中的应用。
    2.1.4 运行需求分析
    用户界面:程序较小,启动速度快,无启动界面。在本地局域网中使用,所以无需进行用户登录,无需认证界面,启动后的应用界面要清爽,设计要简单明了,要具有较高的易用性。故障处理:在遇到可预知的故障与情况时,能提示用户并自动退出;在遇到不可预知的故障时能安全退出。
    2.4 Winsock网络编程Windows Sockets是从Berkeley Sockets扩展而来的,其在继承Berkeley Sockets的基础上,又进行了新的扩充。这些扩充主要是提供了一些异步函数,并增加了符合WINDOWS消息驱动特性的网络事件异步选择机制。
    Windows Sockets由两部分组成:开发组件和运行组件。

    开发组件:Windows Sockets 实现文档、应用程序接口(API)引入库和一些头文件。运行组件:Windows Sockets 应用程序接口的动态链接库(WINSOCK.DLL)。
    2.4.1 Socket套接字(Socket)最初是由加利福尼亚大学Berkeley分校为UNIX操作系统开发的网络通信接口,随着UNIX操作系统的广泛使用,套接字成为当前最流行的网络通信应用程序接口之一。90年代初,由SunMicrosystems,JSBCorporation,FTP software,Microdyne和Microsoft等几家公司共同制定了一套标准,即Windows Sockets规范。
    Windows Sockets API是Microsoft Windows的网络程序设计接口,它在继承了Berkeley Sockets主要特征的基础上,又对它进行了重要扩充。这些扩充主要是提供了一些异步函数,并增加了符合Windows消息驱动特性的网络事件异步选择机制。这些扩充有利于应用程序开发者编制符合Windows编程模式的软件,它使在Windows下开发高性能的网络通信程序成为可能。
    Socket实际上是指一个通信端点,借助于它,用户所开发的Socket应用程序,可以通过网络与其它Socket应用程序进行通信。
    近年来,随着计算机网络与Windows 95的流行,许多用户所开发的应用程序需要实现网络间的数据通信。
    2.4.2 开发Windows Sockets网络通信程序的软、硬件环境所采用的操作系统软件可以是Windows 95,2000,XP,也可以是Windows NT,因为它们都支持Windows Sockets API,在以下的介绍中,我们将以在Windows XP环境下的开发为例。
    所采用的编程语言一般可选目前较流行使用的可视化和采用面向对象技术的Microsoft Visual C++ 6.0。Visual C++ 6.0可在Windows XP或Windows NT环境下运行,其开发系统增加了全面集成的基于Windows 的开发工具以及一个基于传统C/C++开发过程的“可视化”用户界面驱动模型。Visual C++ 6.0中的Microsoft基类(MFC,即MicrosoftFoundation Class)库是一系列C++类,其中封装着为Microsoft Windows操作系统系列编写应用程序的各种功能 。在有关套接字方面,Visual C++ 6.0对原来的Windows Sockets库函数进行了一系列封装,继而产生了CSocket 、CSocketFile等类,它们封装着有关Socket的各种功能。
    所采用的网络通信协议一般是TCP/IP。Windows XP和Windows NT都带有该协议。但是,所开发的网络通信应用程序并不能直接与TCP/IP核心打交道,而是与网络应用编程界面Windows Sockets API打交道。Windows Sockets API则可直接与TCP/IP核心进行沟通。TCP/IP核心协议连同网络物理介质(如网卡)一起,都是提供网络应用程序间相互通信的设施。
    2.4.3 CSocket类编程模型使用CSocket对象涉及CArchive和CSocketFile 类对象。以下介绍的针对字节流型套接字的操作步骤中,只有第三步对于客户方和服务方操作是不同的,其他步骤都相同。

    构造一个CSocket对象。 使用这个对象的Create()成员函数产生一个socket对象。在客户方程序中,除非需要数据报套接字,Create()函数一般情况下应该使用默认参数。而对于服务方程序,必须在调用Create时指定一个端口。需要注意的是,Carchive类对象不能与数据报(UDP)套接字一起工作,因此对于数据报套接字,CAsyncSocket和CSocket 的使用方法是一样的。 如果是客户方套接字,则调用CAsyncSocket∷Connect()函数与服务方套接字连接;如果是服务方套接字,则调用CAsyncSocket∷Listen()开始监听来自客户方的连接请求,收到连接请求后,调用CAsyncSocket∷Accept()函数接受请求,建立连接。请注意Accept()成员函数需要一个新的并且为空的CSocket对象作为它的参数,解释同上。 产生一个CSocketFile对象,并把它与CSocket 对象关联起来。为接收和发送数据各产生一个CArchive对象,把它们与CSocketFile对象关联起来。切记CArchive是不能和数据报套接字一起工作的。 使用CArchive对象的Read()、Write()等函数在客户与服务方传送数据。通讯完毕后,销毁CArchive、CSocketFile和CSocket对象。
    2.4.4 用VC6.0进行Windows Sockets程序开发的技术要点
    同常规编程一样,无论服务器方还是客户方应用程序都要进行所谓的初始化处理,这部分工作仍可采用消息驱动机制来先期完成。一般情况下,网络通信程序是某应用程序中的一模块。在单独调试网络通信程序时,要尽量与采用该通信模块的其它应用程序开发者约定好,统一采用一种界面形式,即单文档界面SDI、多文档界面MDI和基于对话框界面中的一种(这在使用AppWizard形成项目[Project]文件时有提示),尽管这并非必须,但可使通信模块在移植到所需的应用程序时省时省力,因为Visual C++ 6.0这种可视化语言在给我们提供方便的同时,也给我们带来某些不便,譬如所形成的项目文件中的许多相关文件与所采用的界面形式密切联系,许多消息驱动功能,随所采用的界面形式不同而各异。当然,也可将通信模块函数化,并形成一个动态连接库文件(DLL文件),供主程序调用。 以通信程序作为其中一个模块的应用程序往往不是在等待数据发送或接收完之后再做其它工作,因而在主程序中要采用多线程(Multithreaded)技术。即将数据的发或收,放在一个具有一定优先级(一般宜取较高优先级)的辅助线程中,在数据发或收期间,主程序仍可进行其它工作,譬如利用上一个周期收到的数据绘制曲线。Visual C++ 6.0中的MFC提供了许多有关启动线程、管理线程、同步化线程、终止线程等功能函数。在许多情况下,要求通信模块应实时地收、发数据。譬如调用之的主程序以0.5秒为一周期,在这段时间内 ,要进行如下工作:接收数据,利用收到的数据进行运算,将运算结果发送到其它计算机节点,周而复始。我们在充分利用Windows Sockets的基于消息的网络事件异步选择机制,用消息来驱动数据的发送和接收的基础上,结合使用其他措施,如将数据的收和发放在高优先级线程,在软件设计上,安排好时序,尽量避免在同一时间内,双方都在向对方发送大量数据的情况发生,保证网络要有足够的带宽等,成功地实现了数据传输的实时性。
    第3章 详细设计本章将从各个方面介绍本系统的设计。先从基本框架的设计出发,然后逐步介绍好友管理、聊天模块、聊天室模块、传送文件模块、共享屏幕模块、白板模块、音、视频模块和调试模块,所以本章是本论文的重点。
    3.1 基本框架设计本节内容将介绍除各个功能模块外的设计,包括界面上的处理、保持好友在线列表等的处理。有些内容可能并不属性框架设计,但这些内容也不具有单独使用一节来介绍的必要,所以把这些内容也一并放到这些节来介绍。这也是为了区分设计周围的处理与各个功能模块的处理。
    3.1.1 宏和数据结构的定义程序中用到了很多宏和数据结构,这些宏和数据结构在多个模块中都有用到,因此程序中专门新建一个头文件Global.h,此头文件里是程序中很到的宏和数据结构的定义。在StdAfx.h文件包含Global.h,在程序其他地方都可以使用Global.h中的宏和定义的数据结构。这样处理还有一个好处,如果需要修改某些宏的值,可以直接在Global.h中修改,而不用到处去找宏的定义,方便和快捷。
    3.1.2 程序配置文件程序中很多信息需要保存,比如用户名和热键,因些程序用到了配置文件,默认的配置文件名为conf.ini。程序用读取和写入配置文件系列函数来管理此配置文件。
    3.1.3 主界面初始化用过QQ的人都知道,QQ主面板总是处于其他程序的上面,而且QQ在任务栏没有图标,而是把图标放到了托盘区,另外,我们还可以按Ctrl+Alt+Z默认的快捷键隐藏和显示QQ主面板。不但QQ是这样处理的,很多聊天软件都采用此种处理方式。本设计也不例外,同样也要达到这样的目的。
    下面从各个方面来说明本设计的处理方式:
    1.不在任务栏显示图标
    CDialog dlgParent;dlgParent.Create( IDD_DIALOG_BG );dlgParent.ShowWindow( SW_HIDE );CInstantMessagingDlg dlg( &dlgParent );m_pMainWnd = &dlg;ModifyStyleEx( WS_EX_APPWINDOW, 0 );
    上面代码就达到了程序主界面不在任务显示的目的。首先,我们创建一个对话框,并隐藏此对放框,然后把这个对话框作为主界面对话框的父窗口,然后在主界面对话框的初始化函数中修改其风格,去掉WS_EX_APPWINDOW风格。这样,主界面就不会出现在任务了。
    2.将主界面放在最上层
    将程序放到顶层,很多程序都有这功能,比如金山词霸等,实现起来其实很简单,只用一条语句就可以达到目的:
    SetWindowPos( &wndTopMost, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE );
    第一个参数就是将程序放到所有非顶层窗口的顶层,如果有多个程序都是顶层窗口,那么他们谁在上面,就要看当前谁是激活的窗口。最后一个参数,是用位或|组后起来的,从字面意思上我们就能理解到这是不移动不改变大小的意思,忽略了当中的4个参数。
    3.热键的处理
    设计中默认的热键是Ctrl+Alt+Z,当然程序允许用户自己定义热键,自定义的热键将保存在conf.ini文件中。热键的功能可以隐藏、显示主界面,有消息到达时,按热键也可以打开聊天对话框。
    ::RegisterHotKey( m_hWnd, IDHOTKEY, m_wModifiers, m_wVirtualKeyCode );
    使用全局函数RegisterHotKey可以注册热键,如果注册的热键没有被其他程序占用,那么注册成功。注册成功后,如果按热键,那么程序就会接受到WM_HOTKEY消息,因此我们还需要自己处理WM_HOTKEY消息:
    void OnHotkey( WPARAM wParam, LPARAM lParam );BEGIN_MESSAGE_MAP(CInstantMessagingDlg, CDialog) … ON_MESSAGE( WM_HOTKEY, OnHotkey ) … //}}AFX_MSG_MAPEND_MESSAGE_MAP()
    在消息映射中,我们用OnHotkey()函数来处理WM_HOTKEY消息。
    void CInstantMessagingDlg::OnHotkey( WPARAM wParam, LPARAM lParam ){ if( this->IsWindowVisible() ) { ShowWindow( SW_HIDE ); } else { ShowWindow( SW_SHOW ); ::SetForegroundWindow( m_hWnd ); }}
    在OnHotkey()函数中判断主界面是否是可见的,如果是可见的那么隐藏起来,否则显示,并且把主界面设为前景窗口。
    4.最小化和关闭按钮的处理
    我们希望单击程序右上角的最小化按钮时,程序隐藏起来,而单击关闭按钮时,程序会提示是否退出,而不会悄无声息的退出。
    void CInstantMessagingDlg::OnSysCommand(UINT nID, LPARAM lParam){ …if( nID == SC_MINIMIZE ) { this->ShowWindow( SW_HIDE); } else { CDialog::OnSysCommand(nID, lParam); }}void CInstantMessagingDlg::OnCancel() { if( IDOK == MessageBox( "要退出吗?", "退出", MB_OKCANCEL | MB_ICONINFORMATION | MB_DEFBUTTON2 ) ) { … DestroyWindow(); }}
    在程序中处理OnSysCommand()函数和OnCancel()函数就实现了我们要的功能。
    5.托盘图标的显示
    至此,程序已不在任务栏显示图标,已是最顶层窗口,而且也已有热键功能,但是还没有实现托盘图标的显示。实现托盘图标的代码如下:
    NOTIFYICONDATA m_nid;HICON hIcon = AfxGetApp()->LoadIcon( STATE_ONLINE );m_nid.hIcon = hIcon;m_nid.hWnd = m_hWnd;m_nid.cbSize = sizeof( NOTIFYICONDATA );m_nid.uCallbackMessage = WM_SHELLNOTIFY;m_nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;m_nid.uID = IDR_MENU1;sprintf( m_nid.szTip, "即时聊天软件" );Shell_NotifyIcon( NIM_ADD, &m_nid );
    这样我们就在托盘区显示了STATE_ONLINE的图标,把鼠标移动到图标上一会,还会出现“即时聊天软件”的提示框。如果你们对图标有单击和双击等操作,程序会收到WM_SHELLNOTIFY消息,因此,我们还必须处理WM_SHELLNOTIFY消息。
    void OnShellNotifyProc( WPARAM wParam, LPARAM lParam );BEGIN_MESSAGE_MAP(CInstantMessagingDlg, CDialog) //{{AFX_MSG_MAP(CInstantMessagingDlg) … ON_MESSAGE( WM_SHELLNOTIFY, OnShellNotifyProc ) … //}}AFX_MSG_MAPEND_MESSAGE_MAP()void CInstantMessagingDlg::OnShellNotifyProc( WPARAM wParam, LPARAM lParam ){ if( lParam == WM_LBUTTONDBLCLK ) { … } else if( lParam == WM_RBUTTONUP ) { … }}
    与热键处理一样,OnShellNotifyProc()函数响应我们对图标的操作,其中lParam参数表示消息号,在本设计中只处理左键双击(显示主界面)和右键单击(弹出菜单)。
    在托盘添加图标,退出程序前,如果没有从托盘删除图标,那么托盘区的图标会一直保留下来,直到鼠标移过托盘区引起托盘区的重绘,这当然不是我们所希望的结果。
    void CInstantMessagingDlg::OnDestroy() { // 删除在托盘建立的图标 ::Shell_NotifyIcon( NIM_DELETE, &m_nid ); CDialog::OnDestroy();}
    以上代码在程序退出时调用,从托盘从删除图标。
    6.只允许运行唯一实例
    这点与QQ不同,在一台机子上可以运行多个QQ,但本程序只允许运行一个实例。只允许运行一个实例,有多种方法,本设计采用的是创建命名事件的方法:
    HANDLE hEvent = ::CreateEvent( NULL, FALSE, FALSE, "InstantMessaging" );if( hEvent ){ if( ERROR_ALREADY_EXISTS == GetLastError() ) { return FALSE; }}
    事件与普通变量不一样,普通变量只在运行的当前程序中有效,而事件在整个系统中都有效。当首次运行程序时,会创建一个名为“InstantMessaging”的事件,这个事件在系统范围内有效,当再次运行程序时,程序会尝试着创建同名的事件,因为之前已经创建了这个事件,因此系统会返回之前创建事件的句柄,但GetLastError()会返回ERROR_ALREADY_EXISTS,表明需创建的事件之前已经创建,为了保证只允许一个实例,这个实例就不再允许运行,直接返回,退出程序。
    3.1.3 主界面布局程序主界面如下:

    左上角显示的是自己的头像、状态和昵称;右上角的列表框是查找IP输入框,下面是添加按钮;在下面一点的列表框是自己的址列表框,显示了自己的所有IP;主界面中央是用户列表框;最下面是4个功能按钮。
    用户列表框是一列一列的显示添加的好友,最左边是好友的头像;中部上边是好友的昵称,下面是好友的IP;右下角是删除好友按钮和摄像头按钮,当然好友必须有摄像头才会显示摄像头按钮。
    有两种方式添加联系人:

    在右上解的查找IP输入框里输入IP,然后单击下面的添加按钮。也可以从IP输入框里选择以前加过的好友IP。程序允许保存10个最近联系人的IP,当新添加联系人时,如果已保存了10个联系人的IP,程序会按照时间的先后顺序覆盖之前的IP。在列表框展开下拉列表后,可以按DELETE键删除选定的IP。选定一个自己的IP,然后单击“网段”按钮。此功能可以向选定IP的IP段发送添加请求的消息,这相当于批量添加好友的功能。
    在主界面任何地点单击左键不放开,可以拖动程序;单击右键,会弹出菜单,用户选中相应的菜单项,可以执行相应的功能;在任何地点双击左键,可以打开“个人设置”对话框,如下:

    最后一项“允许别人直接将我加为联系”的意思是别人添加我为好友时,不会弹出请求对话框而直接加为好友。
    单击保存后,此对话框里的内容会保存到config.ini配置文件中。运行程序后,会从config.ini读取用户信息,并在主界面中作相应的设置。
    在主界面,添加、聊天室、传送文件、共享屏幕和白板按钮都是自绘按钮,可以显示图片,有提示能力,当鼠标移动到其上一会儿后,会弹出提示框,而且这些按钮都具有XP风格,既鼠标滑过时会显示不同的状态。用户列表框也是自绘的,普通的列表控件无法显示我们所需的信息。自绘按钮和自绘列表框会作为一个单独的模块来介绍,这儿就不作过多的介绍。
    3.1.4 自绘按钮VC++6.0自带的按钮控件不具有XP风格,而且也不能显示图像,作为一款好的软件,应该有个好的界面。在程序的主界面上,主要的按钮都采用了自绘按钮,而不使用自带的按钮控件。
    AdvButton.h和AdvButton.cpp是自绘按钮类的头文件和实现文件。
    在自绘按钮类中定义了如下成员变量:
    int m_nState; // 按钮的状态CBitmap m_bmpNormal; // 正常图标CBitmap m_bmpHover; // 焦点图标CBitmap m_bmpDown; // 按下图标CBitmap m_bmpDisable; // 无效图标CToolTipCtrl m_pToolTipCtrl; // 提示类
    m_nState表示当前按钮的状态,可以为宏:NORMAL,HOVER,DOWN,DISABLE,分别表示按钮正常状态、处于焦点状态、按下状态、无效状态,这4个宏的定义在实现文件。4个CBitmap的变量分别存储4种状态下的图像。m_pToolTipCtrl是提示工具控件类,既是鼠标在其上时,会弹出提示窗口。
    要实现按钮自绘,必须更新按钮的风格为自绘,可以在按钮的属性中更改,也可以使用代码更改。重载PreSubclassWindow(),在这个函数中更改按钮风格并初始化m_pToolTipCtrl。
    void CAdvButton::PreSubclassWindow() { ModifyStyle( 0, BS_OWNERDRAW ); CButton::PreSubclassWindow(); m_pToolTipCtrl.Create( this, TTS_ALWAYSTIP ); m_pToolTipCtrl.SetDelayTime( 100 ); CString strText; GetWindowText( strText ); m_pToolTipCtrl.AddTool( this, strText ); }
    VC++6.0中的ClassWizard不能为我们添加鼠标离开的消息,只能为我们添加鼠标移动、单击等消息,我们得自己为自绘按钮添加上鼠标离开的消息。
    TRACKMOUSEEVENT tme;tme.cbSize = sizeof( TRACKMOUSEEVENT );tme.hwndTrack = m_hWnd;tme.dwFlags = TME_LEAVE;::_TrackMouseEvent( &tme );
    以上代码告诉系统,当鼠标离开m_hWnd窗口时,向这个窗口发送一条WM_MOUSELEAVE消息。下面的处理方式与热键和托盘通知消息的处理方式一样,自定义这个消息处理函数就行了。
    自绘按钮必须重载DrawItem()函数,在DrawItem()函数中根据m_nState的值可以贴上不同的图,表示按钮的一不同状态。
    void CAdvButton::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct) { if( lpDrawItemStruct->itemState & ODS_DISABLED ) { m_nState = DISABLE; } switch( m_nState ) { case NORMAL: DrawNORMAL(); break; case HOVER: DrawHOVER(); break; case DOWN: DrawDOWN(); break; case DISABLE: DrawDISABLE(); break; default: break; }}
    DrawNORMAL() 、DrawHOVER ()、DrawDOWN() 、DrawDISABLE()分别画按钮的4种状态。当鼠标滑过或单击按钮时,更改m_nState的值,然后调用Invalidate(),强制按钮重绘。要使按钮无效,必须调用EnableWindow(FALSE )函数来更改按钮的状态,我们也就无法更改m_nState的值。lpDrawItemStruct->itemState的值表示了当前按钮的状态,可以检测lpDrawItemStruct->itemState,如果按钮是无效状态,则设置m_nState为DISABLE,否则不作改变。
    在画按钮的状态时,使用到了TransparentBlt()函数,这个函数可以贴透明位图。在TransparentBlt()最后一个参数中指定掩码色,贴图时掩码色就不会贴出来。要使用此函数,必须导入 msimg32.dll,在程序使用如下语句导入:
    #pragma comment( lib, "MSIMG32.LIB" )
    3.1.5 自绘好友列表框普通的列表控件无法满足程序的要求,程序要求好友列表框可以显示好友的头像、好友昵称、好友IP和删除、摄像头按钮。
    FriendsListCtrl.h和FriendsListCtrl.cpp是自绘列表框的头文件和实现文件。
    定义的成员变量如下:
    CInstantMessagingDlg *m_pMainDlg; // 主对话框CImageList m_imageList; // 头像图像列表int m_nCamera; // 摄像头激活的序号int m_nDelIcon; // 删除按钮激活的序号int m_nCurSel; // 当前选中用户序号
    与自绘按钮类似,在PreSubclassWindow()函数中更改列表框的风格为自绘:
    void CFriendsListCtrl::PreSubclassWindow() { ModifyStyle( 0, LVS_OWNERDRAWFIXED ); … CListCtrl::PreSubclassWindow();}
    重载MeasureItem()函数更改列表框每一项的高度:
    void CFriendsListCtrl::MeasureItem( LPMEASUREITEMSTRUCT lpMeasureItemStruct ){ lpMeasureItemStruct->itemHeight = DEFAULT_FRIENDSLIST_HEIGHT;}
    宏DEFAULT_FRIENDSLIST_HEIGHT在Global.h文件定义,表示好友列表框每项的高度。
    列表框中的鼠标离开消息与自绘按钮的实现是同一个原理,这儿就不再赘述。
    在向好友列表框中添加好友时,主对话框调用好友列表框的AddUser()函数,参数为USER结构体,这个参数作为列表项的额外数据,这样重绘的时候再读取出这个额外数据就可以得到这一项的用户信息。
    鼠标在好友列表框上移动时,判断鼠标是否在删除或摄像头(如果有摄像头)按钮范围内,如果在,就设置m_nDelIcon或m_nCamera为当前项的序号,否则就设置m_nCurSel为当前项的序号。
    用户双击鼠标时,调用主对框的相应函数,并把项的序号传给此函数。如果是单击,先判断m_nDelIcon、m_nCamera值,如果不为-1,则选中了删除或摄像头按钮,调主对框相应函数执行相应操作。
    在DrawItem()函数中,先得到额外的附加数据,既是添加项时作为参数传递的USER类型的变量,然后再根据m_nCurSel、m_nDelIcon和m_nCamera的值自绘。在自绘时,为了防止闪烁,程序用到了双缓冲技术。
    双缓冲技术,就是先创建一个与目标设备兼容的内存设备上下文,在内存设置上下文中画图或进行其他处理,操作完成了,再一并把内存设备上下文的内容贴到目标设备上,这样就可以有效的防止闪烁。
    3.2 好友管理好友管理包括添加好友、删除好友以及与好友保持连接。
    3.2.1 添加好友添加好友的两种方式4.1节已经介绍过了,这一节介绍的是具体的实现。
    首先然主对话框定义监听Socket并初始化:
    CListeningSocket *m_pLisSocket;m_pLisSocket = new CListeningSocket( this );m_pLisSocket->Create( DEFAULT_PORT, SOCK_DGRAM );
    在添加按钮的响应函数中先判断IP地址是否合法,是否是自己的IP,是否已经添加此好友。如果可以添加此好友,则向此好友发起请求加为好友的请求:
    // 定义数据包DATAPACKET dataPacket;dataPacket.command = REQUEST_ADD;// 设置请求的用户结构USER user;user.bHasCamera = m_bCamera;user.nFace = m_wFace;memcpy( user.strName, m_strNickName.GetBuffer( MAXNICKNAMELENGTH + 1 ), MAXNICKNAMELENGTH + 1 );m_strNickName.ReleaseBuffer();// 分配空间UINT uDataLength = sizeof( DATAPACKET ) + sizeof( USER );BYTE *pSendData = new BYTE[ uDataLength ];memcpy( pSendData, &dataPacket, sizeof( DATAPACKET ) );memcpy( pSendData + sizeof( DATAPACKET ), &user, sizeof( USER ) );// 发送请求m_pLisSocket->SendTo( pSendData, uDataLength, DEFAULT_PORT, strFriendIP );delete pSendData;
    以上代码中的宏都可以在Global.h头文件中找到。向好友发起请求的数据中,还包括自己的USER数据。
    向对方发起请求后,对方的m_pLisSocket就会调用OnReceive()函数,程序中重载了CListeningSocket类的OnReceive()函数,在OnReceive()中调用主对对话框的OnListeningReceive()函数来接收网络数据。
    在OnListeningReceive()函数中,根据DATAPACKET的command值来进行相应的处理,这儿是添加为好友的请法度,先得到发起请求的USER,再调用AddRequest()函数做相应处理。
    在AddRequest()函数中,先进行相应的判断,如果具备加为好友的条件,根据是否允许直接加为好友的值是否弹出提示对话框。如果拒绝加为好友,则向发送者发送拒绝的消息。否则先把发起者的USER信息加入到好友列表中,再向请求者发送允许加为好友的消息,消息中包括自己的USER信息。
    请求者收到对方的拒绝消息,会弹出对话框提示对方拒绝加为好友;如果收到的是允许加为好友的消息,则再得到一同发送过来的USER信息,再加入到列表中。
    在InstantMessaging.h头文件中定了如下变量:
    CArray< USER, USER > m_arrFriends; // 好友列表
    此数组保存的是已连接的好友,添加好友时会向这个变量里添加USER信息,并把USER添加到好友列表框中。
    请求者添加好友流程图如下:

    好友收到请求的流程图如下:

    3.2.2 删除好友删除好友比较简单,在服务器端向要删除的好友发送命令为OFFLINE的消息,然后从好友数组和好友列表框中删除此好友。
    在客户端接受到OFFLINE的网络消息时,得到发送此消息的IP,然后从好友数据和好友列表框中删除与此IP相等的好友。
    另外,当用户改变自己的状态为下线状态或关闭程序时,会调用SendOffLineMessage()函数向所有好友发送下线消息,并删除所有的好友。
    3.2.3 与好友保持连接此功能用到了定时器,间隔一定时间向所有好友发起请求保持连接的消息,并把发送过此消息的好友添加到一个临时好友列表中。好友收到请求保持连接的消息后,会发送回应保持连接的消息。程序收到回应保持连接的消息后,会从临时好友列表中删除对应的好友。在下一次定时器到的时候,程序检查临时好友列表,在临时好友列表中的好友都是没有回应的的好友,这些好友可能是因为程序不正常关闭而未向其他好友发送下线通知,程序就可以将这些好友删除。在程序初始化时设置一定时器:
    SetTimer( TIMER_CONNECT, DEFAULT_REFRESH_TIME, NULL );
    在OnTimer()函数中进行如下处理:
    // 保持接连if( TIMER_CONNECT == nIDEvent ) { // 删除没有回应的联系人 for( int nIndex = 0; nIndex < m_arrFriends.GetSize(); nIndex++ ) { USER userDel = m_arrFriends.GetAt( nIndex ); if( m_strlstKeepConnent.Find( userDel.strIP ) ) { m_listCtrlFriends.DeleteUser( nIndex ); m_arrFriends.RemoveAt( nIndex ); nIndex--; } } m_strlstKeepConnent.RemoveAll(); // 分别发送保持连接的消息,将发送过的IP加入到m_strlstKeepConnent for( nIndex = 0; nIndex < m_arrFriends.GetSize(); nIndex++ ) { USER user = m_arrFriends.GetAt( nIndex ); DATAPACKET dataPacket; dataPacket.command = REQUEST_KEEPCONNECT; // 分配空间 UINT uDataLength = sizeof( DATAPACKET ); BYTE *pSendData = new BYTE[ uDataLength ]; memcpy( pSendData, &dataPacket, sizeof( DATAPACKET ) ); m_pLisSocket->SendTo( pSendData, uDataLength, DEFAULT_PORT, user.strIP ); delete pSendData; m_strlstKeepConnent.AddTail( user.strIP ); }}
    3.3 聊天模块聊天包括文字聊和图片聊天,本系统用到了Microsoft Rich Textbox Control 6.0控件,此控件支持RTF(Rich TextFormat)格式的内容,包括不同颜色、不同字体字号的文本和图片。要使用此控件,系统中必须注册了此控件,如果未注册此控件,那么打开聊天对话框时程序会死掉。为了解决这个问题,本设计在程序中检查系统是否注册过richtx32.ocx控件,如果没有注册,程序会先注册,代码如下:
    HKEY hKey;if( RegOpenKeyEx( HKEY_CLASSES_ROOT, "RICHTEXT.RichtextCtrl\\CLSID", 0, KEY_READ, &hKey ) != ERROR_SUCCESS ) { HINSTANCE hLib = LoadLibrary( "RICHTX32.OCX" ); // 控件不存在 if( !hLib ) { MessageBox( "RICHTX32.OCX控件未找到" ); } else { // 获取注册函数DllRegisterServer地址 FARPROC lpDllEntryPoint; lpDllEntryPoint = GetProcAddress( hLib, "DllRegisterServer" ); // 注册richtx32.ocx控件 lpDllEntryPoint(); }}
    聊天对话框如下:

    聊天对话框用的Socket是主对话框的m_pLisSocket,当要发送消息时,调用主对话框的SendPreChatMessage()函数,主对话框接收到聊天消息时调用聊天对话框的ReceiveMessage()函数。
    聊天对话框中定的发送和接收富文本框变量为:
    CRichText m_rtReceived;CRichText m_rtSend;
    发送消息的主要代码如下:
    void CChatDlg::OnSend() { CTime time = CTime::GetCurrentTime(); CString strTime = time.Format( "%H:%M:%S" ); … CString strSend = m_rtSend.GetTextRTF(); // 发送聊天消息 m_pMainDlg->SendPreChatMessage( m_userChat, strTime, strSend );}
    单击“发送”按钮或CTRL+ENTER键,程序会调用OnSend()函数,首先得到发送的时间和发送的内容,得到的是RTF格式的内容,因此strSend中还包括了文字的格式和图片信息。然后调用主对话框的SendPreChatMessage()函数向m_userChat好友发送聊天消息。
    接收到的消息主要代码如下:
    void CChatDlg::ReceiveMessage( LPCSTR szTime, LPCSTR szMessage ){ CString strText; strText.Format( "%s(%s) %s\r\n ", m_userChat.strName, m_userChat.strIP, szTime ); // 设置接收框 m_rtReceived.SetSelStart( m_rtReceived.GetTextRTF().GetLength() ); m_rtReceived.SetSelText( strText ); m_rtReceived.SetSelRTF( szMessage ); …}
    因为聊天用的Socket是主对话框的m_pLisSocket,因此是主对话框接收到的聊天消息。主对话框接收到聊天消息后,根据消息发送的来源IP把处理消息。如果与此IP的聊天窗口打开的,就直接调用ReceiveMessage()把消息放入接收文本框中,如果聊天窗口没有打开,则把消息追加到CMapStringToOb类型的变量m_mapIPToChat中,并在托盘区动态显示用户的头像。
    聊天消息包括文字格式和图像信息,因此发送的数据可能很大,会超过Socket发送的最大数据长度,这样如果直接发送,会因为数据长度过大,而导致发送失败。所以在发送这种大数据量的消息时,本程序采用的分包发送的方式。
    上面提到了发送的最大数据长度,m_pLisSocket是创建的UDP套接字,不像TCP一样可以发送随意大小的数据,UDP套接字只能发送小于一定大小的数据。UDP可以发送的最大数据量可以由下面的代码得到:
    WSADATA wsaData;if (!AfxSocketInit( &wsaData )){ AfxMessageBox(IDP_SOCKETS_INIT_FAILED); return FALSE;}
    初始化WinSocket后,WSADATA结构中就有我们需要的信息,wsaData.iMaxUdpDg就是UDP可以发送的最大数据量。主对话框定义了一变量,设置成wsaData.iMaxUdpDg的值,在用UDP发送数据的时候,就可以根据这个值的大小来确定分包的大小。
    聊天对话框发送消息时调用主对话框的SendPreChatMessage()函数,这个函数并不是一下子将消息发送出去,而是将消息保存起来,只发送一个通知消息告诉对方有数据需要发送,这个通知消息中带有消息的时间的消息的长度。
    对方接收到这个通知消息时,会根据接收到的消息长度分配一个接收消息的临时空间,并把接收到的消息时间也保存。然后向消息发送者发送请求接收第一个数据包的请求。
    消息发送者收到请求发送消息的消息后,会把相应的数据包发送给聊天对方。
    对方接收到消息数据包后,把消息放入到接收消息的临时空间,并判断数据是否接收完毕。如果没有接收完毕,继续请求发送下一个消息数据包。如果接收完毕,播放声音,并判断与此IP对应的聊天窗口是否打开,是则调用ReceiveMessage()把消息放入到接收消息文本框,否则把消息保存起来,并动态显示托盘的图标,等待用户打开聊天窗口时再把消息放到接收文本框。
    聊天对话框还有记录和打开聊天记录的功能。勾选上“关闭时保存聊天记录”,关闭聊天对话框会聊天记录会自动保存在形如“2011-03-11.102920(192.168.1.3)测试.rtf”的文件中,文件名由聊天开始时间、IP、昵称组成。打开“聊天记录”按钮,可以选择保存的聊天记录,在接收文本框内会显示打开选择的聊天记录。
    3.4 聊天室模块聊天室分为服务器和客户端,每个用户只能创建一个聊天室,也只能加入一个聊天室。聊天室也只能发送普通文本消息,所以聊天室的开发相比聊天来说,简单的多。聊天室服务器如下图:

    服务器右边两个列表框分别表示在聊天室里的好友、不在聊天室里的好友;客户端右边的列表框表示在聊天室中的好友。
    用户创建聊天室可以把自己的所有好友都加入到聊天室,这些好友发的消息可以被所有在聊天室中的好友共享,即使这些好友之间可能并不能互相访问。这也是聊天室的一个好处。
    聊天室服务器和客户端各使用一个SOCKET,创建的是UDP套接字,有自己的端口号。
    用户创建聊天室时,创建端口为CHATROOM_SERVER_PORT的UDP套接定,把自己加入到聊天室好友列表里,同时也把自己添加的所有好友加入到未进入聊天室好友列表中。
    在未进入聊天室好友列表中选定要添加进入聊天室的好友,单击向上的按钮,会向这些选定好的好友发送加入聊天室的请求。
    SendUserCommandToIP( CHATROOM_ADDREQUEST, user.strIP, DEFAULT_PORT, &userSelf );
    以上代码就是向好友user发送加入聊天室请求,因为在加入聊天室之前,这些好友并没有创建聊天室客户端SOCKET,所以必须向主对话框的m_pLisSocket套接字发送消息,这从DEFAULT_PORT端口号就可以看出来。
    如果好友接受了请求,接受请求的好友就会从未进入聊天室聊表框移到聊天室好友列表框。
    单击向下的按钮,会删除聊天室好友列表框中选定的好友,并向他们发送被踢出聊天室的消息:
    SendUserCommandToIP( CHATROOM_CLIENT_KICKED, user.strIP, CHATROOM_CLIENT_PORT, NULL );
    这儿是发往CHATROOM_CLIENT_PORT端口的消息,因为将要删除的好友已经打开了聊天室,而且创建了聊天室客户端的SOCKET,可以接收发往这个端口的消息。
    当聊天室服务器关闭时,会向所有聊天室好友发送关闭聊天室的消息:
    for( int nIndex = 1; nIndex < m_listCtrlInChat.GetItemCount(); nIndex++ ){ USER user = m_arrFriendsInChat.GetAt( nIndex ); SendUserCommandToIP( CHATROOM_SERVER_CLOSED, user.strIP, CHATROOM_CLIENT_PORT, NULL );}
    聊天室好友列表里第的是自己,不必要给自己发送消息,因此nIndex是从1开始的。
    当创建聊天室的用户发送聊天消息时,服务器将文本消息发给所有聊天室好友。在服务器创建的时候,限制过发送文本框的字数:
    m_editSend.SetLimitText( MAXDATAPACKETLENGTH - sizeof( DATAPACKET ) - sizeof( CHATROOMMESSAGEINFO ) );
    因此,发送的消息长度不会超过UDP可以发送的数据最大值,发送消息时直接一次发送就行:
    for( int nIndex = 1; nIndex < m_arrFriendsInChat.GetSize(); nIndex++ ){ USER user = m_arrFriendsInChat.GetAt( nIndex ); SendTextToIP( user.strIP, CHATROOM_CLIENT_PORT, strSend, "" );}
    聊天室服务器接收到消息时,会将接收到的消息加入到聊天室接收文本框,并将消息发给所有在聊天室中的好友,除了向服务器发送消息的好友外:
    for( int nIndex1 = 1; nIndex1 < m_arrFriendsInChat.GetSize(); nIndex1++ ){ USER userSend = m_arrFriendsInChat.GetAt( nIndex1 ); if( 0 == strcmp( userSrc.strIP, userSend.strIP ) ) { continue; } SendTextToIP( userSend.strIP, CHATROOM_CLIENT_PORT, strSend, userSrc.strIP );}
    以上就是服务器的主要功能。
    客户端的处理比服务器要简单一些,客户端只有在聊天室列表中的好友,客户端也只向服务器发送消息,不用向其他聊天室好友发送消息。
    聊天室客户端如下图:

    首先,主对话框接受到CHATROOM_ADDREQUEST消息,弹出对话框询问用户是否进入好友创建的聊天室,如果用户拒绝加入则不用处理此消息,如果用户同意加入聊天室,那么就弹出聊天室客户端。
    在客户端打开的同时,限制发送文本框的最大字数,创建客户端SOCKET,端口号为CHATROOM_CLIENT_PORT的UDP。
    当服务器踢出客户端,或服务器关闭时,弹出对话框提示用户,并清空聊天室好友,设置发送文本框不可用。
    客户端关闭时,会向服务器发送客户端关闭的消息。
    客户端发送消息时,先将发送的消息加入到接收消息文本框,然后将消息发往服务器,由服务器将消息发到各个聊天室好友。
    客户端接到服务器发来的聊天消息时,处理也很简单,直接将聊天消息加入到接收消息文本框。
    以上就是聊天室的设计方法。服务器在当中起到了中转的作用,所有消息都经过服务,服务器向所有好友发送消息,就算聊天室中的好友不能互相连接,只要能连接到服务器,他们就可以一起聊天。
    3.5 传送文件模块正如任务书上写的,文件传送支持断点续传,这是本程序的一个亮点。文件传送模块用到了多线程,可以实现多个文件、多个用户之间的传输。一个线程负责将一个文件传送给一个好友。
    传送文件服务器如下图所示:

    单击添加按钮会打开选择文件对话框,选择好要发送的文件后,会弹出一个好友列表框,要求选择需要发送的好友,选择了好友后,程序会把发送文件信息添加到传输入文件对话框中,并向接收者发起传送文件的请求,如果接收者拒绝接收文件,在传输文件对话框的相应项的速度列就会显示“拒绝”,如果接收者接受文件,文件传送正式开始。下面详细介绍服务器端的工作过程:
    传送文件用到的是TCP连接,因为传送文件必须保证传输的数据不能丢失也不能有误。用户打开文件传输功能,弹出传输文件对话框,在初始对话框的时候,除了初始化传输文件列表框,还会创建一个端口号为SENDFILESSERVER_PORT的TCP套按字m_pSFServerSocket,并设置此套接字为监听状态。
    当用户选择好传送的文件和传送的好友后,传送信息会加入到列表框,并调用主对话框的SendFilesNotify()函数向这些好友发送传送文件的消息,此消息中还包括文件名和文件长度。
    如果接收者拒绝接收文件,列表框的相应项的速度列会被设置为“拒绝”。
    如果接收者接收文件,接收者会先向pSFServerSocket发起连接,程序重载pSFServerSocket的OnAccept()函数,在此函数中调用传输文件服务器对话框的OnAccept()函数:
    void CSendFilesServerDlg::OnAccept(){ CSendFilesSocket sfSocket; m_pSFServerSocket->Accept( sfSocket ); CSendFilesServerThread *pSFSThread = ( CSendFilesServerThread *)AfxBeginThread( RUNTIME_CLASS( CSendFilesServerThread ), 0, 0, CREATE_SUSPENDED, NULL ); pSFSThread->SetSendFilesServerDlg( this ); pSFSThread->AttachSocket( sfSocket.Detach() ); pSFSThread->ResumeThread(); m_arrSendThread.Add( pSFSThread );}
    在此函数中先定义一个CSendFilesSocket类型的变量sfSocket,m_pSFServerSocket接收此WinSocket,就与接收都连接起来了。然后此函数再创建了一个线程,并把此线程加入到线程列表中。在线程中也有一个CSendFilesSocket类型的成员变量,将这儿的sfSocket分离掉套接字句柄,附加到线程中的相同类型的成员变量上。发送数据的功能就由这个线程来处理了。
    在传输入文件对话框中选中某些传输信息,单击“删除”按钮,会弹出删除提示框询问用户是否删除这些传输信息,如果用户确定删除这些传输信息,传略文件对话框会删除这些传输信息,如果文件正在传输入,会关闭线程,停止传输文件。
    在传输文件对话框中设置了一个定时器,用于刷新列表框。每当定时器时间到时,程序调用RefreshListBox()函数刷新列表框,更新文件传输的进度和速度。
    如果接受者停止了接收文件或者文件传输完成,相应的线程也会调用RefreshListBox()函数及时的刷新列表框。
    CSendFilesServerThread类用于发送文件数据,接收者连接到m_pSFServerSocket后,接收者就可以与CSendFilesServerThread类中的CSendFilesSocket成员变量m_sendFilesSocket通信了。
    因为程序支持断点续传,因此接收者首先会通知CSendFilesServerThread线程要发送的文件、大小、已经发送的大小,之后线程会打开需传送的文件,并定位到已传送过的位置,然后向接收者发送可以开始传送文件的消息。
    接收者收到可以开始传送文件的消息后,会向服务器发起请求传输数据的消息,此消息中包含已传输的大小。
    传输线程收到此消息后,会根据文件的大小、已传输的大小、以及包的大小向接收者发送数据包。传输线程只要接收到传输数据的消息,就会传输数据,当接收到传输完毕后,此线程作一些结尾工作,通知传输入文件对话框,然后结束线程。
    接收文件对话框如下:

    服务器向接收者发送文件时,主对话框会弹出对话框询问是否接受文件,如果不接受则不作处理。如果接收,则打开接收文件对话框,并让用户选择保存接收文件的位置,然后定义CReceiveFilesSocket类型的变量rfSocket,向服务器发起连接,创建CSendFilesClientThread线程,把线程加入到线程列表,并分离rfSocket的套接字句柄,附加到CSendFilesClientThread线程中的CReceiveFilesSocket类型的成员变量中。
    接收文件对话框的其他功能,如删除、刷新列表框等与传输文件对话框无太大的区别,在此就不再累赘。接收文件的操作就由CSendFilesClientThread线程来处理。
    断点续传功能主要在是接收端实现的,接收端接受文件时会创建一配置文件,配置文件记录了源文件在服务器的路径、大小以及传输过的大小。当接收者接收到一个数据包并把数据保存起来后,会更新此配置文件,当文件传输完成后,删除此配置文件。
    接收开始时,接收线程会首先寻找在接收文件目录下是否存在配置文件,如果配置文件存在,验证配置文件中记录的源文件在服务器的路么是否一致,大小是否相等增,接收文件是否存在,大小是否比配置文件中读取大小的值大。如果这些条件都成立,说明可以接着传输,否则从头开始传。
    判断出是否续传信息后,接收线程会将这些信息发送给服务器,服务器收到信息后会作相应的设置,然后发消息通知接收者可以传输数据了。接收者收到信息后,就会向服务器发出请求传输数据的消息,消息中包含已传输的大小。服务器收到消息后就会向接收者发送数据包。
    接收者接收到数据包后,更新已传输的大小,并把文件数据追加到接收文件后面。如果已传输大小等于文件的大小,接收者会向服务器发送传输完毕的消息,然后作结尾工作,通知接收对话框接收完毕,结束线程。如果还未传输完成,接收者会再次向服务器发起传输数据的消息,直到文件传输完毕。
    3.6 共享屏幕模块共享屏幕指的是用户可以让其他好友观看自己的屏幕。
    共享屏幕服务器如下:

    共享屏幕界面的设置与聊天室相似,这里不再累赘。
    共享屏幕模块同样使用UDP套接字,客户端同意加入共享屏幕后,服务器所做的功能就是先把自己屏幕的尺寸发送给好友,然后反复把自己桌面图像发送给好友。
    发送桌面图像给好友之前需要截取桌面图像,为了减小图像大小,方便传输,程序截取桌面图像后把图像处理成8位的位图,宏SHARESCREEN_BITCOUNT定义了截取后的位图颜色位数,默认为8位。另外,在共享屏幕中还用到了数据压缩,这儿使用了网上成熟的Zlib压缩。因为并不是所有的数据都能压缩得更小,因此压缩后还必须判断压缩是否在效,如果无效就不压缩,直接传输入原数据:
    // 压缩数据DWORD dwMaxCompressLength = compressBound( dwDIBLength );BYTE *pCompress = new BYTE[ dwMaxCompressLength ];DWORD dwCompressLength;compress( pCompress, &dwCompressLength, pDIB, dwDIBLength );// 压缩有效,则使用压缩后的数据,否则使用原数据if( dwCompressLength < dwDIBLength ){ BYTE *pTmp = pDIB; pDIB = pCompress; dwDIBLength = dwCompressLength; delete pTmp;}else{ delete [] pCompress;}
    以上代码可以看出只有当压缩后的数据大小小于原数据大小时,才使用压缩数据。这样我们得到的数据其实很小了,已经在UDP传输数据允许范围内,因此程序没有再使用分包发送,而是直接一次性发送出去。
    截取出来的图像没有鼠标信息,因此还必须在截取的图像上画上鼠标:
    // 画鼠标CPoint mouse;GetCursorPos( &mouse );::DrawIcon( hdc, mouse.x - 10, mouse.y - 10, ::GetCursor() );
    共享屏幕客户端如下:

    客户端的处理很简单,直接将接收到的图像数据经过解压后贴到对话框的客户区就行。客户端没有什么难点,做法都很正常,因此就不再介绍。
    3.7 白板模块白板服务器如下:

    白板服务器与聊天室服务器类似,相同的地方就不一一介绍。服务器对话框左边是工具和属性按钮,这些按钮都是自绘按钮,自绘按钮前面已经做过介绍,这儿就再说明。中间的画板是重载CStatic后得到的自绘CCanvasStatic。服务器对话框有三个主要的成员变量:
    TOOL m_emTool; // 工具int m_nWidth; // 线宽COLORREF m_clDrawColor; // 颜色
    左边的工具和属性按钮对这三个变量相对应,这些按钮控制着画图的属性。
    绘图操作在CCanvasStatic中处理,绘画结果再传给服务器对话框,服务器对话框然后把绘图信息发送给所有在白板中的好友。同样的,服务器收到绘图信息后,会反绘图信息传递给画板类CCanvasStatic,让绘出图形,然后服务器再将接收到的绘图信息发送给其他好友。
    白板客户端如下:

    客户端与服务器功能相似,而且更简单,客户端绘画后把只把绘图信息发送给服务器,在接收到服务器发送过来的绘图信息后再把绘图信息反应到画板上。
    3.8 音、视频模块这个模块重点介绍如何解决音频的连续和音、视频同步的问题。
    界面设计如下:

    要和好友视频聊天,自己或好友必须至少一方有摄像头,在主对话框初始化的时候进行判断是否有摄像头:
    m_hWndVideo = capCreateCaptureWindow( NULL, WS_CHILD, 0, 0, 1, 1, m_hWnd, IDD_CAPTUREVIDEO );if( capDriverConnect( m_hWndVideo, 0 ) ){ m_bCamera = TRUE; capDriverDisconnect( m_hWndVideo );}
    思路是通过与序号为第一个摄摄头连接,如果连接成功,说明有摄像头,否则不存在摄像头。测试是否有摄头,有更好的代码的,但程序为了方便开发,只是简单的认为一台电脑上只有一个摄像头,以下都是如此处理的,不再说明。
    从以上代码可以看出,视频聊天代码中使用capCreateCaptureWindow()函数,这是VFW中的函数,本程序中的视频聊天都是采用VFW模式来设计的。关于VFW的使用,本论文限于篇幅,就不再介绍。下面介绍视频聊天的构架。
    本程序允许同多个好友进行视频聊天,而一个摄像头只能被连接一次,所以一个摄像头供所有视频聊天对话框共享,出于此,只能在主对话框中连接摄像头,把摄像头捕获到的数据分发给所有视频聊天对话框。
    当向好友发起视频聊天请求或好友向自己发起视频聊天请求时,首先会判断自己是否有摄像头,如果有,再判断之前是否已连接摄像头,如果没有,这时先连接摄像头,然后处理其他的:
    if( !m_bConnectCamera ){ if( capDriverConnect( m_hWndVideo, 0 ) ) { // 设置视频的大小 BITMAPINFO bmpInfo; capGetVideoFormat( m_hWndVideo, &bmpInfo, sizeof( BITMAPINFO ) ); bmpInfo.bmiHeader.biWidth = VIDEOCHAT_VIDEO_WIDTH; bmpInfo.bmiHeader.biHeight = VIDEOCHAT_VIDEO_HEIGHT; … capSetVideoFormat( m_hWndVideo, &bmpInfo, sizeof( BITMAPINFO ) ); … // 连接上视频 m_bConnectCamera = TRUE; }}
    连接上视频后,首先设置摄像头的分辨率,这儿使用的是宏VIDEOCHAT_VIDEO_WIDTH和VIDEOCHAT_VIDEO_HEIGHT定义的大小,默认为320x240。
    连接上视频后,程序设置摄像头为流模式捕获画面,摄像头定时捕获画面,捕获到画面后,会调用回调函数EncodeCallback():
    LRESULT WINAPI EncodeCallback( HWND hWnd, LPVIDEOHDR lpVHdr ){ if( lpVHdr->dwFlags & VHDR_DONE ) { … // 通过主对对话框更新所有视频聊天对话框画面 pDlgMain->UpdateVideoPicture( ::GetTickCount(), bmpInfo, lpVHdr->lpData, lpVHdr->dwBufferLength ); } return 1;}
    在此回调函数中,调用主对话框的UpdateVideoPicture()函数,UpdateVideoPicture()函数只是简单把捕获到的视频数据分发给所有的视频聊天对话框,视频聊天对话框把视频数据显示到自己视频区域,然后向好友发送视频数据。好友接收到视频数据后,再把视频数据显示到对应的区域。不断的发送数据、接收数据、更新区域,这样就达到了视频聊天的目的。
    音频聊天需要处理连续的问题,因为在服务器端是录音,再发送到客户端播放,如果录完音处理发送,再接着录音,这样中间就会有空隙,造成录的音断断续续。所以音频聊天本程序使用的是双缓冲录音,多缓冲接收录音数据的方式。
    音频聊天采用的是WaveX系列API函数来捕获音频数据,在主对话框初始化的时候就准备两个缓冲区来接收录音数据:
    m_pWaveHdr1 = ( PWAVEHDR )new char[ sizeof( WAVEHDR ) ];m_pWaveHdr2 = ( PWAVEHDR )new char[ sizeof( WAVEHDR ) ];…waveInPrepareHeader( m_hWaveIn, m_pWaveHdr1, sizeof( WAVEHDR ) );waveInPrepareHeader( m_hWaveIn, m_pWaveHdr2, sizeof( WAVEHDR ) );
    一个缓冲区录音完毕,程序会收到WaveInData消息,程序就可以调用函数来处理音频数据,同时系统会继续录音,因为另一缓冲区还可以接收数据:
    void CInstantMessagingDlg::WaveInData( WPARAM wParam, LPARAM lParam ){ DWORD dwTickCount = m_dwTickTime - 1200; /// 得到录音的时间 m_dwTickTime = ::GetTickCount(); /// 每个视频窗口发送声音 int nVideoChatDlgIndex = 0; for( nVideoChatDlgIndex = 0; nVideoChatDlgIndex < m_arrVideoChatDlg.GetSize(); nVideoChatDlgIndex++ ) { m_arrVideoChatDlg.GetAt( nVideoChatDlgIndex )->SendAudioData( dwTickCount,( ( PWAVEHDR )lParam )->lpData,( ( PWAVEHDR )lParam )->dwBufferLength ); } /// 将此WAVEHDR再添加进录音设备 waveInAddBuffer( m_hWaveIn, ( PWAVEHDR )lParam, sizeof( WAVEHDR ) ); return; }
    处理完接收到音频数据的缓冲区后,继续把此缓冲区添加到录音中,这样就可以循环录音。
    在客户端采用多缓冲区来接收音频数据,这些自缓冲区组成一个首尾相连的圆圈缓冲区,用两个指针来表示接收和播放的缓冲区序号:
    // 申请音频数据的缓冲区for( int nIndex = 0; nIndex < AUDIO_BUFFER_BLOCK; nIndex++ ){ char *pAudioBuffer = new char[ AUDIO_BUFFER_SIZE + sizeof( DWORD ) ]; m_arrAudioBuffer.Add( pAudioBuffer );}m_nReceive = 0;m_nPlay = 0;
    缓冲区的块数是AUDIO_BUFFER_BLOCK宏定义,刚开始时接收和播放的指针都为0。
    下面是接收到音频数据的代码:
    // 接收到音频数据,保存起来void CVideoChatDlg::SaveAudioData( char *pData, DWORD dwDataLength ){ CDebug debug( "SaveAudioData" ); // 保存接收到的音频数据 char *pSaveData = m_arrAudioBuffer.GetAt( m_nReceive ); memcpy( pSaveData, pData, dwDataLength ); // 新的接收区块号 m_nReceive = ( m_nReceive + 1 ) % AUDIO_BUFFER_BLOCK; // 接收追上播放,播放往前走 if( m_nReceive == m_nPlay ) { m_nPlay = ( m_nPlay + 1 ) % AUDIO_BUFFER_BLOCK; } if( !m_bPlaying && m_bReceiveAudio ) { m_bPlaying = TRUE; char *pPlayData = m_arrAudioBuffer.GetAt( m_nPlay ); memcpy( &m_dwRecordAudioTickTime, ( BYTE * )pPlayData, sizeof( DWORD ) ); m_dwPlayAudioTickTime = ::GetTickCount(); m_pWaveHdr1->lpData = pPlayData + sizeof( DWORD ); waveOutPrepareHeader( m_hWaveOut, m_pWaveHdr1, sizeof( WAVEHDR ) ); waveOutWrite( m_hWaveOut, m_pWaveHdr1, sizeof( WAVEHDR ) ) ; m_nPlay = ( m_nPlay + 1 ) % AUDIO_BUFFER_BLOCK; }}
    接收到音频数据先保存起来,再判断两个指针的位置关系,如果接收到的指针追上了播放指针,则播放指针往前走,如果客户端允许播放声音,而且之前接收到的音频数据已播放完毕,那么接收到数据这时就可以继续播放了。
    至此,视频和音频可以可以正常的显示和播放了。但还有个大问题,就是视频和音频不同步,视频可以说是实时的,但视频需要录制完毕后发送到客户端才播放的,所以音频比视频要慢上几秒。
    为了解决这个问题,程序在每个视频数据和音频数据上都加上了服务器端的时间标签。在显示视频画面时比较时间标签决定视频画面处理。具体的处理如下所述:
    开始播放音频时,记下客户端播放的时间和此音频数据在服务器端录制的时间,在视频数据到达时,记下此时客户端时间,计算出音频已播放的时长,然后在音频数据在服务器端录制的时间上加上这个时长,就得到标准的画面在服务器端时间,然后用这个时间比接收到的视频数据中的时间相比较:

    如果视频时间比标准时间大,说明视频画面还有播放音频后面,把些画面保存起来,不显示在对话框的好友区域;如果视频时间比标准时间小,说明音频播放到视频的后面了,此视频画面可以丢弃,也不显示,但必须从保存起来的视频画面中找到与标准时间最接近的视频画面来显示;如果视频时间与标准时间相等,或者视频时大于标准时间大但是已经进行过第2种情况的处理,则可以显示此视频数据。注意此种情况与第1种情况的区别,第1种情况是没有进行过第2种情况的处理。
    用以上的方法来处理音频和视频数据,就可以达到音、视频的同步,主要代码如下:
    CString strVideoData( ( LPCTSTR )pVideoReceiveData, dwDataLength );m_lstVideoData.AddTail( strVideoData );CString strData;BITMAPINFO bmpInfo;BYTE *pRGBData;// 如果没有播放音频,则直接贴图if( !m_bPlaying ){ strData = m_lstVideoData.GetHead(); m_lstVideoData.RemoveHead(); BYTE *pVideoData = ( BYTE * )strData.GetBuffer( strData.GetLength() ); memcpy( &bmpInfo, pVideoData + sizeof( DWORD ), sizeof( BITMAPINFO ) ); pRGBData = new BYTE[ bmpInfo.bmiHeader.biSizeImage ]; YUY2_RGB( pVideoData + sizeof( DWORD ) + sizeof( BITMAPINFO ), pRGBData, bmpInfo.bmiHeader.biSizeImage * 4 / 6 ); strData.ReleaseBuffer( -1 );}// 正在播放音频,需要对视频数据进行处理else{ DWORD dwStandardVideoTime = ::GetTickCount() - m_dwPlayAudioTickTime + m_dwRecordAudioTickTime; DWORD dwVideoTime; BOOL bSmallerThanStandardVideoTime = FALSE; // 循环判断每个视频时间与标准播放时间的距离 int nVideoDataCount = m_lstVideoData.GetCount(); for( int nIndex = 0; nIndex < nVideoDataCount; nIndex++ ) { strData = m_lstVideoData.GetHead(); BYTE *pVideoData = ( BYTE * )strData.GetBuffer( strData.GetLength() ); memcpy( &dwVideoTime, pVideoData, sizeof( DWORD ) ); // 如果视频时间与标准时间相等或大于但已经过了一个小于的 if( ( dwVideoTime > dwStandardVideoTime && bSmallerThanStandardVideoTime ) ||dwVideoTime == dwStandardVideoTime ) { m_lstVideoData.RemoveHead(); memcpy( &bmpInfo, pVideoData + sizeof( DWORD ), sizeof( BITMAPINFO ) ); pRGBData = new BYTE[ bmpInfo.bmiHeader.biSizeImage ]; YUY2_RGB( pVideoData + sizeof( DWORD ) + sizeof( BITMAPINFO ), pRGBData, bmpInfo.bmiHeader.biSizeImage * 4 / 6 ); strData.ReleaseBuffer( -1 ); break; } // 如果视频时间小于标准时间,丢弃视频数据 else if( dwVideoTime < dwStandardVideoTime ) { bSmallerThanStandardVideoTime = TRUE; m_lstVideoData.RemoveHead(); strData.ReleaseBuffer( -1 ); continue; } // 如果视频时间大于标准时间,则不作处理,直接返回 else if( dwVideoTime > dwStandardVideoTime ) { strData.ReleaseBuffer( -1 ); return; } }}
    3.9 调试模块为了方便调试,在程序中添加了一个用于调试的类CDebug,它具有的成员变量和成员函数如下:
    class CDebug {private: CString m_strMessage; // 信息public: CDebug(); CDebug( CString strMessage ); virtual ~CDebug();};// 构造和析构函数如下CDebug::CDebug( CString strMessage ){ m_strMessage = strMessage; CString strTrace; strTrace = "run in : " + m_strMessage + "\n"; TRACE( strTrace );}CDebug::~CDebug(){ CString strTrace; strTrace = "run out : " + m_strMessage + "\n"; TRACE( strTrace );}
    利用类的构造、析构函数和类的生命周期,我们可以很方便的设计如上的CDebug类,用法如下:
    { CDebug debug( “test” );}
    在函数或语句组的开始处定义一个CDebug变量,当程序运行到变量定义处的时候,会打印出run in : test,当程序执行完函数或语句组时,会打印出run out : test,这样我们就可以知道程序运行到何处,极大的方便了我们对程序的调试。
    1 评论 10 下载 2018-10-02 20:36:20 下载需要16点积分
  • CG树顶端节点集群的设计与实现

    摘要本文描述了一个CG树顶端节点集群的设计与实现,主要内容有:

    详细阐述了顶端节点集群的设计方案。该方案维持集群节点间的通信,当集群内节点失效时能及时发现;负载均衡器(LB)能够将客户端的请求通过一定的调度策略转发给下面的真实服务器(RS);能够保证所有真实服务器上的数据库的一致性。
    实现了一个顶端节点集群的系统原型,技术分析和实验结果表明,该集群系统具有稳定性和高可用性。


    搭建了一个测试系统,对客户端的请求进行分析处理,返回客户端需要的信息,并测试顶端节点集群的性能。根据测试结果分析顶端节点和顶端节点集群之间的性能差异,证明设计方案的有效性。
    [关键词] 集群 CG树 LVS 顶端节点集群
    AbstractThis paper describes the design and implementation for the top node cluster in CG-Tree. The major contents include:

    A detail design solution for the top node cluster is described. It can maintain the communications among cluster nodes and detect node faults whenever cluster fails. The load balancer (LB) can forward the client’s request to the real server (RS) by selected strategy. The databases on all real servers are kept consistency.A prototype of the top nodecluster is implemented. The technical analysis and experimental result showthat the cluster is with stability and highly availability.A test system is built to analyze and process the request from clients, return the required information to clients, and test the performance of the top node cluster. Accorded to the test results, the difference of the performance between the top node and the top node cluster is analyzed to show the effectiveness of the design solution.
    [Keywords] Cluster, CG-Tree, LVS, Top Node Cluster
    第一章 引言1.1 研究背景1.1.1 集群技术Internet 的飞速发展给网络带宽和服务器带来巨大的挑战。从网络技术的发展来看,网络带宽的增长远高于处理器速度和内存访问速度的增长。热门网站引发前所未有的访问流量,很多网络服务因为访问次数爆炸式地增长而不堪重负。一些新兴的网络技术如视频点播,动态网页,CGI 等带来更大的网络带宽需求。这时单一的计算机系统,如单处理器或者SMP 系统,往往因为巨大的网络负载而不堪重负,其存在着诸多的问题,主要表现在:扩展能力差并且扩展的代价昂贵;升级导致的服务中断会带来巨大的商业损失,并造成原有计算资源的浪费;单点故障发生的概率较高导致无法提供持续的可靠服务。解决网络服务的可伸缩性和可靠性已是非常紧迫的问题。
    通过高性能网络或局域网互联的服务器集群[1]正成为实现高可伸缩的、高可用网络服务的有效结构。这种松耦合结构的服务器集群系统有下列优点:

    提高性能
    一些计算密集型应用,如:天气预报、核试验模拟等,需要计算机有很强的运算处理能力。这时,一般都使用计算机集群技术,集中几十台甚至上百台计算机的运算能力来满足要求。提高处理性能一直是集群技术研究的一个重要目标之一。随着网络的普及和计算机硬件性能的不断提高,集群系统的应用领域越来越广,目前集群系统主要应用于Web服务、Cache服务、媒体服务、科学计算以及数据库应用等领域。
    降低成本
    组成集群系统的 PC 服务器或RISC 服务器和标准网络设备因为大规模生产降低成本,价格低,具有很高的性能价格比。若整体性能随着节点数的增长而接近线性增加,该系统的性能价格比接近于PC 服务器。所以,这种松耦合结构比紧耦合结构的多处理器系统具有更好的性能价格比。
    提高可扩展性
    用户若想扩展系统能力,不得不购买更高性能的服务器,才能获得额外所需的CPU和存储器。如果采用集群技术,则只需要将新的服务器加入集群中即可,对于客户来看,服务无论从连续性还是性能上都几乎没有变化,好像系统在不知不觉中完成了升级。
    增强可靠性
    集群技术使系统在故障发生时仍可以继续工作,将系统停运时间减到最小。集群系统在提高系统的可靠性的同时,也大大减小了故障损失。

    目前集群系统因其诸多的优点,已被广泛应用于 Web 服务,Cache 服务,媒体服务,科学计算及数据库等领域。
    1.1.2 CG树简介对于视频点播这种大流量的服务需求,服务器这一端的带宽将在很大程度上影响媒体服务的质量,如果将服务器节点限制在单个局域网内,媒体集群将受到网关流量的制约而提供很有限的服务。
    CG树[2,3]是适用于分布式集群的一种模型,CG树将集群的节点分级分层,组织成以前端调度节点为根节点,服务器池的所有节点分组管理的树形结构。该模型下前端负载均衡器不必跟服务器池的所有节点通信,有效减少了前端节点的负载。集群节点采用分级分层的结构,能有效减轻集群前端负载均衡器的负担,并能有效地减少跨网络集群节点间的通信开销,具有较高的实用价值,是跨网络集群的一个较好的解决方案。
    如图1-1是一棵CG 树的结构图。

    在以往的CG树的实现中,顶端节点(如图1-2)需要负责处理客户端的请求,数据库的查询,CG森林的维护,资源的调度等任务,导致顶端节点的压力非常大,成为了整个系统的瓶颈。其中,数据库的查询操作占据了节点的绝大多数的资源。对于此,本文对顶端节点的性能做出过测试,在千兆局域网内选择了1台计算机作为顶端节点,其详细配置为:

    操作系统:Ubuntu10.10Linux内核版本:Linux 2.6.35CPU:Intel® Core™ 2 Quad CPU Q8400 2.66GHz(四核)内存:2GB数据库:MySQLServer 5.1
    然后客户端不断地发送请求,由客户端记录发送请求和收到响应的数量。实验结果表明,顶端节点在满负荷的条件下,每秒钟只能执行数据库查询一万次左右,即每秒钟可以处理客户端的一万次左右的资源请求。如果系统面向全世界服务,其性能显然无法满足客户的需求,因此,顶端节点成为了整个系统的瓶颈。

    1.1.3 系统的提出为了解决这一问题,本文提出了一种将顶端节点修改为顶端节点集群(如图1-3)的做法。这种做法的思路是,用分布的做法让多台服务器组成顶端节点集群来分担原来单个顶端节点的工作,从而提高系统的整体性能。顶端节点集群由两层计算机组成,第一层仅仅负责请求的转发,第二层由多台计算机组成,作为真实服务器,负责请求的处理,其数量取决于整个CG树的规模。由于顶端节点仅仅在选择真实服务器的时候需要与客户端通讯,真正提供服务时是由真实服务器与客户端直接进行通讯的,因此,基于CG树结构的服务器在管理规模上几乎没有限制。

    1.2 研究工作本文设计与实现了一种CG树顶端节点集群的设计方案,主要包括以下内容:

    集群整体架构的设计:设计了一种可用的集群方案,引入了心跳机制,通过心跳的传递使负载均衡器了解每个节点的运行状态,同时通过TCP消息的传递,保证了各个节点数据库的一致性。调度策略的设计:实现了轮转调度、源地址哈希调度、加权轮转调度三种种可用的调度策略。集群的实现:使用UNIX/Linux下Socket编程技术以及多线程技术,实现了本文设计的集群系统,包括负载均衡器和真实服务器两大模块的实现以及数据库连接池的实现。性能测试:模拟了一个基于CG树的视频点播系统,在系统正常工作的条件下,测试了网络的丢包率,测试了顶端节点和顶端节点集群的性能,分析并比较了顶端节点集群与以往顶端节点之间的性能差异,并且对数据库的一致性进行了简单的测试。最后,本文对顶端节点集群的设计方案和测试结果给出了结论,提出了系统存在的缺点,对未来的研究方向做出了展望。
    1.3 论文结构本论文共有七章。

    第一章为引言。本章首先分析了以往CG树所存在的问题,从而引出了将CG树的顶端节点改为顶端节点集群的想法。第二章为相关技术介绍。主要介绍了本文中用到的相关技术。第三章为系统设计。本章主要介绍了CG树顶端节点集群的设计方案。第四章为系统实现。本章主要介绍了CG树顶端节点集群的具体实现。第五章为实验部分。在校园网的实验环境下进行了相关测试,并对实验结果进行了分析。第六章为总结与展望。提出了本文研究工作的结论和不足,并对课题给予展望。最后是参考文献与致谢。
    第二章 相关实现技术简介2.1 Socket通信简介所谓socket[4]通常也称作“套接字”,应用程序通常通过“套接字”向网络发出请求或者应答网络请求。有两种常用的Socket类型:流式Socket(SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)。流式Socket是一种面向连接的Socket,针对于面向连接的TCP服务应用;数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用。

    用户数据报协议(UDP)
    UDP是一个简单的传输层协议,应用进程往一个UDP套接字写入一个消息,该消息随后被封装到一个UDP数据报,该UDP数据报进而又被封装到一个IP数据报,然后发送到目的地。UDP不能保证UDP数据报会到达其最终目的地,不保证各个数据报的先后顺序跨网络后保持不变,也不保证每个数据报只到达一次。
    传输控制协议(TCP)
    TCP不同于UDP,它提供客户与服务器之间的连接,TCP客户首先与服务器建立一个连接,然后通过该连接与服务器交换数据,然后终止这个连接。TCP提供了可靠的数据传输和流量控制,但是其开销要大于UDP。

    在本文的设计方案中,心跳信息的传递使用UDP协议,数据库更新信息和节点之间的其他信息的传递使用TCP协议。
    2.2 多线程技术简介由于本文涉及的系统涉及到多个功能模块的并发操作,对于并发操作主要有两种方式实现:多进程结构和多线程结构。在多进程结构中,程序通过创建子进程来实现并发操作,如此可以充分发挥多核CPU的性能。然而创建子进程的开销是非常昂贵的,每创建一个子进程,需要把父进程的内存映像复制到子进程,且进程间的通信非常复杂。而线程则可以解决上述问题,线程的创建和切换的开销要比进程小得多。同一进程内的所有线程可以共享相同的全局内存,使得线程之间易于共享信息,随之而来的同步问题则可以通过互斥量来解决。
    为了提高系统运行的效率,本系统将采用多线程技术实现任务的并发操作。本系统使用的是POSIX线程[5],也称为Pthread。
    2.3 MySQL数据库简介MySQL[6]是最流行的开放源码关联数据库管理系统。关联数据库将数据保存在不同的表中,而不是将所有数据放在一个大的仓库内。这样就增加了速度并提高了灵活性。MySQL的SQL指得是“结构化查询语言”。SQL是用于访问数据库的最常用标准化语言,它是由ANSI/ISO SQL标准定义的。MySQL数据库服务器具有快速、可靠和易于使用的特点。
    第三章 系统设计3.1 整体结构3.1.1 整体结构的设计以往的CG树由一个顶端节点负责控制一个CG森林,顶端节点需要负责整个集群系统的维护、客户端请求的处理以及资源的调度等等,这样使得顶端节点的负担很重,成为整个系统的瓶颈,为了解决这一问题,本文提出了将顶端节点改为顶端节点集群的办法,其结构图如图3-1所示。

    顶端节点集群为两层结构,第一层是一台负载均衡器,第二层由若干台真实服务器组成。负载均衡器负责接收第二层节点的心跳信息,保存它们的相关信息。第二层的每台服务器都维持一个同样内容的数据库,保存CG树叶子节点上相关资源的信息。由负载均衡器接受客户端的请求,然后按照一定的转发策略转发给第二层的真实服务器,真实服务器通过数据库查询操作得到保存客户端请求资源的CG树节点,将查询结果发送给客户端。
    3.1.2 与LVS的比较LVS[7]是基于IP层负载均衡技术的典型代表。用户通过虚拟IP地址(Virtual IP Address)访问服务时,访问请求的报文会到达负载均衡器,由它进行负载均衡调度,从一组真实服务器选出一个,将报文的目标地址Virtual IP Address改写成选定服务器的地址,报文的目标端口改写成选定服务器的相应端口,最后将报文发送给选定的服务器。真实服务器的回应报文经过负载均衡器时,将报文的源地址和源端口改为Virtual IP Address和相应的端口,再把报文发给用户;或者真实服务器直接将回应报文的源地址和源端口改为Virtual IP Address和相应的端口,发给用户。
    以LVS为代表的典型集群在结构上一般分为三层:负载调度器(load balancer),它作为整个集群系统对外的前端,负责将客户的请求发送到后台的一组服务器上执行,客户不需要知道真实服务器的IP地址;服务器池(server pool),位于后台的一组真正提供应用程序服务的服务器,他们之间通过传递心跳信息来维持可用的服务器列表;共享存储(shared storage),它为服务器提供一个共享的存储区,这样很容易使得服务器池拥有相同的内容,提供相同的服务。
    图2-1展示了一个LVS系统的基本结构。

    LVS集群由负载调度器、服务器池和共享存储三大部分组成,本文的设计方案借鉴了LVS,同样有一个负载调度器,一组真实服务器。因为CG树顶端节点处理客户请求仅仅需要查询数据库,因此,不需要设置共享存储。
    对于MySQL分布式数据库,LVS的一般做法是使用高冗余的同步集群(MySQL Cluster)或者相对简单的异步集群(MySQL replication)来实现。同步集群的特点是配置和管理方便,不会丢失数据,但是其需要较多的内存且速度一般。异步集群的特点是使用主从(mater/slave)模式,速度较快,但是往往会导致主数据库的压力过大且可能会丢失数据。无论哪种数据库集群,其都会产生较大的数据冗余。
    在本系统的应用中,客户端的请求是查询所需资源的地址,而数据库的更新操作只有在CG树叶子节点上的资源更新时才会发生。因此,本文根据这个特点设计了一种简单的数据库的维护方式,即每台真实服务器保留一个数据库的完整副本,从而提高数据库的查询效率。
    3.2 心跳的设计心跳机制[8]是高可用集群的重要技术之一。心跳周期性的检测集群中节点机器的工作状态,当节点机器的工作状态发生改变时,能够通知集群软件的其他部件。
    本集群将采用心跳机制来维持系统的高可用性,由第二层的真实服务器周期性地向第一级的负载均衡器发送心跳信息,以告知负载均衡器其最新的状态。负载均衡器接收到心跳信息后,更新节点的状态信息,记录节点的心跳时间,同时设置一个心跳超时时间,其一般为心跳周期的2到3倍,若负载均衡器在超时时间内没有接收到真实服务器的心跳信息,则认为该节点发生故障,将节点状态设为故障。在实际应用上,可在心跳信息中捎带其他信息,如节点的工作负载等。
    因为心跳的发送频繁,为了减少网络通信的开销,本文使用UDP协议来进行心跳的传递。
    3.3 数据库一致性的保持因为本文的设计方案中,集群中第二层的所有节点使用内容相同的数据库,因此保持数据库的一致性就是我们要关心的问题之一。
    在本文的设计方案中,负载均衡器和真实服务器都要创建一个用于数据库更新的线程,CG树节点向负载均衡器发出数据库更新消息,当负载均衡器接受到数据库更新消息后,通过TCP将更新消息转发给每一台真实服务器,由真实服务器执行数据库的更新操作,从而保证每台服务器数据库的一致性。
    3.4 调度策略的设计本文为了提供整个集群的可用性,共实现了三种请求的转发调度策略[9],分别是:
    3.4.1 轮转调度轮转调度是最简单的调度策略,当负载均衡器接收到客户端的请求时,将请求轮询式的转发给第二层的节点。使用这种调度策略,负载均衡器的负担最小,同时,当客户端请求资源少而频繁时,此调度策略具有非常高的效率。但是,当请求服务时间变化比较大时,轮转调度算法容易导致服务器间的负载不平衡。
    // 轮转调度算法// 假设有一组服务器S = {S0, S1, …, Sn-1},一个指示变量i表示上一次选择的服务器,W(Si)表示服务器Si的权值,大于0表示服务器可用。变量i被初始化为n-1,其中n > 0j = i;do { j = (j + 1) mod n; if (W(Sj) > 0) { i = j; return Si; }} while (j != i);return NULL;
    3.4.2 源地址哈希调度通过源调度哈希转发即通过哈希函数将客户端的IP映射到唯一的一台服务器上。该调度算法可以使请求得到较均匀的分布。
    // 源地址哈希调度算法// 假设有一组服务器S = {S0, S1, …, Sn-1},W(Si)表示服务器Si的优先级,0表示服务器不可用。ServerNode[]是一个有256个桶(大小可调整)的Hash表,变量i表示上一次选择的服务器,变量ip表示客户端IP,getNext表示使用轮转获取下一个可用节点。算法的初始化是将所有服务器顺序、循环地放置到ServerNode表中j = hash(ip);if (W(Sj) == 0) { j = getNext(i);}Return Sj;hash(unsigned int ip) { return (ip * 2654435761) & HASH_TAB_MASK;}// 其中,2654435761UL是2到2^32 (4294967296)间接近于黄金分割的素数。// 2654435761 / 4294967296 = 0.618033987
    3.4.3 加权轮转调度为了解决集群第二层服务器性能可能存在差异的问题,可以使用加权轮转调度。其具体实现就是根据每台服务器的性能为每个节点设置一个优先级,性能越好,优先级越高。当服务器向负载均衡器注册时,同时告知优先级信息。在客户端发来请求时,负载均衡器根据优先级转发给第二层的真实服务器。
    // 加权轮转调度算法// 假设有一组服务器S = {S0, S1, …, Sn-1},W(Si)表示服务器Si的优先级,变量i表示上一次选择的服务器,变量cw表示当前调度的权值,max(S)表示集合S中所有服务器的最大权值,gcd(S)表示集合S中所有服务器优先级的最大公约数。变量i初始化为-1,cw初始化为零。while (true) { i = (i + 1) mod n; if (i == 0) { cw = cw - gcd(S); if (cw <= 0) { cw = max(S); if (cw == 0) return NULL; } } if (W(Si) >= cw) return Si;}
    例如,有三个服务器A、B和C分别有权值4、3和2,则在一个调度周期内调度序列为AABABCABC。当第二层服务器性能差异较大时,相对于轮询转发,此转发策略可以提高每台服务器的使用效率。
    第四章 系统实现4.1 LB模块的实现LB模块运行于集群第一层的负载均衡器上,负责转发客户端的请求、维持集群的运作、转发数据库更新消息等。
    4.1.1 LB模块的主要数据结构LB模块的主要数据结构为:
    class TopNode{ /**< 存放节点信息的容器 */ vector<Node *> nodeVector; /**< 转发请求时,下一个节点的序号 */ vector<Node *>::size_type m_nextNode; /**< m_nextNode的互斥锁 */ pthread_mutex_t m_nextNodeLock; /**< property的互斥锁 */ pthread_mutex_t m_propertyLock; /**< threadNum的互斥锁 */ pthread_mutex_t m_threadNumLock; /**< 数据库更新的互斥锁 */ pthread_mutex_t m_dbUpdateLock; /**< 接收心跳的端口 */ int m_heartbeatPort; /**< 接收消息的端口 */ int m_messagePort; /**< 接受数据库更新消息的端口 */ int m_dbPort; /**< 对外提供服务的端口 */ int m_servicePort; /**< 客户端请求转发策略 1:轮转调度 2: 源地址哈希转发 3:加权轮转调度 */ int m_policy; /**< 提供服务的套接字 */ int m_sockfd; /**< 接受请求计数 */ int m_count; /**< 二层服务器的数量 */ int m_rsNum; /**< 数据库连接池 */ ConnPool *m_pool; };/** @brief the arg of the function thread_handleRequest */struct requestArgs{ /**< 指向TopNode */ TopNode *topNode; /**< 客户端的地址结构*/ struct sockaddr_in cliaddr; /**< 客户端的socket套接字*/ int connfd; /**< 客户端的请求内容*/ string *request; };class Node{ /**< store node's priority information */ s_propertyMesg m_sProMesg; /**< 数据库更新的套接字 */ int m_sockdbfd; /**< 节点ID */ string ID; /**< 节点状态 */ int status; /**< 节点接收请求的端口 */ int m_servicePort; /**< 节点上次发出心跳的时间 */ time_t heartBeatTime; /**< 节点接收请求的地址结构 */ struct sockaddr_in m_serviceAddr; /**< 节点状态的互斥锁 */ pthread_mutex_t statusLock; /**< 节点心跳时间的互斥锁 */ pthread_mutex_t heatBeatTimeLock; };
    4.1.2 LB模块的接口RS模块提供的接口为:
    class TopNode{ /**< 初始化 */ void init(); /**< 轮询获得下个可用节点 */ Node *getNextNode(); /**< 读取配置文件 */ void readConf(char *); /**< 对外提供服务 */ void *serve(void *arg); /**< 接收心跳 */ void *receiveheartbeat(void *arg); /**< 接收消息 */ void *receiveMessage(void *arg); /**< 检查各字节点状态 */ void *changeStatus(void *arg); /**< 对请求进行处理 */ void *handleRequest(int, const struct sockaddr_in *, string *); /**< 对消息进行处理 */ void *handleMesg(int, const struct sockaddr_in *); /**< 通过IP获得下个可用节点 */ Node *getNextNodeByIP(uint32_t); /**< 通过负载获得下个可用节点*/ Node *getNextNodeByLoad(); /**< 通过优先级获得下个可用节点*/ Node *getNextNodeByProperty(); /**< 处理数据库更新消息*/ void *handledbUpdate(int); /**< 接受数据库更新消息*/ void *dbUpdate(void *arg);};class Node{ /**< 连接服务器,返回套接字 */ int connectServer(); /**< 关闭套接字 */ void closeServer(int); /**< 向服务器发送消息 */ void sendtoServer(int, char *, size_t);};
    4.1.3 LB的启动流程
    读取配置文件。从配置文件中读取服务端口等相关信息,同时读取数据库的相关配置。初始化数据。初始化程序运行所需要的数据,包括网络地址结构、数据库连接池等。创建提供服务的线程。该线程用于处理客户端发过来的请求,并将请求的内容转发给第二层的真实服务器。创建接受数据库更新消息的线程。该线程用于处理CG树发送过来的数据库更新消息,并将消息转发给每一台服务器。创建接受消息的线程。该线程用于接受所有节点发送过来的消息,并对消息进行处理。创建控制线程。该线程用于控制整个程序的运行,包括日志更新,节点状态的检测等功能。创建接受心跳的线程。该线程接受真实服务器发送的心跳信息,并记录真实服务器的状态。
    4.2 RS模块的实现RS模块运行于集群第二层的真实服务器上,负责处理客户端的请求,并把处理结果发送给客户端。
    4.2.1 RS模块的主要数据结构RS模块的数据结构为:
    class RealServer{ /**< 负载均衡服务器的地址 */ char m_lbIP[20]; /**< 负载均衡服务器的接收心跳的端口 */ int m_lbHbPort; /**< 负载均衡服务器的接收消息的端口 */ int m_lbMessagePort; /**< 节点ID */ string m_ID; /**< 提供服务的端口 */ int m_servicePort; /**< 接收数据库更新的端口 */ int m_dbPort; /**< 数据库连接池 */ int m_property; /**< 数据库连接池 */ ConnPool *m_pool; /**< 处理请求数 */ int m_connNum; /**< 处理数据库更新次数 */ int m_dbNum; /**< 提供服务的套接字 */ int m_sockfd; /**< 日志文件 */ FILE *m_log; };/**< 用于创建线程时传递参数 */struct requestArgs{ /**< 指向RealServer */ RealServer *realServer; /**< 客户端的地址结构*/ struct sockaddr_in cliaddr; /**< 客户端的socket套接字*/ int connfd; /**< 客户端的请求内容*/ string *request; };
    4.2.2 RS模块的接口RS模块提供的接口为:
    /**< 发送心跳 */void *heartbeat(void *arg);/**< 向LB注册 */int registerToTopNode();/**< 初始化服务器 */void init(); /**< 对外提供服务 */void *serve();/**< 处理客户端请求 */void *handleRequest(int, const struct sockaddr_in *, string *); /**< 读取配置文件 */void readConf(char *); /**< 数据库更新 */void *dbUpdate(void *);
    4.2.3 RS的启动流程
    读取配置文件。从配置文件中读取负载均衡器的IP和端口的相关信息,同时读取数据库的相关配置。初始化数据。初始化程序运行所需要的数据,包括网络地址结构、数据库连接池等。创建提供服务的线程。该线程用于处理负载均衡器转发过来的客户端请求,并将处理结果发送给客户端。创建接受数据库更新消息的线程。该线程用于处理负载均衡器发送过来的数据库更新消息,对数据库进行更新。向负载均衡器注册。向负载均衡器发送注册消息,告知负载均衡器自己的服务端口、优先级等信息。创建发送心跳信息的线程。该线程按照一定的周期向第一层的负载均衡器发送心跳信息。
    4.3 数据库连接池的实现数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个。在高并发的条件下,数据库连接池可以明显提高数据库操作的效率。因此,为了提高效率,本系统实现了一个简单的数据库连接池。
    4.3.1 数据库连接池的数据结构数据库连接池的数据结构为:
    /** @brief 存放每个连接的地址和状态*/typedef struct _sConStatus{ /**< 数据库连接地址 */ MYSQL *connAddr; /**< 数据库连接状态 0:空闲 1:使用中*/ int status; }sConStatus;class connPool { /**< 数据库地址 */ string m_strHost; /**< 数据库用户名 */ string m_strUser; /**< 密码 */ string m_strPwd; /**< 数据库名 */ string m_strDbName; /**< 数据库服务器端口 */ int m_intMysqlPort; /**< 最大连接数 */ int m_intConnNum; /**< 存放连接的容器 */ vector<sConStatus *> m_vectorConn; /**< 从连接的地址,快速找到索引 */ map<sConStatus *, int> m_mapVI; /**< 从连接快速找到状态 */ map<MYSQL *, sConStatus *> m_mapMysqlScs; /**< 互斥锁 */ pthread_mutex_t m_mutexScs; };
    4.3.2 数据库连接池的接口数据库连接池对外提供的接口为:
    /**< 初始化数据库连接池 */int init(char *strHost, char *strUser, char *strPwd, char *strDbName, int intMysqlPort, int intConnNum);/**< 创建一个数据库连接 */MYSQL *createOneConn();/**< 从连接池取一个连接 */MYSQL *getOneConn();/**< 将连接放回连接池。以便其他人用*/void retOneConn(MYSQL *pMysql);
    第五章 实验及其结果分析5.1 实验环境在千兆局域网内选择了6台计算机,其详细配置为:

    操作系统:Ubuntu10.10Linux内核版本:Linux 2.6.35CPU:Intel® Core™ 2 Quad CPU Q8400 2.66GHz(四核)内存:2GB数据库:MySQLServer 5.1
    5.2 实验测试与分析5.2.1 丢包率测试测试方法:由三台计算机作为客户端,不断的发送UDP请求并记录发出的请求次数,服务器记录收到的请求次数。
    UDP数据报格式:

    测试结果:当发送速率在40万次/秒、50万次/秒、60万次/秒、65万次/秒的条件下,服务器收到包的成功率分别为100%、99.89%、93.19%、84.12%。



    发送速率(万/秒)
    接受成功率




    40
    100%


    50
    99.89%


    60
    93.19%


    65
    84.12%



    结果分析:在丢包率允许的范围内,服务器每秒钟最大可以接受50万次左右的请求,未来整个系统的搭建应该考虑网络的上限。
    5.2.2 顶端节点性能测试测试方法:选择一台计算机作为CG树的顶端节点,同时建立一个数据库,其规模为200万条记录,记录资源的相关信息。客户端不断的发送请求,每次请求1到10个资源,服务器负责查询数据库,将保存资源的CG树节点IP告知客户端。客户端记录收到的响应数,服务器端记录处理的请求数以及数据库查询操作的次数。
    测试结果:在满负荷的条件下,服务器平均每秒钟处理客户端请求1790次,数据库查询操作9836次。
    图5-1记录了顶端节点的CPU、内存以及网络使用等负载情况。

    结果分析:从图5-1中可以看出,CPU的使用率已经达到了90%以上,而每秒钟数据库的查询操作只有不到一万次,整个系统的性能会收到严重的制约。
    5.2.3 顶端节点集群性能测试测试方法:类似于上小节的测试方法,将顶端节点改为顶端节点集群,一台计算机作为负载均衡器,分别选择2、3、4台计算机作为真实服务器,进行同样的测试。
    测试结果:当真实服务器的数量分别为2、3、4台时,整个集群平均每秒钟处理的请求数分别为3746次、5346次、7670次,平均每秒钟数据库查询次数分别为20600次、29405次、42190次。
    满负荷运行时,真实服务器的负载情况如图5-2所示。

    当挂载四台真实服务器时,整个集群最大负荷条件下,负载均衡器的负载情况如图5-3所示。

    结果分析:将顶端节点改为顶端节点集群后,整个系统的处理能力大大加强。表5-1给出了性能测试的统计结果。



    真实服务器数量
    客户端请求数(次/秒)
    数据库查询次数(次/秒)
    性能




    0(即顶端节点)
    1790
    9836
    1


    2
    3746
    20600
    2.09


    3
    5346
    29405
    2.99


    4
    7670
    42190
    4.28



    根据表中数据可以看出,整个系统的性能几乎是和真实服务器的数量成正比的。
    另外,由图5-3所示,因为负载均衡器仅仅负责转发,所以在真实服务器满负荷的条件下,负载均衡器的CPU使用率只有15%左右,在网络条件允许的情况下完全可以挂载更多的服务器,从而继续增强整个系统的处理能力。
    5.2.4 数据库一致性测试测试方法:使用五台计算机组成顶端节点集群,由另外一台计算机向集群发出插入、更新、删除记录的消息,由每台真实服务器统计执行操作的次数,并人工检查几个数据库是否保持了一致性。
    测试结果

    首先测试数据库插入,每次插入一条数据,整个集群的插入效率是每秒钟插入18500条记录。然后测试更新语句的执行,每次更新一条记录,整个集群的更新效率是每秒钟更新16000条记录。
    最后测试删除语句的执行,每次删除一条记录,整个集群的删除效率是每秒钟删除15000条记录。(以上的测试数据会随着数据库的规模改变以及查询条件的改变而有所波动)
    最后检查所有数据库的内容,可以保证所有数据库内容一致。

    结果分析
    由于每台服务器上都有一个数据库,所以数据库更新时,所有的真实服务器都需要进行更新,真实服务器的增加不会使整个集群的效率增加,反而会增加网络的负担。但是由于CG树数据库更新操作较少,大部分的数据库操作都是查询操作,因此数据库一致性保持上所损失的性能是可以接受的。
    5.2.5 数据库更新与查询综合测试
    测试方法:使用五台计算机组成顶端节点集群,由另外的计算机分别发出客户端请求和数据库更新消息,统计满负荷下集群处理客户请求的数量。
    测试结果:当以每秒钟1000次左右的速率发送数据库更新消息时,整个集群每秒钟可以处理客户请求7600次左右,数据库查询42000次左右。
    结果分析:当数据库更新操作数量有限时,对整个集群客户端请求处理能力影响不大。考虑到CG树节点更新频率不高,因此本文的集群方案是可行的。

    第六章 总结与展望本章对本文的工作做了一个全面的总结,并指出了不足之处和下一步的研究内容。
    6.1 工作总结
    本文首先介绍了一种适用于分布式集群的模型——CG树,并且分析了CG树顶端节点存在的问题,然后引出了将顶端节点改为顶端节点集群的想法。
    然后本文介绍了一种顶端节点集群的设计方案,包括心跳设计、数据库一致性的保持以及调度策略等内容。
    随后,本文给出了顶端节点集群的详细设计,并利用socket编程以及多线程技术实现了顶端节点集群。
    最后,本文利用顶端节点集群搭建了一个简单服务器,并对其性能进行了测试,并与以往的顶端节点做出了比较。实验结果表明,顶端节点集群处理客户端请求的能力明显提高,具有一定的可行性。

    6.2 课题展望本文的实现方案中,第二层的所有服务器都使用相同的数据库,有较大的数据冗余,同时增加了数据库同步的开销,如果数据库的规模达到一定的程度,可以考虑将第二层的服务器分组,从而进步一减轻每台服务器的负担。
    另外,本文的系统建立于数据库更新操作不频繁的基础上,如果要应用于数据库更新频繁的平台,数据库的更新策略需要修改。
    本文的实现只针对Linux操作系统,在系统的实现中大量用到了Linux系统相关的API,今后如果有意把系统用在面向跨平台应用上,这些部分都需要进行扩充和修改。
    参考文献[1] William Stallings. 操作系统——精髓与设计原理(第五版)[M],北京:电子工业出版社,2006.
    [2] 刘维峰. 分布式媒体集群的设计与实现[D],厦门:厦门大学,2005.
    [3] 吴国才. 基于CG树的分布式服务器集群的设计与实现[D].,厦门:厦门大学,2008.
    [4] W.Richard Stevens,UNIX网络编程卷一:套接字联网API(第三版)[M],北京:人民邮电出版社,2010.
    [5] W.Richard Stevens,UNIX环境高级编程(第二版)[M],尤晋元等译,北京:人名邮电出版社,2006.
    [6] MySQL官方网站,http://www.mysql.com/.
    [7] 章文嵩. LVS项目介绍[Z],http://www.linuxvirtualserver.org/zh/lvs1.html,2002.
    [8] 李大夜. 基于Linux的集群和心跳设计[D],哈尔滨:哈尔滨工业大学,2006.
    [9] 章文嵩. LVS集群的负载调度[Z],http://www.linuxvirtualserver.org/zh/lvs4.html,2002.
    致谢语大学本科生活即将结束,回首总结这四年的求学生涯,许多老师、同学和朋友给予了我真诚、无私的指导和帮助,在此我要表示我最真挚的感激和谢意。
    首先,我要衷心感谢指导我完成毕业设计的老师。老师对我们认真负责,严格要求,从课题确定到最后论文的定稿,为我们倾注了许多的心血与汗水。在毕业设计期间,老师每周都抽出时间与我们讨论,了解我们毕设的进展,并提出了许多宝贵的建议和意见。正是由于老师的严格要求和悉心指导,本文才得以顺利完成。
    其次,我要感谢在这四年里教导我的所有老师们,是你们的辛苦付出让我在计算机学科道路上不断成长,不断成熟。
    感谢实验室里的几位学长,在毕业设计中给了我很大的支持。
    最后,特别感谢我的父母和家人,一直以来你们给予了我最大的关爱和帮助。在这二十年的学习生活中,正是因为有了你们作为我的坚强后盾,我才能在人生道路上一往无前,也正是有了你们,在我遇到挫折的时候,你们给了我避风的港湾。
    1 评论 1 下载 2018-10-02 20:20:25 下载需要15点积分
  • 基于拉普拉斯平滑算法的视频去雾系统的实现

    摘 要视频去雾一直是图像处理领域一个备受关注的题目,其主要目的是将被大雾天气所影响的视频尽可能地还原成没有被大雾情况下的真实图像。相比于雾天下的视频,去雾后的视频还原了一部分被雾天所模糊的图像细节。它减少了雾天视频对实际生活和工作中对人们带来的影响,如交通出行,户外监测系统等。举例说,早年间由于视频去雾技术受限,许多户外监测系统在大雾天气下无法正常工作,影响到了人们的正常出行和生产,所以人们希望利用有效的方法将这些雾天影响下的视频尽可能真实地还原。本文旨在探究一种不同于现有的最小化能量函数,基于拉普拉斯平滑项建立一个新的能量函数模型。对于待处理的视频帧序列,先生成每一帧的深度图,然后通过初试的深度图去分别计算对应的颜色一致性项,几何相关项,平滑项和拉普拉斯平滑项。其中,我们提出了新的颜色一致性项和几何相关项,它们考虑到大气散射效应的影响;同样,拉普拉斯平滑项可以更好地保存物体的细节,避免失真和模糊。最后,通过迭代化解能量函数最小方程,得到对应的立体重建后的去雾视频。本文最终展示的,是基于拉普拉斯平滑项的立体重建后的图像,与原待测试视频帧序列对比后所能达到的最好效果。
    【关键词】 视频去雾;立体重建;拉普拉斯平滑;场景深度
    ABSTRACTVideo defogging has always been a topic of concern in the image processing field. The main purpose of the video defogging is to restore as much as possible the video that is affected by fog weather to real images without being affected by heavy fog. Compared to the video in the foggy world, the post-fog video restores a part of the blurred image details of fog.It reduces the impact of foggy video on people in real life and work, such as traffic and outdoor monitoring systems. For example, due to the limited video defogging technology in the early years, many outdoor monitoring systems were unable to work properly under heavy fog, affecting people’ s normal travel and production. So people want to use effective methods to restore the video under the influence of these fogs as realistically as possible.
    This paper aims to explore a new energy function model based on the Laplacian smoothing term that is different from the existing minimum energy function. For the sequence of video frames to be processed, a depth map of each frame is generated, and then the corresponding depth map is used to separately calculate the corresponding color consistency item,geometric correlation item, smooth item, and Laplacian smoothing item. Among them, we propose new color consistency terms and geometric correlation terms that take into account the effects of atmospheric scattering effects.Similarly, Laplacian smoothing items can better preserve the details of objects and avoid distortion and blurring. Finally, by iteratively solving the minimum equation of the energy function, a corresponding stereoscopically reconstructed defogging video is obtained. The final display of this paper is based on the stereoscopic reconstruction of Laplacian smoothing terms, and the best results that can be achieved after comparing with the original video frame to be tested.
    [Keywords]: video defogging; stereoscopic reconstruction; Laplacian smoothing; scene depth
    第1章 引言视频去雾是数字图像处理中一个重要的课题,应用面极其广泛。在本章节中,我们首先将介绍视频去雾,和到目前为止的研究背景、应用的领域及其具体的实际意义,并且概括性地描述本文所做的工作。
    1.1 研究背景和意义随着城市化现象的加快,人口密度的急剧增长以及全球变暖等气候变化,城市的大雾现象变得越来越普遍。大雾天气给城市交通的监测和治安的监管等方面带来了极大的影响,严重时甚至导致城市安全系统完全失效。海港,河床,岸边也会因为水汽的蒸发,而出现难以消散的雾气,这样一种雾气弥漫的情况使得这些区域的监控难以顺利进行。森林和大面积的植被也会由于呼吸作用,在清晨和黄昏时分常常会形成大面积的雾气,这通常会使得使用长焦距摄像镜头的摄像机系统丧失原有的功能,这可能对森林火灾的安全防范造成极大的影响。尤其在当今由于人类破坏环境的加剧,导致了各种极端天气现象的发生频率升高,因此变化多端的天气对目前主要依赖于智能机器设备的现代生产生活的影响比以前更加大。大雾天气则是其中一种比较常见而且影响又比较大的一种天气现象,而且对此的有效措施也比较少,视频去雾的技术研究可以给实际生产生活中带来更多的效率提升和防范危害。
    视频户外系统在被大雾影响的环境下拍摄的实际图像,因为受到雾气的影响,容易出现图像分辨率降低、质量退化、失真等现象,这就使得户外的视频系统不能稳定正常地工作,因而对人们的生活造成了严重的影响,如图1-1所示为中国除雾无人机在城市上空拍摄到的图像。因此,为了增强户外视频系统的稳定性和适用性,使其在天气条件很糟糕的情况下也可以被正常地使用,就很有必要研究有雾环境下视频图像的去雾技术。针对有雾环境对户外视频系统造成的诸多不良影响,我们需要着力研究视频图像的去雾技术。

    这不仅能对其他图像清晰化技术的研究起到一定的推动作用,还可以尽可能地削弱外界天气条件对图像采集造成的不良影响。近年来,在图像处理技术和计算机视觉领域,图像去雾技术已逐渐成为研究的热点问题,而现有的去雾算法多关注于单一图像的去雾,有关视频去雾的理论却很少。视频去雾与单一图像去雾有所不同,因为视频的帧与帧之间存相关性,且视频处理过程更注重自动性和实时性,这就使得视频去雾更具有挑战性。
    目前,视频图像去雾技术还处在研发起步阶段,各方面可供参考的文献并不多。相较国内,国外的研究工作起步较早,进展相对快一些。由于有雾环境下图像的去雾技术本身的复杂性,虽然这方面已经取得了很大的进展,但所提出的研究算法仍存在一些有待完善的地方,需要我们以此为基础继续研究。
    1.2 视频去雾问题的描述视频去雾是将因为将由于大雾导致可见度下降的视频还原出真实的物理场景的图像处理技术。它不仅是数字图像处理方面一个具有挑战性、前景优秀且相当活跃的课题之一,同时也在社会价值上,有着不可估量的潜在经济利益。视频去雾可以有效地降低因大雾天气对户外视频系统的影响,提高户外视频系统的可用性和适应性。如图1-2为某车载视觉系统下的雾天透视效果。

    图像的去雾方法大体上可以分为两种,一种是基于图像增强的去雾方法,另一种则是基于图像复原的去雾方法。而基于图像增强,多是指通过一些图像增强算法,例如,自适应直方图均衡化,自适应色阶和对比度,和多尺度Retinex等算法,它们主要是通过场景反照率,场景深度等图像,视频因素来对图像进行还原,增强。该方法能在一定程度上提高图像的对比度,使图像的细节更加突出,使图像的视觉效果更好,但可能对突出的那部分信息造成一定损失。而基于图像复原的去雾方法则是指根据物理模型的去雾方法,在提出大气衰减的物理模型的基础上,提取物理信息,然后去除无关信息而得到去雾图像的方法。基于大气退化物理模型的单幅图像去雾是通过合理的数学推演和假设,还原清晰、高品质的图像,得到近似的最优退化还原图像。这种方法有较强的针对性,一般不会造成信息的损失,得到的除雾图像也比较自然,模型中参数的估计是该类方法的关键点。在应用中,需要结合所拍摄的有雾图像的具体特点,不同方法的适用条件以及实际需要来选择合适的去雾方法,以达到较理想的去雾效果。
    在图像去雾的基础上,再通过一系列的帧组成的图像,在光照,雾,色彩等因素了提供了一些列的共性,我们可以通过帧与帧之间的联系得到更为有用的信息。
    而本文涉及到的视频的去雾以及立体重建,在去雾过程中,有对于物理模型的因素考虑而进行的计算工作,而在此基础上也有基于视频的有序序列特征进行的图像增强。
    1.3 本文的工作在本文中,我们联合研究立体视觉和去雾问题,设计一种同时估计场景深度和立体重建的算法。 我们的方法是基于这样一个观察结果,即立体和雾的厚度的深度线索是相互补充的(即对于附近的物体立体信息更可靠,并且雾的厚度信息对于远处的物体更可靠)。首先介绍了了一个雾模型,以及一个传统的对于视频的立体重建的能量函数,然后在该能量函数使用了基于散射效应的颜色一致性项,新的平滑项,基于传输的排序约束条件的平滑项,并且上加入拉普拉斯平滑项,使用这个新的能量函数,最后通过迭代地最小化该能量函数,得到最优的每一帧的深度图,从而完成立体重建。
    1.4 本章小结在本章节中,主要阐述了图像去雾,视频去雾等的研究背景和意义,描述了视频去雾的价值和意义,做了相应的分类和一些概念的介绍。此外,还梳理了本文的工作,大致的思想流程和会涉及到的一些方法,对后续章节的编写做一个引导和铺垫。
    第2章 综述在初步了解了视频去雾的工作背景和研究意义之后,在本章中将进一步对立体视觉,图像增强,场景深度等一些相关领域的研究做更深入的介绍,并且在介绍的过程中,会提及到对于本文有借鉴参考意义的文章或步骤,从而让读者对后续文章的实现部分有一个先前认知。
    2.1 传统的视频去雾方法视频去雾是计算机视觉领域的一个经典问题。对于一幅在雾天干扰下的图片,通过图像处理的方法尽可能地去除雾,还原图片,得到清晰的图片。基于图像增强的去雾方法是指通过改变明亮程度和对比度来改善图像的视觉效果,该方法能在一定程度上提高雾天图像的质量。但该方法在去除随空间变化的雾时,其作用会受限。基于图像复原的去雾方法基本是根据物理模型的去雾方法,多是指提出在大气衰减的物理模型的基础上,提取物理信息,去除无关信息而还原真实图像的方法。基于大气退化物理模型的单幅图像去雾是通过合理的数学推演和假设,还原清晰、高品质的图像,得到近似的最优还原图像。
    下面是几种经典的基于图像增强的去雾方法
    1.直方图均衡化
    直方图均衡化的基本思想是通过灰度的映射来修正图像的直方图,使其分布更加均衡,该方法是一种简单而有效的图像增强方法。雾天退化图像往往会体现出比较低的对比度,其直方图的分布也常集中在某个区域,因此可以通过均衡化的方法来调整直方图的分布,从而增强图像的对比度,实现有雾图像的清晰化。全局直方图均衡化则是基于整幅图像的,即使得整幅有雾图像的直方图满足均匀分布,为了使雾天图像的对比度得到整体上的增强,可以增加像素的灰度值的动态范围。这种方法的计算量不大,且易于实现,但对于一些细节的增强还不够。而局部的直方图均衡化,则对于被处理图像的所有局部区域,都要采用直方图均衡化,并且为了使图像的局部信息得到自适应地增强,需要在局部范围内对运算进行叠加。

    2.小波变换
    近十几年来,小波变换的信号处理方法也逐渐发展起来。该方法可以理解为对一系列的小波函数进行缩放和平移,并用它们代替傅立叶变换中的余弦和正弦函数,再进行傅立叶变换得到的结果。小波和局部信号的相关程度可以通过小波系数反映出来,而它的计算要通过对母小波的缩放和平移操作来实现。信号的时间信息可以通过平移母小波来得到,而信号的频率特征则可以通过缩放母小波的宽度获取。小波变换具有提供局部分析和细化的能力,这是小波变换的主要优点,小波变换良好的局部特性体现在时域和频域上,在高频区域,要想聚焦到所要分析的对象所具有的任意细节,可以利用逐渐精细的时域,也可以利用空域的步长。该方法既能较好地保留图像的细节特征,又能有效地去除噪声。

    3.Retinex算法
    Retinex理论是由Edwin.H.Land提出的一种经典的图像增强算法,该理论是基于颜色恒常理论。颜色恒常可以体现人眼对环境光线的变化的反应,也就是说,即使环境光线发生了变化,人眼也可以对物体表面反射光线的强度的增强或减弱进行辨别,还可以保证所看到的物体的表面颜色不发生变化。而Retinex理论,即视网膜大脑皮层理论则认为:当人的眼睛感受到亮度变化的时候,人的视觉系统只会对可以体现物体本身属性的信息有印象,所以可以在一定程度上不被外界环境光线的变化所影响,而保证所看到的物体颜色恒定。因为Retinex既具有颜色不变的特性,又具有动态范围压缩的特点,并且它是一种用来描述颜色不变性的模型,所以采用该方法对那些由于光照不均而造成的低对比度的彩色图像进行增强处理时,也会取得很明显的效果。在图像去雾过程中,可以把雾天作为环境照度的变化,利用Retinex算法可以很好地保持物体原本具有的色彩,从而获得具有较好视觉效果的无雾图像。一般来说,应用Retinex处理彩色图像是分别对R,G,B通道进行Retinex处理将得到的结果作为R,G,B通道。这种方式对于三个通道比较均衡的图像来说效果比较好。但是有的图像就只有某一通道,如R通道分量特别小,然后经过Retinex强行将R通道调整到[0,255]区间,这样效果不会太好。
    而基于物理模型对有雾图像进行复原的方法有多种,大体可分为以下3类,分别是基于偏微分方程的、基于深度关系的、以及基于先验信息的。这些方法常常是基于两种大气散射模型,一类是McCartney提出的单色大气散射物理模型,一类是Narasimhan等人从RGB色彩空间出发,推导出的二色大气散射模型。而单纯地基于物理模型实现去雾,由于天气的复杂多变性导致物理模型拟合的困难,而且传统立体算法中的光密度测量不考虑光传播过程中的散射和吸收现象,从而产生系统的匹配误差。

    2.2 基于拉普拉斯平滑的视频去雾方法雾对立体重建算法提出了挑战,同时也带来了补偿优势。从计算的观点来看,众所周知,立体重建对远距离图无法很好地工作。进一步的深度平滑会导致表面细节损失,例如细长的结构和孔洞。雾信息包含的深度信息在性质上是不同的,因为较厚的雾与较大的距离相关联。
    在本文中,联合研究立体视觉和去雾问题,实现了一种同时估计场景深度和对输入图像除雾的算法。在立体视觉融合技术中,分为局部的和全局的立体视觉技术。局部的立体视觉技术围绕个别的像素周围的灰度值或者边缘模式进行匹配,但是其忽略了可能连接邻近点的约束。而立体视觉的全局方法,是将立体视觉问题建模为一个基于近邻像素的序列或者平滑约束的能量函数,并对其进行最小化。本文在基于传统的雾模型和能量函数上,使用了包含更加复杂的颜色一致性项,拉普拉斯平滑项,基于传输的排序约束的平滑项的新能量函数。在本文公式中,立体匹配和雾信息的深度线索相互加强,基于在计算机视觉广泛使用的散射模型,使用这个新的能量函数,可以同时实现立体重建和去雾。
    立体匹配有五个约束条件:极线约束,顺序性约束,连续性约束(也称作表面平滑约束),唯一性约束,相似性约束。提出的衡量函数在一定程度上衡量了立体匹配的连续性约束和相似性约束。该方法包括四个关键特征。首先,我们改进了立体重建中的颜色一致性项以结合散射效应。当从不同的角度评估两个像素的一致性时,我们明确地模拟了由于雾而造成的外观变化。其次,我们直接从场景深度计算每个像素处的雾的传输。这确保了我们的立体效果和除雾效果相互一致。第三步,我们将有效的雾传输的先验结合到立体重建除雾公式中。具体而言,由于雾的传输可以直接从深度计算,所以我们在场景深度处加入了拉普拉斯平滑约束,这个约束有助于捕捉深度图中的细节。最后,我们还利用基于传输的排序深度信息中的可靠信息,采用了成对的场景深度排序约束。
    共同优化新的能量函数中的分项有助于为立体深度恢复和除雾带来双赢,通过定义新的颜色一致性项和拉普拉斯项,有助于保存深度图中的细节,这也是由于它与光谱图像分割密切相关。
    2.3 本章小结在本章中,主要介绍了视频去雾和立体重建等问题的研究现状,总结了其优缺点,并从实际待解决的问题出发,分析了有借鉴意义的方法,提出了本文理论模型的大致框架,这也是接下来后续章节待实现和实验的概括纲要和方向。
    第3章 视频去雾和立体重建方法3.1 颜色一致性项的构建我们知道,在视频序列中,每个点和相邻帧的同一个点的物理信息应该是近似不变的,而像素点的颜色则是最基本的一个物理性质。
    3.1.1 评估深度图的颜色一致性在立体重建中,颜色一致性f(p, V)是一种标量函数,用于测量给定三维重建p与一组图像的视觉兼容性。在3D点p处的简单颜色一致性函数被定义如下:将p投影到V中的每个可见图像中,并且将它们投影附近的图像纹理的相似度计算为颜色一致性。不是比较每个图像中的单个像素颜色,而是比较每个局部图像区域中的一组像素颜色的差异大小。
    (比如说下图就是比较3*3的局部像素区域的差异)

    为了评估立体重建图像和实际视频邻近帧的视觉差异,我们使用颜色一致性项。
    假设我们的视频有n个连续的帧I={It|t=1,…,n},根据任意的标准structure from-motion (Sfm)算法,我们可以得到照相机参数C=(Kt,Rt,tt),其中Kt是内在矩阵,Rt是旋转矩阵,tt是传输向量。简而言之,Sfm算法是一种基于各种收集到的无序图片进行三维重建的离线算法,首先进行特征检测和特征匹配。当所有的两两匹配图像对被确定以后,就可以考虑把多个图像中都出现的共同特征匹配点连接起来,就能形成轨迹了。一旦符合的轨迹都找到后,就构造图像连接图,包含每个图像的节点,和有共同轨迹的图像边缘。最后通过不断地进行Bundle Adjustment,不断加入3D点,得到摄像机参数和场景几何信息。
    也可以根据zhang等人提出的根据视频进行立体重建的方法,近似得到每一帧的深度图 D={Dt|t=1,…,n}, 因此也可以通过求倒数得到其逆深度图Z={Zt|t=1,…,n}。
    为了定义颜色一致性项,按照多视图立体重建的原理,对于第i帧的每一个像素点x,其逆深度为Di(x),那么可以通过以下公式将其投影到第j帧,得到其在第j帧的相应位置

    因此,我们由初始近似得到的逆深度图和邻近帧中的深度信息,可以去估计当前的逆深度图中的立体重建的效果,使用颜色一致性项Ep(Dt) :

    其中N(t)是第t帧的邻近帧数,|N(t)|是邻近帧集合的大小。
    3.1.2 基于散射效应的颜色一致性项的计算由于传统的基于视频的立体重建方法并没有考虑到雾的因素,由于雾天环境下存在着散射效应,因此我们提出了基于雾天散射效应的新的颜色一致性项。
    在计算机视觉中,广泛使用的一个散射模型是:

    其中I:是在散射介质中观察到的图像,IJ是不受散射效应影响的真实图像,A是大气光,a是媒介传输参数,a决定了没有散射并且到达到摄像机的光的比例。当A是同质时,a可以简单的表示如下:

    其中B是散射系数,其决定了传输媒介的密度,而z是该点和摄像机中心的距离,为了简化该公式,我们假设该点的场景深度如那样的方法大致估算。
    为了得到A和B,为了简化该问题,我们使用的方法去估算A。根据得到的A以及一些第i帧和第j帧中匹配的像素点(这也是根据公式3.1),

    根据方程3.3,它们满足于

    由于Xk和Yk其实是对应于同一个场景点,可知:

    因此,我们有:

    其中Zi(xt)是根据zhang G等人提出的获取深度图信息的方法获得的深度信息。根据公式3.6,我们能获得每一对匹配点之间估算的B值,舍弃一些逆深度差别小于某个阈值(通常取0.001)的匹配像素点。为了简化问题,可以简单的取得到的B集合的平均值作为B值。
    我们定义

    其中,

    是Ri的行。

    是ti的行。现在我们使用投影

    来计算在具有媒介传输参数ai(x)的第i帧的像素点x在第y帧的媒介传输参数a:

    其中,

    是矩阵的最后一行。由公式3.4,我们可以从Zi(x)得到ai(x)。因此,我们可以使用基于雾的散射效应的颜色一致性项来评估立体重建的效果:

    其中,

    这可以根据公式3.3简单推导得到。注意到利用公式3.4我们可以利用D(x)来表示,a(x) 因此在公式3.8中,深度D(t)是唯一的自变量。
    3.2 几何相关项的构建几何相关项专门设计用于基于视频的立体重建,以确保恢复的深度图的时间一致性和处理遮挡。我们借鉴zhang等人在2009年提出的几何相关项的计算方法。与典型的多视角立体重建方法不同,该方法不仅强加颜色一致性约束,以统计方式将几何相关性与多个帧明确关联。它们有助于可靠地减少图像噪声和多帧数据遮挡的影响,从而使我们的优化不受过度平滑的影响。它会检查相邻帧中每一像素点与所对应的共轭像素的距离。所谓共轭像素,其实也就是我们之前所说的一个像素点在相邻帧中所对应的像素点。

    其中,

    是第t帧中具有逆深度Dt(x)像素点x的共轭像素在t帧中的位置,与公式3.8提到的相同。让计算机自动建立多幅图像之间的匹配关系其实是立体重建最困难的一个问题。可以看出,这项是为了评估立体重建后的深度图中,每个像素点和邻近帧所对应的像素点的距离关系,在此,我们可以简单的使用街区距离来衡量,当然也可以使用欧式距离。因为如果在相邻帧中出现了较大的像素位移,而照相机显然是在非常缓慢移动的过程中,这个位移较大的点往往是误差导致的。所以我们用几何相关项来评估这个误差的。
    3.3 平滑项的构建平滑的本义是指:重新分配概率,即使没出现的事件也会赋予一个概率。而体现在数学上的平滑法,就是对不断获得的实际数据和原预测数据给以加权平均,使预测结果更接近于实际情况的预测方法,又称光滑法或递推修正法。对于实际数据接近于平稳不变的情况,可以应用一次平滑法,以消除偶然因素的影响。
    3.3.1 朴素的平滑在同一帧的图像中,邻近的像素的深度变化往往变化非常微弱,因此在相邻的像素之间,大部分情况下深度应该是近似不变的。在实际情况中,基于像素点的匹配代价并不能完全正确地反映两幅图像中两个点匹配的正确性;比如噪声、大范围的相似区域等,其结果是错误匹配的代价常常会小于正确匹配代价,从而影响算法在该点的深度估计。因此,必须增加一些额外的平滑约束到能量的定义中,这种约束通常是采用对深度或者灰度的变化的惩罚,以抑制噪声对匹配结果的影响。为了衡量立体匹配的正确性,我们在我们的能量函数中引入了平滑项。
    通常,在立体重建中,平滑项被定义为如下:

    考虑鲁棒性,f(Dt(x),Dt(y))通常是截断的l1函数,即:

    其中t1是截断参数,w(x,y)是权重函数,表示像素点x和像素点y应该拥有相同逆深度的可能性。为了鼓励深度不连续性与颜色变化一致,w(x,y)通常基于相邻像素的色差来定义。在此处,我们简单地采用zhang等人提出的方法去定义平滑项权重系数w(x,y)。
    3.3.2 基于传输的排序约束的平滑之前介绍的朴素的平滑项,通常已经能够较好地在一般的立体重建中衡量立体匹配的效果。然而,我们的立体重建是对于受到大雾天气影响的视频进行的。与绝对深度值的传输相比,雾传输对点与点之间的深度顺序有更可靠的约束。我们可以进一步的利用雾这方面的信息。更具体地说,假设x和y是两个相邻像素,如果at(x)>at(y),我们期望Dt(x)>=Dt(y)。 理论上,公式3.4通过简单的推导可以得出该结论。
    因此,当这个条件被违反时,我们分配一个大的惩罚t2。 在数学上,我们将公式3.10中的f(Dt(x),Dt(y))修改为

    见图3-2,这是我们专门分别对某一帧的利用朴素的平滑项和基于传输的排序约束的平滑项实验的效果,可以看出,朴素的平滑项明显的有很多边缘模糊的现象,而使用基于传输的排序约束的平滑项可以比较好地保持物体的边缘细节。不过在某一些帧的效果上可能现象没有那么明显,这也可能跟某一帧的场景中的物体细节和边缘部分是否比较多有关。因此我们挑出对比效果较为明显的其中一帧的对比效果,原图中场景深度较小的物体拥有比较多的边缘和细节部分,而基于传输的排序约束的实验结果也较好地保存下了细节部分。

    3.4 拉普拉斯平滑项的构建在实际情况中,很有可能出现一种零概率问题,就是在计算某概率时,由于某个分子项为0,而导致整个项的结果也为0。这个问题也常出现在人工智能领域的文本分类上。这是不合理的,不能因为某个分子项为0而认为整个项的最后结果都是0。为了解决零概率问题,法国数学家拉普拉斯最早提出用加1的方法估计没有出现过的现象的概率,所以拉普拉斯平滑也叫做加法平滑。
    雾的出现也开启了丰富重建深度细节的可能性。为此,我们将拉普拉斯约束作为细节保留平滑项。雾透射图应该满足拉普拉斯平滑先验。考虑到该点,我们发现拉普拉斯先验不仅可以重新定义传输的图像,而且有助于保存深度图中的细节部分,可能是因为其和光谱图像分割有密切的联系。拉普拉斯项定义为如下:

    其中vec(at)将at转化成向量形式,It是拉普拉斯矩阵,与Levin提出的一致,它的第i行第j列的元素定义为如下:

    由图3-2可知,相比之下,使用拉普拉斯平滑项能够较好地保留深度图中的细节。

    3.5 立体重建深度图的构建之前提出了若干个评估立体重建中邻近帧之间差异的项,这些在一定程度上都体现了立体重建的实际效果。为了能够同时评估上述项,我们构建了新的能量方程,并且在各项前加入权重系数。此时的能量方程的唯一自变量就是逆深度图Dt,通过能量方程的最优化,可以使得我们选择到对应的最优逆深度图,从而得到立体重建后的深度图,由此,便最后可得到立体重建后的视频去雾图像。
    3.5.1 能量方程的构建在有雾的视频中测量颜色一致性通常很困难,因为场景辐射的衰减与不同的相机位置不同。为了克服这个困难,我们提出一个更复杂的颜色一致性项来考虑散射效应。同时,雾的出现也为丰富重建深度的细节提供了可能性。为此,我们利用传输提供的排序约束,在逆深度施加平滑度。最后,我们将拉普拉斯约束作为细节保留平滑项。
    在目前传统的已有的立体重建方法中,该能量方程已经较为准确地评估了颜色一致性,几何相干项,平滑项所表示的立体重建的效果。在本文中,我们参考HeK等人提出的方法,新加入了拉普拉斯平滑项,以满足拉普拉斯先验,并且提出了优化之后的颜色一致性项,基于传输约束的平滑项,因此综上,我们可以得到一个新的衡量立体重建效果的能量方程,如下:

    其中Eg(Dt)是不变的几何相干项,Eps(Dt),Eso(Dt)和Elop(Dt)分别对应于新的颜色一致性项,新的基于排序约束条件的平滑项和以及拉普拉斯平滑项n 和p,^是平衡这些项的参数。共同优化这些项有助于为立体重建深度恢复和除雾带来共同利益。特别是,远距离物体的模糊性被解除,并且深度和颜色细节都被更好地恢复。
    3.5.2 能量方程的最优化参考zhang等人的方法,我们也采用了两步优化策略。我们通过忽略第一步中的几何相关项来初始化深度图,然后在第二步中迭代地求解方程3.14的完整版本。由于拉普拉斯项的存在,方程3.14并不能轻易求解。因此,我们基于引入一个辅助变量来解耦术语和交替更新的想法。这种策略在许多计算机视觉算法中被广泛使用,并表现出良好的性能。
    我们将函数分成两部分:能量函数可以重新写成:

    我们迭代地最小化新的目标函数,直到收敛或迭代次数超过最大限制。在每次迭代中,我们在固定at时求解Dt,然后在固定Dt时求解at。因此,2个新的子问题是:

    由于方程(3.16)中的最后一项是一元的,并且具有排序约束的Eso(Dt)不会带来额外的困难,因此方程(3.16)可以通过图像分割来求解。第二个子问题方程(3.17)是一个无约束的凸问题并且具有封闭形式的解:

    通过最小化能量方程,我们最终得到了最优情况下的逆深度图,继而可以得到深度图,从而实现了立体重建。
    3.6 本章小结在本章中,主要分部分介绍了整个立体重建的过程,是模糊概念转到实现原理的过程。分别对颜色一致性项,几何相关项,平滑项,拉普拉斯平滑项进行了阐述,通过这些分项的和来共同评估立体重建的效果。每一小节都对具体的项所对应的算法或详或略进行了介绍,下一章中将对输出结果进行展示。
    第4章 实验和分析4.1 实验结果展示我们使用了ZhuwenLi等人用以测试视频去雾效果的视频序列来对我们的算法进行测试和展示,并且将中间过程产生的拉普拉斯平滑图像也展示出来,以对实验的过程和效果有一个感知。下列为其中的部分结果,每组结果上方彩色图为原图,左下方为中间过程的拉普拉斯平滑图像,右下方的是对应的去雾后的图像。

    4.2 实验结果与问题分析从得到的结果可以看出,整个程序能较好地通过拉普拉斯平滑得到细节保存较为完整的深度图,从而从深度图中恢复出对应的立体重建和去雾之后的实际图片。
    但是,也有一些明显的问题和缺点:

    算法运行的使用时间较长,这是因为我们在迭代求优化的最小值的时候,尽可能地迭代多次以达到最优效果,因此处理完整个帧序列的运行时间会稍微比较长。部分图像的细节存在比较严重的失真现象,这可能是由于我们认为的大气光是一种同质的介质,简化了计算媒介传输参数与像素点的场景深度的方法。但是在实际中,大气光往往不是同质的,而且构成也比较复杂,尤其是在一些空气环境不够单一的情况下会更为复杂。部分图像的可见边变得模糊,对比度减小,且有一定的色彩失真,可能是由于我们在计算时,没有按照等人的比较准确但是计算量较大的方法,而是为了降低算法复杂度,简单地取一个平均值,所以随之效果也会有一部分的衰减,导致了边缘的部分模糊。视频图像的帧与帧之间具有一定的相关性,即使单帧图像的处理效果很好,但如果不能保证相邻两帧亮度的相似性,就会使处理后的视频序列中出现频闪,忽明忽暗,亮度不均等不稳定的现象,会对视觉效果有一定影响。这个可能跟我们对于颜色一致性项的权重系数和局部窗口的大小有关,当颜色一致性项的权重系数较小或者局部窗口较小时,相邻帧之间的相关性会被一定程度的弱化,就会导致上述的一些现象。
    4.3 本章小结本章中展示了部分基于拉普拉斯平滑的立体重建和视频去雾的效果图,总结了实际立体重建和视频去雾中出现的一些很明显的问题和缺陷,并对每一个问题和缺陷可能出现在什么地方、哪一个步骤中进行了原因分析和解释,为后续工作总结、展望改进指明了方向。
    第5章 总结与展望至此,我们实现了一种基于拉普拉斯平滑的视频去雾和立体重建的方法,该方法相较于其他的着色方法有优势也有不足,并且自身也存在一定的缺陷,今后的工作将围绕着改善立体重建和视频去雾结果、优化运算方法、 减少程序运行时间花费的方向进行。
    5.1 研究成果总结本文提出的视频去雾方法,理论上只需要指定对应路径的视频帧序列,就可以通过对应分别计算对应的项,最小化能量函数而得到对应的立体重建后的图像,从而实现视频去雾的效果。
    区别于传统的视频去雾方法,本文主要是提出新的颜色一致性项,基于排序约束的平滑项和拉普拉斯平滑项,从而得出一个有别于传统的能量函数的新的表达式,继续可以通过最小化该能量函数获得立体重建的最佳效果。同时相较于现有的视频去雾方法,虽然本文的方法在实验结果上仍有不足,但是对于对应的视频帧序列,大部分的物体细节都能够较好地保留下来,而没有因为立体重建的影响导致失真或者模糊的现象。然而本文的方法是建立在能量函数的迭代最小化基础上,迭代的次数在理论上也会影响最终实验的结果,那么不可避免的一个带来的问题就是程序运行时间可能会比较长。那么为了在一定程度的减短程序运行时间,可能会使得处理的效果还没有达到最优。
    但是如上一章中提到的,本文的方法在实际实现的过程中,还存在着许多的问题和缺陷,这些问题由不同步骤中使用的算法产生。例如, 由于我们认为的大气光是一种同质的介质,简化了计算媒介传输参数与像素点的场景深度的方法。但是在实际中,大气光往往不是同质的,而且构成也比较复杂,尤其是在一些空气环境不够单一的情况下会更为复杂;在计算时,没有按照等人的比较准确但是计算量较大的方法,而是为了降低算法复杂度,简单地取一个平均值。所以随之效果也会有一部分的衰减,导致了边缘的部分模糊;能量函数的各个子项的权重系数的权衡等等。
    5.2 本章小结本章中将本文算法与一些传统视频去雾算法进行比较,并大致总结了本文算法的优劣所在,同时对算法的薄弱的地方提出了可能有效的方法和设想,今后将着重围绕这些部分进行修改和完善。
    参考文献[1] Zhang G, Jia J, Wong T T, et al. Consistent Depth Maps Recovery from a Video Sequence[J]. IEEE Transactions on Pattern Analysis & Machine Intelligence, 2009, 31(6):974-988.
    [2] 涂雅瑗. 雾天降质图像的对比度增强方法研究[D];大连海事大学;2009
    [3] Bobick A F, Intille S S. Large Occlusion Stereo[J]. International Journal of ComputerVision, 1999, 33(3):181-200.
    [4] Boykov Y, Veksler O, Zabih R. Fast approximate energy minimization via graph cuts[C]// The Proceedings of the Seventh IEEE International Conference on Computer Vision. IEEE, 2002:377-384 vol.1.
    [5] 纪松.多视匹配策略与优化方法研究[D].郑州:信息工程大学测绘学院,2012.
    [6] 李学明 . 基于Retinex 理论的图像增强算法[J]. 计算机应用研究, 2005(02):235-237.
    [7] Caraffa L, Tarel J P. Stereo reconstruction and contrast restoration in daytime fog[C]// Asian Conference on Computer Vision. Springer-Verlag, 2012:13-25.
    [8] Carr P, Hartley R. Improved Single Image Dehazing Using Geometry[C]// Digital Image Computing: Techniques and Applications. IEEE, 2010:103-110.
    [9] Cozman F, Krotkov E. Depth from Scattering[C]// Computer Vision and Pattern Recognition, 1997. Proceedings. 1997 IEEE Computer Society Conference on. IEEE, 1997:801-806.
    [10] Fattal R. Single image dehazing[C]// ACM SIGGRAPH. ACM, 2008:72.
    [11] Felzenszwalb PF, Huttenlocher D P. Efficient Belief Propagation for Early Vision[J].International Journal of Computer Vision, 2006, 70(1):41-54.
    [12] 赵莹.基于单幅图像的去雾算法研究 [D].天津大学,2009.
    [13] 董长虹, 高志, 余啸海.Matlab小波分析工具箱原理与应用.北京:国防工业出版社, 2004
    [14] Geman D, Reynolds G. ConstrainedRestoration and the Recovery of Discontinuities[M]. IEEE Computer Society,1992.
    [15] 潘泉, 张磊, 梦晋丽, 等.小波滤波方法及应用[M]. 北京: 清华出版社, 2004: 40−78.
    [16] 董卫军, 周明全, 黎晓, 耿国华. 基于小波分析的边缘检测技术研究[J]. 计算机工程与应用 2004(25)
    [17] He K, Sun J, Tang X. Single ImageHaze Removal Using Dark Channel Prior[J]. IEEE Transactions on Pattern Analysis& Machine Intelligence, 2011, 33(12):2341-2353.
    [18] Jiang N, Cui Z, Tan P. A Global Linear Method for Camera PoseRegistration[C]// IEEE International Conference on Computer Vision. IEEE,2014:481-488.
    [19] Kopf J, Neubert B, Chen B, et al. Deep photo: model-basedphotograph enhancement and viewing[J]. Acm Transactions on Graphics, 2008,27(5):1-10.
    [20] Krishnan D, Fergus R. Fast image deconvolution usinghyper-Laplacian priors[C]// International Conference on Neural InformationProcessing Systems. Curran Associates Inc. 2009:1033-1041.
    [21] Levin A, Lischinski D, Weiss Y. A closed-form solution to naturalimage matting.[C]// IEEE Computer Society Conference on Computer Vision andPattern Recognition. IEEE Computer Society, 2006:61-68.
    [22] 纪松, 范大昭, 戴晨光, 等. 线阵影像GC3多视匹配及其扩展模型研究[J]. 测绘科学技术学报,2009,26(6): 44-47.
    [23] Zhuwen Li, Tan, Ping, Tan,Robby T, et al. Simultaneous video defogging and stereo reconstruction[J].2015:4988-4997.
    [24] 陈永亮, 灰度图像的直方图均衡化处理研究[D];安徽大学;2014[25] Narasimhan S G, Nayar S K. Contrast restoration of weather degradedimages[J]. IEEE Transactions on Pattern Analysis & Machine Intelligence,2003, 25(6):713-724.
    [26] 肖燕峰, 基于Retinex理论的图像增强恢复算法研究[D];上海交通大学;2007
    [27] 张璇, Retinex理论及其在压缩域图像增强中的应用研究[D];合肥工业大学;2010
    1 评论 4 下载 2018-10-01 22:48:38 下载需要13点积分
  • 基于VC++的四国军棋局域网联网游戏的设计与实现

    摘 要本文详细介绍了一个在Windows环境下的基于游戏大厅框架的四国军棋网络游戏的设计和实现。该游戏可在局域网上联机对战,实现了游戏、聊天、积分等功能。该软件在Windows环境下用VC++6.0实现。
    关键词:VC++ , 网络协议,游戏协议,套接字,军棋。
    AbstractThis article introduces the design and realization of a network game named junqi in Chinese,which runs under the game hall architecture. The game can run on LAN with the functions playing game, chatting, score. And the program is made in the Windows and Visual C++6.0 environment.
    Key words: VC++ ; Network Protocol; Game Protocol; Socket; junqi
    第一章 绪论1.1 问题概述1.1.1 问题的来源随着Internet的普及,网络游戏已经成为大家耳濡目染的新生事物。网络游戏从出现到现在的发展的时间很短,但是其发展速度却非常之快。现在,可以说网络游戏已经成为人们休闲、娱乐的有效方式。国内比较有名气的网络游戏有联众网络游戏世界(http://www.ourgame.com)、qq游戏中心(http://www.tencent.com)、中国游戏中心(http://www.chinagames.net)以及这几年受玩家亲睐的传奇、魔兽世界等。
    1.1.2 目的和意义对当今网络游戏的设计、架构进行分析、探索和实践。提供友好的客户操作界面,通过客户端与服务器之间的网络传递数据,实现了多人协同游戏的目的。分析现今网络游戏体系结构及设计模式的优缺点,及对网络游戏的发展给予分析和展望。
    1.1.3 国内外研究现状现今网络游戏的体系结构(见图1-1),包括客户机程序、服务器程序、数据库服务器。

    1.2 问题剖析1.2.1 主要问题在开发网络游戏时,首先要建立底层的网络通信类,利用网络通信类连接构建客户服务器之间的TCP/IP连接,然后在该连接的基础上利用自设定的协议进行客户端登录、进行游戏等操作。在以上协议的基础上,根据不同的游戏编写不同的游戏逻辑处理类,在该逻辑处理类中实现了对应的游戏逻辑,如实例中的军棋,则实现相互之间的对弈等功能。同时在服务器端还需要和数据库服务器交互,用于读取或保存客户信息(如用户积分、密码、个人资料等数据)。
    1.2.2 难点和关键
    有一个或多个游戏服务器启动特定游戏服务。游戏者到游戏网站上下载客户端程序并且申请游戏账号ID。然后启动客户端程序通过某种网络协议连接游戏服务器。客户端程序负责处理客户端显示和操作界面,具有简单的逻辑处理功能,同时负责接收发送与服务器端交互的数据包。服务器程序负责处理服务器端逻辑、游戏逻辑、客户之间的网络信息传递,以及数据库之间的数据读取保存工作。同时服务器端还要承担客户端数据的接收、转发工作。
    1.2.3 思路和方法网络游戏通常的运行方式(见图1-2)。

    第二章 相关的知识和工具2.1 解决问题的知识基础网络游戏常用的网络协议有适用于Internet的TCP/IP协议、适用局域网(比如星际)的IPX协议。
    1.TCP/IP协议
    TCP/IP协议(Transmission Control Protocol/InternetProtocol,传输控制协议/网际协议)是Internet中计算机进行通信的标准,其命名起源于该组协议中最重要的两个协议TCP和IP。任何关于Internet协议的讨论必须由TCP/IP开始,它也是其他所有协议的基础。TCP/IP协议是Internet网络的共同语言,主机之间必须利用TCP/IP互通信息。
    TCP/IP协议目前已经成为发展最成功的通信协议之一,它起源于20世纪60年代末美国政府资助的一个分级交换网络研究项目,允许分布在各地的使用不同操作系统的网络进行通信。随着世界范围个人电脑的普及,日常无论收发邮件、访问网页和文件传输都已经离不开TCP/IP协议,TCP/IP协议已经成为Internet的基础。
    2.TCP/IP结构
    TCP/IP实际上就是在物理网上的一组完整的协议。其核心部分是传输层协议(TCP/UDP)、网络层(IP)和物理接口层,这三层通常在操作系统内核中实现。TCP/UDP层提供了传输层服务,而IP协议提供了网络层服务。
    TCP/IP协议是一个四层协议,其结构如图2-1所示。

    应用程序与TCP/IP可靠传输之间接口具有五大特性:

    面向数据流 
    当两个用户进程传输大量数据时,我们把这些数据当做可划分为八位组(octer,字节)的比特流,在目的机器上运行的数据流投递服务软件提给接收方的八位组与信源机上发送方送出来的完全相同。
    虚电路连接 
    数据流的传输与电话相似,使用“虚电路”这个术语来描述这种连接是因为在应用程序看来这种连接像是一条专用的硬件电路,这种可靠连接的错觉是由数据流投递服务提供。
    有缓冲的传送 
    使用虚电路服务来发送数据流的应用程序不断向协议软件递交数据八位组。为了提高效率以及减少网络延迟,协议软件在实现时都会从数据流中收集到足够多的数据,组成大小合理的数据包后再送到网络上传输。
    无结构的数据 
    TCP/IP协议并不区分结构化的数据流。使用数据流的应用程序必须在开始连接之前就了解数据流的内容并对其格式进行协商。这点很重要,在程序中表现为send函数只能发送字符串,这就需要将接收的字符串转化需要的结构化的数据。
    全双工连接 
    TCP/IP流服务所提供的功能是双向的全双工连接。其中包括了两个独立、流向相反的数据流,而这两个数据流之间不进行显式的交互。

    常用协议主要包括TCP/UDP层协议和IP层协议。TCP和UDP都是传输层协议,都使用IP协议作为网络层协议。使用UDP协议的应用程序必须承担可靠性的工作,包括报文的丢失、重复、乱序以及连接失效等问题,而程序员编程时则容易疏忽。
    2.2 开发平台操作系统Windows 7,开发工具Visual C++ 6.0。
    2.3 数据库SQLServer 2000,是一个全面的数据库平台,引擎为关系型数据和结构化数据提供了安全可靠的存储功能。
    第三章 总体设计3.1 总体设计的框架军棋游戏的总体设计框架,客户端如图3-1所示。

    军棋游戏的总体设计框架,服务器如图3-2所示。

    3.2 模块功能概述客户端类的划分:

    游戏基本类:负责处理游戏中一些完成基本功能的类,如处理声音类、处理动画图标,图形按钮等类,基本类的特点是被其他类在特定处所调用,并不动生成对象。游戏框架类:负责处理游戏中客户端用于显示程序界面和绘制游戏界面以及显示用户信息和广告信息等处理任务。游戏通信类:负责处理游戏中客户服务器之间的网络传输细节,从而在编程中不用考虑网络通信细节,达到客户和服务器之间的透明的效果。游戏应用程序类:主要负责处理应用程序中各种设置显示对话框、程序主线程处理、程序中基本的运行类框架的管理,以及游戏中图形的处理和显示等任务的处理。游戏处理类:主要用于处理游戏简单逻辑、负责解析和处理与服务器端交互的游戏数据,以及在游戏运行中维护游戏中的各种数据,同时维护处理游戏主线程逻辑等功能。
    服务器类的划分:

    游戏通信类:负责处理游戏中客户服务器之间的网络传输细节,从而在编程中不用考虑网络通信细节,达到客户和服务器之间透明传输的效果。游戏协议类:负责处理游戏中客户服务器之间交互所传递的数据,并且对该数据格式进行打包和解包,同时根据该包中所包含的指令串进行相应的操作。游戏逻辑类:负责处理游戏逻辑,如军棋游戏中用于维护军棋逻辑,判断下棋,得分等处理类。用户管理类:用于管理用户资料,在用户登录后通过数据库验证用户名和密码,通过验证后从数据库读取用户的详细资料。同时在程序中维护用户数据,在用户游戏结束和退出游戏时将用户数据保存到数据库中。服务器框架类:用于管理游戏大厅的数据,包括一些数据的列表。数据库类:用于网络游戏的服务器端在处理大量的客户资料时,使用数据库进行大量数据的存储和查询所调用的类方法。
    3.3 关键算法模块间的数据传递设计,如图3-3所示。

    这样,在CTableView和CGameDlg中通过指针,也可向服务器发送消息。而CCGameHallFrameView收到消息后,同时也控制CTableView和CGameDlg的行为。
    采用内存作图的方式,消除了闪烁。首先创建一个内存dc,将绘图的工作先在内存dc中做好,再贴到实际dc上去。做法如下:
    首先创建关于屏幕的内存DC,代码为MemDC.CreateCompatibleDC( pDC); 之后创建一幅关于屏幕DC的图画,部分代码如下:
    CRect rect; this->GetClientRect(rect); CBitmap bmpFace; bmpFace.CreateCompatibleBitmap(pDC, rect.Width(), rect.Height());
    注意把握rect的尺寸为客户区域大小; 之后将这幅画选入内存DC中,部分代码如下:
    CBitmap* pOldBmp = NULL; pOldBmp = MemDC.SelectObject(&bmpFace);
    之后可以开始在内存DC中进行任何绘制动作;部分代码如下:
    CBrush brush(RGB(255, 255, 255)); MemDC.FillRect(rect, &brush); for(int i=0; i<500; i++) { MemDC.MoveTo(22+i, 22); MemDC.LineTo(22+i, 277); }
    绘制完后将内存DC中的这幅图绘制到屏幕DC中来,部分代码如下:
    pDC->BitBlt(rect.left, rect.top, rect.Width(), rect.Height(), &MemDC, rect.left, rect.top, SRCCOPY);
    最后进行相关的资源回收动作,部分代码如下:
    MemDC.SelectObject(pOldBmp); bmpFace.DeleteObject();
    同时我们要把系统的ON_WM_ERASEBKGND消息函数重载为return FALSE,否则还是会出现闪烁情况。
    在对话框中用这个方法的时候,要注意将有控件的部分和需要绘图的部分分开,可采用如下方法,部分代码如下:
    CRect rectClient;CRgn rgn1,rgn2; //rgn3,rgn4; GetClientRect(rectClient); rgn1.CreateRectRgnIndirect(rectClient); rgn2.CreateRectRgn(730,0,962,670); if(rgn1.CombineRgn(&rgn1,&rgn2,RGN_XOR) == ERROR) { return; } MemDC.FillRgn(&rgn1,&brush);
    服务器对连接的用户数据的数据结构的设计,用下面两个结构:
    typedef struct TT{ int d[4]; int ID[4]; int board[17][17]; BOOL begin[4]; }; typedef struct MM{ CString name,sex; int score; };
    连接的socket保存在如下链表中:
    typedef CList <SOCKET,SOCKET&> SOCKET_ARRAY;SOCKET_ARRAY m_connectionList;
    与服务器建立连接后,相应的用户数据,保存在MM结构数组中,位置为其socket在m_connectionList中的相应位置,这样就可以通过连接的socket找到相应的信息。TT是保存大厅数据的结构,d表示四个坐位有没有人,ID表示就坐的人的socket在链表中的位置。board为棋盘数据。每个桌子有每个桌子的棋盘数据,互不干扰。随着游戏的进行而不断更新。begin表示四个玩家有没有下调度完成的指令。
    应用矩阵的变换实现坐标旋转。不管你坐在哪个方位,玩游戏时,你始终是在正下方,这就需要实现虚拟坐标到目标坐标的旋转变换,如图3-4。

    X为实际坐标,x为虚拟坐标,转换公式推导如下,见图3-5。

    // direct 为自己座位方向switch(direct)case 0:(a=90度) [X,Y,1]=[-y+16,x,1] //东case 1:(a=0度) [X,Y,1]=[x,y,1] //南case 2:(a=-90度) [X,Y,1]=[y,-x+16,1] //西case 3:(a=180度) [X,Y,1]=[-x+16,-y+16,1] //北
    回溯法求工兵路径,军棋游戏逻辑不复杂,只有工兵能够自由飞行。在这里没有求工兵起点到目标点的最短路径,而只是用回溯法求出其中一条路径,所以在游戏中你会发现工兵有时候会绕些弯路。这和老鼠走迷宫差不多的。
    工兵迷宫数组:
    int RAILWAY[17][17]={ {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,1,1,1,1,1,0,0,0,0,0,0}, {0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0}, {0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0}, {0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0}, {0,0,0,0,0,1,1,3,1,3,1,1,0,0,0,0,0}, {0,1,1,1,1,1,1,3,1,3,1,1,1,1,1,1,0}, {0,1,0,0,0,2,2,0,2,0,2,2,0,0,0,1,0}, {0,1,0,0,0,1,1,3,1,3,1,1,0,0,0,1,0}, {0,1,0,0,0,2,2,0,2,0,2,2,0,0,0,1,0}, {0,1,1,1,1,1,1,3,1,3,1,1,1,1,1,1,0}, {0,0,0,0,0,1,1,3,1,3,1,1,0,0,0,0,0}, {0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0}, {0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0}, {0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0}, {0,0,0,0,0,0,1,1,1,1,1,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},};
    0表示该位置不在工兵铁道上,1表示在该位置可向上下左右方向移动,2表示在该位置只能向上下方向移动,3表示在该位置只能向左右方向移动。
    3.4 关键技术网络通信,Windows Socket 编程接口:
    Windows Socket (简称 WinSock)是在Win32平台上访问基层网络协议的接口。在不同的Win32平台上,Windows Socket以不同的方式存在着,作为网络编程接口而不是协议存在。
    套接字(Socket)概念,套接字是从英文单词Socket翻译而来,它是网络通信的基本操作单元,是应用层到传输层的接口,可以将套接字看作不同主机间的进程进行双向通信的端点。
    Windows Socket组成部分,Windwos Sockets实现一般都由两部分组成:开发组件和运行组件。开发组件是供程序员开发Windows Sockets应用程序使用的,主要是WinSock.h头文件。对于Windows Sockets应用程序的源文件来说,只要包括WinSock.h就可以了。除此之外,在使用WinSock的项目中还需要加入WinSock API引入库wsock32.lib。运行组件是Windows Sockets应用程序接口的动态连接库(DLL),文件名为WinSock.dll,应用程序在执行时通过装入它实现网络通信功能。
    Windows Sockets编程的基本模式,要通过互联网进行通信,你至少需要一对套接字,其中一个运行于客户端,我们称之为Client Socket,另一个运行于服务器端,我们称之为Server Socket。使用Socket进行网络通信一般有两种方式:基于面向连接的流方式和基于无连接的数据报方式。面向连接的的流方式调用过程如图3-6所示。

    第四章 详细设计4.1 数据库结构服务器端数据库结构如表4-1,玩家信息表,用作记录玩家游戏数据。




    列名
    数据类型
    长度
    允许空
    描述




    主键
    name
    char
    10
    no
    用户名



    code
    char
    10
    no
    密码



    score
    int
    4
    yes
    游戏积分



    sex
    char
    2
    yes
    性别



    4.2 模块结构客户端模块结构:

    游戏基本类,该类包中包括CBorderButton、CClockObject、CWave类。游戏框架类,该类包中包括CMainFrame、CCGameHallFrameView、CHtmlViewEx、CTableView类。游戏通信类,该类包中包括CClient类。游戏应用程序类,该类包包括CGameDlg类,其中包括对游戏处理类的调用。游戏处理类,该类包中主要包括TakeGame类。
    服务器模块结构:

    游戏通信类,由CServer类实现。游戏协议类,由CServerProtocol类实现。游戏逻辑类,由CServerLogic类实现。数据库类,由CServerFrameSet类实现。用户管理类,由CServerFrameView类实现。服务器框架类,由CServerFrame类实现。
    4.3 重要模块详述客户端模块:
    CBorderButton类是带有边框的图片按钮类,CClockObject 类是时钟类,CWave是用来播放声音的类,当游戏用户下棋,吃子,起身或坐下时要播放声音,选择在程序中调用API函数PlaySound直接播放在资源中的声音文件。模块描述如图4-1。

    CMainFrame类是游戏窗口中的游戏大厅框架类,其中包括构架广告显示窗口,可玩的游戏类、工具栏、游戏桌的信息、显示信息等,CCGameHallFrameView类是用来在游戏大厅窗口左侧显示可以玩的游戏以及游戏室,采用树状结构显示。CHtmlViewEx类用于构造广告显示窗口,用于游戏框架中显示广告页。CTableView显示包含游戏桌的游戏大厅,在大厅中多人在等待开始游戏。模块描述如图4-2。

    CClient游戏通信类,功能为建立和服务器的连接,能及处理通信,采用异步机制,以自定义消息事件处理通信等功能。采用自定义消息:WM_USER + 101。模块描述如图4-3。

    CGameDlg游戏应用程序类,其中包括对游戏处理类的调用。CGameDlg 主要负责处理应用程序中各种设置显示对话框、程序主线程处理、以及游戏中的图形的处理和显示等任务的处理。模块描述如图4-4。

    TakeGame游戏处理类,主要用于处理游戏简单逻辑、负责解析和处理与服务器端交互的游戏数据,以及在游戏运行时维护游戏中的各种数据,同时维护处理游戏主线程逻辑等功能。模块描述如图4-5。

    服务器模块:
    CServer游戏通信类,负责处理游戏中客户与服务器之间的网络连接细节,从而使得对于客户和服务器之间的数据传输可以忽略通信细节。模块描述如图4-6。

    CServerProtocol游戏协议类,游戏协议类负责解析客户/服务器端所传输的协议,同时根据不同的协议调用不同的操作函数,并根据用户状态变化维护用户信息。模块描述如图4-7。

    CServerLogic游戏逻辑类,负责处理游戏逻辑,在军棋游戏中包括计算得分,计算赢等。游戏逻辑类和游戏协议类分离的优点是可以只通过修改逻辑类来改变为不同的网络游戏,如修改为“象棋逻辑”即可以成为象棋游戏,修改为“升级逻辑”即可以成为升级游戏。
    用户管理类,直接在CCServerFrameView中实现。
    数据库类,采用SQL Server 2000数据库,只是用来保存玩家的资料等一些数据。采用MFCODBC数据库编程,在程序中为CServerFrameSet类。正如MFC提供的其他类库很好地对相应的Win32 API作了封装,MFC提供的ODBC类库也相应地对ODBCAPI作了封装,通过提供一种高级接口而避免直接使用ODBC API所涉及的种种繁琐处理,简化了对ODBC数据库的应用程序编程。模块描述如图4-8。

    第五章 程序编码5.1 数据结构用17×17的数组表示军棋的棋盘,如下:
    int BOARD[17][17] = { {-1,-1,-1,-1,-1,-1,0,0,0,0,0,-1,-1,-1,-1,-1,-1}, {-1,-1,-1,-1,-1,-1,0,0,0,0,0,-1,-1,-1,-1,-1,-1}, {-1,-1,-1,-1,-1,-1,0,0,0,0,0,-1,-1,-1,-1,-1,-1}, {-1,-1,-1,-1,-1,-1,0,0,0,0,0,-1,-1,-1,-1,-1,-1}, {-1,-1,-1,-1,-1,-1,0,0,0,0,0,-1,-1,-1,-1,-1,-1}, {-1,-1,-1,-1,-1,0,0,0,0,0,0,0,-1,-1,-1,-1,-1}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,-1,0,-1,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,-1,0,-1,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, {-1,-1,-1,-1,-1,0,7,3,4,5,3,0,-1,-1,-1,-1,-1}, {-1,-1,-1,-1,-1,-1,10,0,1,0,9,-1,-1,-1,-1,-1,-1}, {-1,-1,-1,-1,-1,-1,8,10,0,1,5,-1,-1,-1,-1,-1,-1}, {-1,-1,-1,-1,-1,-1,6,0,4,0,3,-1,-1,-1,-1,-1,-1}, {-1,-1,-1,-1,-1,-1,2,7,6,1,11,-1,-1,-1,-1,-1,-1}, {-1,-1,-1,-1,-1,-1,11,12,11,2,2,-1,-1,-1,-1,-1,-1},};
    其中,为了处理工兵的走法的方便,将四个转角斜对的位置恒置为0,这样在为工兵寻找路径时会方便一些。数字的含义如下所示:
    -1 表示棋盘以外的位置0 表示棋盘内位置,但无棋子1 工兵2 排长3 连长4 营长5 团长6 旅长7 师长8 军长9 司令10 炸弹11 地雷12 我方军旗13 上方盟友或敌方的棋子14 左边敌方的棋子15 右边敌方的棋子16 上方军旗17 左方军旗18 右方军旗游戏协议
    以char(20)作为一次信息的起始点,终点。“+”在字符串中用空格代替。以一字符串作为向服务发出的请求信息,服务器也以相应的一字符串发送回客户端作为应答。
    对话:A+桌子号+聊天内容坐下:B+桌子号+方位调度完成:C+桌子号+方位+(棋盘数据)[用一维数据形式表示6×5的二维数组,数据之间用空格开开始游戏:(只由服务器发出)D+对战类型下棋:E +桌子号+方位+起点(x,y)+终点(x,y)吃:(只由服务器发出)(包括移动)F +起点(x,y) + 终点(x,y)被吃:(只由服务器发出)G +起点(x,y) + 终点(x,y)炸:(只由服务器发出)H +起点(x,y) + 终点(x,y)输:离开:(包括断线)投降:I +方位 求和:K +桌子号+方位注册:L +用户名+密码注册成功:M + 用户名登录:N + 用户名+密码登录结果:O + 1/0离开:P + 桌子号 + 方位请求接收信息:Q + 桌子号 + 方位碰:R + 起点(x,y) + 终点(x,y)5.2 主要界面服务器界面如图5-1所示。

    客户端大厅界面如图5-2所示。

    军棋游戏界面,如图5-3所示:

    5.3 重要模块程序实现客户端三拆分窗口的实现
    BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext) { if(NULL==m_wndSplitter.CreateStatic(this,1,2)) return FALSE; if(!m_wndSplitter.CreateView(0,0,RUNTIME_CLASS(CTableView),CSize(620,800),pContext)) return FALSE; if(NULL==m_wndLeftSplitter.CreateStatic(&m_wndSplitter,2,1, WS_CHILD|WS_VISIBLE,m_wndSplitter.IdFromRowCol(0,1))) return FALSE; if(!m_wndLeftSplitter.CreateView(0,0,RUNTIME_CLASS(CCGameHallFrameView),CSize(380,300),pContext) || !m_wndLeftSplitter.CreateView(1,0,RUNTIME_CLASS(CHtmlViewEx),CSize(380,500),pContext)) return FALSE; pWebView=(CHtmlViewEx*)m_wndLeftSplitter.GetPane(1,0); pServerTreeView=(CCGameHallFrameView*)m_wndLeftSplitter.GetPane(0,0) pTableView=(CTableView*)m_wndSplitter.GetPane(0,0); pServerTreeView->pTable=pTableView; pWebView->Navigate2("http://free2.e-168.cn/zhou207",0,NULL); return TRUE;}
    客户端初始化及连接
    void CClient::ClientInit(){ if(WSAAsyncSelect(m_hSocket,m_hWnd,CLI_MESSAGE,FD_READ|FD_WRITE|FD_CLOSE|FD_CONNECT)>0) { AfxMessageBox("Error in select"); }}BOOL CClient::InitAndConnect(HWND hWnd, UINT port, CString strServer){ m_hWnd=hWnd; m_uPort=port; m_strServer=strServer; if(m_hSocket!=NULL) { //先将以前打开的套接字关闭 closesocket(m_hSocket); m_hSocket=NULL; } //创建面向连接的socket m_hSocket=socket(AF_INET,SOCK_STREAM,0); ASSERT(m_hSocket!=NULL); ClientInit(); //设置连接信息:网络协议+IP地址+端口 m_addr.sin_family=AF_INET; m_addr.sin_addr.S_un.S_addr=inet_addr(m_strServer.GetBuffer(0)); m_addr.sin_port=htons(m_uPort); //连接服务器 int ret=0; int error=0; ret=connect(m_hSocket,(LPSOCKADDR)&m_addr,sizeof(m_addr)); if(ret==SOCKET_ERROR) { //连接失败 if(GetLastError()!=WSAEWOULDBLOCK) { AfxMessageBox(_T("请确认服务器确实已经打开并工作在同样的端口!")); return FALSE; } } return TRUE;}
    服务器初始化及连接
    void CServer::ServerInit(){ //设置socket的异步模式 if(WSAAsyncSelect(m_hSocket,m_hWnd,SER_MESSAGE,FD_ACCEPT|FD_READ|FD_WRITE|FD_CLOSE)>0) AfxMessageBox("error select");}BOOL CServer::InitAndListen(HWND hWnd, UINT port){ m_uPort=port; m_hWnd=hWnd; if(m_hSocket!=NULL) { //先关闭已经打开的socket closesocket(m_hSocket); m_hSocket=NULL; } //创建面向连接的流方式的套接字 m_hSocket=socket(AF_INET,SOCK_STREAM,0); ASSERT(m_hSocket!=NULL); ServerInit(); m_addr.sin_family=AF_INET; m_addr.sin_addr.S_un.S_addr=INADDR_ANY; m_addr.sin_port=htons(m_uPort); int ret=0; int error=0; //绑定套接字到本机的某个端口 ret=bind(m_hSocket,(LPSOCKADDR)&m_addr,sizeof(m_addr)); if(ret == SOCKET_ERROR) { AfxMessageBox("Binding Error"); return FALSE; } ret = listen(m_hSocket, 64); if(ret == SOCKET_ERROR) { AfxMessageBox("Listen Error"); return FALSE; } return TRUE;}
    回溯法寻找工兵路径
    BOOL TakeGame::TakeEngineer(CPoint from, CPoint to){ POS p;int x,y;engineer=TRUE; p.d=0;p.x=from.x;p.y=from.y;top=0; if(RAILWAY[p.x][p.y]==2) p.d=1; stack[top]=p;int f[17][17]; while(top>=0) { p=stack[top]; if(p.x==to.x&&p.y==to.y) return TRUE; if(p.d<4) { x=p.x+d[p.d][0];y=p.y+d[p.d][1]; if(x==to.x&&p.y==to.y) return TRUE; if(board[x][y]==0&&RAILWAY[x][y]!=0&&f[x][y]!=1) { if(RAILWAY[p.x][p.y]==2||RAILWAY[p.x][p.y]==3) stack[top].d+=2; else stack[top].d+=1; if(RAILWAY[x][y]==2) { f[stack[top].x][stack[top].y]=1; p.x=x;p.y=y;p.d=1; stack[++top]=p; }else { f[stack[top].x][stack[top].y]=1; p.x=x;p.y=y;p.d=0; stack[++top]=p; } } else { if(RAILWAY[p.x][p.y]==2||RAILWAY[p.x][p.y]==3) stack[top].d+=2; else stack[top].d+=1; } }else top--; } return FALSE;}
    坐标旋转变换
    //根据坐标和所在的方位,得到旋转后的坐标。POINT CCServerFrameView::rotate(POINT original,int direct){ POINT n; switch(direct) { case 0: n.x=16-original.y; n.y=original.x; break; case 1: n.x=original.x; n.y=original.y; break; case 2: n.x=original.y; n.y=16-original.x; break; case 3: n.x=16-original.x; n.y=16-original.y; break; } return n;}//根据坐标和所在的方位,得到逆旋转后的坐标。POINT CGameDlg::AdverseRotate(POINT p, int dir){ POINT pos; switch(dir) { case 0: pos.x=p.y;pos.y=16-p.x; break; case 2: pos.x=16-p.y;pos.y=p.x; break; case 1: pos.x=p.x;pos.y=p.y; break; case 3: pos.x=16-p.x;pos.y=16-p.y; break; } return pos;}
    第六章 问题和展望6.1 特色与成功实现了一个具有小型网络游戏特征的四国军旗游戏,有一个完整的框架,数据库、服务器、客户端、大厅、游戏框架、玩家信息、游戏中的聊天、广告信息、以及防止了闪烁的画面,游戏音乐。
    应用矩阵变换,实现不同方位之间的坐标变换。
    将游戏的仲裁权交给服务器,如在这个军棋游戏中,由服务器判断两个棋子的大小,客户端只识别已方每个棋子的类型,限制了客户端得到的数据流,一定程度上防止了作弊。
    6.2 问题与展望将游戏的仲裁权交给服务器并不能完全防止作弊,而且也相应增加了服务器的负担。这对于大型网络游戏来说,是不可忍受的。即使是用现在最好的服务器,网络游戏,在服务器端,都不会介入逻辑判断,重点在数据的存储、保护和更新。而在客户端,它能获得全部逻辑判断的数据。
    也就是说,只要能破译数据流的格式,也就是破译前面所提到的游戏协议,就能随心所欲的操纵、更改、模拟数据,达到作弊的目的。这也是为什么在现在的网络游戏中,有那么多的作弊器、外挂的原因之一。
    目前做的最好的游戏之一,如魔兽世界,也都没能杜绝外挂的出现。而它采取了一种人为的方式——玩家监督。在目前来说,已经是最好的方法了。
    参考文献[1] 朗锐. Visual C++数据库开发基础及实例解析. 第一版. 北京: 机械工业出版社, 2005
    [2] 苏羽, 王媛媛. Visual C++网络游戏建模与实现. 第一版. 北京: 北京科海电子出版社, 2003
    [3] 清华天则工作室. 网络游戏从入门到精通. 第一版. 北京: 内蒙古人民出版社, 2002
    [4] 荣钦科技. Visual C++游戏设计. 第一版. 北京: 科海电子出版社, 2003
    [5] 史银雪, 陈洪, 王荣静. 3D数学基础: 图形与游戏开发. 第一版. 北京: 清华大学出版社, 2005
    [6] 王小虎, 靳自愚译. C 语言计算机游戏程序设计. 第一版. 北京: 科学出版社, 1995
    [7] http://www.Microsoft.com/china/msdn/
    [8] 荣钦科技. 游戏设计概论. 第一版. 北京: 科海电子出版社, 2003
    [9] 潘志翔, 岑进锋. 黑客攻防编程解析. 第一版. 北京: 机械工业出版社, 2003
    [10] 贾斌. 网络编程技巧与实例. 第一版. 北京: 人民邮电出版社, 2001
    1 评论 9 下载 2018-10-01 22:39:07 下载需要8点积分
  • 基于SMTP协议的E-MAIL电子邮件发送客户端软件C#实现

    摘 要电子邮件在当今社会中扮演了一个很重要的角色。越来越多的人在使用它。而且用它的人数势必会继续增加。虽然,现在已经有很多的邮件收发软件例如著名的FoxMail 但是对于大多数的非专业的人来说它还是有点难度稍嫌负责。因此,我们就利用SMTP和Pop协议从底层开发了这个软件。SMTP全称是简单邮件传输协议,它专门用来发送邮件用的。Pop全称是邮局协议,是专门用于接收邮件的。我主要是负责如何实现发送邮件功能的。MailSend命名空间是我整个程序的核心。它包括两个类。在SmtpMail的类中包含了一个SendMail的方法,它从底层详细地实现了和服务器的交互操作。你既可以用它发送一个纯文本邮件,也可以发送一个带有附件的邮件,理所当然地,你也可以使用不同的SMTP服务器。经过测试,证实此软件是一个支持多收信人,多附件的群发软件。虽然它没有FoxMail那么强大的功能,但是它容易掌握和使用。
    关键词:SMTP,命名空间,类,附件
    AbstractE-Mail play a veryimportant role in modern times.More and more people are using it,and the numberof it will larger and larger.Though there are a lot of software for sending andreceiving letters such as FoxMail which are also multifunctional,it isdifficult and complicated to the Most of people who are curbstone.For thisreason,we do this software with the rock-bottom protocol of SMTP and Pop. Thefull name of SMTP is Simple Mail Transfer Protocol.It is Used to sendingletters.The full name of Pop is Post Office Protocol which is Special to receiveletters.I basically take charge to how to realize the function of sendingletters. A namespace which is named MailSend is the soul of my programe.Itincludes two classes.A method named sendmial which realize the fuction step bystep belongs to the class of SmtpMail. It detailedly note the track of clientexchange to the server. You can use the software to send either a text E –Mailor a textE-mail with Attachments.You also can Send a letter to many addressee.In the nature of things,you can use a different SMTP service. The software Idid support multiletters and multisender after I test.It is simplier thanFoxMail and other professional softwares,but it is easy to hold and use.
    Key Words: SMTP, nameSpace, Class, Attachment
    1 引言1.1 电子邮件介绍电子邮件(简称E-mail)又称电子信箱、电子邮政,它是一种用电子手段提供信息交换的通信方式。它是全球多种网络上使用最普遍的一项服务。这种非交互式的通信,加速了信息的交流及数据传送,它是一个简易、快速的方法。通过连接全世界的Internet,实现各类信号的传送、接收、存贮等处理,将邮件送到世界的各个角落。到目前为止,可以说电子邮件是Internet资源使用最多的一种服务,E-mai1不只局限于信件的传递,还可用来传递文件、声音及图形、图像等不同类型的信息。
    电子邮件不是一种“终端到终端”的服务,是被称为“存贮转发式”服务。这正是电子信箱系统的核心,利用存贮转发可进行非实时通信,属异步通信方式。即信件发送者可随时随地发送邮件,不要求接收者同时在场,即使对方现在不在,仍可将邮件立刻送到对方的信箱内,且存储在对方的电子邮箱中。接收者可在他认为方便的时候读取信件,不受时空限制。在这里,“发送”邮件意味着将邮件放到收件人的信箱中,而“接收”邮件则意味着从自己的信箱中读取信件,信箱实际上是由文件管理系统支持的一个实体。因为电子邮件是通过邮件服务器(mai1server)来传递档的。通常mailserver是执行多任务操作系统UNIX的计算机,它提供24小时的电子邮件服务,用户只要向 mail server管理人员申请一个信箱账号,就可使用这项快速的邮件服务。
    电子邮件的工作原理:

    电子邮件系统是一种新型的信息系统,是通信技术和计算机技术结合的产物。电子邮件的传输是通过电子邮件简单传输协议(Simple Mail Transfer Protocol,简称SMTP)这一系统软件来完成的,它是Internet下的一种电子邮件通信协议。 电子邮件的基本原理,是在通信网上设立“电子信箱系统”,它实际上是一个计算机系统。系统的硬件是一个高性能、大容量的计算机。硬盘作为信箱的存储介质,在硬盘上为用户分一定的存储空间作为用户的“信箱”,每位用户都有属于自己的—个电子信箱。并确定—个用户名和用户可以自己随意修改的口令。存储空间包含存放所收信件、编辑信件以及信件存盘三部分空间,用户使用口令开启自己的信箱,并进行发信、读信、编辑、转发、存档等各种操作。系统功能主要由软件实现。电子邮件的通信是在信箱之间进行的。用户首先开启自己的信箱,然后通过键入命令的方式将需要发送的邮件发到对方的信箱中。邮件在信箱之间进行传递和交换,也可以与另—个邮件系统进行传递和交换。收方在取信时,使用特定账号从信箱提取。
    1.2 开发背景当前流行的各大邮件客户端软件的除了最主要的收发信件之外,功能越来越复杂,但是人们平常真正用到的功能很少,很多功能尤其对于那些计算机知识相对缺乏的人来说,更加显得太过于华丽而不太实用。有鉴于此,在了解RFC底层协议的基础上,我们开发了这个各种功能相对简单实用的邮件客户端程序,简化了很多不必要的功能。
    1.3 开发环境及运行环境1.3.1 开发环境
    AMD Athlon(TM),512M内存,80G硬盘Microsoft Windows XPProfessionalMicrosoft VisualStudio 2003(C #)Microsoft DeveloperNetwork for Visual Studio.NET 2003
    1.3.2 运行环境
    Intel® Pentium® 2及以上处理器,32M以上内存,4G以上硬盘Microsoft® Windows™9X/NT操作系统800*600或以上的屏幕分辨率确保机器上安装有.Net FrameWork 1.0或者以上版本
    2 软件架构及系统用例图2.1系统架构软件的总体架构如图2.1:

    2.2 系统总体用例
    2.3 程序功能框图
    2.4 发送邮件类是发送邮件的核心,类名为SmtpMail,隶属于命名空间MailSend。封装了发送邮件的具体实现方法,也是具体的RFC用代码实现的过程。而用户通过具体的操作接口,接口与SmtpMail类通过交互操作来实现用户发送信件的操作。
    2.5 附加小功能类是获取一些诸如系统时间,当前用户名,以及本机IP之类的类,类名为AddExtra,隶属于命名空间MailSend。
    3 SMTP协议的研究由于要开发的是邮件客户端程序,就不得不用到SMTP协议和POP协议。而我个人负责的是邮件发送功能的实现,因此就必然会涉及到SMTP(Simple Mail Transfer Protocol)协议。SMTP被用来在因特网上发送邮件,该协议规定了一些基本的命令和方法使客户端与服务器进行交互,以达到发送邮件的目的。
    3.1 SMTP协议简介及工作原理3.1.1 介绍简单邮件传输协议(SMTP)的目标是可靠高效地传送邮件,它独立于传送子系统而且仅要求一条可以保证传送数据单元顺序的通道。
    SMTP的一个重要特点是它能够在传送中接力传送邮件,传送服务提供了进程间通信环境(IPCE),此环境可以包括一个网络,几个网络或一个网络的子网。理解到传送系统(或IPCE)不是一对一的是很重要的。进程可能直接和其它进程通过已知的IPCE通信。邮件是一个应用程序或进程间通信。邮件可以通过连接在不同IPCE上的进程跨网络进行邮件传送。更特别的是,邮件可以通过不同网络上的主机接力式传送。
    3.1.2 SMTP模型SMTP设计基于以下通信模型:针对用户的邮件请求,发送SMTP建立与接收SMTP之间建立一个双向传送通道。接收SMTP可以是最终接收者也可以是中间传送者。SMTP命令由发送SMTP发出,由接收SMTP接收,而应答则反方面传送。
    一旦传送通道建立,SMTP发送者发送MAIL命令指明邮件发送者。如果SMTP接收者可以接收邮件则返回OK应答。SMTP发送者再发出RCPT命令确认邮件是否接收到。如果SMTP接收者接收,则返回OK应答;如果不能接收到,则发出拒绝接收应答(但不中止整个邮件操作),双方将如此重复多次。当接收者收到全部邮件后会接收到特别的序列,如果接收者成功处理了邮件,则返回OK应答。
    SMTP提供传送邮件的机制,如果接收方与发送方连接在同一个传送服务下时,邮件可以直接由发送方主机传送到接收方主机;或者,当两者不在同一个传送服务下时,通过中继SMTP服务器传送。为了能够对SMTP服务器提供中继能力,它必须拥有最终目的主机地址和邮箱名称。
    MAIL命令参数是回复路径,它指定邮件从何处来;而RCPT命令的参数是转发路径的,它指定邮件向何处去。向前路径是源路径,而回复路径是返回路径(它用于发生错误时返回邮件)。
    当同一个消息要发往不同的接收者时,SMTP遇到了向不同接收者发送同一份数据的复制品的问题,邮件命令和应答有一个比较奇怪的语法,应答也有一个数字代码。在下面,例子中可以看到哪些使用实际的命令和应答。完整的命令和应答在第四节。
    命令与应答对大小写不敏感,也就是说,命令和应答可以是大写,小写或两者的混合,但这一点对用户邮件名称却不一定是对的,因为有的主机对用户名大小写是敏感的。这样SMTP实现中就将用户邮箱名称保留成初始时的样子,主机名称对大小写不敏感。
    命令与应答由ASCII字母表组成,当传送服务提供8位字节传送通道,每7位字符正确传送,而最高位被填充为0。当指定一般的命令或应答格式后,参数会由一些类似于语言的字符串表示出来,如”\<string\>“或”\<reverse-path\>“,这里尖括号表示这是一种类似于语言的变量。
    3.2 SMTP协议的命令和应答3.2.1 SMTP协议的命令SMTP命令定义了邮件传输或由用户定义的系统功能。它的命令是由\<CRLF\>结束的字符串。而在带有参数的情况下,命令本身由\<SP\>和参数分开,如果未带参数可以直接和\<CRLF\>连接。邮箱的语法格式必须和接收站点的格式一致。下面讨论SMTP命令和应答。
    发送邮件操作涉及到不同的数据对象,它们由不同的参数相互连接。回复路径就是MAIL命令的参数,而转发路径则是RCPT命令的参数,邮件日期是DATA命令的参数。这些参数或者数据对象必须跟在命令后。这种模式也就要求有不同的缓冲区来存储这些对象,也就是说,有一个回复路径缓冲区,一个转发路径缓冲区,一个邮件内容缓冲区。特定的命令产生自己的缓冲区,或使一个或多个缓冲的内容被清除。
    HELLO (HELO)
    此命令用于向接收SMTP确认发送SMTP。参数域包括发送SMTP的主机名。接收SMTP通过连接确认命令来向发送SMTP确认接收SMTP。引命令和OK响应确认发送和接收SMTP进入了初始状态,也就是说,没有操作正在执行,所有状态表和缓冲区已经被子清除。
    MAIL (MAIL)
    此命令用于开始将邮件发送到一个多个邮箱中。参数域包括回复路径。返回路径中包括了可选的主机和发送者邮箱列表。当有主机列表时,它是一个回复路径源,它说明此邮箱是由在表中的主机一一传递发送(第一个主机是最后一个接收到此邮件的主机)过来的。此表也有作向发送者返回非传递信号的源路径。因为每个传递主机地址都被加在此表起始处,它就必须使用发送IPCE而不是接收IPCE(如果它们不是一个IPCE的话)清楚的名称。一些出错信息的回复路径可能就是空的。
    此命令清除回复路径缓冲区,转发路径缓冲区和邮件内容缓冲区,并且将此命令的回复路径信息插入到回复路径缓冲区中。
    RECIPIENT (RCPT)
    此命令用于确定邮件内容的唯一接收者;多个接收者将由多个此命令指定。转发路径中包括一个可选的主机和一个必须的目的邮箱。当出现主机列表时,这就是一个源路径,它指明邮件必须向列表中的上一个主机发送。如果接收SMTP未实现邮件的传递发送,就会返回如未知本地用户(550)的信息给用户。
    当邮件被传递发送时,传递主机必须将自己的名称由转发路径的开始处移至回复路径的结束处。当邮件最终到达目的地时,接收SMTP将以它的主机邮件格式自己的名称插入目标邮件中。例如,由传递主机A接收的带有如下参数的邮件时:
    FROM:USERX@HOSTY.ARPA  TO:<@HOSTA.ARPA,@HOSTB.ARPA:USERC@HOSTD.ARPA>将会变成如下形式:
      FROM:<@HOSTA.ARPA:USERX@HOSTY.ARPA>  TO:<@HOSTB.ARPA:USERC@HOSTD.ARPA>.此命令导致它的转发路径参数加入转发路径缓冲区中。
    DATA (DATA)
    接收者将跟在命令后的行作为邮件内容。此命令导致此命令后的邮件内容加入邮件内容缓冲区。邮件内容可以包括所有128个ASCII码字符。邮件内容由只包括一个句号的行结束,也就是如下的字符序列:”\<CRLF\>.\<CRLF\>“,它指示了邮件的结束。
    邮件内容的结束指示要求接收者现在就处理保存的邮件内容。此过程将回复路径缓冲区,转发路径缓冲区和邮件内容缓冲区的内容全部清空。如果操作成功,接收者必须返回OK应答;如果失败也必须返回失败应答。
    当接收SMTP收到一条信息时,无论是用作转发还是此邮件已经到达目的地,它都必须在邮件内容的开始处加上时间戳这一行,这一行指示了接收到邮件主机和发出此邮件主机的标识,以及接收到邮件内容的时间和日期。转发的信件将有多行这样的时间戳。当接收SMTP作最后一站的传送时,它将返回路径信息行插入邮件中。此行包括了发送命令中的\<reverse-path\>的信息。在这里,最后一站的传送的意思是邮件将被送到目的用户手中,但在一些情况下,邮件可能需要更进一步的加工并由另外的邮件系统传送。
    可能在返回路径中的邮箱与实际发送的邮件不一致,这个情况可能发生在需要传送一个特定的错误处理信箱而不是信件发送者那里。上面所述说明了,最后的邮件内容由一个返回路径行,和在其后的一个或多个时间戳行构成。这些行后面是邮件内容的头和体信息。
    当处理后面的邮件数据指示部分成功时就需要特定的说明。这种情况可能发生在发送SMTP发现当邮件需要传送给多个用户时,只能够成功地向其中的一部分发送信息这种情况下。在这种情况下,必须对DATA命令发送OK应答,而接收SMTP组织并发送一个”不可传递邮件”信息到信息的发送者。在此信息中或者发送一个不成功接收者的列表,或者每次发送一个不成接收者,而发送多次。所有不可传递邮件信息由MAIL命令发送。
    返回路径和接收时间戳例子:
    Return-Path: <@GHI.ARPA,@DEF.ARPA,@ABC.ARPA:JOE@ABC.ARPA>  Received: from GHI.ARPA by JKL.ARPA ; 27 Oct 81 15:27:39 PST  Received: from DEF.ARPA by GHI.ARPA ; 27 Oct 81 15:15:13 PST  Received: from ABC.ARPA by DEF.ARPA ; 27 Oct 81 15:01:59 PST  Date: 27 Oct 81 15:01:01 PST   From: JOE@ABC.ARPA   Subject: Improved Mailing System Installed   To: SAM@JKL.ARPA   This is to inform you that ...SEND(SEND)
    此命令用于开始一个发送命令,将邮件发送到一个或多个终端上。参数域包括了一个回复路径,此命令如果成功就将邮件发送到终端上了。
    回复路径包括一个可选的主机列表和发送者邮箱。当出现主机列表时,表示这是一个传送路径,邮件就是经过这个路径上的每个主机发送到这里的(列表上第一个主机是最后经手的主机)。此表用于返回非传递信号到发送者。因为每个传递主机地址都被加在此表起始处,它就必须使用发送IPCE而不是接收IPCE(如果它们不是一个IPCE的话)清楚的名称。一些出错信息的回复路径可能就是空的。
    此命令清除回复路径缓冲区,转发路径缓冲区和邮件内容缓冲区,并且将此命令的回复路径信息插入到回复路径缓冲区中。
    SEND OR MAIL (SOML)
    此命令用于开始一个邮件操作将邮件内容传送到一个或多个终端上,或者传送到邮箱中。对于每个接收者,如果接收者终端打开,邮件内容将被传送到接收者的终端上,否则就送到接收者的邮箱中。参数域包括回复路径,如果成功地将信息送到终端或邮箱中此命令成功。
    回复路径包括一个可选的主机列表和发送者邮箱。当出现主机列表时,表示这是一个传送路径,邮件就是经过这个路径上的每个主机发送到这里的(列表上第一个主机是最后经手的主机)。此表用于返回非传递信号到发送者。因为每个传递主机地址都被加在此表起始处,它就必须使用发送IPCE而不是接收IPCE(如果它们不是一个IPCE的话)清楚的名称。一些出错信息的回复路径可能就是空的。
    此命令清除回复路径缓冲区,转发路径缓冲区和邮件内容缓冲区,并且将此命令的回复路径信息插入到回复路径缓冲区中。
    SEND AND MAIL (SAML)
    此命令用于开始一个邮件操作将邮件内容传送到一个或多个终端上,并传送到邮箱中。如果接收者终端打开,邮件内容将被传送到接收者的终端上和接收者的邮箱中。参数域包括回复路径,如果成功地将信息送到邮箱中此命令成功。
    回复路径包括一个可选的主机列表和发送者邮箱。当出现主机列表时,表示这是一个传送路径,邮件就是经过这个路径上的每个主机发送到这里的(列表上第一个主机是最后经手的主机)。此表用于返回非传递信号到发送者。因为每个传递主机地址都被加在此表起始处,它就必须使用发送IPCE而不是接收IPCE(如果它们不是一个IPCE的话)清楚的名称。一些出错信息的回复路径可能就是空的。
    此命令清除回复路径缓冲区,转发路径缓冲区和邮件内容缓冲区,并且将此命令的回复路径信息插入到回复路径缓冲区中。
    RESET (RSET)
    此命令指示当送邮件操作将被放弃。任何保存的发送者,接收者和邮件内容应该被抛弃,所有缓冲区和状态表应该被清除,接收方必须返回OK应答。
    VERIFY (VRFY)
    此命令要求接收者确认参数是一个用户。如果这是(已经知道的)用户名,返回用户的全名和指定的邮箱。此命令对回复路径缓冲区,转发路径缓冲区和邮件内容缓冲区没有影响。
    EXPAND (EXPN)
    此命令要求接收者确认参数指定了一个邮件发送列表,如果是一个邮件发送列表,就返回表中的成员。如果这是(已经知道的)用户名,返回用户的全名和指定的邮箱。此命令对回复路径缓冲区,转发路径缓冲区和邮件内容缓冲区没有影响。
    HELP (HELP)
    此命令导致接收者向HELP命令的发送者发出帮助信息。此命令可以带参数,并返回特定的信息作为应答。此命令对回复路径缓冲区,转发路径缓冲区和邮件内容缓冲区没有影响。
    NOOP (NOOP)
    此命令不影响任何参数和已经发出的命令。它只是说明没有任何操作而不是说明接收者发送了一个OK应答。此命令对回复路径缓冲区,转发路径缓冲区和邮件内容缓冲区没有影响。
    QUIT (QUIT)
    此命令指示接收方必须发送OK应答然后关闭传送信道。接收方在接到QUIT命令并做出响应之前不应该关闭通信信道。发送方在发送QUIT命令和接收到响应之前也不应该关闭信道。即使出错,也不应该关闭信道。如果连接被提前关闭,接收方应该象接收到RSET命令一样,取消所有等待的操作,但不恢复原先已经做过的操作。而发送方应该象接收到暂时错误(4XX)一样假定命令和操作仍在支持之中。
    TURN (TURN)
    此命令指定接收方要么发送OK应答并改变角色为发送SMTP,要么发送拒绝信息并保持自己的角色。如果程序A现在是发送SMTP,它发出TURN命令后接收到OK(250)应答,它就变成了接收SMTP。程序A就进入初始状态,好象通信信道刚打开一样,这时它发送220准备好服务信号。如果程序B现在是接收SMTP,它发出TURN命令后接收到OK(250)应答,它就变成了发送SMTP。程序A就进入初始状态,好象通信信道刚打开一样,这时它准备接收220准备好服务信号。
    若要拒绝改变角色,接收方可以发送502应答。
    对于这些命令的顺序有一定的限制。对话的第一个命令必须是HELLO命令,此命令在此后的会话中也可以使用。如果HELLO命令的参数不可接受,必须由返回一个501失败应答,同时接收到的SMTP必须保持在与刚才一致的状态下。 NOOP,HELP,EXPN和VRFY命令可以在会话的任何时候使用。MAIL,SEND,SOML或SAML命令开始一个邮件操作。一旦开始了以后就要发送RCPT和DATA命令。邮件操作可以由RSET命令终止。在一个会话中可以有一个或多个操作。
    如果在操作开始参数不可接受,必须返回501失败应答,同时接收到的SMTP必须保持在与刚才一致的状态下。如果操作中的命令顺序出错,必须返回503失败应答,同时接收到的SMTP必须保持在与刚才一致的状态下。
    会话的最后一个命令必须是QUIT命令。此命令在会话的其它时间不能使用。
    COMMAND语法格式
    命令是由命令码和其后的参数域组成的。命令码是四个字母组成的,不区别大小写。因为下面的命令的作用是相同的:
    MAIL Mail mail MaIl mAIl这对于引导任何参数值的标记也是适用的,如TO和to就是一样的。命令码和参数由一个或多个空格分开。然而在回复路径和转发路径中的参数是区别大小写的。特别是在一些主机上,”smith”和”Smith”就根本不是一个用户。
    参数域由不定长的字符串组成,它由\<CRLF\>结束,接收方在完全接收到此序列前不会采取任何行动。方括号代表可选的参数域。如果不选择的话,系统选择默认的设置。
    下面是SMTP命令:
    HELO <SP> <domain> <CRLF> MAIL <SP> FROM:<reverse-path> <CRLF>RCPT <SP> TO:<forward-path> <CRLF>DATA <CRLF>RSET <CRLF>SEND <SP> FROM:<reverse-path> <CRLF>SOML <SP> FROM:<reverse-path> <CRLF>SAML <SP> FROM:<reverse-path> <CRLF>VRFY <SP> <string> <CRLF>EXPN <SP> <string> <CRLF>HELP [<SP> <string>] <CRLF>NOOP <CRLF>QUIT <CRLF>TURN <CRLF>3.2.2 SMTP的应答码对SMTP命令的响应是多样的,它确定了在邮件传输过程中请求和处理的同步,也保证了发送SMTP知道接收SMTP的状态。每个命令必须有且只有一个响应。
    SMTP响应由三位数字组成,其后跟一些文本。数字帮助决定下一个应该进入的状态,而文本对人是有意义的。三位的响应已经包括了足够的信息,不用再阅读文本,文本可以直接抛弃或者传递给用户。特别的是,文本是与接收和环境相关的,所以每次接收到的文本可能不同。在附录E中可以看到全部的响应码。正规的情况下,响应由下面序列构成:三位的数字,\<SP\>,一行文本和一个\<CRLF\>,或者也可以是一个多行响应。只有EXPN和HELP命令可以导致多行应答,然而,对所有命令,多行响应都是允许的。
    500 格式错误,命令不可识别(此错误也包括命令行过长)501 参数格式错误502 命令不可实现503 错误的命令序列504 命令参数不可实现211 系统状态或系统帮助响应214 帮助信息220 <domain> 服务就绪221 <domain> 服务关闭传输信道 421 <domain> 服务未就绪,关闭传输信道(当必须关闭时,此应答可以作为对任何命令的响应)250 要求的邮件操作完成251 用户非本地,将转发向<forward-path>450 要求的邮件操作未完成,邮箱不可用(例如,邮箱忙)550 要求的邮件操作未完成,邮箱不可用(例如,邮箱未找到,或不可访问)451 放弃要求的操作;处理过程中出错551 用户非本地,请尝试<forward-path>452 系统存储不足,要求的操作未执行552 过量的存储分配,要求的操作未执行553 邮箱名不可用,要求的操作未执行(例如邮箱格式错误)354 开始邮件输入,以<CRLF>.<CRLF>结束554 操作失败4 RFC822说道发送和接受邮件,我们就必须不得不提RFC822了。RFC822的全称是“ARPA因特网文本信件格式的标准”(Standard for theFormat of ARPA Internet Text Messages)。该标准提供了邮件内容的格式和相关语义。
    4.1 RFC822简单介绍RFC822规定的电子邮件内容全部由ASCII字符组成,就是通常所说的文本文件,因而标准将它称为Internet文本信件(Internet Text Messages)。
    从直观上看,信件非常简单,就是一系列由ASCII字符组成的文本行,每一行以回车换行符(“CRLF“,就是ASCII码的13和10)结束。
    从组织上看,信件内容结构分为两大部分,中间用一个空白行(只有CRLF符的行)来分隔。第一部分称为信件的头部(the header of the message),包括有关发送方、接收方、发送日期等信息。第二部分称为信件的体部(Body of the message),包括信件内容的正文文本。信头是必需的,信体是可选的,即信体可有可无。如果不存在信体,用作分隔的空白行也就不需要。在信体中,也可以有用作分隔的空白行。这样设计的信件便于进行语法分析,提取信件的基本信息。
    在RFC822中规定,信件体就是一系列的向收信人表达信息的文本行,比较简单,可以包含任意文本,并没有附加的结构。信件头则具有比较复杂的结构,在下一小节中详述。
    4.2 信件的头部4.2.1 信头的一般格式信头的结构比较复杂,信头由若干信头字段(header field)组成,这些字段为用户和程序提供了关于信件的信息。要了解信头的结构就要弄清楚各种信头字段。
    所有的信头字段都具有相同的语法结构,从逻辑上说,包括四部分,字段名(field name),紧跟冒号”:” (colon),后跟字段体(field body),最后以回车换行符(CRLF)终止。即
    信头字段=字段名:字段体 CRLF字段名必须由除了冒号和空格以外的可打印US-ASCII字符(其值在33和126之间)组成,大多数字段的字段名称由一系列字母,数字组成,中间经常插入横线符。字段名告诉电子邮件软件如何翻译该行中剩下的内容。
    字段体可以包括除了CR和LF之外的任何ASCII字符。但是其中的空格,加括号的注释,引号和多行字段都比较复杂,另外,字段体的语法和语义依赖于字段名,每个类型的字段有特定的格式。
    RFC822为信件定义了一些标准字段,并提供了用户自行定义非标准字段的方法。
    4.2.2 结构化字段和非结构化字段每个字段所包含的信息不同,字段大体可以分为结构化字段和非结构化字段。
    结构化字段有特定的格式,由语法分析程序检测。Sender 字段就是一个很好的例子,它的字段内容是信箱,有一个离散的结构。
    非结构化的字段含有任意的数据,没有固定格式。例如,Subject字段可以含有任意的文字,并且没有固定格式。非结构化的字段数量较少,只有Subject、 Comments、扩展字段,非标准字段、IN-Reply和References等。所有其它字段都是结构化的。
    4.2.3 信头字段的元素尽管Email信件的总体结构非常简单,但一些信头字段的结构是很复杂的。下面介绍一些大多数字段共有的元素。
    1.空白符
    像其它文本文件一样,空白符包括空格符(ASCII码32)和制表符Tab(ASCII码19)。此外,行末的回车换行符CRLF也应算是空白符。使用空白符可以对字段进行格式化,增加它的可读性。例如,每个字段间用CRLF来分离,在字段内用空格来分隔字段名和字段内容。在Subject后面的冒号和内容之间插入空格字符,会使字段结构更加清晰。在Email中,空白符的使用并没有固定的规则,但应当正确地使用,仅在需要时才使用空白符,以便接收软件进行语法分析。
    2.注解
    注解是由括号括起来的一系列字符,例如,(这份礼物)。注解一般用在非结构化的信头字段中,没有语法语义,仅为人提供了一些附加的信息。如果在加引号的字符串中有包括在括号中的字符,那是字符串的一部分,不是注解。在解释信件的时候,会将注解忽略,可以用一个空格字符代替它们,这样就什么也不会破坏。
    3.字段折叠
    每个信头字段从逻辑上说应当是一个由字段名、冒号、字段体和CRLF组成的单一的行,但为了书写与显示的方便,增加可读性,也为了符合1000/80的行字符数的限制,可以将超过80个字符的信头字段分为多行,即对于比较长的字段,可以分割成几行,形成折叠。在结构化和非结构化字段中都允许折叠。通过在字段中某些点插入CRLF符和至少一个或多个空白字符来实现字段的折叠,第一行后面的行称为信头字段的续行。续行都以一个空白符开始,这种方法称为折叠(folding),例如标题字段Subject: This is a test可以表示为:
    Subject: This is a test反之,将一个被折叠成多行的信头字段恢复到它的单行表示的过程叫做去折叠,只要简单地移除后面跟着空格的CRLF,将折叠空白符CRLF转换成空格字符,就可以完成去折叠(unfolding)。在分析被折叠的字段的语法时,要把一个多行的折叠字段展开为一行,根据它的非折叠的形式来分析它的语法与语义。
    4.字段大小写
    字段名称是不区分大小写的,所以Subject、subject或SUBJECT都一样。不过字段名称大小写有习惯的常用形式,如主题字段的大小写形式通常为Subject。字段体的大小写稍微复杂点,要视情况而定。比如Subject后面的字段体,其中的大写可能就是缩写的专用名词,不能改动。
    4.2.4 标准的信头字段下面介绍RFC822中定义的常用的标准信头字段。



    与发信方有关的信头字段





    格式:From:mailbox 举例:From:wang@163.com
    写信人字段。说明信件的原始创建者,给出他的电子信箱地址。创建者对信件的原始内容负责。


    格式:Sender:mailbox 举例:From:wang@163.com Sender:li@sina.com
    发送者字段。说明实际提交发送这个信件的人,给出他的电子信箱地址。当发信人与写信人不一样时使用。比如,秘书替经理发信。发送者对发送负责。


    格式:Reply-TO:mailbox 举例:From:wang@163.com From:zhao@soho.com
    回复字段。指定应当把回信发到哪里。如果有此字段,回信将会发给它指定的邮箱,而不会发给From字段指定的邮箱。比如,发送的是经理的信,但回信应交办公室处理。


    与收信方有关的信头字段



    格式:TO:mailbox list 举例:TO:zhang@263.com
    收信人字段。指定主要收信人的邮箱地址,可以是多个邮箱地址的列表,地址中间用逗号隔开。


    格式:Cc:mailbox list 举例:Cc:zhang@863.com
    抄送字段。指定此信件要同时发给哪些人,也称为抄送。也可以使用邮箱地址列表,抄送给多个人。


    格式:Bcc:mailbox list
    密抄字段。指定此信件要同时秘密发给哪些人,也称为密件抄送。也可以使用邮箱地址列表,密抄给多个人。


    其它的信头字段



    格式:Date:date-time 举例:Date:Tue,04 Dec 2004 16:18:08 +800
    日期字段:Date字段含有电子邮件创建的日期和时间。


    格式:Subject:*text 举例:Subject:Hello! Subject:Re:Hello!
    信件主题字段。描述信件的主题。当回复信件时,通常在主题前面增加“Re:”前缀,标记为该信件为回复信件:当信件被转发时,通常在主题文字前面加上“Fw:”,“Fwd:”这样的前缀。


    格式:Received: [“from” domain] ;发送主机 [“by” domain] ;接收主机 [“via” atom] ;物理路径 [“id” msg-id] ;接收者msg id
    接受字段。是投递信件的特定邮件服务器所作的记录。处理邮件投递的每个服务器必须给它处理的每个信头的前面加一个Received字段,用以描述信件到达目的地所经过的路径以及相关信息。当跟踪各个电子邮件问题时,这个信息很有帮助。


    举例:Received:from wang[195.0.0.1] by li[129.5.0.4] Tue dec 2003 12:18:02 +800



    格式:Comments:*text
    注释字段。用于把一个注解添加到信件中。


    格式:Resent-* 举例:Resent-From Resent-Sender Resent-date Resent-Reply-To
    重发字段。当需要把收到的信件重发给另一组收信人的时候,可以保持整个原始信件不变,并简单地产生重发信件所要求的新信头字段。为避免与以前的字段相混。新添加的信头字段都加上Resent-前缀字符串,它们的语法与未加前缀的同名字段相同。


    格式:Message-ID:msg-id
    信件标识字段。用于表示一个信件唯一标识,该字段通常有Smtp服务器生成,这个值通常是唯一的。形式根据使用的软件而定。通常左边是标识符,右边指定电脑名



    表中的关键字表明了电子邮件借用了办公室备忘录中的概念和术语:电子邮件的头部能够包含一行说明应当接收到该备忘录的接收方。象传统的办公室备忘录一样,电子邮件使用关键字Cc指明一个复写副本(carboncopy).电子邮件软件必须向Cc:后面的电子邮件地址表中的每个地址发送一份消息的副本。
    传统的办公室过程要求备忘录的发送方通知接收方副本是否传给其它人。有时发送方希望将备忘录的一个副本给别人而不显示出有一个副本被发送出去。一些电子邮件系统提供这样的选项,遵循传统的办公室术语,用盲复写副本(blind carbon copy)来表示。创建消息的用户
    在关键字Bcc后给出一个电子邮件地址表,指定一个或多个盲复写副本。虽然Bcc在发送方出现,但当信息发送时,邮件系统将它从消息中除去。每个接收方必须检查头部的To和Cc行以决定信息是直接发送还是作为盲副本发送的(有些邮件系统在正文部分附加信息来告诉接收者它是一个盲副本)。其它接收者不知道有哪些用户接收到盲副本。
    电子邮件使用与传统的办公室备忘录相同的格式和术语:头部包括与消息有关的信息,正文包括消息文本。电子邮件头部的行说明发送方、接收方、日期、主题、应当收到副本的人的列表。
    扩展字段
    如果想在信头中加入RFC822中没有规定的字段,就需要创建非标准字段。方法非常简单,只要在自定义的信头字段名的前面使用X-前缀。RFC822将这种方法称为扩展字段。 事实上已经有许多扩展字段被广泛应用,但没有标准定义。例如:

    X-LOOP字段
    X-LOOP字段用来防止邮件的循环传送。过滤或邮件列表处理程序,可以给它处理的每个信件增加一个X-LOOP字段,以后就可以根据这个字段中含有的特别值,判断一个信件是否被循环传送。如果确认邮件发生了循环,过滤或邮件列表处理程序就可以用不同的方式处理该信件。
    X-Mailer字段
    X-Mailer字段用于指示什么样的程序产生了这个信件,它是使用最广泛的扩展字段。产生邮件的软件可以为所有发送的信件增加合适的X-Mailer字段,该字段不仅含有软件的名称,还包含软件的版本号。例如软件名为Littlefox Mailer,版本为V1.0, 可以将“X-Mailer:Littlefox Mailer V1.0”加到邮件信头中去。

    下面列出了一些在因特网电子邮件中可以找到的普通关键字,以及使用它们的目的。



    关键字
    含义




    From
    发送方地址


    To
    接收方地址


    Cc
    复制副本地址


    Date
    信息创建日期


    Subject
    信息主题


    Reply-To
    回复地址


    X-Charset
    使用的字符集(通常为ASCII)


    X-Mailer
    发送信息所使用的软件


    X-Sender
    发送方地址的副本


    X-Face
    经编码的发送方面孔的图象



    整个系统的核心是收发信件的操作,因此为了方便维护,以后的升级,故将这两个最主要的操作写成类库(.dll)的形式,以组件的形式加载到主程序中,而且其它的功能如果需要的话,也可以通过这样的组件的形式增加到主程序中。这也体现了C#这一新的微软主推语言的方便和高效,而且这样做也方便程序的顺利结合。
    5 命名控件MailSend由于在C#语言中,都是以命名控件来组织程序的。而所有的类都归属于一个特定的命名空间下。需要的命名空间系统本身自带了一部分,而且如果系统没有你需要的命名空间的话,就可以自己编写,本节中的这个命名空间就是由于需要而编写的。而调用某一个类中的某个变量成员的方法就是通过命名空间名.类名.变量成员 来访问的,当然在C#中如果在程序开始通过Using 命名空间名,就可以直接的象C++那样来访问成员变量,可以说相当的方便,这些都会在程序中体现出来,再次不再做过多的叙述。
    5.1 发送邮件类SmtpMail5.1.1 主要成员变量说明1.网络连接类及实例TcpClient tc
    为 TCP 网络服务提供客户端连接类TcpClient实例对象tc。TcpClient 类提供了一些简单的方法,用于在同步阻塞模式下通过网络来连接、发送和接收流数据。而实例化的过程也是连接SMTP服务器的过程。它的重载方法之一的两个参数一个为服务器名称字符串,另一个为服务器的埠。
    2.提供用于网络访问的基础数据流及其实例 NetworkStream ns
    此类提供访问网络的基础数据流的方法。其中最基本也是最重要的两个方法就是Write()和Read()方法,至于参数不再次赘述。
    3.一维字符串数组变量FilePath
    此字符串数组主要用来存放用户选择的附件的绝对路径名,并在发送带附件的邮件时用到。
    4.发送邮件所需的基本参数
    比如用于ESMTP等录检验用的用户名、密码,发送邮件需要的收信人,发信人地址以及主题等等在此不再赘述。
    5.1.2 主要成员函数说明1.重载的构造函数 SmtpMail()
    此函数主要用于在初始化过程中,把用户选择的附件的路径以参数的形式传给FilePath。
    2.添加附件的函数AddAttachment
    传给FilePath的路径,通过这样一个函数就可以循环的动态的添加到IList接口的一个对象中了,方便以后在具体的实现的过程中的使用。
    3.得到上传的附件的文件流GetStream
    由于在网络中的操作都是以网络流的形式来实现的,因此先将上传的附件转换成文件流,然后再用Write的方法把这些附件的文件流写入到网络中,来完成发送附件的操作。具体实现代码如下所示:
    private string GetStream(string FilePath) { //建立文件流对象 System.IO.FileStream FileStr=new System.IO.FileStream(FilePath,System.IO.FileMode.Open); byte[] by=new byte[System.Convert.ToInt32(FileStr.Length)]; FileStr.Read(by,0,by.Length); FileStr.Close(); return(System.Convert.ToBase64String(by)); }
    4.将字符串编码为Base64字符串的函数Base64Encode
    由于ESMTP的LOGIN认证机制是采用Base64编码,当用户发出AUTHLOGIN的命令后,服务器返回334的应答码等待用户输入。如果身份确认后服务器返回235的应答码,否则返回失败信息。所以要将用户名和密码转换成Base64编码然后再发给服务器。此函数的作用就是把给定的字符串转换成相应的Base64编码的字符串。
    5.发送SMTP命令的函数SendCommand
    这个函数的作用是把SMTP命令的字符串转换成对应的字节型值(C#中规定的Write方法只能写入字节型的数据)然后写入网络中,如果操作成功就返回一个标志为真的布尔型变量,如果操作失败或者发生异常就返回标志为假的布尔型变量。具体代码如下所示:
    private bool SendCommand(string str) { //定义一个数组 byte[] WriteBuffer; //设定一个布尔类型的变量 bool state=false; WriteBuffer = Encoding.Default.GetBytes(str); //加入防错机制,可以有效提高程序运行的效率和捕获出错信息 try { //向网络中写入数据 ns.Write(WriteBuffer,0,WriteBuffer.Length); state=true; } catch(Exception ex) { //返回出错信息 MessageBox.Show (ex.ToString ()); state=false; } //返回标志位 return state; }
    6.接受服务器应答的函数RecvResponse
    它的作用就是从网络流中读取服务器返回的字节型的信息,将其转换成字符串型的变量,然后将其返回,可以通过其返回值来判断操作是否成功。具体实现代码如下所示:
    private string RecvResponse() { int StreamSize=0; string ReturnValue =""; //定义一个字节型的数组 byte[] ReadBuffer = new byte[1024] ; try { //从网络流中读取数据,并返回读取的个数 StreamSize=ns.Read(ReadBuffer,0,ReadBuffer.Length); } catch (Exception ex) { //返回异常信息 MessageBox.Show(ex.ToString ()); } if (StreamSize!=0) { //将当前读取的信息转换成字符串型然后返回ReturnValue= Encoding.Default.GetString(ReadBuffer).Substring(0,StreamSize); } return ReturnValue; }
    7.重载的函数Dialog
    它们的作用是与服务器交互,发送命令并接收回应。不同的是参数是字符串类型的那个函数,每次发送一条命令,并接受服务器的响应,根据响应的信息来判断交互的结果是否成功。而参数是字符串数组的函数每次发送的是一组命令,用于和服务器的交互,这个函数主要是用于ESMTP服务器的验证的功能,因为验证的过程是一个等待然后又输入的过程,因此将他们放在一个数组中有利于理解和操作。而他们的实现主要是通过调用上面的发送SMTP命令函数SendCommand以及接受SMTP服务器响应的函数RecvResponse来实现的。具体的代码如下所示:
    private bool Dialog(string str,string errstr) { bool flag=false; if(str==null||str.Trim()=="") { flag=true; } if(SendCommand(str)) { string RR=RecvResponse(); //从返回的数据中截取前三位 string RRCode=RR.Substring(0,3); //然后用这前三位与哈希表中正确的回应码比较 if(RightCodeHT[RRCode]!=null) { flag=true; } else { flag=false; } } else { flag=false; } return flag; }
    发送一组命令主要用于服务器验证的重载函数为:
    private bool Dialog(string[] str,string errstr) { for(int i=0;i<str.Length;i++) { //循环调用单个的与服务器的交互过程 if(!Dialog(str[i],"")) { return false; } } return true; }
    8.邮件发送程序SendMail
    这是整个程序的核心部分。具体的实现SMTP协议的程序正是通过它一步一步实现并最终实现发送简单邮件甚至带附件的邮件的功能。而它的实现是调用以上给出的各个函数的结果。以下就简单的通过几个SMTP命令的格式来实现:
    private bool SendEmail() { //连接网络 try { //建立一个TCP连接 tc=new TcpClient(mailserver,mailserverport); } catch { MessageBox.Show ("连接失败","请确认"); return false; } //获取当前流的资料 ns = tc.GetStream(); SMTPCodeAdd(); //验证网络连接是否正确 if(RightCodeHT[RecvResponse().Substring(0,3)]==null) { return false; } string[] SendBuffer; string SendBufferstr; //进行SMTP验证 //具体的SMTP命令与代码的结合 if(ESmtp) { SendBuffer=new String[4]; SendBuffer[0]="EHLO " + mailserver + enter; SendBuffer[1]="AUTH LOGIN" + enter; SendBuffer[2]=Base64Encode(username) + enter; SendBuffer[3]=Base64Encode(password) + enter; if(!Dialog(SendBuffer,"SMTP服务器验证失败,请核对用户名和密码。")) return false; } else { SendBufferstr="HELO " + mailserver + enter; if(!Dialog(SendBufferstr,"")) return false; } SendBufferstr="MAIL FROM:<" + From + ">" + enter; if(!Dialog(SendBufferstr,"发件人地址错误,或不能为空")) return false; //把传过来的收件人的地址分割然后提交给服务器 string split=";"; string []address=Regex.Split (Recipient,split); SendBuffer=new string [address.Length]; for(int i=0;i<SendBuffer.Length;i++) { SendBuffer[i]="RCPT TO:<" +address[i]+">" + enter; } if(!Dialog(SendBuffer,"收件人地址有误")) return false; SendBufferstr="DATA" + enter; if(!Dialog(SendBufferstr,"")) return false; SendBufferstr="From:" + FromName + "<" + From +">" +enter;SendBufferstr += enter + "." + enter; if(!Dialog(SendBufferstr,"错误信件信息")) return false; SendBufferstr="QUIT" + enter; if(!Dialog(SendBufferstr,"断开连接时错误")) return false; //关闭流对象 ns.Close(); //关闭连接 tc.Close(); FilePath=null; return true; }
    以上即为发送不带附件的邮件SMTP命令用代码实现的过程。
    5.2 AddExtra类这个附加的小类只是提供一些返回当前系统时间,获取主机名,主机IP,有关帮助等小的功能,在此仅对帮助信息中的“关于”操作函数稍加说明。因为它说明了在C#中调用 Windows API 函数所需如下几个步骤:
    5.2.1 调用Windows API 所需的命名空间using System.Runtime.InteropServices;
    而调用显示关于对话框的函数ShellAbout还需要用到两个命名空间如下所示:
    using System.Reflection;using System.Diagnostics;
    5.2.2 在程序中声明所需的API函数[DllImport("shell32.dll")]static extern int ShellAbout(IntPtr hWnd, string szApp, string szOtherStuff,IntPtr hIcon);
    5.2.3 在程序中具体的使用Assembly ass=Assembly.GetExecutingAssembly();FileVersionInfo myVersion=FileVersionInfo.GetVersionInfo(ass.Location );ShellAbout(this.Handle ,"邮件收发系统#","版本"+myVersion.FileMajorPart +"."+myVersion.FileMinorPart+"." +myVersion.CompanyName ,this.Icon .Handle );
    至此就完成了在C#中调用 Windows API 函数的过程。
    6 软件运行时的界面6.1 新建邮件帐号用户打开软件之后,需要新建一个邮件帐号,在这个信件帐号的过程中,需要指定SMTP服务器,SMTP的端口,以及用于ESMTP验证的用户名和密码。指定这些发邮件的必须参数之后,再回到系统的主界面如下所示:

    6.2 发送邮件界面6.2.1 发送不带附件的邮件在新建帐号的过程中已经指定了邮件地址,和帐号名称,所以默认的以这些参数来发送邮件。通过调用参数的不同程序会自动的调用相对应的代码来执行不同的操作。发送简单的邮件运行界面如下。

    6.2.2 发送带附件的邮件和简单的邮件不同之处在于多了发送附件的功能,软件模拟FoxMail里面发送邮件时,在程序的下面自动显示增添的附件的名称,以及图标等信息。并且邮件支持添加,删除,排列图标等功能。运行界面如下所示:

    6.3 验证邮件发送是否成功邮件发送出去之后,用FoxMail跟踪接收之后,证明邮件和附件都可以正常接收,具体的FoxMail的接收界面如下所示:

    7 系统测试我个人做的是这个软件收发系统的一个最基本也是最主要的功能之一:发送邮件。
    所以主要的测试也是围绕发送邮件展开的,具体的可以分为以下几个方面。
    7.1 同一SMTP服务器发送邮件的测试这个方面的测试测的是,用户登录一个服务器(测试中用的是163的SMTP服务器)来发送一封邮件的测试。而这个测试又可以分为以下两个方面:
    7.1.1 同一服务器,发送一封纯文本邮件的测试1.发送一封文本邮件给一个收信人
    测试中用163的邮箱分别往163的邮箱以及新浪的邮箱发送邮件均可以用FoxMail正常的接收到发送的普通的纯文本文件。
    2.发送一封文本邮件给多个收件人
    测试中仍然用163的邮箱同时发往不同的邮箱,通过FoxMail都可以正常的接收到。从而很好的验证了,我们的邮件发送系统支持群发的功能。
    7.1.2 同一服务器,发送一封带附件的邮件的测试。1.发送一封带附件(可以是多附件)的邮件给一个收件人
    测试中用163的邮箱分别往163的邮箱以及新浪的邮箱发送之外,又添加了不同的邮件类型(个数分别为等于1,大于1即验证是否支持多附件的发送),用FoxMail接收之后,所有发送的纯文本信息,以及附件信息都正常无误。经过这些验证可以证明本软件支持对一个收件人发送多附件。由于带有多附件的信件,所以写入速度明显慢于纯文本邮件的速度。
    2.发送一封带附件(可以是多附件)的邮件给多个收件人
    测试中用163的邮箱分别往163的邮箱以及新浪的邮箱发送之外,又添加了不同的邮件类型(个数分别为等于1,大于1即验证是否支持多附件的发送),用FoxMail接收之后,所有发送的纯文本信息,以及附件信息都正常无误。经过这些验证可以证明本软件支持对多个收件人发送多附件。
    7.2 利用不同的SMTP服务器发送邮件的测试这个方面的测试是指利用不同的邮箱来发送邮件,至于测试的分类雷同于利用同一服务器发送邮件的测试,所以不再此赘述。
    总之,通过以上的各方面的测试,使我改正了代码中的许多不合理以及错误之处,最终也证明了,我们的软件系统是支持多种服务器,支持多附件发送的群发软件。
    8 结论这次编写的邮件客户端系统,我负责的是邮件的发送的功能。在熟悉了专门用于发送邮件的SMTP协议以及RFC规定的邮件的格式的基础上,运用了微软新推出的C#这一新型的面向对象语言的便利性和灵活性,从SMTP协议规定的底层命令做起,一步步的与服务器进行交互操作,最终实现发送多附件多接收人的功能。其中,具体的和服务器的交互操作,都封装了在SmtpMail.dll这个动态链接库里面了。而为了方便最终的调用和整合,所有的有关后台操作发送邮件的类以及其他的附加功能的类,全部都归属于MailSend这个命名空间了。在力求达到FoxMail功能的同时,又加了一点个人的思想并把它体现到了这一软件上。最主要的体现就是新建帐号的提前检测这一特色上,这一功能类似于很多Web页面的“检测新帐号”的功能,这样就免去了用户一直到确定注册完成时,才因为帐户因为已经被使用而注册失败的麻烦。总之,通过这次的编程,使我对网络编程有了一个很好的认识和锻炼,也使我对C#这一语言的掌握程度又上了一个新台阶,虽然编出来的软件不能和功能强大的FoxMail相提并论,但是相信它简单,易操作性,和FoxMail的很多强大但却“鸡肋”似的功能比较起来,更多了几分实用性。以后的日子,随着我技术的提高和思想的成熟,我一定会把它做的更好,更趋近于完美。
    参考文献[1] Simon Robinson, K.Scott Allen等.C#高级编程. 北京:清华大学出版社, 2002,3
    [2] Tom Archer. C#技术内幕. 北京:清华大学出版社, 2002,1
    [3] 沉舟.Microsoft.NET编程语言C#. 北京:希望电子出版社. 2001,3
    [4] 罗军舟,黎波涛,杨明等.TCP/IP 协议及网络编程技术. 北京: 清华大学出版. 2004,10
    [5] Tim Parker .TCP/IP 协议及网络编程技术. 北京: 机械工业出版社, 2000,7
    [6] 周存杰. Visual C#.NET网络核心编程. 北京:清华大学出版社, 2002,11
    [7] 电脑编程技巧与维护杂志社.C#编程技巧典型案例解析. 北京:中国电力出版社, 2005,8
    [8] 云颠工作室. Visual C#中文版全面剖析. 北京:中国水利水电出版社, 2003,5
    [9] 叶树华《电子协议与编程》,《电子邮件格式》,《电子邮件接收》,《mime 编码解码与发送附件》
    [10] MSDN中文网站网络广播 C#设计模式纵谈http://www.microsoft.com/china/msdn/events/webcasts/shared/Webcast/MSDNWebCast.aspx
    [11] 滁州,马金虎,朱力勇. 编写基于SMTP网络应用程序. 电脑爱好者,2003,5:92~94
    [12] 滁州,马金虎,朱力勇. 编写基于POP3网络应用程序. 电脑爱好者,2003,6:92~94
    [13] 潘泰国. 新一代电子邮件系统. 电子技术应用. 1992,11
    [14] 代继红. SMTP认证机制模块化设计及实现. 中南民族大学学报(自然科学版), 2005,4
    [15] 胡安廷. 简单实现中文邮件. 中国计算机报, 2004,11
    1 评论 11 下载 2018-10-01 22:31:48 下载需要12点积分
  • 基于C#和SQL SERVER的校园知识问答论坛网站的设计与实现

    摘 要本文使用Asp.Net Core 和MsSqlServer技术,详细说明开发校园知识论坛系统的开发。校园知识论坛系统是基本B/S模式的一种交互性极强的电子信息服务系统。它为使用者提供一个交流的平台,每一个用户都可以在上面问答知识,获取信息,发布观点和评论,极大地促进了信息的共享。
    关键词:Asp.Net Core;MsSqlServer;知识论坛;B/S
    AbtractThis article uses Asp.Net Core and MsSqlServer technology to explain in detail the development of the campus knowledge forum system. The campus knowledge forum system is a highly interactive electronic information service system based on the basic B/S model. It provides users with a platform for communication. Each user can ask questions, obtain information, publish opinions and comments, and greatly promote the sharing of information.
    Keywords:Asp.Net Core; MsSqlServer; Knowledge Forum; B/S
    1 绪论1.1 开发背景及意义论坛又名网络论坛BBS,它是一种电子信息服务系统,为使用者提供一个平台,用户可以在上面撰写、发布信息或者是提出意见看法[3]。它是一种交互极强,知识高度共享的电子信息服务系统,使用者可以在平台上获取各种信息服务,问题、聊天等。
    论坛随着网络的发展,迅速发展壮大。现在的论坛涵盖我们生活的各个方面,而校园知识论坛是面向在校学生的信息共享和交流的平台,可以促进在校学生的学术交流、增强互动性。它可以为在校学生提供一个解决方法的途径,一个共享知识的平台,一个展示才华的舞台。
    在本文中,主要介绍使用Asp.Net Core技术来实现校园知识论坛建设。通过基本Internet和Web数据库技术,解决用户间的数据共享,用户可以通过浏览器来获取想要的数据,同时可以向服务器存储自己的数据。服务端把用户授权的信息展示出来,构建出一个小型的数据中心,方便用户查阅信息,获取解决方案,兴趣交流。
    目前,可以通过校园知识论坛系统,向广大的同学提问自己的疑问,同时可以回答别人提出的疑问,阅读分享的知识。而志同道合的人可以关注同一个话题进行交流、阐述观点。
    1.2 课题目标本课题主旨在于阐述如何使用Asp.Net Core为基础开发校园知识论坛系统。根据软件的开发流程,在开发前需要对需求和功能进行分析和深入的了解,然后根据需求和功能模拟出流程,最后利用Asp.Net Core平台构建项目架构和功能的开发。在开发的过程中,将会使用到很多技术,本课题会阐述开发的技巧以及功能实现的思路。
    2 开发技术介绍2.1 服务器由于开发使用的C#语言,基于Asp.Net Core2.0.5运行环境,在服务器上选择Windows Service 2016数据中心版的IIS10(Internet Information Services)进行部署和托管。基于IIS的托管及其方便维护和调试。
    2.2 数据库Microsoft SQL Server是Microsoft 公司推出的关系型数据库管理系统,具有使用方便可伸缩性好与相关软件集成程度高等优点,被企业广泛使用[4]。Microsoft SQL Server 数据库引擎为关系型数据和结构化数据提供了更安全可靠的存储功能,可以构建和管理用于业务的高可用和高性能的数据应用程序[4]。对Asp.Net Core2.0有着良好的支持,可以高效处理大量数据和并发操作,事务机制可以保证数据的一致性,同时可以构建集群用于大数据的处理,所以采用Microsoft SQL Server作为数据服务。
    Redis 是完全开源免费的,遵守BSD协议,是一个高性能的key-value数据库[5]。对网站中重复读取的数据,如果采用Redis进行缓存可以减轻数据库服务器的负担,同时能够提高访问速度[5]。
    2.3 Asp.Net Core.Net是Microsoft针对Windows开发而编写的集成框架。.Net 允许开发者使用C#,VB,F#语言开发能够在Windows设备上运行的程序。Asp.Net Core是.Net跨平台的产品,它允许程序能够在不同的操作系统上运行。Asp.Net Core基于.Net Core环境,无托管模式,MVC结构使得代码结构清晰,同时包含Css,JavaScript自压缩功能,使得开发的东西更健壮。
    3 系统分析3.1 系统功能分析用例图是指项目的蓝图,通过用例图明确项目的功能[6]。以下是用户和管理员的用例图。
    在此校园知识论坛系统内,用户将可以写文章、提问、评论、管理个人信息和关注他人的社交。如下图3-1所示:

    管理员拥拥有着用户管理、运维管理、数据统计、内容管理和权限管理等功能。如下图3-2所示:

    3.2 系统数据分析在知识论坛中的问答模块,用户可以提问多个问题,我们通过标签来区分每个问题涉及的领域,所以,1个问题可以对应n个标签。
    每一的问题可以由多个人进行回答,因此,问题好和回答之间是1:n的关系。专栏是是给用户写文章的地方,同样,一篇专栏对应多个标签,一个用户可以发表多篇专栏文章,专栏和标签的关系时1:n,用户和专栏之间的也是1:n。具体如下图3-3所示:

    3.3 系统流程分析用户在没有登录的状态下,只能查看问题、文章和、评论。用户需要登录后提问、评论、写文章。如果用户没有登录账户则需要进行注册。一般流程是用户进行登录,没有账户进行注册,注册成功返回登录,登录成功后提问、回答问题、写文章。
    登录模块。具体操作流程如下图3-4所示:

    问答模块。其具体的流程图如下图3-5所示:

    文章发布流程图如图3-6所示:

    3.4 功能模块设计在此次研究中,使用Asp.NetCore 开发高可用的站点是本次研究的重点。知识论坛由于其言论的自由性,我们需要对网站的内容进行严格的管理,因此,知识论坛有多个模块组成,由管理后台对全站的内容进行管理。各个模块相应结构图如下图3-7所示:

    Web站点是对用户提供的可视化交流平台,后台管理系统是对网站运维提供的操作平台。
    Web站点主要包含用户的注册、登录、找回密码、完善个人信息、提问、回答、评论、写文章、申请话题、关注其他用户等。Web站点是用户的操作平台,为用户提供良好的用户体验和高效的问题解决途径,同时为用户提供良好的沟通平台。她作为重要的一个大模块连接的是用户。
    后台管理系统是管理人员对网站的运行提供支持,净化网络环境,为用户提供良好的上网环境,也是服务于用户。它主要包含内容管理(包含提问、回答、写专栏、用户资料的管理)、基础数据管理(包含敏感词库、网站运行参数等的管理)、角色和权限管理(管理人员之间的角色和权限,能够访问到的和可操作的权限管理)。
    3.5 系统架构设计在系统的架构方面上,我采用微软提供的“简介架构”。采用仓储-服务模型。在项目的基础设施中包含仓库服务,其他公共的类库,项目核心中包含实体类,服务,接口,主要用于处理业务核心。Web站点和后台都使用的一套核心代码,这样处理能后减轻代码量,同时能够让项目更容易管理和扩展。具体的系统架构如图3-8所示:

    4 数据库分析与设计数据库在整个系统中起着重要的所有,它存储着整站的基本数据。再设计数据库的时候要哦根据业务需求,把数据拆分为多个表,通过各表之间的逻辑关系,构建表与表之间的关系模型。同时,数据库在网站的并发中是重要的一环,在保证数据库能够支持高并发的同时,数据的安全性、稳定性、一致性都是需要进行考虑。
    数据库设计包括数据库系统的需求分析、概念设计、逻辑设计、物理设计阶段[1]。
    4.1 数据项说明
    用户信息表:Id、UId、用户名、邮箱、密码、图像、电话、QQ、微信、邮箱是否被验证、注册时间 问题表:Id、标题、问题描述、提问者Id、是否匿名、提问时间回答表:Id、回答内容、是否采用、问题Id,回答者Id、回答时间标签表:Id、标签名、标签描述、父级Id、是否可用、创建时间专栏表:Id、标题、内容、是否匿名、创建时间问题和标签中间表:问题Id、标签Id专栏和标签中间表:专栏Id、标签Id管理员信息表:Id、管理员用户名、密码、手机号、创建时间
    4.2 数据表说明校园知识论坛共包含8张表,系统数据表如下所示:



    表名
    说明
    功能




    UserInfo
    用户信息表
    存储用户个人信息


    Question
    问题表
    存储用户提问的问题


    Answer
    回答表
    存放问题的回答


    Tag
    标签表
    存放标签信息


    Blog
    专栏表
    存放用户撰写的文章


    QuestionTags
    问题和标签中间表
    存放问题和标签的关系


    BlogTags
    专栏和标签的中间表
    存放专栏和标签的关系


    Admin
    管理员信息表
    存放管理员信息表



    4.3 表结构设计根据系统的需求,创建不同的表用来存储用户的信息,根据不同表之间的关系,构建一对一、一对多、多对多的关系。用户产生的数据大多都是文本数据,很少涉及图片,视频等数据资料,MsSQLServer这种关系型数据库完全满足系统的使用。具体的表信息如下所示:
    4.3.1 用户信息表此表用于记录所有用户的个人资料,包括Id、UId、用户名、邮箱、密码、图像、电话、QQ、微信、邮箱是否被验证、注册时间。



    字段名
    说明
    类型
    长度
    是否为空
    主键或外键




    Id
    自动编号
    Int
    -

    主键


    UId
    用户唯一Id
    Nvarchar
    32

    -


    UserName
    用户名
    Nvarchar
    32

    -


    Email
    邮箱
    Nvarchar
    128

    -


    Password
    密码
    Nvarchar
    32

    -


    Icon
    用户图标 Url
    Nvarchar
    128

    -


    TelNumber
    图手机号
    Nvarchar
    11

    -


    QQ
    QQ
    Nvarchar
    18

    -


    WeChat
    微信号
    Nvarchar
    18

    -


    IsEmailValidate
    邮箱被验证
    Bit
    -

    -


    SubTime
    创建时间
    datetime2(7)
    -

    -



    4.3.2 问题表此表用于记录问题信息,包括Id、标题、问题描述、自定义标签、所属话题、提问者Id、是否匿名、提问时间。



    字段名
    说明
    类型
    长度
    是否为空
    主键或外键




    Id
    自动编号
    int
    -

    主键


    Title
    标题
    nvarchar
    64

    -


    Description
    问题描述
    nvarchar
    Max

    -


    Tag
    自定义标签
    nvarchar
    32

    -


    UserInfoId
    提问者Id
    int
    -

    外键


    Anonymous
    是否匿名
    bit
    -

    -


    SubTime
    提问时间
    datetime2(7)
    -

    -



    4.3.3 回答表此表用于用户的回答,其包含Id、回答内容、是否匿名、是否采用、问题Id,回答者Id、回答时间。



    字段名
    说明
    类型
    长度
    是否为空
    主键或外键




    Id
    自动编号
    int
    -

    主键


    Content
    回答内容
    nvarchar
    Max




    Adoption
    是否采用
    bit
    -




    QuestionId
    问题Id
    int
    -

    外键


    UserInfoId
    回答者Id
    int
    -

    外键


    SubTime
    回答时间
    datetime2(7)
    -





    4.3.4 话题表此表用于记录话题信息,包括Id、话题、话题描述、父级Id、是否可用、创建时间。



    字段名
    说明
    类型
    长度
    是否为空
    主键或外键




    Id
    自动编号
    int
    -

    主键


    TagName
    话题名称
    nvarchar
    32

    -


    Description
    描述
    nvarchar
    Max

    -


    PerId
    父级Id
    int
    -

    -


    Enable
    是否可用
    bit
    -

    -


    SubTime
    创建时间
    datetime2(7)
    -

    -



    4.3.5 专栏表此表用于记录系统用户撰写的文章信息,包括Id、标题、内容、所属话题、是否匿名、创建时间。



    字段名
    说明
    类型
    长度
    是否为空
    主键或外键




    Id
    自动编号
    int
    -

    主键


    Title
    话题名称
    nvarchar
    1238

    -


    Content
    描述
    nvarchar
    Max

    -


    Anonymous
    是否可用
    bit
    -

    -


    SubTime
    创建时间
    datetime2(7)
    -

    -



    4.3.6 问题和标签中间表记录问题和标签之间的关系。



    字段名
    说明
    类型
    长度
    是否为空
    主键或外键




    TagId
    标签Id
    int
    -

    外键


    QuestionId
    问题Id
    int
    -

    外键



    4.3.7 专栏和标签中间表 记录专栏个标签之间的关系。



    字段名
    说明
    类型
    长度
    是否为空
    主键或外键




    TagId
    标签Id
    int
    -

    外键


    BlogId
    专栏Id
    int
    -

    外键



    4.3.8 管理员信息表此表用于记录系统中网站管理员的信息,包括Id、用户名、密码、手机号、创建时间。



    字段名
    说明
    类型
    长度
    是否为空
    主键或外键




    Id
    自动编号
    int
    -

    主键


    UserName
    话题名称
    nvarchar
    32

    -


    Password
    描述
    nvarchar
    32

    -


    TelNumber
    所属话题
    nvarchar
    11

    -


    SubTime
    创建时间
    datetime2(7)
    -

    -



    5 详细设计每一个站点,用户看到的是界面,而界面的配色,排版会给用户很深的印象。因此网站的界面需要设计的简洁,美观,以此来提高用户的体验度。
    5.1 问题列表页主页主要展示用户的提问和回答详情。

    5.2 提问界面
    5.3 回答界面
    5.4 标签界面
    5.5 专栏页面
    5.6 个人中心
    6 系统运行与测试6.1 测试目的软件测试是对全部或部分的功能、模块的计算机程序在正式使用前的检测,以确保该程序能够按照预定的方式运行。在软件投入生产性运行之前,尽可能多地发现软件中的错误。测试是对软件规格说明、设计和编码的最后复审,所以软件测试贯穿在整个软件开发期的全过程[2]。
    6.2 测试环境
    WindowsMsSQLServer.Net Core2.0.5集成环境
    6.3 测试方法6.3.1 单元测试单元测试时在开发的过程中,对最小单元代码的测试。通过单元测试能够及时发现系统存在的语法、逻辑等问题。在开发的过程中,我们会对每一部分的功能代码进行单元测试,以期达到预期目标。
    6.3.2 集成测试集成测试是在单元测试的基础上,把所有的模块进行组装,整体测试,以发现模块与模块之间是否存在问题。在集成测试中,通过走业务流程,逐步查看程序是否达到预期目标
    结束语本次毕设耗时近两个月的时间,数据库迁移8次,SVN代码提交52次。在这次的制作中,因为使用Asp.Net Core 2.0.5,在国内并没有大规模应用,解决方法不够,遇到很多的问题,同时也踩了很多坑,但是学习到很多东西。在实践中学会了Entity Freamword Core First一对多,多对多的关系模型设计,使用Asp.NetCore,了解并应用了IOC这种优雅的设计模式。这促使着我不断的学习新的知识,技术在不断的更新我们需要不断的学习来充实自己。
    参考文献[1] 段有志. 浅谈关系数据库的系统设计[J].中国科技纵横,2011,(09)
    [2] 刘欣怡, 周跃东, 田秀丽. 软件工程[M].清华大学出版社,北京交通大学出版社,2007
    [3] 赵晓军.基于ASP.NET的在线教学论坛设计[D].河北大学,硕士论文,2016
    [4] 朱丹.关于教务管理系统的设计与实现[J].卷宗,2015,(10)
    [5] 李会娟.缓存技术研究及其在电子政务平台中的应用[D]. 北京工业大学, 硕士论文,2009
    [6] 钟珞 袁景凌 魏志华汤练兵.软件工程[M]. 清华大学出版社,2005
    [7] SteveSmith.Architecting Modern Web Applications with Asp.Net Core and MicrosoftAzure[M], DevDiv, .NET and Visual Studio product teams,2017
    [8] 严文涛,张明.MSSQL SERVER数据库服务器的安全问题和安全配置[J].山东电子,2002
    [9] 超宇,李金香.Redis在高速缓存系统中的应用[J].微型机与应用,2013,(06)
    [10] 徐鹏民.用MSSQL Serve 建立Internet上的全文检索系统[J].计算机世界,2000,(08)
    [11] 叶红卫.基于ASP.NET MVC框架的Web设计[J].河北北方学院学报(自然科学版),2009,(12)
    [12] Nagel.C,李铭(译),黄静(审).C#高级编程[M],清华大学出版社,2017(10)
    1 评论 6 下载 2018-10-01 22:13:12 下载需要10点积分
显示 765 到 780 ,共 15 条
eject