Qt实现的宠物小精灵对战游戏阶段二-用户的联网注册和登录

Daisy

发布日期: 2019-03-17 15:27:35 浏览量: 1722
评分:
star star star star star star star star star star_border
*转载请注明来自write-bug.com

1、题目要求

  • 每个用户需要注册一个账号,用户名全局唯一,不能有任何两个用户名相同,要考虑注册失败的场景时的反馈

  • 实现注册、登录、登出功能,均采用C/S模式,客户端和服务端用socket进行通信,服务端保存所有用户的信息(文件存储或数据库均可,数据库有额外加分)

  • 每个用户拥有:用户名、拥有的精灵,两个属性。 用户注册成功时,系统自动随机分发三个1级精灵给用户

  • 用户可以查看所有成功注册用户拥有的精灵,也可以查看所有当前在线的用户

  • 如有界面设计可酌情加分

  • 题目考察点:socket通信,交互场景反馈

2、总体设计

在第一阶段的基础上,实现以下改进:

  • 增加用户类

  • 使用数据库存储信息

  • 实现socket网络通信

  • 完成服务端接口

第一版本的设计是从小精灵类的实现入手的。第二版本不同,将从服务端接口的角度开始设计,并同时考虑数据库的引入

服务器在启动时将处于一个阻塞持续监听的状态,使用多线程技术实现按下任意键停止监听。所以在Hub里面设置了两个线程函数listenFunc和terminateFunc,前者用于实现正常的阻塞监听,后者实现按键停止服务器。

针对多用户登录的问题,服务器设置一个统一的登录端口A0,这个端口将写死到客户端程序中。客户端向服务端的端口A0请求登录时,服务端程序将分配给客户用户一个目前未被使用的endpoint端口Ax,客户端将连接此Ax端口来实现持续的连接。

用户名格式要求:

  • 不包含空格、回车、制表等空白字符

  • 长度6-30

    • 密码格式要求:
      • 仅包含字母、数字、下划线_
      • 长度6-30

数据库schema:

  1. User(
  2. id integer primary key,
  3. name text unique not null,
  4. password text not null
  5. );
  6. Pokemon(
  7. id integer primary key,
  8. userid integer not null,
  9. name text not null,
  10. race int not null,
  11. atk int not null,
  12. def int not null,
  13. maxHp int not null,
  14. speed int not null,
  15. lv int not null,
  16. exp int not null,
  17. );

其中设置User的id为integer类型的primary key的话,可以由sqlite3自动生成id

Endpoint有一个timer,当用户超过timer没有给服务器发数据时服务器断开连接并delete Endpoint

多Endpoint管理:

  • 使用vector保存Endpoint的指针,使所有Endpoints可以通过下标访问,并方便处理内存。

  • Endpoint结束条件:当timer超时且发现socket链接已断开,则告诉Hub关闭此Endpoint。具体方案如下:

    • public Endpoint::start()函数启动服务并返回端口号,如果返回0则表示启动失败
    • public Endpoint::process()函数里面写socket的accept函数,并在process函数中处理各种请求,返回时代表endpoint结束(超时或用户退出)
    • Hub::mornitor(Endpoint *)是线程函数,每当新建Endpoint的时候新建一个线程,调用此函数,参数为新的Endpoint的指针
    • Hub首先使用Endpoint::start()获取端口号,然后新建线程把mornitor函数detach。
    • Hub::mornitor函数中调用Endpoint::process()开始recv,当process函数返回时表示此endpoint结束运行,在Hub的Endpoint指针容器中清除此对象并delete之
  • 因为Hub的mornitor线程和listenFunc线程函数都需要访问Endpoint指针容器,所以设置锁来防止多线程出现故障

通信方式:客户端发送一条请求,服务器回复一条信息,服务端不需持续监听。客户端发送注销请求时服务器不回复。

Endpoint断线重连方案:

  • Endpoint使用长连接

  • 客户端断开时,recv函数返回SOCKET_ERROR,设置online为false

  • Endpoint在accept成功后设置online为true

  • timer超时后如果online仍为false则判定玩家长时间未登陆而退出

  • 如果timer超时前玩家重新登陆则终止timer

    • 使用condition_variable实现带有条件的sleep

Hub提供的接口:

  1. login <username> <password>
  2. logon <username> <password>

