前言:
内容参考《操作系统实践-基于Linux应用与内核编程》一书的示例代码和教材内容,所做的读书笔记。本文记录再这里按照书中示例做一遍代码编程实践加深对操作系统的理解。
引用:
《操作系统实践-基于Linux应用与内核编程》
作者:房胜、李旭健、黄玲、李哲
出版社:清华大学出版社
资源:
教材资源
链接: https://caiyun.139.com/m/i?1A5Ch36dl1whD 提取码:jdQe
课件和电子资料源码
链接: https://caiyun.139.com/m/i?1A5CvEKY07Uzs 提取码:xyv0
参考链接:
正文
本章综合多进(线)程,进(线)程通信,Qt及MySQL数据库等知识,设计开发一个Qt GUI应用程序。旨在巩固知识基础,培养在 Linux + Qt 环境下应用程序开发的English,初步形成软件工程的思想。本章将要实现的是一个聊天程序,该程序具有基本的聊天功能和清晰的系统构架,希望能达到抛砖引玉的效果。读者可以在此基础上进一步丰富其功能,提高性能和可靠性,以达到锻炼时间能力的目的。本章具体实践参见电子资源:源代码/ch10/QChat.zip。
本章学习目标:
- 加深对多进程,进程通信,数据库等知识的理解
- 熟练掌握利用Qt进行应用开发的技能
- 了解软件工程实施的大体流程
10.1 概述
本节介绍系统需求以及本章的内容安排,以便读者了解本章知识轮廓和与前面一些章节的关系。
10.1.1 系统需求
1. 功能需求
本系统主要实现即时通信中的文字通信功能。具体地,所有注册并在线用户可以在一个公共聊天室进行文字聊天,并具有注册和登录功能。
2. 界面需求
用户界面美观,友好,即便没有操作手册永和也可以易学,易操作。
3. 技术需求
a) 跨平台。可以在不通操作系统环境下编译,运行。
b) 可靠性。无论用户操作错误还是系统异常,都要保证系统不会出现崩溃限行。
c) 可扩展性。在设计系统构架,数据结构以及编程实现时,要充分考虑扩展性,保证未来的工鞥呢扩展不会导致系统构架,数据结构的变动以及对代码的大幅度修改。
10.1.2 本章内容结构
10.2 节的原型设计主要介绍系统的界面设计,可以看做是对第8章的继续。当时仅对Qt有了初步的了解,并未涉及太多具体操作,这一节会用到多种 Qt 窗口部件。10.2 节介绍系统构架以及服务器,客户端所应具有的功能。10.4节介绍系统的实现,包括数据结构及程序结构。10.5 节这一实训题目的方式给出扩展和完善本系统的方向和建议。
10.2 原型设计
在现实的商业软件开发过程中,原型设计(Prototype Design)是至关重要的一环。原型反应的是用户需求,通过不断地与用户交流,反复修改原型系统,可以进一步理解并确认用户需求,避免未来开发过程中频繁出现返工现象,减少工作量,缩短开发周期。一般来说,原型一旦确定,根据协议,用户就不能再变动需求了。
目前有很多程序的原型设计工具可供选择,如 Axure Rp, Balsamiq Mockup, Prototype Composer等。但是,在这里直接利用 Qt 的强大 GUI 功能来进行原型设计,好处是这个原型设计可以直接用于该项目,同事也是对第8章 Qt 动手少的一个弥补。
用 Qt Creator 新建一个 Qt 部件应用 (Qt Widget Application)项目,项目名称 QChat,基类选择 QDialog,类名取为 QDlgGChat。Qt Creator会自动生成一些文件,包括醒目文件(.pro),头文件(.h),源文件(.cpp)和界面文件(.ui),原型设计主要是对界面文件的设计。
10.2.1 添加资源文件
一般应用程序会用到一些图片,图标作为部件背景或其它装饰,这里用资源文件的方式为下一步的界面设计准备一些图片。Qt中可以用资源文件将各种类型的文件添加到最终生成的可执行文件中,这样可以避免使用外码文件可能带来的一些问题。而且,在编译时 Qt 还会降资源文件进行压缩,甚至最终生成的可执行文件比添加到其中的资源文件还要小。
现在,向项目中添加新文件,模板选择Qt资源文件(Qt Resource File),文件名设置为images。添加完成后悔发现项目中多了一个资源文件 images.qrc,且改文件已自动打开。单价下方的“添加”按钮选择“添加文件”,在弹出的对话框中选择实现准备好的图片文件(最好在项目目录中专门建一个子目录用于存放图片)。添加完图片文件后的结果如图10.1所示。这个资源文件会在接下来的界面设计中用到,如字体颜色按钮的icon属性。
10.2.2 界面设计
界面设计主要包括客户端的“聊天”对话框,“登录”对话框和“注册”对话框及服务器端的主窗口。界面设计操作多事对各种部件用鼠标拖动和利用属性编辑器进行属性设置,下面的叙述中将不再反复体积这些具体操作,而是强调结果。
添加资源文件,"Open In Editor",选择" Add Prefix"
默认的前缀是 /new/prefix1,将其修改为 images。再次点击“添加”按钮选择,选择“添加文件”,在弹出的对话框中选择实现已经准备好的图片。
1. 聊天对话框设计
双击打开界面设计文件 qdlggchat.ui ,进入设计模式。将聊天对话框设为 630*400,窗口标题(windowTitle)设为Q-Chat。从部件盒(Widget Box)向对话框拖入一些部件,并用属性编辑器编辑设置它们的属性,具体的部件类型,用途,对象名称和关键属性如表10.1所示。最终的运行效果如图10.2所示。
对象名称 | 部件类型 | 用途 | 关键属性 |
txtbrosRecv | Text Browser | 显示收到的信息 | sizePolicy:水平Expanding ,垂直Exapnding |
txtSend | Txt Edit | 编辑发出的消息 | sizePolicy:水平Expanding ,垂直Fixed |
ListUser | List Widget | 用户列表 | sizePolicy:水平 Fixed, 垂直Expanding |
btnSend | Push Button | 发送按钮 | sizePolicy:水平Minimum, 垂直fixed |
btnClose | Push Button | 关闭按钮 | sizePolicy:水平Minimum, 垂直fixed |
coboxFont | Font Combo Box | 选择字体 | SizePolicy:水平Preferred, 垂直Fixed |
coboxSize | combo Box | 选择字体大小 | SizePolicy:水平 prefered, 在弹出窗口为题添加9-22共14个选项, currentIndex设置为3 |
toolbtnBold | Tool Button | 加粗 | autoRaise, checkable, toolTip: 加粗 |
toolbtnItalic | Tool Button | 倾斜 | autoRaise, checkable, toolTip:倾斜 |
toolbtnUnderline | Tool Button | 下划线 | autoRaise, checkable , toolTip:下划线 |
toolbtnColor | Tool Button | 字体颜色 | autoRaise, icon:color.png, toolTip:字体颜色 |
2. 注册,登录及服务器主窗口界面设计
以注册对话框为例,添加对话框类的过程如下:切换到设计模式,在左边栏的项目文件夹上右击,选择快捷菜单中的“添加新文件”命令,在弹出的“模版选择”对话框中选择"Qt设计师界面类"->Dialog Without Buttons,类名选择QDlgRegister。系统会自动创建 ui文件 (qdlgregister.ui),相应类文件(头文件qdlgregister.h和源文件qdlgregister.cpp)。打开ui文件,添加必要的部件并设置属性,具体设计过程与聊天对话框设计类似,不再赘述。最终注册,登录和服务器主窗口如下图10.4至图10.6所示。
图标资源下载:
需要的图标资源可以到网站上下载,并添加到 Images 资源库
9个icon图标网站,海量免费矢量图标库! - 知乎
iconfont-阿里巴巴矢量图标库
- 聊天窗口设计
- Qt添加文件选择“Qt设计师界面类”
- “注册对话框”
- “登录”对话框
10.2.3 界面布局
在10.2.2节的界面设计中并未考虑窗口大小变化是窗口上的部件如何变化,本节介绍如何利用布局管理器来管理窗口的布局。布局管理器除了可以对部件进行布局以外,还可以使部件随着窗口的大小变化而变化。
在QtCreator的设计模式下,左侧的部件和中有一组布局(Layouts)部件,包括垂直(Vertical),水平(Horizontal),栅格(Grid)和窗体(Form)4种部件。以水平布局为例,将该部件拖入界面,然后将几个其它部件(如按钮)拖入其中,则其中这些部件会水平分布。读者可以通过实验体验这些布局的使用,这里不再详述。图10.7是聊天窗口对话框应用了布局管理器之后的设计,其中用到水平,垂直和栅格3中布局器件,另外还用到了水平间隔(Horizontal Spacer)部件来布局中的空白。
10.2.4 添加动作
现在为界面天机一些基本动作,以便演示最终的系统运行效果。
1. 将“登陆”对话框设为启动界面
要实现的效果是:程序启动后首先显示“登陆”对话框,单击“登陆”按钮后便能进入主窗口“聊天对话框”,如果直接关闭“登陆”对话框,则不能进入主窗口,真个程序也将退出。
首先需要设置信号和槽的关联。单击设计界面上方的 按钮(或F4键)进入信号-槽编辑模式。按住鼠标左键,从“登陆”按钮拖箱界面,如果10.8所示。
松开鼠标后,会弹出“配置连接”对话框,入股10.9所示,这里选择 btnLogin 的 clicked()信号和 QDlgLogin的accept()槽。确定后,就完成了信号和槽的关联,此时界面如图10.10所示。单击设计界面上的或者按下F3键来返回控件编辑模式。
信号和槽关联之后,运行时当单击了“登录”按钮时就会发送clicked()信号,“登录”对话框接收到信号就会执行相应的操作,即执行 accept()槽。一般情况下,只需要修改槽函数即可。不过,这里的accept()已经实现了默认的功能,它将会对画框关闭并返回 Accepted,所以无需在做修改。
下面用“登录”对话框返回的 Accepted 来判断是否按下了“登录”按钮。切换到编辑模式,打开main.cpp文件,添加包含了“登录”对话框的头文件 qdlglogin.h,屏蔽掉 w.show();和 return a.exec();,添加如下代码片段。
#include "qdlggchat.h"
#include "qdlglogin.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QDlgGChat w;
//w.show();
//return a.exec();
//新建一个"登陆"对话框类的对象
qdlglogin dlogin;
//根据登陆对话框的返回值判断登陆按钮是否按下,若是则展示主窗口,否则结束程序
if(dlogin.exec() == QDialog::Accepted){
w.show();
return a.exec();
}
else
return 0;
}
需要说明的是,在 8.3.2 节中讲过一种信号-槽的关联方法,这里用的另外一种方法。至于采用哪一种,需要根据情况而定。一般来讲,如果不需要自定义槽则采用本节的方式;如果需要自己编写槽的函数体,则采用8.3.2节中的“转到槽”的方式。当然关关旭信号-槽的关联方式还有其他方式,如显式关联(见8.3.3节),这里不集中罗列,用到的时候再讲。
2)关联信号和槽
目前已经将“登录”对话框的“登录”按钮与主窗口(聊天对话框)关联。根据功能要求还要实现以下关联(具体操作步骤不再赘述):
- 将“登录”对话框的注册按钮与注册界面关联
- 主窗口的关闭按钮clincked()信号与主窗口的 reject()槽关联
- 定义主窗口的发送按钮 clicked()信号所关联的槽 on_btnSend_clikced(),功能是将发送文本框 txtSend文本最追加到txtBrosRecv,然后清空 txtSend。代码如下
QString strMsg = ui->txtSend->toHtml();
if(strMsg.trimmed().isEmpty()) return;
ui->txtbrosRecv->append(strMsg);
ui->txtSend->clear();
ui->txtSend->setFocus();
这个原型反应了系统的大体运行效果。由于这个原型是直接使用Qt设计的,所以它已经是程序的一部分了。在后面的开发过程中,基本不用在考虑界面的问题,可以把主要精力放在编写代码上。
10.3 系统设计
本节首先介绍系统构架,并以此为基础分贝介绍客户端和服务器要实现的功能。
10.3.1系统构架
软件系统构架(Software Architecture)是一个系统的草图,用于指导大型软件系统各个方面的设计。它描述的对象是构成系统的抽象组件。各个组件之间的连接则描述系统组件之间的通信。在实现阶段,这些抽象组件将被细化为实际的之间,如获取某个具体的类或对象。
基于系统需求和原型设计,并考虑到系统的可扩展性,本系统采用 Client/Server(客户端/服务器)结构。用户通过运行客户端进行聊天,服务器则提供消息中专服务,集用户发言都通过客户端已消息的方式发送给服务器,服务器收到后将其广播到各个客户端,客户端收到广播后显示收到的信息。系统构架如图 10.12 所示
10.3.2 客户端功能设计
- 聊天。发送聊天信息;接收并显示聊天信息
- 接收用户信息。接收并显示用户信息
- 注册。向服务器发送注册信息,接收并处理来自服务器的注册响应。如注册失败,返回注册页面;成功则直接进入主窗口。
- 登录。向服务器发送登录信息,接收并处理来自服务器的登录响应。若干登录失败,则返回登录界面;成功则进入主窗口;
- 退出登录。向服务器发送退出登录信息,并退出程序。
10.3.3 服务器功能设计
1. 监听并响应客户端的请求
监听来自客户端的各种请求并作出响应。各种可能的请求列表如10.2 所示
请求类型 | 响应 |
---|---|
注册 | 检查注册消息合法性,如果合法则添加用户,并响应请求方(注册成功),广播该用户资料;若非法用户则直接向该客户发送拒绝信息(含拒绝原因)。 |
登录 | 检查登录信息合法性,如果合法则修改用户状态,并响应请求方(登录成功),广播该用户状态;若非法则直接向该客户发送拒绝信息(含拒绝原因)。 |
聊天消息 | 广播该消息 |
退出登录 | 广播该用户状态 |
2. 监听并广播用户状态
侦测用户在线状态,当状态发生变化是通知到各客户端。
3. 用户管理
维护用户账户资料,网络地址,状态信息等
10.4 系统实现
限于篇幅,这里只简单介绍系统实现的思想和程序结构,详细实现请参考电子资源“源代码/ch10/QChat.zip”。建议读者结合源码(注释)阅读本章内容,下面提到的各个类在项目源码中都分别有两个对应文件:头文件(.h)和源文件(.cpp),文件名与类名相同。
10.4.1 数据结构(通信协议)
这里是服务器和客户端共同的数据结构,包括消息类,消息类型和用户信息类。他们实际上是本系统的自定义通信协议。
1. 宏定义
本系统涉及的各种宏都定义在文件 comm.h 中,主要包括消息类型,用户状态等。例如,来自客户端的注册登录消息类型分别定义如下
#define MSG_CLINET_REG 2
#define MSG_CLIENT_LOGIN 3
当定义以“注册”消息是,将其类型复制为 MSG_CLIENT_REG,而不是2。这么做的好处是:含义直观,不会引起类型定义混乱。在开发过程中,可以尽可能地将一些公共的定义放在该文件中。
2. 消息类QMsg
消息类用于客户端与服务器之间的通信。主要包括以下属性:消息类型,发送者ID,接受者ID,消息内容。另外还有方法:pack()和 load(),分别用于消息数据发送前的打包和接收后的解包。
3. 用户类QUser
其主要属性包括用户ID,密码,昵称,状态,IP,端口号,性别,出生日期,简介等。
10.4.2 客户端实现
1. 登录窗口(QDlgLogin)
“登录”对话框如图10.5所示。客户端启动后首先显示该窗口,主要功能是将登录信息(用户账号和密码)发送给服务器。当接收到服务区的登录成功 (MSG_SERVER_LOGIN_SUCCESS)消息时,新建聊天窗口(QDlogGChart),并将登录用户ID,服务器IP地址集端口号传递给他,然后关闭自身。登录失败是,服务器返回失败原因(账号不存在,密码错误,重复登录)。
通过“登录”对话框可以打开注册窗口和设置服务器IP地址集端口号。另外,需要强调的是,登录以及后面注册用到的套接字都是TCP套接字(基类是QTcpSocket),这是基于可靠性的考虑。
2. 注册窗口(QDlogRegister)
“注册”对话框如图10.4所示。该对话框通过“登录”对话框打开,主要功能是将注册信息(昵称,密码)发往服务器。如果注册成功,服务器会返回一个用户ID,并直接登录。
3. 聊天窗口(QDlogGChat)
“聊天”对话框如图10.2 所示。他是客户端的主要窗口,功能是发送,接收并显示聊天信息,显示用户列表。所有聊天信息都是通过服务器转发的,并没有采用点对点通信;聊天所有的套接字是数据报(UDP)套接字(派生自QUdpSocket),这是综合考虑文字聊天特点和网络负载平衡的结果。关于流式套接字和数据报套接字的区别,可以参考7.3.1节的内容。
10.4.3 服务器端实现
服务器的主要任务有响应客户端的注册和登录请求,转发聊天消息,维护用户信息。
1. 数据库(db_chat)集访问接口类(QMyDB)
数据库主要用来存储所有注册用户信息,包括用户ID,密码,昵称,性别等。用户信息表名为 tb_user。
数据库管理系统采用的是MySQL。在项目源码中有一个文件db_chat.sql,读者可以登录MySQL用".\"命令执行该文件(.\ db_chat.sql)来创建数据库。关于MySQL的使用可以参考第9章。
QMyDB为服务器的其它部分提供了访问数据库的方法,包括了解数据库,关闭数据库,新建用户,获取用户信息,更新用户状态等。
2. 主窗口
服务器主窗口实现如图10.6 所示。他为服务器管理人员提供管理操作界面,主要功能是启动,停止服务器,监视用户状态。该类有两个重要成员:一个是派生自QTcpServer服务器Server(详见接下里QServer);另一个是UDP套接字udpSocket,主要用来处理来自客户端的聊天消息。
3. 服务器(QServer)
QServer派生自 QTcpServer,主要功能是监听并维护来自客户端的连接请求。每当监听到新连接,它就会创建一个线程(QtcpThread)来处理客户端的注册和登录请求。
10.4.4 几点说明
项目中用到一些前面未曾讲到的Qt特有内容,限于篇幅,在此仅做简单介绍,如果想了解它们的细节可以查阅相关资料。
1)项目中用到了网络通信和数据库,因此在醒目文件(.pro)的开始位置上应该加上QT+=network\sql。
2)项目中为类命名时,首字母Q,表示它的基类为QObject或它的派生类,这些类都可以利用Qt的信号-槽机制。
3)QUdpsocket, QTcpSockeet及QTcpServer类
QUdpSocket类用发送和接收UDP数据报,它其实就是第7章介绍的数据报(UDP)类型套接字,只不过QUdpSocket封装了更多功能,用起来也更加方便。类似地,QTcpScoket是流套接字,传输的是连续的数据流,尤其适合连续的数据传输。QTcpServer用于处理到达的TCP连接,一般用于服务器程序中。关于套接字的更多细节可参考7.3节。
10.5 Linux应用综合实训
本章节采用C/S构架实现了基本的文字聊天功能,聊天消息一概通过服务器中转。用户注册,登录用的是TCP套接字,而聊天消息传递用的是UDP套接字。电子资源/ch10/QChat.zip提供的程序仅具有本章所述的基本功能,读者可以以此为基础,在以下几个方面进行扩展完善。在做下面的实训之前,读者首先要参考电子资源中的源代码解释,读懂原始的程序;否则理解起来会有困难。
1. 私聊
客户端首先需要增加一个机遇QMinWindow类的主窗口,主要用于显示用户列表以及打开聊天窗口。在增加一个QDialog类的私聊窗口,外观上与QDlgGChat相比少一个用户列表。消息类型增加一个私聊类型(在文件common.h中)。每当客户端收到一个私聊消息,根据发送者ID判断对应私聊窗口是否已经存在,如果存在则立即将消息发往该窗口,否则新建一个与该发送者关联的私聊窗口。当用户双击用户列表中的某个用户是,则打开相应的私聊窗口,主动发起聊天。关于如何天机窗口以及关联信号和槽,读者可以参考10.2节。
服务器端则需要相应增加细聊消息处理,即将消息发往消息指定的接收者。
2. 好友功能
目前,任何一个用户可以看到所有注册用户并与他们聊天。当用户量巨大时,就不现实了。可以在服务器端的数据库中增加一个好友关系映射表(包括用户ID和好友ID两个字段)空挡服务器收到客户端请求时,检索该表中与该客户ID的用户(好友)ID,仅为该客户提供这些好友信息。
3. 群聊
群聊的形式上和本章实现的公共聊天很相似,参与聊天的不再是所有注册用户,而是属于某个分组(群)的用户。客户端需要设计基于QDialgo类的群聊窗口,并可以向服务器发送出群的注册,解散,资料维护以及设置群管理员等请求。服务器端要增加群表(主要包括群ID,创建者ID,群名称等字段),并响应有关群的各种请求。
4. 历史记录
在客户定义数据库或自定义文件用以保存历史聊天几率(至少包括发送者ID,发送时间,聊天内容等3个字段),并提供历史记录删除,导出,导出等维护功能,这样的历史记录功能不需要服务器的任何支持。当然,服务器端可以增加历史记录存储功能,以允许用户将联调记录上传到服务器,并提供下载功能。
5. 离线消息
在服务器增加离线消息,包裹发送者ID,接受者DI,发送时间,消息内容等字段。为用户转发聊天消息是,如果该用户处于离线状态,则奖盖消息内容存入离线消息表。当介绍到用户登录请求时,出一般的登录响应外,还要检查离线消息表中是否有发往该用户的消息,如有则发给该用户,并将发送成功的离线消息删除。
10.6 章节小结
本章介绍了一个聊天工具的设计及实现,涉及多线程,套接字通信,MySQL和Qt等主导知识。通过对本章的学习,死亡读者掌握在Linux+Qt环境下进行综合应用开发的技能,并通过对QChat项目的扩展和完善,锻炼自学和创新能力。