3.5.1任务描述
在软件编码阶段,开发者根据《软件系统详细设计报告》中对数据结构、算法分析和模块实现等方面的设计要求,开始具体的编写程序工作,分别实现各模块的功能,从而实现对目标系统的功能、性能、接口、界面等方面的要求。
3.5.2知识准备
一、Qt网络(一)简介
从这一节开始我们讲述Qt网络应用方面的编程知识。在开始这部分知识的学习之前,你最好已经拥有了一定的网络知识和Qt的编程基础。在下面的教程中我们不会对一个常用的网络名词去进行详细解释,对于不太了解的地方,你可以参考相关书籍。不过,你也没有必要非得先去学习网络教材,而后再学习本部分内容,因为Qt提供了简单明了的接口函数,使得我们这里并没有涉及太多专业的知识。看完教程后,你也许会发现,自己虽然不懂网络,但却可以编写网络应用程序了。
下面我们打开QtCreator,在Help页面中我们搜索QtNetworkModule 关键字,其内容如下图。
在Qt中提供了网络模块(QtNetwork Module)来用于网络程序的开发,可以看到,在这里提供了多个相关类。有用于FTP编程的QFtp类,用于HTTP编程的QNetworkAccessManager类和QNetworkReply类,用于获得本机信息的QHostInfo类,用于Tcp编程的QtcpServer类和QtcpSocket类,用于UDP编程的QUdpSocket类,用于网络加密的QSslSocket类,用于网络代理的QNetworkProxy类等等。
如果你以前就使用过Qt进行网络部分编程,或者看过其他教材上相关内容,你可能会问,这里怎么没有了QHttp类。我们现在搜索QHttp关键字,其内容如下。

可以看到这里有一个警告:
This class is obsolete. It is provided tokeep old source code working. We strongly advise against using it in new code.
大概意思是:这个类是过时的。它的提供只是为了保证旧的源代码。我们强烈建议在新代码中不要使用它。
所以在我们的教程中不会再讲解这个类,对于HTTP部分的编程,我们使用QNetworkAccessManager类和QNetworkReply类 。
最后需要说明的是:使用这个模块我们需要在工程文件中添加 Qt += network ,然后使用时添加 #include <QtNetwork> 头文件。
对于网络部分相关的例子,我们可以查看其演示程序。在Windows的开始菜单中选择Qt Creator的安装目录,然后选择Qt Demo菜单。我们可以在Networking菜单中找到网络部分的例子。如下图。


我们可以运行这些例子查看效果,也可以查看它们的帮助文件,如下图,点击Documentation即可。

当我们对Qt中的网络编程有了一定了解之后,我们就可以开始下一步的学习了。
二、Qt网络(二)HTTP编程
HTTP即超文本传输协议,它是一种文件传输协议。这一节中我们将讲解如何利用HTTP从网站上下载文件。
上一节中我们已经提到过了,现在Qt中使用QNetworkAccessManager类和QNetworkReply类来进行HTTP的编程。下面我们先看一个简单的例子,然后再进行扩展。
(一)最简单的实现。
1.我们新建Qt4 GuiQApplication 。
工程名为“http”,然后选中QtNetwork模块,最后Base class选择QWidget 。注意:如果新建工程时没有添加QtNetwork模块,那么就要手动在工程文件.pro中添加代码 Qt += network ,表明我们使用了网络模块。
2.我们在widget.ui文件中添加一个 Text Browser ,如下图。