Endpoint提供的接口:

  1. logout
  2. resetPassword <oldPassword> <newPassword>
  3. getPlayerList
  4. getPokemonList [playerID]
  5. getPokemon <pokemonID>
  6. pokemonChangeName <pokemonID> <newName>

编码相关:

服务端使用GB2312编码,客户端识别时使用QString QString::fromLocal8Bit(const char *str, int size = -1)来实现字符转换,发送时如果信息包含中文也需要使用QString.toLocal8Bit来实现转换,以此实现中文信息传递。

目前能够出现中文的地方有:

  • 用户名

  • 精灵名

  • 种族名

需要安装LAV filters才能播放音频。音频文件保存在exe目录下的media目录下

使用QSS实现样式与内容分离。注意QSS文件一定不要使用UTF-8编码,建议使用ASCII编码。此处使用了GB2312!

3、服务端设计与实现

3.1 宏观设计

关于Socket通信:服务器使用vs 2017作为开发环境,使用原生windows Socket作为通信基础。客户端使用Qt Creator作为开发环境,使用Qt提供的QTcpSocket作为通信基础。

关于数据存储:使用Sqlite3作为数据库管理程序,所有数据保存在服务器以防止外挂程序。

关于中文显示与信息传递:Qt使用UTF-8作为编码。由于vs 2017不支持不带BOM的UTF-8编码,所以服务端出现中文的文件使用GB2312编码。客户端识别时使用QString QString::fromLocal8Bit(const char *str, int size = -1)来实现字符转换,发送时如果信息包含中文也需要使用QString.toLocal8Bit来实现转换,以此实现中文信息传递。所以玩家可以把用户名和精灵名设置为中文。

背景音乐:Qt的媒体播放器库使用了第三方的插件。如果程序启动后没有背景音乐,可以尝试安装LAV filters。

资源文件:图片、音频等文件均作为资源文件保存在exe中,防止被用户替换。

界面美化:使用了类似于CSS语法的QSS设置程序样式,实现了样式与内容的分离。QSS文件不能使用UTF-8编码,此处使用了GB2312编码。

3.2 详细设计-数据库相关

数据库schema设计:数据库采用sqlite3数据库,包含两个表,分别是用户表和精灵表。用户的用户名全局唯一,且用户拥有一个唯一的内部id。精灵表中保存了每个精灵的持有者的id,以便查询用户拥有的所有精灵。精灵表保存了所有精灵的属性。具体格式如下:

  1. User(
  2. id integer primary key,
  3. name text unique not null,
  4. password text not null
  5. );
  6. Pokemon(
  7. id integer primary key,
  8. userid integer not null,
  9. name text not null,
  10. race int not null,
  11. atk int not null,
  12. def int not null,
  13. maxHp int not null,
  14. speed int not null,
  15. lv int not null,
  16. exp int not null,
  17. );

4、详细设计-网络通信相关

服务器分为两个部分:一个Hub和一个Endpoint集合。Hub运行在7500端口,负责处理注册和登录的请求,为短链接,返回数据后立即断开连接。如果是登录请求,在登录成功的情况下会new一个Endpoint负责与用户对接,Socket会返回一个新的端口号。此端口号为操作系统分配的空闲端口号。

Endpoint将和每个用户进行长链接,直到用户退出登录。Endpoint处理用户在登录状态下的各种请求,包括:

  • 重置密码

  • 获取玩家列表

  • 获取精灵列表

  • 获取单个精灵详细信息

  • 精灵改名

  • 退出登录

此外,Endpoint还能够检测用户的意外断线。如果用户没有正常退出登录,则Endpoint不会立即销毁,而是会等待一段时间给用户重连,防止用户频繁掉线时Endpoint被频繁生成与销毁的问题。

网络通信接口定义如下:

  • Hub提供的接口

    • login <username> <password>
      • 返回端口号则登录成功,否则返回错误信息
    • logon <username> <passwprd>
      • 返回Accept.\n则注册成功,否则返回错误信息
  • Endpoint提供的接口

    • logout
      • 退出登录,无返回消息
    • resetPassword <oldPassword> <newPassword>
      • 重置密码。返回Accept.\n则重置成功,否则返回错误信息
    • getPlayerList
      • 返回所有玩家的信息。
      • 单个玩家信息格式为<userID> <userName> <online: 0 | 1>
      • 不同玩家信息使用换行符隔开
    • getPokemonList [playerID]
      • 如果不给出playerID则返回自己的小精灵概要信息。否则返回指定玩家的小精灵概要信息
      • 单个小精灵信息格式为<id> <name> <raceName> <lv>
      • 不同小精灵信息使用换行符隔开
    • getPokemon <pokemonID>
      • 根据小精灵id获取小精灵的详细信息
      • 返回信息格式为<id> <name> <raceName> <atk> <def> <maxHp> <speed> <lv> <exp>
    • pokemonChangeName <pokemonID> <newName>
      • 返回Accept.\n表示改名成功

