张立立,徐博文,杨金柱*,王 彤,高东博
(1.东北大学 计算机科学与工程学院 计算机国家级实验教学示范中心,沈阳 110819;2.中核控制系统工程有限公司,北京 102400)
学习利用cpp 知识、参考Qt 手册,综合运用计算机网络,操作系统原理(多线程同步),数据结构,数据库技术,MVC 设计模式,面向对象程序设计方法,软件工程学科等知识开发实际系统。熟悉应用软件的开发过程,要求软件界面美观,操作简便,符合象棋棋牌室的实际游戏场景。
基于以上基本教学要求,本文开发了一套象棋棋牌室游戏系统,课程目标是通过学生熟悉的网络游戏入手,逐步完善游戏功能,让学生在编程学习中建立信心和成就感,从而对编程产生兴趣,进行更深入的研究和探索。
系统由服务器、客户端两部分组成,客户端具有选座界面、对局界面。
用户启动客户端软件之后,客户端显示界面并自动尝试与服务器建立连接,若连接成功,界面显示“已连接”;若连接失败,系统显示“未连接”,用户可以通过“重新连接”按钮来重新连接服务器。
服务器连接成功后,用户可以输入用户名、密码向服务器发送登录请求,服务器校验后向客户端发送成功/失败反馈。若已有客户端使用此用户名登录,则不可以重复登陆。
完成登陆后,系统显示当前登录的用户名和积分。此时,客户端显示选座界面,用户可以:(1)创建比赛对局(房间);(2)取消创建的对局(房间);(3)加入已有的对局(房间);(4)观战正在进行的对局。
当找到对手(加入房间/创建的房间被加入)或选择观战模式之后,界面切换到对局界面。非观战模式下,用户可以依据棋规移动棋子,移动不符合棋规时,系统不响应。
双人对战过程中如果某一方获胜,对战终止。
此时,胜负两方分别加减积分,客户端返回选座界面;如果有一方点击“退出/认输”按钮,则判定这一方为负,其他同上。观战过程中,点击“退出/认输”按钮,客户端返回选座界面。流程如图1 所示。
图1 系统功能流程图
整个系统由一个中心服务器和若干个客户端组成。用户功能上主要有对局系统和棋牌室座位管理系统两部分。
服务器和客户端均利用C++和Qt 框架编写。为了实现了多人时操作、客户端网络通信不影响用户操作,使用了多线程技术。服务器和客户端之间采用TCP 协议进行通信,服务器采用改进过的bio 模式,即为每个客户端连接建立两个线程分别用来收发网络消息。
选座系统、棋规判定系统和棋盘模型等均位于服务器端。客户端部分除了网络模块之外,仅保存视图。服务器端连接mysql 数据库存储用户名、密码和积分信息。
Qt 是一个跨平台的C++应用程序开发框架。它提供给开发者建立图形用户界面所需的功能,广泛用于开发GUI 程序,也可用于开发非GUI 程序。Qt 是完全面向对象的,很容易扩展,并且允许真正地组件编程。
服务器端由一个线程负责建立TCP 连接,连接建立成功后,开启两个新的线程用来进行socket 通信。
一个线程用于向客户端发送数据,相当于“生产者消费者”模型中的消费者。在没有需要发送的消息时利用Qt 框架的WaitCondition 等待(相当于优化后的自旋锁,可以有效降低cpu 使用率)。当需要发送消息时,其他线程设置好此消息线程对象的消息内容成员,并唤醒此线程(相当于打开自旋锁),就可以实现消息发送。在使用Qt 的信号与槽连接时,使用Qt::Dirrectconnection连接方案,否则会因为目标线程阻塞而无法唤醒线程发送消息。
另一个线程用于接收客户端数据,对于每一次接收的数据。利用Qt 的信号与槽机制注册到UserConnection类的doRecv 函数。这样就形成了如下设计。
每个抽象的UserConnection 对应一个实际的用户连接。
所有连接(UserConnection)共享一个RoomManager类的对象roommanager。
所有连接(UserConnection)共享一个数据库操作类。
每个连接有一个独立的连接抽象层(组合了收发线程的TCPConnection 类)用于管理收发线程。
对于服务器的消息发送,调用连接抽象层的send函数即可向客户端发送消息。
对于服务器的消息接收,直接回调doRecv 函数,相应的业务就可以在此函数中得到处理,而无需考虑网络具体实现。
用这样类似java web 中servlet 的方式可以对底层的网络部分更好地封装,降低耦合便于拓展。
客户端的网络模块设计与服务器类似,只是不同于服务器多个连接,客户端仅有收发两个线程。
系统整体采用mvc 设计模式,便于降低代码耦合性,有利于组件的重用。如图2 所示。
图2 mvc 设计模式
客户端的ChessView 等类继承自Qt 的Widget,用来与用户交互。
服务器的ChessField 类是棋盘模型,棋规判定、胜负判定等业务逻辑在此类中实现。这个类聚合了抽象类ChessPiece 的不同实现,对应不同种类的棋子。每个子类有各自的边界判断,移动判断函数。
控制器为上文提到的UserConnection 类。
每一个UserConnection 中都有一个相同的Room-Manager 类指针指向一个共享的RoomManager 对象,程序截图如图3 所示。
图3 RoomManager 类指针函数程序
可以看到:该类提供了新建房间、加入房间、观看对局等接口,接收的参数是User,用户连接使用时通过传入this 指针就可以实现操作。
存储的数据结构使用了键值对(QMap),其结果如图4 所示。
图4 QMap 结构图
对于一个没有对手的“单人房间”,在room_struct 当中存放两个nullptr,这时键UserConnection 代表房间,作为房间句柄。
“加入比赛”时,为新加入的人也创建一个键值对,即一个“房间”分配两个空间,这样可以更加高效地定位棋盘模型和对手。
调用roomInfo 函数不直接返回该数据结构,而是将冗余的第二份房间信息去除,这样得到的QMap 的键UserConnection 可以唯一标识一个房间,作为房间句柄使用。程序截图如图5 所示。
图5 QMap 的键函数程序
这个类同时也维护用户列表,提供重复登陆判断等接口。
1.并发过程中数据一致性问题
问题描述:调试时,程序运行过程中出现非预期的不合理结果。比如修改某些数据不生效等情况。
原因:roommanager 共享对象在多线程环境下有可能同时被多个线程同时写入,进而出现数据一致性问题。
解决方案:roommanager 等多线程共享对象,在需要写入时需要用Qt 提供的QMutex 互斥锁对给资源加锁,在修改完成后释放互斥锁,在此过程中其他的线程将不能进行修改。
2.客户端连接释放问题
问题描述:调试过程中,客户端程序突然终止连接后,服务器程序常常无响应或是异常终止。而作为一个服务器,因为某客户端的某些行为导致崩溃是不能接受的。
原因:C++语言是不同于java 一类具有垃圾回收器的编程语言,需要手动管理内存及相应的资源。在客户端中断连接以后,必须要结束为该用户创建的连接线程,释放内存,清除所有此用户的对局状态信息,同时从已登陆用户列表中移除该用户。
解决方案:必须找到合理的方式释放资源。
在线程创建过程中将RecvThread 类的die 信号与TCPConnection 类的析构方法连接。当检测到客户端连接中断后,RecvThread 发出die 信号。这时,首先析构TCPConnection 对象,在~TCPConnection()中析构User-Connection 类的对象。
为了清除所有此用户的对局状态信息,同时从已登陆用户列表中移除该用户:UserConnection 类的析构方法调用RoomManager::logoff,此函数释放所有包含UserConnection 的对局。并在users:QSet 中删除这个User,从而实现将该用户标记为非登陆状态。
完成以上这些之后,关闭TCPSendThread 线程,由于此线程处于阻塞状态(等待网络消息的生产者生产消息),故不能直接结束线程。使用Qt 提供的terminate()函数强制终止线程有会带来一系列的问题,比如影响到其他正常运行的线程。故设计了如图6 所示的interrupt 方法。
图6 interrupt 方法程序设计
最后,需要终止TCPRecvThread。
图7 为系统登录界面,正确输入用户名和密码后,点击登录按钮,可以进入到选座系统界面,如图8 所示。
图7 系统登录成功界面
图8 选座系统界面
选座完成后,点击建立房间,进入对战模式,如图9所示。
图9 对战界面
本文给出了使用C++和Qt 技术进行象棋棋牌室游戏软件的设计方案,详细介绍了系统各部分的功能原理、运行流程及具体的实现方案,最后对系统各个功能进行测试。本设计案例重在巩固学生对理论知识的理解,让学生明白只有通过实践才能验证所学所得、才能熟练掌握课程知识。在实践中多层次全方位的学习知识、在实践中加深对所学知识的印象,锻炼软件开发的能力,以及对系统调试和解决实际问题的能力。