3..在widget.h中我们添加代码。
添加头文件:#include <QtNetwork>
私有变量private中:QNetworkAccessManager*manager;
私有槽函数private slots 中:voidreplyFinished(QNetworkReply *);
4.在widget.cpp文件中添加代码。
在构造函数中添加如下代码:
manager = new QNetworkAccessManager(this); //新建QNetworkAccessManager对象
connect(manager,SIGNAL(finished(QNetworkReply*)), //关联信号和槽
this,SLOT(replyFinished(QNetworkReply*)));
manager->get(QNetworkRequest(QUrl(“http://www.yafeilinux.com”))); //发送请求
然后定义函数:
void Widget::replyFinished(QNetworkReply *reply) //当回复结束后
{
QtextCodec *codec = QtextCodec::codecForName(“utf8″);
//使用utf8编码,这样才可以显示中文
QString all = codec->toUnicode(reply->readAll());
ui->textBrowser->setText(all);
reply->deleteLater(); //最后要释放reply对象
}
5.运行效果如下。

6.代码分析。
上面实现了最简单的应用HTTP协议下载网页的程序。QNetworkAccessManager类用于发送网络请求和接受回复,具体的,它是用QNetworkRequest 类来管理请求,QNetworkReply类进行接收回复,并对数据进行处理。
在上面的代码中,我们使用了下面的代码来发送请求:
manager->get(QNetworkRequest(QUrl(“http://www.yafeilinux.com”)));
它返回一个QNetworkReply对象,这个下面再讲。我们只需知道只要发送请求成功,它就会下载数据。而当数据下载完成后,manager会发出finished()信号,我们对它进行了关联:
connect(manager,SIGNAL(finished(QNetworkReply*)),
this,SLOT(replyFinished(QNetworkReply*)));
也就是说,当下载数据结束时,就会执行replyFinished()函数。在这个函数中我们对接收的数据进行处理:
QtextCodec *codec = QtextCodec::codecForName(“utf8″);
QString all =codec->toUnicode(reply->readAll());
ui->textBrowser->setText(all);
这里,为了能显示下载的网页中的中文,我们使用了QtextCodec 类对象,应用utf8编码。
使用reply->readAll()函数就可以将下载的所有数据读出。然后,我们在textBrowser中将数据显示出来。当reply对象已经完成了它的功能时,我们需要将它释放,就是最后一条代码:
reply->deleteLater();
(二)功能扩展
通过上面的例子可以看到,Qt中编写基于HTTP协议的程序是十分简单的,只有十几行代码。不过,一般我们下载文件都想要看到下载进度。下面我们就更改上面的程序,让它可以下载任意的文件,并且显示下载进度。
1.我们更改widget.ui文件如下图。

这里我们添加了一个Line Edit ,一个Label ,一个Progress Bar 和一个Push Button ,它们的熟悉保持默认即可。我们在Push Button上点击鼠标右键,选择Go to slot ,然后选择clicked() ,进入其单击事件槽函数,现在我们先不写代码。
在写代码之前,我们先介绍一下整个程序执行的流程:
开始我们先让进度条隐藏。当我们在Line Edit中输入下载地址,点击下载按钮后,我们应用输入的下载地址,获得文件名,在磁盘上新建一个文件,用于保存下载的数据,然后进行链接,并显示进度条。在下载过程中,我们将每次获得的数据都写入文件中,并更新进度条,在接收完文件后,我们重新隐藏进度条,并做一些清理工作。
根据这个思路,我们开始代码的编写。
2.我们在widget.h文件中添加代码,完成后其部分内容如下。
class Widget : public QWidget {
Q_OBJECT
public:
Widget(QWidget *parent = 0);
~Widget();
void startRequest(QUrl url); //请求链接
protected:
void changeEvent(QEvent *e);
private:
Ui::Widget *ui;
QNetworkAccessManager *manager;
QNetworkReply *reply;
QUrl url; //存储网络地址
QFile *file; //文件指针
private slots:
void on_pushButton_clicked(); //下载按钮的单击事件槽函数
void httpFinished(); //完成下载后的处理
void httpReadyRead(); //接收到数据时的处理
voidupdateDataReadProgress(qint64,qint64); //更新进度条
};

3.widget.cpp文件中的相关内容如下。
(1)构造函数中:
manager = new QNetworkAccessManager(this);
ui->progressBar->hide();

我们在构造函数中先隐藏进度条。等开始下载时再显示它。
(2)下载按钮的单击事件槽函数。
void Widget::on_pushButton_clicked() //下载按钮
{
url = ui->lineEdit->text();
//获取在界面中输入的url地址,如http://zz.onlinedown.net/down/laolafangkuaijin.rar
QFileInfo info(url.path());
QString fileName(info.fileName());
//获取文件名
if (fileName.isEmpty()) fileName = “index.html”;
//如果文件名为空,则使用“index.html”,
//例如使用“http://www.yafeilinux.com”时,文件名就为空
file = new QFile(fileName);
if(!file->open(QIODevice::WriteOnly))
{ //如果打开文件失败,则删除file,并使file指针为0,然后返回
qDebug()<< “file open error”;
delete file;
file = 0;
return;
}
startRequest(url); //进行链接请求
ui->progressBar->setValue(0); //进度条的值设为0
ui->progressBar->show(); //显示进度条
}

这里我们先从界面中获取输入的地址,然后分解出文件名。因为地址中可能没有文件名,这时我们就使用一个默认的文件名。然后我们用这个文件名新建一个文件,这个文件会保存到工程文件夹的debug文件夹下。下面我们打开文件,然后进行链接,并显示进度条。
(3)链接请求函数。
void Widget::startRequest(QUrl url) //链接请求
{
reply =manager->get(QNetworkRequest(url));
//下面关联信号和槽
connect(reply,SIGNAL(finished()),this,SLOT(httpFinished()));
//下载完成后
connect(reply,SIGNAL(readyRead()),this,SLOT(httpReadyRead()));
//有可用数据时
connect(reply,SIGNAL(downloadProgress(qint64,qint64)),
this,SLOT(updateDataReadProgress(qint64,qint64)));
//更新进度条
}

在上一个例子中我们就提到了manager->get(QNetworkRequest(url)),返回的是一个QNetworkReply对象,这里我们获得这个对象,使用它完成显示数据下载进度的功能。这里主要是关联了几个信号和槽。当有可用数据时,reply就会发出readyRead()信号,我们这时就可以将可用的数据保存下来。就是在这里,实现了数据分段下载保存,这样比下载完所有数据再保存,要节省很多内存。而利用reply的downloadProgress()信号,很容易就实现了进度条的显示。
(4)保存数据函数。
void Widget::httpReadyRead() //有可用数据
{
if (file)file->write(reply->readAll()); //如果文件存在,则写入文件
}
![]()
这里当file可用时,将下载的数据写入文件。
(5)更新进度条函数。
void Widget::updateDataReadProgress(qint64 bytesRead,qint64 totalBytes)
{
ui->progressBar->setMaximum(totalBytes); //最大值
ui->progressBar->setValue(bytesRead); //当前值
}
每当有数据到来时,都更新进度条。
(6)完成下载。
void Widget::httpFinished() //完成下载
{
ui->progressBar->hide();
file->flush();
file->close();
reply->deleteLater();
reply = 0;
delete file;
file = 0;
}

这里只是当下载完成后,进行一些处理。
4.我们运行程序,效果如下。
下载网页文件:

下载华军软件园上的劳拉方块:

下载完成后可以看到工程文件夹中debug文件夹中的下载的文件。

我们HTTP应用的内容就讲到这里,可以看到它是很容易的,也不需要你了解太多的HTTP的原理知识。关于相关的类的其他使用,你可以查看其帮助。在上面的例子中,我们只是为了讲解知识,所以程序还很不完善,对于一个真正的工程,我们还需要注意更多的细节,你可以查看Qt演示程序HTTP Client的源代码。
下一节我们将讲述FTP的内容。
三、Qt网络(三)FTP(一)
上一节我们讲述了HTTP的编程,这一节讲述与其及其相似的FTP的编程。FTP即FileTransfer Protocol,也就是文件传输协议。FTP的主要作用,就是让用户连接上一个远程计算机,查看远程计算机有哪些文件,然后把文件从远程计算机上拷贝到本地计算机,或者把本地计算机的文件送到远程计算机上。
在Qt中,我们可以使用上一节讲述的QNetworkAccessManager和QNetworkReply类来进行FTP 程序的编写,因为它们用起来很简单。但是,对于较复杂的FTP操作,Qt还提供了QFtp类,利用这个类,我们很容易写出一个FTP客户端程序。下面我们先在帮助中查看这个类。

在QFtp中,所有的操作都对应一个特定的函数,我们可以称它们为命令。如connectToHost()连接到服务器命令,login()登录命令,get()下载命令,mkdir()新建目录命令等。因为QFtp类以异步方式工作,所以所有的这些函数都不是阻塞函数。也就是说,如果一个操作不能立即执行,那么这个函数就会直接返回,直到程序控制权返回Qt事件循环后才真正执行,它们不会影响界面的显示。
所有的命令都返回一个int 型的编号,使用这个编号让我们可以跟踪这个命令,查看其执行状态。当每条命令开始执行时,都会发出commandStarted()信号,当该命令执行结束时,会发出commandFinished()信号。我们可以利用这两个信号和命令的编号来获取命令的执行状态。当然,我们不想执行每条命令都要记下它的编号,所以我们也可以使用currentCommand()来获取现在执行的命令,其返回值与命令的对应关系如下图。

下面我们先看一个简单的FTP客户端的例子,然后对它进行扩展。
在这个例子中我们从FTP服务器上下载一个文件并显示出来。
1.我们新建Qt4 GuiQApplication 。
工程名为“myFtp”,然后选中QtNetwork模块,最后Base class选择QWidget 。
2.修改widget.ui文件。
在其中添加一个Text Browser 和一个 Label,效果如下。

3.在main.cpp中进行修改。
为了在程序中可以使用中文,我们在main.cpp中添加头文件#include<QtextCodec>
并在main()函数中添加代码:QtextCodec::setCodecForTr(QtextCodec::codecForLocale());
4.在widget.h中进行修改。
先添加头文件:#include <QFtp>
再在private中定义对象:QFtp *ftp;
添加私有槽函数:
private slots:
void ftpCommandStarted(int);
void ftpCommandFinished(int,bool);
5.在widget.cpp中进行更改。
(1)在构造函数中添加代码:
ftp = new QFtp(this);
ftp->connectToHost(“ftp.qt.nokia.com”); //连接到服务器
ftp->login(); //登录
ftp->cd(“qt”); //跳转到“qt”目录下
ftp->get(“INSTALL”); //下载“INSTALL”文件
ftp->close(); //关闭连接
connect(ftp,SIGNAL(commandStarted(int)),
this,SLOT(ftpCommandStarted(int)));
//当每条命令开始执行时发出相应的信号
connect(ftp,SIGNAL(commandFinished(int,bool)),
this,SLOT(ftpCommandFinished(int,bool)));
//当每条命令执行结束时发出相应的信号
我们在构造函数里执行了几个FTP的操作,登录站点,并下载了一个文件。然后我们又关联了两个信号和槽,用来跟踪命令的执行情况。
(2)实现槽函数:
void Widget::ftpCommandStarted(int)
{
if(ftp->currentCommand() ==QFtp::ConnectToHost){
ui->label->setText(tr(“正在连接到服务器…”));
}
if (ftp->currentCommand() ==QFtp::Login){
ui->label->setText(tr(“正在登录…”));
}
if (ftp->currentCommand() ==QFtp::Get){
ui->label->setText(tr(“正在下载…”));
}
else if (ftp->currentCommand() == QFtp::Close){
ui->label->setText(tr(“正在关闭连接…”));
}
}
每当命令执行时,都会执行ftpCommandStarted()函数,它有一个参数int id,这个id就是调用命令时返回的id,如int loginID = ftp->login(); 这时,我们就可以用if(id == loginID)来判断执行的是否是login()函数。但是,我们不想为每个命令都设置一个变量来存储其返回值,所以,我们这里使用了ftp->currentCommand() ,它也能获取当前执行的命令的类型。在这个函数里我们让开始不同的命令时显示不同的状态信息。
void Widget::ftpCommandFinished(int,bool error)
{
if(ftp->currentCommand() ==QFtp::ConnectToHost){
if(error)ui->label->setText(tr(“连接服务器出现错误:%1″).arg(ftp->errorString()));
else ui->label->setText(tr(“连接到服务器成功”));
}
if (ftp->currentCommand() ==QFtp::Login){
if(error)ui->label->setText(tr(“登录出现错误:%1″).arg(ftp->errorString()));
elseui->label->setText(tr(“登录成功”));
}
if (ftp->currentCommand() ==QFtp::Get){
if(error)ui->label->setText(tr(“下载出现错误:%1″).arg(ftp->errorString()));
else {
ui->label->setText(tr(“已经完成下载”));
ui->textBrowser->setText(ftp->readAll());
}
}
else if (ftp->currentCommand() ==QFtp::Close){
ui->label->setText(tr(“已经关闭连接”));
}
}
这个函数与ftpCommandStarted()函数相似,但是,它是在一个命令执行结束时执行的。它有两个参数,第一个int id,就是调用命令时返回的编号,我们在上面已经讲过了。第二个是boolerror,它标志现在执行的命令是否出现了错误。如果出现了错误,那么error 为true ,否则为false。我们可以利用它来输出错误信息。在这个函数中,我们在完成一条命令时显示不同的状态信息,并显示可能的出错信息。在if (ftp->currentCommand() == QFtp::Get) 中,也就是已经完成下载时,我们让textBrowser显示下载的信息。
6.运行程序,效果如下。
登录状态。

下载完成后。

7.出错演示。
下面我们演示一下出错时的情况。
将构造函数中的代码ftp->login();改为ftp->login(“tom”,”123456″);
这时我们再运行程序:

可以看到,它输出了错误信息,指明了错误的指令和出错的内容。其实我们设置的这个错误,也是想告诉大家,在FTP中如果没有设置用户名和密码,那么默认的用户名应该是anonymous,这时密码可以任意填写,而使用其他用户名是会出错的。
在下一节中,我们将会对这个程序进行扩展,让它可以浏览服务器上的所有文件,并进行下载。
四、Qt网络(四)FTP(二)
前面讲述了一个最简单的FTP客户端程序的编写,这一节我们将这个程序进行扩展,使其可以浏览并能下载服务器上的所有文件。
1.更改widget.ui文件如下。
我们删除了Text Browser ,加入了几个Label ,Line Edit ,Push Button部件,一个Tree Widget及一个Progress Bar部件。然后我们对其中几个部件做如下更改。
(1)将“FTP服务器”标签后的Line Edit的objectName属性改为“ftpServerLineEdit”,其text 属性改为“ftp.qt.nokia.com”。
(2)将“用户名”标签后的Line Edit的objectName属性改为“userNameLineEdit”,其text属性改为“anonymous”,将其toolTip属性改为“默认用户名请使用:anonymous ,此时密码任意。”
(3)将“密码”标签后的Line Edit的objectName属性改为“passWordLineEdit”,其text属性改为“123456”,将其echoMode属性改为“Password”。
(4)将“连接”按钮的objectName属性改为“connectButton”。
(5)将“返回上一级目录”按钮的objectName属性改为“cdToParentButton”。
(6)将“下载”按钮的objectName属性改为“downloadButton”。
(7)将Tree Widget的objectName属性改为“fileList”,然后在Tree Widget部件上单击鼠标右键,选择Edit Items菜单,添加列属性如下。
最终的界面如下。
下面我们的程序中,就是实现在用户填写完相关信息后,按下“连接”按钮,就可以连接到FTP服务器,并在Tree Widget中显示服务器上的所有文件,我们可以按下“下载”按钮来下载选中的文件,并使用进度条显示下载进度。
2.更改widget.h文件。
(1)添加头文件#include <QtGui>
(2)在private中添加变量:
QHash<QString, bool> isDirectory;
//用来存储一个路径是否为目录的信息
QString currentPath;
//用来存储现在的路径
(3)添加槽函数:
private slots:
void on_downloadButton_clicked();
void on_cdToParentButton_clicked();
void on_connectButton_clicked();
void ftpCommandFinished(int,bool);
void ftpCommandStarted(int);
voidupdateDataTransferProgress(qint64,qint64 );
//更新进度条
void addToList(const QUrlInfo&urlInfo);
//将服务器上的文件添加到TreeWidget中
void processItem(QtreeWidgetItem*,int);
//双击一个目录时显示其内容
3.更改widget.cpp的内容。
(1)实现“连接”按钮的单击事件槽函数。
void Widget::on_connectButton_clicked() //连接按钮
{
ui->fileList->clear();
currentPath.clear();
isDirectory.clear();
ftp = new QFtp(this);
connect(ftp,SIGNAL(commandStarted(int)),this,SLOT(ftpCommandStarted(int)));
connect(ftp,SIGNAL(commandFinished(int,bool)),
this,SLOT(ftpCommandFinished(int,bool)));
connect(ftp,SIGNAL(listInfo(QUrlInfo)),this,SLOT(addToList(QUrlInfo)));
connect(ftp,SIGNAL(dataTransferProgress(qint64,qint64)),
this,SLOT(updateDataTransferProgress(qint64,qint64)));
QString ftpServer =ui->ftpServerLineEdit->text();
QString userName =ui->userNameLineEdit->text();
QString passWord =ui->passWordLineEdit->text();
ftp->connectToHost(ftpServer,21); //连接到服务器,默认端口号是21
ftp->login(userName,passWord); //登录
}
我们在“连接”按钮的单击事件槽函数中新建了ftp对象,然后关联了相关的信号和槽。这里的listInfo()信号由ftp->list()函数发射,它将在登录命令完成时调用,下面我们提到。而dataTransferProgress()信号在数据传输时自动发射。最后我们从界面上获得服务器地址,用户名和密码等信息,并以它们为参数执行连接和登录命令。
(2)更改ftpCommandFinished()函数。
我们在相应位置做更改。
首先,在登录命令完成时,我们调用list()函数:
ui->label->setText(tr(“登录成功”));
ftp->list(); //发射listInfo()信号,显示文件列表
然后,在下载命令完成时,我们使下载按钮可用:
ui->label->setText(tr(“已经完成下载”));
ui->downloadButton->setEnabled(true);
最后再添加一个if语句,处理list命令完成时的情况:
if (ftp->currentCommand() == QFtp::List){
if(isDirectory.isEmpty())
{ //如果目录为空,显示“empty”
ui->fileList->addTopLevelItem(
new QtreeWidgetItem(QStringList()<< tr(“<empty>”)));
ui->fileList->setEnabled(false);
ui->label->setText(tr(“该目录为空”));
}
}
我们在list命令完成时,判断文件列表是否为空,如果为空,就让TreeWidget不可用,并显示“empty”条目。
(3)添加文件列表函数的内容如下。
void Widget::addToList(const QUrlInfo &urlInfo) //添加文件列表
{
QtreeWidgetItem *item = new QtreeWidgetItem;
item->setText(0,urlInfo.name());
item->setText(1,QString::number(urlInfo.size()));
item->setText(2,urlInfo.owner());
item->setText(3,urlInfo.group());
item->setText(4,urlInfo.lastModified().toString(“MMM dd yyyy”));
QPixmap pixmap(urlInfo.isDir() ? “../dir.png” : “../file.png”);
item->setIcon(0, pixmap);
isDirectory[urlInfo.name()] =urlInfo.isDir();
//存储该路径是否为目录的信息
ui->fileList->addTopLevelItem(item);
if(!ui->fileList->currentItem()) {
ui->fileList->setCurrentItem(ui->fileList->topLevelItem(0));
ui->fileList->setEnabled(true);
}
}
当ftp->list()函数执行时会发射listInfo()信号,此时就会执行addToList()函数,在这里我们将文件信息显示在Tree Widget上,并在isDirectory中存储该文件的路径及其是否为目录的信息。为了使文件与目录进行区分,我们使用了不同的图标file.png 和dir.png来表示它们,这两个图标放在了工程文件夹中。
(4)将构造函数的内容更改如下。
{
ui->setupUi(this);
ui->progressBar->setValue(0);
connect(ui->fileList,SIGNAL(itemActivated(QtreeWidgetItem*,int)),
this,SLOT(processItem(QtreeWidgetItem*,int)));
//鼠标双击列表中的目录时,我们进入该目录
}
这里我们只是让进度条的值为0,然后关联了Tree Widget的一个信号itemActivated()。当鼠标双击一个条目时,发射该信号,我们在槽函数中判断该条目是否为目录,如果是则进入该目录。
(5)processItem()函数的实现如下。
void Widget::processItem(QtreeWidgetItem* item,int) //打开一个目录
{
QString name = item->text(0);
if (isDirectory.value(name)) { //如果这个文件是个目录,则打开
ui->fileList->clear();
isDirectory.clear();
currentPath +=‘/’;
currentPath +=name;
ftp->cd(name);
ftp->list();
ui->cdToParentButton->setEnabled(true);
}
}
(6)“返回上一级目录”按钮的单击事件槽函数如下。
void Widget::on_cdToParentButton_clicked() //返回上级目录按钮
{
ui->fileList->clear();
isDirectory.clear();
currentPath =currentPath.left(currentPath.lastIndexOf(‘/’));
if (currentPath.isEmpty()) {
ui->cdToParentButton->setEnabled(false);
ftp->cd(“/”);
} else {
ftp->cd(currentPath);
}
ftp->list();
}
在返回上一级目录时,我们取当前路径的最后一个“/”之前的部分,如果此时路径为空了,我们就让“返回上一级目录”按钮不可用。
(7)“下载”按钮单击事件槽函数如下。
void Widget::on_downloadButton_clicked() //下载按钮
{
QString fileName =ui->fileList->currentItem()->text(0);
QFile *file = new QFile(fileName);
if(!file->open(QIODevice::WriteOnly))
{
delete file;
return;
}
ui->downloadButton->setEnabled(false); //下载按钮不可用,等下载完成后才可用
ftp->get(ui->fileList->currentItem()->text(0), file);
}
在这里我们获取了当前项目的文件名,然后新建文件,使用get()命令下载服务器上的文件到我们新建的文件中。
(8)更新进度条函数内容如下。
void Widget::updateDataTransferProgress( //进度条
qint64readBytes,qint64 totalBytes)
{
ui->progressBar->setMaximum(totalBytes);
ui->progressBar->setValue(readBytes);
}




4.运行程序,效果如下。
开始界面如下。

登录成功时界面如下。

下载文件时界面如下。

当一个目录为空时界面如下。
4.流程说明。
整个程序的流程就和我们实现函数的顺序一样。用户在界面上输入服务器的相关信息 ,然后我们利用这些信息进行连接并登录服务器,等登录服务器成功时,我们列出服务器上所有的文件。对于一个目录,我们可以进入其中,并返回上一级目录,我们可以下载文件,并显示下载的进度。
对于ftp的操作,全部由那些命令和信号来完成,我们只需要调用相应的命令,并在其发出信号时,进行对应的处理就可以了。而对于文件的显示,则是视图部分的知识了。
5.其他说明。
最后需要说明的是,因为为了更好的讲解知识,使得程序简单化,所以我们省去了很多细节上的处理,如果需要,你可以自己添加。比如断开连接和取消下载,你都可以使用
ftp->abort()函数。你也可以参考Qt自带的Ftp Example例子。对于其他操作,比如上传等,你可以根据需要添加。
FTP的相关编程就讲到到这里。
(错误更改提示: ——-2010年08月19日
在程序中的QFile 应该使用全局变量,然后file = new QFile(fileName);
当下载完成后,我们要使用file->close();关闭文件,不然下载的小文件可能为空。
更改如下:
1. 在widget.h中的声明private对象
QFile *file;
2.在widget.cpp文件中:
(1)在void Widget::on_downloadButton_clicked()函数里将file的定义改为:
file = new QFile(fileName);
(2)然后在voidWidget::ftpCommandFinished(int,bool error) //结束命令
函数中更改:
if (ftp->currentCommand() == QFtp::Get){
if(error)ui->label->setText(tr(“下载出现错误:%1″).arg(ftp->errorString()));
else {
ui->label->setText(tr(“已经完成下载”));
ui->downloadButton->setEnabled(true);
file->close(); //添加这行代码
}
}
)
五、Qt网络(五)获取本机网络信息
前面讲完了HTTP和FTP,下面本来该讲解UDP和TCP了。不过,在讲解它们之前,我们先在这一节里讲解一个以后要经常用到的名词,那就是IP地址。
对于IP地址,其实,会上网的人都应该听说过它。如果你实在很不了解它,那么我们简单的说:IP即Internet Protocol (网络之间互联的协议),协议就是规则,地球人都用一样的规则,所以我们可以访问全球任何的网站;而IP地址就是你联网时分配给你机子的一个地址。如果把网络比喻成地图,那IP地址就像地图上的经纬度一样,它确定了你的主机在网络中的位置。其实知道我们以后要用IP地址来代表网络中的一台计算机就够了。(^_^不一定科学但是很直白的表述)
下面我们就讲解如何获取自己电脑的IP地址以及其他网络信息。这一节中,我们会涉及到网络模块(QtNetwork Module)中的QHostInfo ,QHostAddress ,QNetworkInterface和QNetworkAddressEntry等几个类。下面是详细内容。
我们新建Qt4 Gui Application 工程,工程名为myIP ,选中QtNetwork模块,Baseclass选择QWidget。
我们在widget.h文件中包含头文件:#include <QtNetwork>
1.使用QHostInfo获取主机名和IP地址。
(1)获取主机名。
我们在widget.cpp文件中的构造函数中添加代码:
QString localHostName =QHostInfo::localHostName();
qDebug() <<”localHostName: “<<localHostName;
这里我们使用了QHostInfo类的localHostName类来获取本机的计算机名称。
运行程序,在下面的输出栏里的信息如下:

可以看到,这里获取了计算机名。我们可以在桌面上“我的电脑”图标上点击鼠标右键,然后选择“属性”菜单,查看“计算机名”一项,和我们的输出结果是一样的,如下图。

(2)获取本机的IP地址。
我们继续在构造函数中添加代码:
QHostInfo info =QHostInfo::fromName(localHostName);
qDebug() <<”IP Address: “<<info.addresses();
我们应用QHostInfo类的fromName()函数,使用上面获得的主机名为参数,来获取本机的信息。然后再利用QHostInfo类的addresses()函数,获取本机的所有IP地址信息。运行程序,输出信息如下:

在我这里只有一条IP地址。但是,在其他系统上,可能出现多条IP地址,其中可能包含了IPv4和IPv6的地址,一般我们需要使用IPv4的地址,所以我们可以只输出IPv4的地址。
我们继续添加代码:
foreach(QHostAddress address,info.addresses())
{
if(address.protocol() ==QAbstractSocket::IPv4Protocol)
qDebug()<< address.toString();
}
因为IP地址由QHostAddress 类来管理,所以我们可以使用该类来获取一条IP地址,然后使用该类的protocol()函数来判断其是否为IPv4地址。如果是IPv6地址,可以使用QAbstractSocket::IPv6Protocol 来判断。最后我们将IP地址以QString类型输出。
我们以后要使用的IP地址都是用这个方法获得的,所以这个一定要掌握。运行效果如下:

(3)以主机名获取IP地址。
我们在上面讲述了用本机的计算机名获取本机的IP地址。其实QHostInfo类也可以用来获取任意主机名的IP地址,如一个网站的IP地址。在这里我们可以使用lookupHost()函数。它是基于信号和槽的,一旦查找到了IP地址,就会触发槽函数。具体用法如下。
我们在widget.h文件中添加一个私有槽函数:
private slots:
void lookedUp(const QHostInfo &host);
然后在widget.cpp中的构造函数中先将上面添加的代码全部删除,然后添加以下代码:
QHostInfo::lookupHost(“www.baidu.com”,
this,SLOT(lookedUp(QHostInfo)));
这里我们查询百度网站的IP地址,如果查找到,就会执行我们的lookedUp()函数。
在widget.cpp中添加lookedUp()函数的实现代码:
void Widget::lookedUp(const QHostInfo &host)
{
qDebug() <<host.addresses().first().toString();
}
这里我们只是简单地输出第一个IP地址。输出信息如下:

其实,我们也可以使用lookupHost()函数,通过输入IP地址反向查找主机名,只需要将上面代码中的“www.baidu.com”换成一个IP地址就可以了,如果你有兴趣可以研究一下,不过返回的结果可能不是你想象中的那样。
小结:可以看到QHostInfo类的作用:通过主机名来查找IP地址,或者通过IP地址来反向查找主机名。
2.通过QNetworkInterface类来获取本机的IP地址和网络接口信息。
QNetworkInterface类提供了程序所运行时的主机的IP地址和网络接口信息的列表。在每一个网络接口信息中都包含了0个或多个IP地址,而每一个IP地址又包含了和它相关的子网掩码和广播地址,它们三者被封装在一个QNetworkAddressEntry对象中。网络接口信息中也提供了硬件地址信息。我们将widge.cpp构造函数中以前添加的代码删除,然后添加以下代码。
QList<QNetworkInterface> list =QNetworkInterface::allInterfaces();
//获取所有网络接口的列表
foreach(QNetworkInterfaceinterface,list)
{ //遍历每一个网络接口
qDebug()<< “Device: “<<interface.name();
//设备名
qDebug()<< “HardwareAddress: “<<interface.hardwareAddress();
//硬件地址
QList<QNetworkAddressEntry> entryList = interface.addressEntries();
//获取IP地址条目列表,每个条目中包含一个IP地址,一个子网掩码和一个广播地址
foreach(QNetworkAddressEntry entry,entryList)
{//遍历每一个IP地址条目
qDebug()<<”IP Address: “<<entry.ip().toString();
//IP地址
qDebug()<<”Netmask: “<<entry.netmask().toString();
//子网掩码
qDebug()<<”Broadcast: “<<entry.broadcast().toString();
//广播地址
}
}
这里我们获取了本机的网络设备的相关信息。运行程序,输出如下:

其实,如果我们只想利用QNetworkInterface类来获取IP地址,那么就没必要像上面那样复杂,这个类提供了一个便捷的函数allAddresses()来获取IP地址,例如:
QString address =QNetworkInterface::allAddresses().first().toString();
3.总结。
在这一节中我们学习了如何来查找本机网络设备的相关信息。其实,以后最常用的还是其中获取IP地址的方法。我们以后可以利用一个函数来获取IP地址:
QString Widget::getIP() //获取ip地址
{
QList<QHostAddress> list =QNetworkInterface::allAddresses();
foreach (QHostAddress address, list)
{
if(address.protocol() ==QAbstractSocket::IPv4Protocol)
//我们使用IPv4地址
returnaddress.toString();
}
return 0;
}
这一节就讲到这里,在下面的几节中我们将利用IP地址进行UDP和TCP的编程。
六、Qt网络(六)UDP
这一节讲述UDP编程的知识。UDP(User Datagram Protocol即用户数据报协议)是一个轻量级的,不可靠的,面向数据报的无连接协议。对于UDP我们不再进行过多介绍,如果你对UDP不是很了解,而且不知道它有什么用,那么我们这里就举个简单的例子:我们现在几乎每个人都使用的腾讯QQ,其聊天时就是使用UDP协议进行消息发送的。就像QQ那样,当有很多用户,发送的大部分都是短消息,要求能及时响应,并且对安全性要求不是很高的情况下使用UDP协议。
在Qt中提供了QUdpSocket类来进行UDP数据报(datagrams)的发送和接收。这里我们还要了解一个名词Socket,也就是常说的“套接字”。 Socket简单地说,就是一个IP地址加一个port端口。因为我们要传输数据,就要知道往哪个机子上传送,而IP地址确定了一台主机,但是这台机子上可能运行着各种各样的网络程序,我们要往哪个程序中发送呢?这时就要使用一个端口来指定UDP程序。所以说,Socket指明了数据报传输的路径。
下面我们将编写两个程序,一个用来发送数据报,可以叫做客户端;另一个用来接收数据报,可以叫做服务器端,它们均应用UDP协议。这样也就构成了所谓的C/S(客户端/服务器)编程模型。我们会在编写程序的过程中讲解一些相关的网络知识。
(一)发送端(客户端)
1.我们新建Qt4 GuiApplication,工程名为“udpSender”,选中QtNetwork模块,Base class选择QWidget。
2.我们在widget.ui文件中,往界面上添加一个Push Button,更改其显示文本为“开始广播”,然后进入其单击事件槽函数。

3.我们在widget.h文件中更改。
添加头文件:#include <QtNetwork>
添加private私有对象:QUdpSocket *sender;
4.我们在widget.cpp中进行更改。
在构造函数中添加:sender = new QUdpSocket(this);
更改“开始广播”按钮的单击事件槽函数:
void Widget::on_pushButton_clicked() //发送广播
{
QByteArray datagram = “hello world!”;
sender->writeDatagram(datagram.data(),datagram.size(),
QHostAddress::Broadcast,45454);
}
这里我们定义了一个QByteArray类型的数据报datagram,其内容为“hello world!”。然后我们使用QUdpSocket类的writeDatagram()函数来发送数据报,这个函数有四个参数,分别是数据报的内容,数据报的大小,主机地址和端口号。对于数据报的大小,它根据平台的不同而不同,但是这里建议不要超过512字节。这里我们使用了广播地址QHostAddress::Broadcast,这样就可以同时给网络中所有的主机发送数据报了。对于端口号,它是可以随意指定的,但是一般1024以下的端口号通常属于保留端口号,所以我们最好使用大于1024的端口,最大为65535。我们这里使用了45454这个端口号,一定要注意,在下面要讲的服务器程序中,也要使用相同的端口号。
5.发送端就这么简单,我们运行程序,效果如下。

(二)接收端(服务器端)
1.我们新建Qt4 GuiApplication,工程名为“udpReceiver”,选中QtNetwork模块,Base class选择QWidget。此时工程文件列表中应包含两个工程,如下图。

2.我们在udpReceiver工程中的widget.ui文件中,向界面上添加一个Label部件,更改其显示文本为“等待接收数据!”,效果如下。

3.我们在udpReceiver工程中的widget.h文件中更改。
添加头文件:#include <QtNetwork>
添加private私有对象:QUdpSocket *receiver;
添加私有槽函数:
private slots:
void processPendingDatagram();
4.我们在udpReceiver工程中的widget.cpp文件中更改。
在构造函数中:
receiver = new QUdpSocket(this);
receiver->bind(45454,QUdpSocket::ShareAddress);
connect(receiver,SIGNAL(readyRead()),this,SLOT(processPendingDatagram()));
我们在构造函数中将receiver绑定到45454端口,这个端口就是上面发送端设置的端口,二者必须一样才能保证接收到数据报。我们这里使用了绑定模式QUdpSocket::ShareAddress,它表明其他服务也可以绑定到这个端口上。因为当receiver发现有数据报到达时就会发出readyRead()信号,所以我们将其和我们的数据报处理函数相关联。
数据报处理槽函数实现如下:
void Widget::processPendingDatagram() //处理等待的数据报
{
while(receiver->hasPendingDatagrams()) //拥有等待的数据报
{
QByteArraydatagram; //拥于存放接收的数据报
datagram.resize(receiver->pendingDatagramSize());
//让datagram的大小为等待处理的数据报的大小,这样才能接收到完整的数据
receiver->readDatagram(datagram.data(),datagram.size());
//接收数据报,将其存放到datagram中
ui->label->setText(datagram);
//将数据报内容显示出来
}
}
5.我们在工程列表中udpReceiver工程上点击鼠标右键,在弹出的菜单上选择run菜单来运行该工程。

6.第一次运行该程序时,系统可能会提示警告,我们选择“解除阻止”。

如果是在linux下,你可能还需要关闭防火墙。
7.我们同时再运行udpSender程序。然后点击其上的“发送广播”按钮,这时会在udpReceiver上显示数据报的内容。效果如下。
可以看到,UDP的应用是很简单的。我们只需要在发送端执行writeDatagram()函数进行数据报的发送,然后在接收端绑定端口,并关联readyRead()信号和数据报处理函数即可。
七、Qt网络(七)TCP(一)
TCP即Transmission Control Protocol,传输控制协议。与UDP不同,它是面向连接和数据流的可靠传输协议。也就是说,它能使一台计算机上的数据无差错的发往网络上的其他计算机,所以当要传输大量数据时,我们选用TCP协议。
TCP协议的程序使用的是客户端/服务器模式,在Qt中提供了QtcpSocket类来编写客户端程序,使用QtcpServer类编写服务器端程序。我们在服务器端进行端口的监听,一旦发现客户端的连接请求,就会发出newConnection()信号,我们可以关联这个信号到我们自己的槽函数,进行数据的发送。而在客户端,一旦有数据到来就会发出readyRead()信号,我们可以关联此信号,进行数据的接收。其实,在程序中最难理解的地方就是程序的发送和接收了,为了让大家更好的理解,我们在这一节只是讲述一个传输简单的字符串的例子,在下一节再进行扩展,实现任意文件的传输。
一、服务器端。
在服务器端的程序中,我们监听本地主机的一个端口,这里使用6666,然后我们关联newConnection()信号与自己写的sendMessage()槽函数。就是说一旦有客户端的连接请求,就会执行sendMessage()函数,在这个函数里我们发送一个简单的字符串。
1.我们新建Qt4 GuiApplication,工程名为“tcpServer”,选中QtNetwork模块,Base class选择QWidget。(说明:如果一些Qt Creator版本没有添加模块一项,我们就需要在工程文件tcpServer.pro中添加一行代码:Qt += network)
2.我们在widget.ui的设计区添加一个Label,更改其objectName为statusLabel,用于显示一些状态信息。如下:

3.在widget.h文件中做以下更改。
添加头文件:#include <QtNetWork>
添加private对象:QtcpServer *tcpServer;
添加私有槽函数:
private slots:
void sendMessage();
4.在widget.cpp文件中进行更改。
在其构造函数中添加代码:
tcpServer = new QtcpServer(this);
if(!tcpServer->listen(QHostAddress::LocalHost,6666))
{ //监听本地主机的6666端口,如果出错就输出错误信息,并关闭
qDebug()<< tcpServer->errorString();
close();
}
connect(tcpServer,SIGNAL(newConnection()),this,SLOT(sendMessage()));
//连接信号和相应槽函数
我们在构造函数中使用tcpServer的listen()函数进行监听,然后关联了newConnection()和我们自己的sendMessage()函数。
下面我们实现sendMessage()函数。
void Widget::sendMessage()
{
QByteArray block; //用于暂存我们要发送的数据
QDataStream out(&block,QIODevice::WriteOnly);
//使用数据流写入数据
out.setVersion(QDataStream::Qt_4_6);
//设置数据流的版本,客户端和服务器端使用的版本要相同
out<<(quint16) 0;
out<<tr(“helloTcp!!!”);
out.device()->seek(0);
out<<(quint16) (block.size() –sizeof(quint16));
QtcpSocket *clientConnection =tcpServer->nextPendingConnection();
//我们获取已经建立的连接的子套接字
connect(clientConnection,SIGNAL(disconnected()),clientConnection,
SLOT(deleteLater()));
clientConnection->write(block);
clientConnection->disconnectFromHost();
ui->statusLabel->setText(“send message successful!!!”);
//发送数据成功后,显示提示
}
这个是数据发送函数,我们主要介绍两点:
(1)为了保证在客户端能接收到完整的文件,我们都在数据流的最开始写入完整文件的大小信息,这样客户端就可以根据大小信息来判断是否接受到了完整的文件。而在服务器端,我们在发送数据时就要首先发送实际文件的大小信息,但是,文件的大小一开始是无法预知的,所以我们先使用了out<<(quint16) 0;在block的开始添加了一个quint16大小的空间,也就是两字节的空间,它用于后面放置文件的大小信息。然后out<<tr(“hello Tcp!!!”);输入实际的文件,这里是字符串。当文件输入完成后我们在使用out.device()->seek(0);返回到block的开始,加入实际的文件大小信息,也就是后面的代码,它是实际文件的大小:out<<(quint16) (block.size() – sizeof(quint16));
(2)在服务器端我们可以使用tcpServer的nextPendingConnection()函数来获取已经建立的连接的Tcp套接字,使用它来完成数据的发送和其它操作。比如这里,我们关联了disconnected()信号和deleteLater()槽函数,然后我们发送数据
clientConnection->write(block);
然后是clientConnection->disconnectFromHost();它表示当发送完成时就会断开连接,这时就会发出disconnected()信号,而最后调用deleteLater()函数保证在关闭连接后删除该套接字clientConnection。
5.这样服务器的程序就完成了,我们先运行一下程序。

二、客户端。
我们在客户端程序中向服务器发送连接请求,当连接成功时接收服务器发送的数据。
1. .我们新建Qt4 GuiApplication,工程名为“tcpClient”,选中QtNetwork模块,Base class选择QWidget。
2,我们在widget.ui中添加几个标签Label和两个Line Edit以及一个按钮Push Button。

其中“主机”后的Line Edit的objectName为hostLineEdit,“端口号”后的为portLineEdit。
“收到的信息”标签的objectName为messageLabel 。
3.在widget.h文件中做更改。
添加头文件:#include <QtNetwork>
添加private变量:
QtcpSocket *tcpSocket;
QString message; //存放从服务器接收到的字符串
quint16 blockSize; //存放文件的大小信息
添加私有槽函数:
private slots:
void newConnect(); //连接服务器
void readMessage(); //接收数据
void displayError(QAbstractSocket::SocketError); //显示错误
4.在widget.cpp文件中做更改。
(1)在构造函数中添加代码:
tcpSocket = new QtcpSocket(this);
connect(tcpSocket,SIGNAL(readyRead()),this,SLOT(readMessage()));
connect(tcpSocket,SIGNAL(error(QAbstractSocket::SocketError)),
this,SLOT(displayError(QAbstractSocket::SocketError)));
这里关联了tcpSocket的两个信号,当有数据到来时发出readyRead()信号,我们执行读取数据的readMessage()函数。当出现错误时发出error()信号,我们执行displayError()槽函数。
(2)实现newConnect()函数。
void Widget::newConnect()
{
blockSize = 0; //初始化其为0
tcpSocket->abort(); //取消已有的连接
tcpSocket->connectToHost(ui->hostLineEdit->text(),
ui->portLineEdit->text().toInt());
//连接到主机,这里从界面获取主机地址和端口号
}
这个函数实现了连接到服务器,下面会在“连接”按钮的单击事件槽函数中调用这个函数。
(3)实现readMessage()函数。
void Widget::readMessage()
{
QDataStream in(tcpSocket);
in.setVersion(QDataStream::Qt_4_6);
//设置数据流版本,这里要和服务器端相同
if(blockSize==0) //如果是刚开始接收数据
{
//判断接收的数据是否有两字节,也就是文件的大小信息
//如果有则保存到blockSize变量中,没有则返回,继续接收数据
if(tcpSocket->bytesAvailable() < (int)sizeof(quint16)) return;
in >>blockSize;
}
if(tcpSocket->bytesAvailable() <blockSize) return;
//如果没有得到全部的数据,则返回,继续接收数据
in >> message;
//将接收到的数据存放到变量中
ui->messageLabel->setText(message);
//显示接收到的数据
}
这个函数实现了数据的接收,它与服务器端的发送函数相对应。首先我们要获取文件的大小信息,然后根据文件的大小来判断是否接收到了完整的文件。
(4)实现displayError()函数。
void Widget::displayError(QAbstractSocket::SocketError)
{
qDebug() <<tcpSocket->errorString(); //输出错误信息
}
这里简单的实现了错误信息的输出。
(5)我们在widget.ui中进入“连接”按钮的单击事件槽函数,然后更改如下。
void Widget::on_pushButton_clicked() //连接按钮
{
newConnect(); //请求连接
}
这里直接调用了newConnect()函数。
5.我们运行程序,同时运行服务器程序,然后在“主机”后填入“localhost”,在“端口号”后填入“6666”,点击“连接”按钮,效果如下。
可以看到我们正确地接收到了数据。因为服务器端和客户端是在同一台机子上运行的,所以我这里填写了“主机”为“localhost”,如果你在不同的机子上运行,需要在“主机”后填写其正确的IP地址。
到这里我们最简单的TCP应用程序就完成了,在下一节我们将会对它进行扩展,实现任意文件的传输。
八、Qt网络(八)TCP(二)
在上一节里我们使用TCP服务器发送一个字符串,然后在TCP客户端进行接收。在这一节我们重新写一个客户端程序和一个服务器程序,这次我们让客户端进行文件的发送,服务器进行文件的接收。有了上一节的基础,这一节的内容就很好理解了,注意一下几个信号和槽的关联即可。当然,我们这次要更深入了解一下数据的发送和接收的处理方法。
一、客户端
这次我们先讲解客户端,在客户端里我们与服务器进行连接,一旦连接成功,就会发出connected()信号,这时我们就进行文件的发送。
在上一节我们已经看到,发送数据时我们先发送了数据的大小信息。这一次,我们要先发送文件的总大小,然后文件名长度,然后是文件名,这三部分我们合称为文件头结构,最后再发送文件数据。所以在发送函数里我们就要进行相应的处理,当然,在服务器的接收函数里我们也要进行相应的处理。对于文件大小,这次我们使用了qint64,它是64位的,可以表示一个很大的文件了。
1.同前一节,我们新建工程,将工程命名为“tcpSender”。注意添加network模块。
2.我们在widget.ui文件中将界面设计如下。

这里“主机”后的Line Edit的objectName为hostLineEdit;“端口”后的Line Edit的objectName为portLineEdit;下面的Progress Bar的objectName为clientProgressBar,其value属性设为0;“状态”Label的objetName为clientStatusLabel;“打开”按钮的objectName为openButton;“发送”按钮的objectName为sendButton;
3.在widget.h 文件中进行更改。
(1)添加头文件#include <QtNetwork>
(2)添加private变量:
QtcpSocket *tcpClient;
QFile *localFile; //要发送的文件
qint64 totalBytes; //数据总大小
qint64 bytesWritten; //已经发送数据大小
qint64 bytesToWrite; //剩余数据大小
qint64 loadSize; //每次发送数据的大小
QString fileName; //保存文件路径
QByteArray outBlock; //数据缓冲区,即存放每次要发送的数据
(3)添加私有槽函数:
private slots:
void send(); //连接服务器
void startTransfer(); //发送文件大小等信息
void updateClientProgress(qint64); //发送数据,更新进度条
voiddisplayError(QAbstractSocket::SocketError); //显示错误
void openFile(); //打开文件
4.在widget.cpp文件中进行更改。
添加头文件:#include <QFileDialog>
(1)在构造函数中添加代码:
loadSize = 4*1024;
totalBytes = 0;
bytesWritten = 0;
bytesToWrite = 0;
tcpClient = new QtcpSocket(this);
connect(tcpClient,SIGNAL(connected()),this,SLOT(startTransfer()));
//当连接服务器成功时,发出connected()信号,我们开始传送文件
connect(tcpClient,SIGNAL(bytesWritten(qint64)),this,
SLOT(updateClientProgress(qint64)));
//当有数据发送成功时,我们更新进度条
connect(tcpClient,SIGNAL(error(QAbstractSocket::SocketError)),this,
SLOT(displayError(QAbstractSocket::SocketError)));
ui->sendButton->setEnabled(false);
//开始使”发送“按钮不可用
我们主要是进行了变量的初始化和几个信号和槽函数的关联。
(2)实现打开文件函数。
void Widget::openFile() //打开文件
{
fileName =QFileDialog::getOpenFileName(this);
if(!fileName.isEmpty())
{
ui->sendButton->setEnabled(true);
ui->clientStatusLabel->setText(tr(“打开文件 %1 成功!”)
.arg(fileName));
}
}
该函数将在下面的“打开”按钮单击事件槽函数中调用。
(3)实现连接函数。
void Widget::send() //连接到服务器,执行发送
{
ui->sendButton->setEnabled(false);
bytesWritten = 0;
//初始化已发送字节为0
ui->clientStatusLabel->setText(tr(“连接中…”));
tcpClient->connectToHost(ui->hostLineEdit->text(),
ui->portLineEdit->text().toInt());//连接
}
该函数将在“发送”按钮的单击事件槽函数中调用。
(4)实现文件头结构的发送。
void Widget::startTransfer() //实现文件大小等信息的发送
{localFile = new QFile(fileName);
if(!localFile->open(QFile::ReadOnly))
{qDebug() << "open file error!";
return;
}
totalBytes = localFile->size();
//文件总大小
QDataStream sendOut(&outBlock,QIODevice::WriteOnly);
sendOut.setVersion(QDataStream::Qt_4_6);
QString currentFileName = fileName.right(fileName.size() - fileName.lastIndexOf('/')-1);sendOut << qint64(0) << qint64(0) << currentFileName;
//依次写入总大小信息空间,文件名大小信息空间,文件名
totalBytes += outBlock.size();
//这里的总大小是文件名大小等信息和实际文件大小的总和
sendOut.device()->seek(0);
sendOut<<totalBytes<<qint64((outBlock.size() - sizeof(qint64)*2));
//返回outBolock的开始,用实际的大小信息代替两个qint64(0)空间
bytesToWrite = totalBytes - tcpClient->write(outBlock);
//发送完头数据后剩余数据的大小
ui->clientStatusLabel->setText(tr("已连接"));outBlock.resize(0);
}
(5)下面是更新进度条,也就是发送文件数据。
void Widget::updateClientProgress(qint64 numBytes) //更新进度条,实现文件的传送
{
bytesWritten += (int)numBytes;
//已经发送数据的大小
if(bytesToWrite > 0) //如果已经发送了数据
{
outBlock =localFile->read(qMin(bytesToWrite,loadSize));
//每次发送loadSize大小的数据,这里设置为4KB,如果剩余的数据不足4KB,
//就发送剩余数据的大小
bytesToWrite -=(int)tcpClient->write(outBlock);
//发送完一次数据后还剩余数据的大小
outBlock.resize(0);
//清空发送缓冲区
}
else
{
localFile->close(); //如果没有发送任何数据,则关闭文件
}
ui->clientProgressBar->setMaximum(totalBytes);
ui->clientProgressBar->setValue(bytesWritten);
//更新进度条
if(bytesWritten == totalBytes) //发送完毕
{
ui->clientStatusLabel->setText(tr(“传送文件 %1 成功”).arg(fileName));
localFile->close();
tcpClient->close();
}
}
(6)实现错误处理函数。
void Widget::displayError(QAbstractSocket::SocketError) //显示错误
{
qDebug() <<tcpClient->errorString();
tcpClient->close();
ui->clientProgressBar->reset();
ui->clientStatusLabel->setText(tr(“客户端就绪”));
ui->sendButton->setEnabled(true);
}
(7)我们从widget.ui中分别进行“打开”按钮和“发送”按钮的单击事件槽函数,然后更改如下。
void Widget::on_openButton_clicked() //打开按钮
{
openFile();
}
void Widget::on_sendButton_clicked() //发送按钮
{
send();
}
5.我们为了使程序中的中文不显示乱码,在main.cpp文件中更改。
添加头文件:#include <QtextCodec>
在main函数中添加代码:QtextCodec::setCodecForTr(QtextCodec::codecForLocale());
6.运行程序,效果如下。

7.程序整体思路分析。
我们设计好界面,然后按下“打开”按钮,选择我们要发送的文件,这时调用了openFile()函数。然后我们点击“发送”按钮,调用send()函数,与服务器进行连接。当连接成功时就会发出connected()信号,这时就会执行startTransfer()函数,进行文件头结构的发送,当发送成功时就会发出bytesWritten(qint64)信号,这时我们执行updateClientProgress(qint64numBytes)进行文件数据的传输和进度条的更新。这里使用了一个loadSize变量,我们在构造函数中将其初始化为4*1024即4字节,它的作用是,我们将整个大的文件分成很多小的部分进行发送,每部分为4字节。而当连接出现问题时就会发出error(QAbstractSocket::SocketError)信号,这时就会执行displayError()函数。对于程序中其他细节我们就不再分析,希望大家能自己编程研究一下。
二、服务器端。
我们在服务器端进行数据的接收。服务器端程序是很简单的,我们开始进行监听,一旦发现有连接请求就发出newConnection()信号,然后我们便接受连接,开始接收数据。
1.新建工程,名字为“tcpReceiver”。
2.我们更改widget.ui文件,设计界面如下。
其中“服务器端”Label的objectName为serverStatusLabel;进度条Progress Bar的objectName为serverProgressBar,设置其value属性为0;“开始监听”按钮的objectName为startButton。
效果如下。

3.更改widget.h文件的内容。
(1)添加头文件:#include <QtNetwork>
(2)添加私有变量:
QtcpServer tcpServer;
QtcpSocket *tcpServerConnection;
qint64 totalBytes; //存放总大小信息
qint64 bytesReceived; //已收到数据的大小
qint64 fileNameSize; //文件名的大小信息
QString fileName; //存放文件名
QFile *localFile; //本地文件
QByteArray inBlock; //数据缓冲区
(3)添加私有槽函数:
private slots:
void on_startButton_clicked();
void start(); //开始监听
void acceptConnection(); //建立连接
void updateServerProgress(); //更新进度条,接收数据
void displayError(QAbstractSocket::SocketErrorsocketError);
//显示错误
4.更改widget.cpp文件。
(1)在构造函数中添加代码:
totalBytes = 0;
bytesReceived = 0;
fileNameSize = 0;
connect(&tcpServer,SIGNAL(newConnection()),this,
SLOT(acceptConnection()));
//当发现新连接时发出newConnection()信号
(2)实现start()函数。
void Widget::start() //开始监听
{
ui->startButton->setEnabled(false);
bytesReceived =0;
if(!tcpServer.listen(QHostAddress::LocalHost,6666))
{
qDebug()<< tcpServer.errorString();
close();
return;
}
ui->serverStatusLabel->setText(tr(“监听”));
}
(3)实现接受连接函数。
void Widget::acceptConnection() //接受连接
{
tcpServerConnection =tcpServer.nextPendingConnection();
connect(tcpServerConnection,SIGNAL(readyRead()),this,
SLOT(updateServerProgress()));
connect(tcpServerConnection,
SIGNAL(error(QAbstractSocket::SocketError)),this,
SLOT(displayError(QAbstractSocket::SocketError)));
ui->serverStatusLabel->setText(tr(“接受连接”));
tcpServer.close();
}
(4)实现更新进度条函数。
void Widget::updateServerProgress() //更新进度条,接收数据
{
QDataStream in(tcpServerConnection);
in.setVersion(QDataStream::Qt_4_6);
if(bytesReceived <= sizeof(qint64)*2)
{ //如果接收到的数据小于16个字节,那么是刚开始接收数据,我们保存到//来的头文件信息
if((tcpServerConnection->bytesAvailable() >= sizeof(qint64)*2)
&& (fileNameSize == 0))
{ //接收数据总大小信息和文件名大小信息
in >> totalBytes >> fileNameSize;
bytesReceived += sizeof(qint64) * 2;
}
if((tcpServerConnection->bytesAvailable() >= fileNameSize)
&& (fileNameSize != 0))
{ //接收文件名,并建立文件
in >> fileName;
ui->serverStatusLabel->setText(tr(“接收文件 %1 …”)
.arg(fileName));
bytesReceived += fileNameSize;
localFile = new QFile(fileName);
if(!localFile->open(QFile::WriteOnly))
{
qDebug() << “open file error!”;
return;
}
}
else return;
}
if(bytesReceived < totalBytes)
{ //如果接收的数据小于总数据,那么写入文件
bytesReceived +=tcpServerConnection->bytesAvailable();
inBlock =tcpServerConnection->readAll();
localFile->write(inBlock);
inBlock.resize(0);
}
ui->serverProgressBar->setMaximum(totalBytes);
ui->serverProgressBar->setValue(bytesReceived);
//更新进度条
if(bytesReceived == totalBytes)
{ //接收数据完成时
tcpServerConnection->close();
localFile->close();
ui->startButton->setEnabled(true);
ui->serverStatusLabel->setText(tr(“接收文件 %1 成功!”)
.arg(fileName));
}
}
(5)错误处理函数。
void Widget::displayError(QAbstractSocket::SocketError) //错误处理
{
qDebug() <<tcpServerConnection->errorString();
tcpServerConnection->close();
ui->serverProgressBar->reset();
ui->serverStatusLabel->setText(tr(“服务端就绪”));
ui->startButton->setEnabled(true);
}
(6)我们在widget.ui中进入“开始监听”按钮的单击事件槽函数,更改如下。
void Widget::on_startButton_clicked() //开始监听按钮
{
start();
}
5.我们为了使程序中的中文不显示乱码,在main.cpp文件中更改。
添加头文件:#include <QtextCodec>
在main函数中添加代码:QtextCodec::setCodecForTr(QtextCodec::codecForLocale());
6.运行程序,并同时运行tcpSender程序,效果如下。
我们先在服务器端按下“开始监听”按钮,然后在客户端输入主机地址和端口号,然后打开要发送的文件,点击“发送”按钮进行发送。
在这两节里我们介绍了TCP的应用,可以看到服务器端和客户度端都可以当做发送端或者接收端,而且数据的发送与接收只要使用相对应的协议即可,它是可以根据用户的需要来进行编程的,没有固定的格式。
3.5.3任务实现
一、网络聊天客户端的设计
聊天客户端实现方法:
聊天程序的客户端有四个用户界面构成:
1. 注册界面
2. 好友显示界面
3. 登录界面
4. 聊天界面
1、注册界面的设计
分别拖动widget , 并给每个widget设置合适名称例如:
密码标签:passwordLabel
密码输入: passwordLineEdit
之后设计用户的登录界面,这里牵扯到一些美工方面的知识,最主要的是加载一个资源管理文件, 在资源管理文件当中声明所使用到的资源。
this->ip = ip;
this->port = port;
tcpSocket = new QtcpSocket(this);
connect(tcpSocket,SIGNAL(readyRead()), this, SLOT(on_ready_Ready()));
connect(tcpSocket,SIGNAL(error(QAbstractSocket::SocketError)), this,SLOT(on_display_Error(QAbstractSocket::SocketError)));
在注册构造函数当中,传递的参数包括由用户输入的信息,建立一个tcpSocket,并设置侦听,一旦可以读写套接字,则跳转到读写套接字的槽。
void RegDialog::on_ready_Ready()
{
QByteArray block =tcpSocket->readAll();
QDataStreamin(&block, QIODevice::ReadOnly); //QDataStream in(tcpSocket);
quint16 dataGramSize;
QString msgType;
in >>dataGramSize >> msgType;
if ("MSG_ID_ALREADY_EXIST" == msgType )
{
QMessageBox::warning(NULL, tr("提示"), tr("该号码已被注册."));
}
else if ("MSG_CLIENT_REGISTER_SUCCESS" == msgType )
{
//UDP: to sendudp msg to refresh tableView
QString msgType ="MSG_CLIENT_REGISTER_SUCCESS";
QByteArray block;
QDataStreamout(&block, QIODevice::WriteOnly);
out.setVersion(QDataStream::Qt_4_6);
out <<(quint16)0 << msgType;
out.device()->seek(0);
out <<(quint16)(block.size() - sizeof(quint16));
QUdpSocket*udpSocket = new QUdpSocket(this);
udpSocket->writeDatagram(block.data(), block.size(),QHostAddress(ip), (quint16)port.toUInt()+1);
}
}
经过判断,当前发现传递回来的信息为当前用户存在,那么将出现一个信息提示,否则,将提示注册一个当前用户。
void RegDialog::on_display_Error(QAbstractSocket::SocketErrorsocketError)
{
if (socketError !=QAbstractSocket::RemoteHostClosedError)
{
QMessageBox::critical(NULL, tr("提示"),tr("网络有错误: %1.").arg(tcpSocket->errorString()));
}
}
如果出现了错误将发出错误信息,这里是使用的QAbstractSocket套接字的错误信息类型,打印出错误信息的原因。
下面所列出的是登录后所提出的模式匹配的判断,首先获得当前的用户的id、密码、重复密码与名字。清除用户输入信息中的空格。
void RegDialog::on_okButton_clicked()
{
QString id =ui->idLineEdit->text().trimmed();
QString password =ui->pwdLineEdit->text();
QString againPassword= ui->rePwdLineEdit->text();
QString name =ui->nameLineEdit->text().trimmed();
QRegExprx("^[1-9]{1,2}[0-9]{4,7}$");
rx.setPatternSyntax(QRegExp::RegExp);
通过模式匹配,匹配当前用户输入的是否是一个数字序列
if(!rx.exactMatch(id))
{
QMessageBox::warning(NULL, tr("提示"), tr("请输入5~10位数的QQ号."));
}
判断密码的大小,是否两次重复输入密码都正确
else if ( (password!= againPassword) || ( password.size() > 9 ) || ( password.size() == 0 ))
{
QMessageBox::warning(NULL, tr("提示"),tr("请输入1~9位数的密码,两次输入要一致."));
}
判断是否为空,也可以使用 name.isEmpty() 进行判断。
else if ( name.size()== 0 )
{
QMessageBox::warning(NULL, tr("提示"), tr("昵称不能为空."));
}
经过判断,向服务器作出链接并发送信息。
else
{
if ( 1 )
{
tcpSocket->abort();
tcpSocket->connectToHost(QHostAddress(ip),(quint16)port.toUInt());
QStringmsgType = "MSG_CLIENT_USER_REGISTER";
QByteArrayblock;
QDataStreamout(&block, QIODevice::WriteOnly);
out.setVersion(QDataStream::Qt_4_6);
out <<(quint16)0 << msgType << id << password << name;
out.device()->seek(0);
out <<(quint16)(block.size() - sizeof(quint16));
tcpSocket->write(block);
}
if ( 1 )
{
QStringmsgType = "MSG_CLIENT_REGISTER_SUCCESS";
QByteArrayblock;
QDataStreamout(&block, QIODevice::WriteOnly);
out.setVersion(QDataStream::Qt_4_6);
out <<(quint16)0 << msgType << id << password << name;
out.device()->seek(0);
out <<(quint16)(block.size() - sizeof(quint16));
tcpSocket->write(block);
}
}
发送完毕信息关闭窗口
this->close();
}
2、登录界面的设计

可以分别给予命名。在登录对话框的构造函数当中有以下代码, 出于教学考虑, 可以更改当前登录框的头标题, 在程序运行过程中,将输入框的部分部件置空,调整主窗口的大小。当前程序的logo 标签最上面的部分, 使用一个图片进行代替, 当然也可使用资源管理器进行添加。
tcpSocket = new QtcpSocket(this);
ip.clear();
port.clear();
setFlag = true;
this->resize(326,190);
this->setFixedWidth(326);
this->setMaximumHeight(330);
this->setWindowTitle("QQ2012 世界末日版");
ui->regButton->setFlat(true);
ui->findPwdButton->setFlat(true);
QPixmappixmap("images\\head.png");
ui->headLabel->setPixmap(pixmap);
建立对应的信号与槽的链接, 访问服务器以获取验证的信息。
connect(tcpSocket,SIGNAL(readyRead()), this, SLOT(on_ready_Ready()));
connect(tcpSocket,SIGNAL(disconnected()), this, SLOT(on_disconnected()));
connect(tcpSocket,SIGNAL(error(QAbstractSocket::SocketError)), this,SLOT(on_display_Error(QAbstractSocket::SocketError)));
当前登录按钮被点击,将进行一下操作,首先判断用户输入的信息是否为空,之后再判断用户输入的号码是否为一个数字。最后链接服务器,向服务器发送数据报文。
void Login::on_loginButton_clicked()
{
if ( ip.isEmpty() ||port.isEmpty() )
{
QMessageBox::warning(NULL, tr("提示"), tr("请先设置IP和端口."));
}
else
{
id =ui->idLineEdit->text().trimmed();
password =ui->pwdLineEdit->text().trimmed();
QRegExprx("^[1-9]{1,2}[0-9]{4,7}$");
rx.setPatternSyntax(QRegExp::RegExp);
if (!rx.exactMatch(id))
{
QMessageBox::warning(NULL, tr("提示"), tr("请输入5~10位数的QQ号."));
}
else
{
//connect tohost
//blockSize =0;
tcpSocket->abort();
tcpSocket->connectToHost(QHostAddress(ip),(quint16)port.toUInt());
QStringmsgType = "MSG_USER_LOGIN";
QByteArrayblock;
QDataStreamout(&block, QIODevice::WriteOnly);
out.setVersion(QDataStream::Qt_4_6);
out <<(quint16)0 << msgType << id << password;
out.device()->seek(0);
out <<(quint16)(block.size() - sizeof(quint16));
tcpSocket->write(block);
}
}
}
当设置按钮被按下的时候,将发出以下的操作,采取默认的方式,等待用户的修改,并弹出另外一个widget接受用户数据。
void Login::on_setButton_clicked()
{
if (setFlag)
{
this->resize(326, 330);
ui->setButton->setText("设 置 ^");
setFlag = false;
ui->ipLineEdit->setText("192.168.1.190");
ui->portLineEdit->setText("8990");
}
else
{
this->resize(326, 190);
ui->setButton->setText("设 置 >");
setFlag = true;
}
}
如果当前用户输入的信息经过模式匹配正确,将保存当前设置信息,否则将会清空用户输入信息栏,并提示错误的信息。
void Login::on_okButton_clicked()
{
ip =ui->ipLineEdit->text().trimmed();
port =ui->portLineEdit->text().trimmed();
QRegExprxIp("\\d+\\.\\d+\\.\\d+\\.\\d+");
QRegExprxPort(("[1-9]\\d{3,4}"));
rxIp.setPatternSyntax(QRegExp::RegExp);
rxPort.setPatternSyntax(QRegExp::RegExp);
if (!rxPort.exactMatch(port) || !rxIp.exactMatch(ip))
{
ip.clear();
port.clear();
QMessageBox::critical( NULL, tr("提示"), tr("请输入正确的IP和端口.") );
}
else
{
this->resize(326, 190);
ui->setButton->setText("设 置 >");
setFlag = true;
}
}
此时用户如果还没有进行注册, 那么用户会按下注册按钮, 这时候注册对话框将会被弹出, 让用户进行第一次注册。
void Login::on_regButton_clicked()
{
if ( ip.isEmpty() ||port.isEmpty() )
{
QMessageBox::warning(NULL, tr("提示"), tr("请先设置IP和端口."));
}
else
{
regdialog = newRegDialog(ip,port);
regdialog->setModal(false);
regdialog->setWindowTitle("注册");
//regdialog->setWindowIcon();
regdialog->show();
}
}
下面是有信息到来而进行的条件判断, 包括是否存在用户, 密码是否正确。
void Login::on_ready_Ready()
{
QByteArray block =tcpSocket->readAll();
QDataStreamin(&block, QIODevice::ReadOnly);
quint16 dataGramSize;
QString msgType;
in >>dataGramSize >> msgType;
if ("MSG_ID_NOTEXIST" == msgType )
{
QMessageBox::warning(NULL, tr("提示"), tr("该号码不存在,请先注册."));
}
else if ("MSG_LOGIN_SUCCESS" == msgType )//MSG_CLIENT_REGISTER_SUCCESS
{
panel = newPanel(id,ip,port);
panel->setWindowTitle(tr("QQ2012"));
panel->show();
this->close();
}
else if ("MSG_PWD_ERROR" == msgType )
{
QMessageBox::information(NULL, tr("提示"), tr("密码错误."));
}
else if ("MSG_LOGIN_ALREADY" == msgType )
{
QMessageBox::information(NULL, tr("提示"), tr("请不要重复登录."));
}
}
如果网络发生错误, 将提示出网络信息, 这里只是验证了一种错误信息, 远程服务器已经关闭。
void Login::on_display_Error(QAbstractSocket::SocketErrorsocketError)
{
if (socketError !=QAbstractSocket::RemoteHostClosedError)
{
QMessageBox::critical(NULL, tr("提示"),tr("网络有错误: %1.").arg(tcpSocket->errorString()));
}
}
找回密码是一个额外的模块,可以做也可以不做,使用的方法也是通过客户端的查询来匹配字段。
void Login::on_findPwdButton_clicked()
{
QMessageBox::warning(NULL, tr("提示"), tr("地球都快灭亡了,还找什么密码."));
}
3、聊天界面的设计

用户之间聊天的对话框设计的有一些简单,如果有兴趣的同学可以让他们设计漂亮的美工。在构造函数当中设置当前传递过来的对方信息。
chatForm::chatForm(QStringid, QString peerIp, QString peerPort, QUdpSocket *udpSocket):ui(newUi::chatForm)
{
this->id = id;
this->serverIp =peerIp;
this->serverPort =peerPort;
this->udpSocket =udpSocket;
ui->setupUi(this);
}
这里最重要的是发送按钮被按下,接受inputTextEdit 当中的纯文本,判断文本是否为空,如果不为空,当发送按钮被按下,发送信息。
void chatForm::on_sendButton_clicked()
{
QString sendText =ui->inputTextEdit->toPlainText();
if(!sendText.isEmpty())
{
QStringwindowTitle = this->windowTitle().replace(").","");
QString toId =QString(windowTitle.split("(").at(1));
QString msgType ="MSG_CLIENT_CHAT";
QByteArray block;
QDataStreamout(&block, QIODevice::WriteOnly);
out.setVersion(QDataStream::Qt_4_6);
out <<(quint16)0 << msgType << id << toId << sendText;
out.device()->seek(0);
out <<(quint16)(block.size() - sizeof(quint16));
udpSocket->writeDatagram(block.data(), block.size(),QHostAddress(serverIp), (quint16)serverPort.toUInt()+1);
ui->displayListWidget->addItem("Isay :\n" + sendText + "\n");
}
ui->inputTextEdit->clear();
}
4、好友显示界面的设计
用户列表框放置在一个组合框当中,分别拖动两个部件,

通过udp 套接字来做初始化,当前的信息为客户的新链接。当套接字有信息到达,调用recvMsg() 处理数据。
void Panel::init()
{
udpSocket = newQUdpSocket(this);
udpSocket->bind(6666);
QString msgType ="MSG_CLIENT_NEW_CONN";
QByteArray block;
QDataStreamout(&block, QIODevice::WriteOnly);
out.setVersion(QDataStream::Qt_4_6);
out <<(quint16)0 << msgType << id;
out.device()->seek(0);
out <<(quint16)(block.size() - sizeof(quint16));
udpSocket->writeDatagram(block.data(), block.size(),QHostAddress(ip), (quint16)port.toUInt()+1);
connect(this->udpSocket,SIGNAL(readyRead()),this,SLOT(recvMsg()));
}
在接受信息中,判断传递过来的信息类型,讲用户的信息插入当前的列表部件中。
void Panel::recvMsg()
{
QByteArray block;
QString msgType;
QStringList idList;
QStringList nameList;
quint16 size;
QHostAddress peerIp;
quint16 peerPort;
block.resize(udpSocket->pendingDatagramSize());
this->udpSocket->readDatagram(block.data(),block.size(),&peerIp,&peerPort);
QDataStreamin(&block,QIODevice::ReadOnly);
in.setVersion(QDataStream::Qt_4_6);
in>>size>>msgType;
如果当前用户的类型是在线,那么执行以下指令
if("MSG_ALL_USER_ONLINE" == msgType)
{
in>>idList>>nameList;
for(int i = 0; i< idList.count(); i++)
{
QStringitemString;
itemString =nameList.at(i) +"("+ idList.at(i)+")";
ui->userListWidget->addItem(itemString);
}
ui->countLabel->setText(QString::number(ui->userListWidget->count())+ " user online");
}
如果当前用户处于聊天的状态, 这里将不再弹出另外一个聊天窗口。
if("MSG_CLIENT_CHAT" == msgType)
{
QString peerName;
QString peerId;
QString msg;
in>>peerName>>peerId>>msg;
QStringvalueHash;
valueHash.append(peerName + "(" + peerId + ")");
chatForm *c;
if(chatFormHash.contains(valueHash))
{
c =chatFormHash.value(valueHash);
}
else
{
c = newchatForm(this->id,this->ip,this->port,this->udpSocket);
c->setWindowTitle("chatting with " + peerName +"(" + peerId + ").");
chatFormHash.insert(valueHash,c);
}
c->show();
c->displayText(peerName,peerId,msg);
}
if("MSG_SERVER_INFO" == msgType)
{
QString msg;
in>>msg;
ui->showListWidget->addItem("server : " + msg);
}
if("MSG_NEW_USER_LOGIN" == msgType)
{
QString peerName;
QString peerId;
QString user;
in>>peerId>>peerName;
if(this->id !=peerId)
{
user.append(peerName + "(" + peerId + ")");
for(int i =0; i < ui->userListWidget->count(); i++)
{
if(ui->userListWidget->item(i)->text()== user)
{
delete ui->userListWidget->takeItem(i);
}
}
ui->userListWidget->addItem(user);
ui->showListWidget->addItem(user + " login.");
ui->countLabel->setText(QString::number(ui->userListWidget->count())+ " user online");
}
}
如果用户处于退出状态。
if("MSG_CLIENT_LOGOUT" == msgType)
{
QString peerName;
QString peerId;
QString userStr;
in>>peerId>>peerName;
userStr.append(peerName + "(" + peerId + ")");
如果是用户退出则需要在列表当中将执行的用户删除掉, 记得这里使用的是takeItem() 主要通过此方法来获得在列表中已经创建的用户列表项目。
for(int i = 0; i <ui->userListWidget->count(); i++)
{
if(ui->userListWidget->item(i)->text()== userStr)
{
deleteui->userListWidget->takeItem(i);
}
}
chatForm *c =chatFormHash.value(userStr);
if(c != 0)
{
c->close();
chatFormHash.remove(userStr);
}
ui->showListWidget->addItem(userStr + " logout.");
ui->countLabel->setText(QString::number(ui->userListWidget->count())+ " user online");
}
}
如果当前列表被双击,是用户可以选择一个在线的用户进行聊天,此时弹出一个对话框,这个对话框同时也经过判断,如果当前聊天窗口存在,那么将不会弹出一个新的窗口。
voidPanel::on_userListWidget_itemDoubleClicked(QListWidgetItem* item)
{
QString nameStr =ui->userListWidget->currentItem()->text();
nameStr.replace("\n","");
QStringtempstr(nameStr);
chatForm *c = chatFormHash.value(nameStr);
if(c == 0)
{
c = newchatForm(this->id,this->ip,this->port,udpSocket);
c->setWindowTitle("chatting with " + nameStr +".");
chatFormHash.insert(nameStr,c);
}
c->show();
}
如果窗口上的quit 被点击了,将采取一下的操作,首先设置一个消息的类型, MSG_USER_LOGOUT之后发送给服务器端口,通知自己将要下线。后面通过使用一个closeEvent() 事件来重复调用这个槽。
void Panel::on_quitButton_clicked()
{
QString msgType ="MSG_USER_LOGOUT";
QByteArray block;
QDataStreamout(&block, QIODevice::WriteOnly);
out.setVersion(QDataStream::Qt_4_6);
out <<(quint16)0 << msgType << id;
out.device()->seek(0);
out <<(quint16)(block.size() - sizeof(quint16));
if(!udpSocket->writeDatagram(block.data(), block.size(), QHostAddress(ip),(quint16)port.toUInt()+1))
{
QMessageBox::warning(NULL, tr("udpSocket"),tr("writeDatagram."));
}
this->close();
}
二、网络聊天服务器端的实现

1、数据库操作封装
上面图片为服务器端设计的主界面 ,包含了发送给客户端的信息,包括设置绑定ip地址与端口。首先设计一个QtableWidget 名称用户自己定义,右边是一个textEdit 用于显示在线的用户,服务器端实现了发送给所有用户信息的方法,根据服务器运行的环境不同,设置当前的ip 地址、与端口号。如果没有局域网的话,那么可以绑定环令牌地址:127.0.0.1.
服务器端最主要的是关联数据库:
#ifndef SQLITEDB_H
#define SQLITEDB_H
#include <QObject>
#include <QSqlDatabase>
#include <QSqlQuery>
#include <QSqlQueryModel>
#include <QMessageBox>
#include <QString>
#include <QStringList>
class SqliteDB : public QObject
{public:
SqliteDB();
初始化一个数据库,指定的数据库,从编程的效率角度来说,在使用数据库的时候插入数据库,而不需要重新封装一个关于数据库操作的类。
QStringList strListUser;
QStringList strListId;
QStringList strListName;
下面是在类当中封装的成员函数,将来在数据库实例化后可以调用。
void connectDB();
void closeDB();
void getUserInfo( QString id );
void updateUserLogStat( QString id, QString stat );
int insertNewUser( QString id, QString password, QString name, QString ip, QString port);
void getUserAllOnline();
void updateUserIp(QString id, QString ip);
private:
QSqlDatabase db;
};
#endif // SQLITEDB_H
定义了一个私有的db 句柄,接下来初始化用户状态信息的主界面:
void Daemon::tableViewRefresh()
{db->connectDB();
myModel = new MySqlQueryModel;
myModel->setQuery(QObject::tr("select id, name, logstat from user")); myModel->setHeaderData(0, Qt::Horizontal, tr("QQ号")); myModel->setHeaderData(1, Qt::Horizontal, tr("昵称")); myModel->setHeaderData(2, Qt::Horizontal, tr("状态"));ui->userTableView->setModel(myModel);
ui->userTableView->setColumnWidth(0, 80);
ui->userTableView->setColumnWidth(1, 80);
ui->userTableView->setColumnWidth(2, 75);
ui->userTableView->show();
db->closeDB();
}
打开数据库并且载入数据。
void Daemon::on_startListenButton_clicked()
{ip.clear();
port.clear();
当监听发生后清除文本框接下来清楚当前数据
ip = ui->ipLineEdit->text().trimmed();
port = ui->portLineEdit->text().trimmed();
if ( "开始监听" == ui->startListenButton->text() )
{判断按钮的监听的状态,之后设置监听状态,下面是正则表达式,表示当前的输入的ip 地址与端口号必须是按照格式进行输入。
server.close();
QRegExp rxIp("\\d+\\.\\d+\\.\\d+\\.\\d+"); QRegExp rxPort(("[1-9]\\d{3,4}"));rxIp.setPatternSyntax(QRegExp::RegExp);
rxPort.setPatternSyntax(QRegExp::RegExp);
if ( !rxPort.exactMatch(port) || !rxIp.exactMatch(ip) )
{ QMessageBox::critical( NULL, tr("提示"), tr("请输入正确的IP和端口.") );}
else
如果服务器发生错误,则提示出错误信息
{if ( !server.listen( QHostAddress(ip), (quint16)port.toUInt() ) )
{ QMessageBox::critical(NULL, tr("提示"), tr("TCP监听失败: %1.").arg(server.errorString() ) );}
else
{当创建成功后,之后绑定套接字的端口与地址
udpSocket = new QUdpSocket(this);
if ( !udpSocket->bind(QHostAddress(ip), (quint16)port.toUInt()+1 ) )
{ QMessageBox::critical(NULL, tr("提示"), tr("UDP监听失败: %1.").arg(udpSocket->errorString() ) );}
connect(udpSocket, SIGNAL(readyRead()), this, SLOT(on_read_Datagrams()));
当有数据段发送过来消息处理消息
ui->startListenButton->setText("断开监听");ui->ipLineEdit->setEnabled(false);
ui->portLineEdit->setEnabled(false);
}
}
}
如果是断开侦听,怎断开服务器的监听
else if ( "断开监听" == ui->startListenButton->text() )
{server.close();
udpSocket->close();
ui->startListenButton->setText("开始监听");ui->ipLineEdit->setEnabled(true);
ui->portLineEdit->setEnabled(true);
}
判断是否是断开监听状态
}
以上是通过按钮的文本判断当前的监听状态接下来是侦听当前接收的文本,将读出的文本放入到processDatagram()中处理
void Daemon::on_read_Datagrams()
{while (udpSocket->hasPendingDatagrams())
{QByteArray block;
block.resize(udpSocket->pendingDatagramSize());
if ( -1 == udpSocket->readDatagram(block.data(), block.size(), &senderIp, &senderPort))
continue;
processDatagram(block);
}
}
服务器的守护进程大部分的判断是在当前的目录下完成的,例如新的用户连接。
void Daemon::processDatagram(QByteArray block)
{QDataStream in(&block,QIODevice::ReadOnly);
quint16 dataGramSize;
QString msgType;
in >> dataGramSize >> msgType;
if ( "MSG_CLIENT_NEW_CONN" == msgType )
{QString id;
in >> id;
if ( !id.isEmpty() )
{tableViewRefresh();
}
db->getUserAllOnline();
QStringList idList = db->strListId;
QStringList nameList = db->strListName;
QString msgType = "MSG_ALL_USER_ONLINE";
QByteArray block;
QDataStream out(&block, QIODevice::WriteOnly);
out.setVersion(QDataStream::Qt_4_6);
out << (quint16)0 << msgType << idList << nameList;
out.device()->seek(0);
out << (quint16)(block.size() - sizeof(quint16));
if ( !udpSocket->writeDatagram(block.data(), block.size(), senderIp, this->senderPort) )
{ QMessageBox::critical(NULL, tr("提示"), tr("!udpSocket->writeDatagram.") );}
msgType= "MSG_NEW_USER_LOGIN";
block.clear();
out.device()->seek(0);
db->getUserInfo(id);
out << (quint16)0 << msgType << id << db->strListUser.at(2);
out.device()->seek(0);
out << (quint16)(block.size() - sizeof(quint16));
if ( !udpSocket->writeDatagram(block.data(), block.size(), QHostAddress("255.255.255.255"), this->senderPort) ) { QMessageBox::critical(NULL, tr("提示"), tr("!udpSocket->writeDatagram.") );}
}
如果用户注册成功那么:
if ( "MSG_CLIENT_REGISTER_SUCCESS" == msgType )
{tableViewRefresh();
}
如果是用户注销那么:
if ( "MSG_USER_LOGOUT"==msgType )
{QString id;
in >> id;
if( id.isEmpty() )
{;
}
else
{db->updateUserLogStat(id,"0");
this->tableViewRefresh();
msgType= "MSG_CLIENT_LOGOUT";
block.clear();
QDataStream out(&block,QIODevice::WriteOnly);
out.device()->seek(0);
db->getUserInfo(id);
out << (quint16)0 << msgType << id << db->strListUser.at(2);
out.device()->seek(0);
out << (quint16)(block.size() - sizeof(quint16));
if ( !udpSocket->writeDatagram(block.data(), block.size(), QHostAddress("192.168.1.255"), 6666) ) { QMessageBox::critical(NULL, tr("提示"), tr("!udpSocket->writeDatagram.") );}
}
}
用户发送聊天
if( "MSG_CLIENT_CHAT" == msgType)
{//QMessageBox::warning(NULL,"MSG_CLIENT_CHAT","tt");
QString toid,fromId,fromName,toIp,buffer;
in >>fromId>>toid>>buffer;
db->getUserInfo(toid);
toIp=db->strListUser.at(4);
db->getUserInfo(fromId);
fromName=db->strListUser.at(2);
// QMessageBox::warning(NULL,"message sending",fromName);
QByteArray blockTosend;
QDataStream tosend(&blockTosend,QIODevice::WriteOnly);
QString mytype="MSG_CLIENT_CHAT";
tosend<<(quint16)0<<mytype<<fromName<<fromId<<buffer;
tosend.device()->seek(0);
tosend << (quint16)(blockTosend.size() - sizeof(quint16));
if(!udpSocket->writeDatagram(blockTosend.data(), blockTosend.size(), QHostAddress(toIp),6666))
QMessageBox::warning(NULL,"message sending","error");
}
}
当服务器退出后,需要给局域网路由器发送一个广播,内容为当前服务器的状态。
void Daemon::on_sendButton_clicked()
{QByteArray sysMsg;
QDataStream tosend(&sysMsg,QIODevice::WriteOnly);
tosend.setVersion(QDataStream::Qt_4_6);
QString mytype="MSG_SERVER_INFO";
tosend<<(quint16)0<<mytype<<ui->servTextEdit->toPlainText();
tosend.device()->seek(0);
tosend<<(quint16)(sysMsg.size()-sizeof(quint16));
if(!udpSocket->writeDatagram(sysMsg.data(),sysMsg.size(),QHostAddress("192.168.1.255"),6666))QMessageBox::warning(NULL,"message broadcast","error");
}
接下来是封装一个数据库的操作信息,包含了插入操作,显示操作,删除操作。在服务器端,维护用户状态的信息完全可以使用哈希表方法。下面提供的是数据库操作。
#ifndef SQLITEDB_H
#define SQLITEDB_H
#include <QObject>
#include <QSqlDatabase>
#include <QSqlQuery>
#include <QSqlQueryModel>
#include <QMessageBox>
#include <QString>
#include <QStringList>
class SqliteDB : public QObject
{public:
SqliteDB();
//member
QStringList strListUser;
QStringList strListId;
QStringList strListName;
//成员函数
void connectDB();
void closeDB();
void getUserInfo( QString id );
void updateUserLogStat( QString id, QString stat );
int insertNewUser( QString id, QString password, QString name, QString ip, QString port);
void getUserAllOnline();
void updateUserIp(QString id, QString ip);
private:
QSqlDatabase db;
};
#endif // SQLITEDB_H
2、针对数据库的实现
SqliteDB::SqliteDB()
{
}
连接到指定的数据库, 如果发生错误, 则提示信息.
void SqliteDB::connectDB()
{
db =QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName("chat.db");
if ( !db.open())
{
QMessageBox::critical(NULL, "Connect to db...", "Connectfailed.");
}
}
关闭数据库
void SqliteDB::closeDB()
{
db.close();
}
获得用户的信息,传递的参数是当前用户名称,使用数据的方式进行查询,当然学生可以使用返回结果集的方法进行查询。
void SqliteDB::getUserInfo(QString id)
{
this->connectDB();
QSqlQuery query;
strListUser.clear();
/*if(!(query.prepare("select id, password, name from user where id= :id")))
{QMessageBox::critical(NULL, "prepare", "Preparefailed."+id);}
query.bindValue(":id",id);
if(!query.first())
{QMessageBox::critical(NULL, "exec", "No record.");}
*/
if(!(query.exec("SELECT id, password, name, logstat,ip FROM user")))
{
QMessageBox::critical(NULL, "exec", "Exec failed.");
}
当出现错误返回错误信息下面是便利查询,如果怀疑查询的过程中发生错误使用 qDebug() 进行调试,包含的头文件在 <QtDebug>
while (query.next())
{
if (query.value(0).toString() == id )
{
strListUser.append(query.value(0).toString());
strListUser.append(query.value(1).toString());
strListUser.append(query.value(2).toString());
strListUser.append(query.value(3).toString());
strListUser.append(query.value(4).toString());
}
}
this->closeDB();
}
更新当前用户在线的信息,如果用户下线当前的状态设置为0 ,如果侦测用户在线,那么将状态设置为1
void SqliteDB::updateUserLogStat(QString id, QString stat)
{
this->connectDB();
QSqlQuery query;
strListUser.clear();
if(!(query.prepare("UPDATE user SET logstat = :stat WHERE id =:id")))
{
QMessageBox::critical(NULL, "prepare", "Preparefailed.");
}
query.bindValue(":id",id);
query.bindValue(":stat",stat);
if(!query.exec())
{
QMessageBox::critical(NULL, "exec", "Exec failed.");
}
this->closeDB();
}
设置当前用户的ip 地址, 管理字段是用户的名称
void SqliteDB::updateUserIp(QString id, QString ip)
{
this->connectDB();
QSqlQuery query;
strListUser.clear();
if(!(query.prepare("UPDATE user SET ip = :ip WHERE id =:id")))
{
QMessageBox::critical(NULL, "prepare", "Prepare failed."+id);
}
query.bindValue(":id",id);
query.bindValue(":ip",ip);
if(!query.exec())
{
QMessageBox::critical(NULL, "exec", "Exec failed.");
}
this->closeDB();
}
设置完毕信息后关闭数据库。
void SqliteDB::getUserAllOnline()
{
this->connectDB();
QSqlQuery query;
strListId.clear();
strListName.clear();
if(!(query.prepare("SELECT id, name FROM user WHERE logstat =:logstat")))
{
QMessageBox::critical(NULL, "prepare", "Preparefailed.");
}
query.bindValue(":logstat","1");
if(!query.exec())
{
QMessageBox::critical(NULL, "exec", "Exec failed.");
}
while (query.next())
{
strListId.append(query.value(0).toString());
strListName.append(query.value(1).toString());
}
this->closeDB();
}
当一个新的用户登录上来的时候就需要插入一个新的数据集合,数据集合是跟当前的用户名相关联,包含了用户的名称,用户的密码,用户的ip 以及端口。
int SqliteDB::insertNewUser( QString id, QString password,QString name, QString ip, QString port)
{
this->connectDB();
QSqlQuery query;
if(!(query.exec("SELECT id FROM user")))
{
QMessageBox::critical(NULL, "exec", "Exec failed.");
return -1;
}
//This id alreadyexist
while (query.next())
{
if (query.value(0).toString() == id )
{
return 0;
}
}
query.prepare("INSERT INTO user (id, password, name, ip, port,logstat)" "VALUES (:id, :password, :name, :ip, :port,:logstat)");
query.bindValue(":id", id);
query.bindValue(":password", password);
query.bindValue(":name",name);
query.bindValue(":ip", ip);
query.bindValue(":port", port);
使用分拆的方法,来绑定用户插入的数值,意思是当用户需要输入自定义的内容的时候,需要数据库处理分开, 一方面是指定字段, 另一方面是绑定数据。
query.bindValue(":logstat","0");
query.exec();
this->closeDB();
return 1;
}
这里需要注意的是,当更新一次数据库操作后,需要关闭数据库,保证数据存储的完整。
试验手册只是提供了一个软件设计的架构,当然学员在完成聊天程序中需要加入一些特色的话,是可以的,在此基础上丰富聊天的功能。