4.1 Hub类的设计与实现

Hub类被设计为单件,其所有构造函数的访问属性均为private,且除了默认构造函数外的其他构造函数与赋值函数均被delete。只能通过public静态函数getInstance来获得单件的引用。

Hub管理着一个Endpoint的列表。此列表使用vector实现频繁的插入与删除。

因为endpoints会被多线程访问,所以使用互斥量std::mutex保护多线程下的endpoints。

初始情况下Hub只有两个线程,一个是listenFunc线程,用来处理登录和注册的请求。另一个是ternimateFunc线程,用来实现Hub运行时按下任意键停止Hub的效果。

用户发出登录请求时,首先会判断此用户在数据库是否存在。如果存在则检查当前是否已经有对应此用户的Endpoint。如果有,而且此Endpoint正在运行,则因为用户多次登录而拒绝请求。如果Endpoint处于用户意外断线的等待状态,则重新激活此Endpoint并和用户建立连接。如果不存在对应的Endpoint,则new一个Endpoint,并detach出去一个mornitor线程。mornitor线程中会调用Endpoint::process函数以启动Endpoint。因为Endpoint::process是阻塞函数,所以当此函数返回时,mornitor线程会从endpoints中delete掉此Endpoint。所以每个Endpoint都会有一个对应的mornitor线程在运行。拓扑图如下:

调用Hub::start函数以启动服务器(阻塞方式)。此函数会连接数据库(或新建数据库),初始化Socket,并启动上述两个Hub中的基础线程。任何一个初始化项目失败,或者两个基础线程终止,则start函数会返回。

4.2 Endpoint类的设计与实现

Endpoint类负责实现登陆后的其他用户请求。这些请求无非就是简单数据库操作,此处不再赘述。下文将重点描述如何实现Endpoint检测用户意外离线后的等待机制。

Endpoint::process函数如下:

  1. void Endpoint::process()
  2. {
  3. while (running)
  4. {
  5. online = false;
  6. timing = true;
  7. thread timerThread(&Endpoint::timer, this);
  8. thread listenThread(&Endpoint::listenFunc, this);
  9. timerThread.join();
  10. listenThread.join();
  11. }
  12. }

Endpoint类设置了两个标志变量online和timing负责记录“是否在线”和“是否在计时”两个值。然后启动了timer线程和listen线程。也就是说用户发送登录请求,Hub回复Endpoint的端口之后,Hub::mornitor线程调用Endpoint::process的时候,计时就已经开始了。玩家需要在指定时间之内连接到Endpoint,否则也会因为长时间未连接而导致Endpoint被销毁。

当用户连接到Endpoint,Endpoint::listenFunc中的accept函数被触发后,Endpoint将会把online置为true,然后尝试停止计时。此处使用了std::condition_variable条件变量来实现。在timer线程中,函数被condition_variable::wait_for函数阻塞,相当于是一个非忙等待的条件sleep函数。listenFunc会调用condition_variable::notify_one函数来通知timer中的wait_for函数,以此实现timer停止等待的效果。timer停止等待后判断online标志,如果online为true表示online已经被listenFunc处理了,则把timing设置为false。否则即为等待超时,关闭Socket以停止listenFunc,并把running设置为false以停止process函数的循环。process函数执行完毕后就会被Hub::mornitor函数销毁。

客户端连接Endpoint后timer线程执行完毕。客户端意外断开时listen线程也会执行完毕,但是running此时仍为true,此时就会执行process函数中的循环,重新启动timer和listen线程监听客户端时间与计时,以此实现离线等待机制。

4.3 Pokemon相关的改动

服务器的Pokemon函数增加了构造函数,给出所有属性值以构造一个精灵,用来把从数据库读出的精灵数据构造为精灵。

Pokemon类内定义了静态public成员变量races,为PokemonBase的指针。用来以静态的方式初始化四个种族。代码如下:

  1. const PokemonBase *Pokemon::races[4] = {new Race<0>(), new Race<1>(), new Race<2>(), new Race<3>()};

由于第二版不涉及战斗,所以其他Pokemon相关函数没有进行修改。

4.4 主程序

主程序只要启动Hub即可。代码如下:

  1. int main()
  2. {
  3. srand(time(NULL));
  4. Hub &hub = Hub::getInstance();
  5. hub.start();
  6. system("pause");
  7. }

5、客户端设计与实现

5.1 Mainwindow类的设计与实现

本次项目并没有很多地使用ui文件,几乎一切Qt控件都是用代码构造出来初始化与管理的。

MainWindow使用了有限状态自动机来管理当前所处状态(页面)。一共有如下若干状态:

  • 初始状态START

    • 显示“开始游戏”按钮和“退出游戏”按钮。
  • 登录状态LOGIN

    • 显示登录时需要的输入框、返回按钮、注册按钮和登录按钮。
  • 主状态MAIN

    • 显示所有功能,即查看自己的精灵、查看所有用户和他们的精灵、修改密码、退出登录。
  • 精灵表POKEMON_TABLE

    • 一个表格,显示精灵的简要属性。表格中有显示精灵属性详情的按钮
  • 玩家表PLAYER_TABLE

    • 一个表格,显示玩家的属性。表格中有查看指定玩家精灵的按钮

界面的转换通过changeState函数实现。此函数首先将所有控件重置并隐藏,然后根据当前状态显示指定的控件,并使用QGridLayout进行布局

网络通信使用QTcpSocket异步通信实现,使用connect(client, &QTcpSocket::readyRead, this, &MainWindow::getServerMsg);连接readyRead函数与getServerMsg函数,所有信息通过getServerMsg函数进行处理。

getServerMsg函数也会根据当前状态对来自服务器的信息进行不同的解析,然后对显示的内容进行一定程度的修改。

MainWindow还使用了QMediaPlayer实现了背景音乐的播放。

5.2 LogonDlg类的设计与实现

LogonDlg是玩家点击注册按钮时弹出的窗体。此窗体使用模式化显示,此窗体关闭前无法对MainWindow进行操作。

玩家点击注册按钮时,注册按钮会被disable以防网络速度慢时玩家多次点击注册按钮造成其他不可预测结果。但是取消按钮没有disable,玩家依然可以点击取消按钮终止注册。

玩家注册成功时,窗体会自动消失,并把刚才注册成功的账号与密码自动填充到MainWindow的输入框中。

5.3 PokemonDlg类的设计与实现

PokemonDlg是玩家点击精灵表中的精灵详情按钮时弹出的窗体,显示精灵详情。如图:

为了方便玩家进行精灵之间属性的对比,玩家可以同时打开不限量个PokemonDlg。玩家每次点击MainWindow中的查看详情按钮都会new一个PokemonDlg出来。对此,为了防止内存泄露,在PokemonDlg的构造函数中添加了setAttribute(Qt::WA_DeleteOnClose);使用户在关闭窗体时就会delete掉窗体。

玩家可以在MainWindow的精灵表中对自己的精灵进行重命名,也可以在PokemonDlg中进行重命名。双击对应的表项,PokemonDlg会发送一个信号来改变MainWindow中的表项,从而实现和MainWindow的精灵表同步更改精灵名称的效果。

6、测试

服务器初始化

服务器接收用户登录

用户正常退出

用户非正常退出

用户超时,Endpoint销毁

按下任意键停止服务器

客户端初始界面

登录界面

注册界面

客户端主界面

重置密码界面

精灵列表界面

玩家列表界面

上传的附件 cloud_download 宠物小精灵对战游戏阶段二-用户的联网注册和登录.7z ( 18.22mb, 147次下载 )
error_outline 下载需要12点积分

keyboard_arrow_left上一篇 : 基于JSP和SQL SERVER数据库实现的图书信息管理系统 基于Jsp和Mysql的教务管理系统 : 下一篇keyboard_arrow_right



Daisy
2019-03-17 15:27:25
宠物小精灵对战游戏阶段二-用户的联网注册和登录
一只快乐的野指针
2020-09-02 15:55:16
大佬,只用qt就能打开吗? 我这里显示:-1: error: No rule to make target 'changepassworddlg.ui', needed by 'ui_changepassworddlg.h'. Stop.

发送私信

离开的不会再回来,回来的不再完美

12
文章数
15
评论数
eject