Qt 网络编程

文章目录

  • 网络访问接口
    • HTTP
    • FTP
  • 获取网络接口信息
  • UDP
  • TCP

  Qt中的 Qt Network模块用来编写基于TCP/IP的网络程序,其中提供了较低层次的类,比如 QTcpSocket,QTcpServer和QUdpSocket等,来表示低层次的网络概念;还有高层次的类,比如 QNetworkRequest、QNetworkReply和QNetworkAccessManager,使用通用的协议来执行网络操作;也提供了QNetworkConfiguration、NetworkConfigurationManager和QNetworkSession等类来实现负载管理。如果要使用Qt Network模块中的类,则需要在项目文件中添加 QT += network一行代码。

网络访问接口

  网络访问接口是一组执行常见网络操作的类的集合。该接口在特定的操作和协议(例如通过HTTP获取和发送数据)上提供了一个抽象层,开发者只需要使用其提供的类,函数和信号即可完成操作,而不需要知道底层是如何实现的。

  网络请求由QNetworkRequest类来表示,它也作为与请求有关的信息(例如,任何头信息和使用加密)的容器。在创建请求对象时指定的URL决定了请求使用的协议,目前支持HTTp。FTP和本地文件URL的上传和下载。

  QNetworkAccessManager类用来协调网络操作,可以调度创建好的请求,并发射信号来报告进度。该类还可以协调cookies的使用,身份验证请求和代理的使用等。每一个应用程序或者库文件都可以创建一个或者多个QNetworkAccessManager实例来处理网络通信。

  网络请求的应答使用QNetworkReply类表示,它会在请求调度完成时由QNetworkAccessManager创建。QNetworkReply提供的信号可以用来单独监视每一个应答,也可以使用QNetworkManager的信号来实现,这样就可以丢弃对应答对象的引用。因为QNetworkReply是QIODevice的子类,应答可以使用同步或者异步的方式来处理,比如作为阻塞或者非阻塞操作。

HTTP

  HTTP是一个客户端和服务器端之间进行请求和应答的标准。通常由HTTP客户端发起一个请求,建立一个到服务器指定端口(默认是80端口)的TCP连接;HTTP服务器在指定的端口监听客户端发送过来的请求,一旦收到请求,服务器就会客户端发回一个应答。

  新建Qt Widgets应用,名称为myHTTP,类名为MainWindow,基类保持QMainWindow不变。完成后先在myHTTP.pro文件中添加代码QT += network吗,然后保存该文件。

  双击mainwindow.ui文件进入设计模式,往界面上拖入一个Text Brower,然后进入mainwindow.h文件,先添加类的前置声明:

class QNetworkReply;
class QNetworkAccessManager;

然后声明一个私有对象:

private:
	QNetworkAccessManage * manager;

再添加一个私有槽的声明:

private slots:
	void replyFinished(QNetworkReply);

  然后到mainwindow.cpp文件中,先包含头文件,,然后在构造函数中添加如下代码:

manager = new QNetworkAccessManager(this);	// QNetworkAccessManager实例对象用来发送网络请求和接收应答
// 每当网络应答结束后都会发射这个信号
connect(manager, &QNetworkAccessManager::finished, this, &MainWindow::replyFinished);
manager->get(QNetworkRequest(QUrl("http://www.baidu.com")));	// 使用get()函数来发送一个网络请求,QNetworkRequest类表示网络请求

  网络请求的get()函数返回一个QNetworkReply对象。除了get(),网络请求管理器还提供了发送POST请求的post()函数,发送PUT请求的put()函数以及发送DELETE请求的deleteResource()函数。

下面添加replyFinished槽的定义:

void MainWindow::replyFinished(QNetworkReply *reply){
    QString all = reply->readAll();
    ui->textBrowser->setText(all);
    reply->deleteLater();   // 删除reply对象
}

Qt 网络编程_第1张图片

  因为QNetworkReply类继承自QIODevice类,所以其实可以像操作一般I/O设备那样操作该类。上面使用readAll()函数来读取所有的应答数据,完成数据的读取后需要使用deleteLater()来删除reply对象。

  下面打开设计模式,删除原来界面上的TextBrowser,在向界面上拖入Label,LineEdit,ProgressBar和PushButton等部件,最终布局出的界面如下:

Qt 网络编程_第2张图片

到mainwindow.h文件中先添加头文件和类的前置声明:

#include 
class QFile;

  然后将前面的replyFinished(QNetworkReply * )槽声明删除掉,并添加如下私有槽声明:

private slots:
    void httpFinished();
    void httpReadyRead();
    void updateDataReadProgress(qint64, qint64);

总之最后mainwindow.h中的内容改为:

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include 
#include 

namespace Ui {
class MainWindow;
}

class QNetworkReply;
class QNetworkAccessManager;
class QFile;
class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();
    void startRequest(QUrl url);

private:
    Ui::MainWindow *ui;
    QNetworkAccessManager *manager;
    QNetworkReply *reply;
    QUrl url;
    QFile *file;

private slots:
    void httpFinished();
    void httpReadyRead();
    void updateDataReadProgress(qint64, qint64);
    void on_downloadButton_clicked();
};

#endif // MAINWINDOW_H

再到mainwindow.cpp文件中,将前面在构造函数中添加的内容删除,然后添加如下代码:

manager = new QNetworkAccessManager(this);
ui->progressBar->hide();
ui->lineEdit->setPlaceholderText("http://www.***.com");	// 设置lineEdit部件中的提示内容

之后再添加网络请求函数startRequest()的定义:

void MainWindow::startRequest(QUrl url){
    reply = manager->get(QNetworkRequest(url));
    connect(reply, &QNetworkReply::readyRead, this, &MainWindow::httpReadyRead);
    connect(reply, &QNetworkReply::downloadProgress, this, &MainWindow::updateDataReadProgress);
    connect(reply, &QNetworkReply::finished, this, &MainWindow::httpFinished);
}

下面对以上头文件中声明过的槽做出定义:

void MainWindow::httpReadyRead(){
    if(file) file->write(reply->readAll());		// 将网络响应得到的信息写入file中
}

void MainWindow::updateDataReadProgress(qint64 bytesRead, qint64 totalBytes){
	// 设置进度条的最大值和当前值
    ui->progressBar->setMaximum(totalBytes);
    ui->progressBar->setValue(bytesRead);
}

void MainWindow::httpFinished(){
    ui->progressBar->hide();    // 下载完成后,再度隐藏进度条
    if(file) {
        file->close();
        delete file;		// 删除file对象
        file = 0;
    }
    reply->deleteLater();	// 删除reply对象
    reply = 0;
}

void MainWindow::on_downloadButton_clicked()
{
    url = ui->lineEdit->text();
    QFileInfo info(url.path());
    QString fileName(info.fileName());
    if(fileName.isEmpty()) fileName = "index.html";	// 保存GET请求得到的网页内容
    file = new QFile(fileName);
    if(!file->open(QIODevice::WriteOnly)){
        delete file;
        file = 0;
        return;
    }
    startRequest(url);
    ui->progressBar->setValue(0);
    ui->progressBar->show();
}

Qt 网络编程_第3张图片

FTP

获取网络接口信息

  在进行TCP/UDP编程时,需要先将连接的主机名解析为IP地址,这个操作一般使用DNS协议执行。IP是计算机网络相互连接进行通信时使用的协议,规定了计算机在互联网上进行通信时应当遵循的规则,有IPV4和IPV6两个版本。IP地址就是给每一个连接在互联网上的主机分配的一个唯一地址,IP协议使用这个地址来进行主机之间的信息传递。

  Qt Network模块中的QHostInfo类提供了静态函数,可以进行主机名的查找,它使用了操作系统提供的查找机制来获取与主机名关联的IP地址,或者获取与IP地址关联的主机名。

  这个类提供了两个便捷的静态函数进行查找:

  • lookupHost() 函数异步进行工作,每当找到主机时都会发射信号
  • fromName() 函数会在查找时阻塞,并返回包含了查找结果的QHostInfo对象

  新建Qt Widgets应用,项目名称myIP,类名MainWindow,基类选择QMainWindow。项目创建完成后在myIP.pro文件中添加"QT += network"这一行代码。

  然后进入mainwindow.cpp,将其内容改为:

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include 
#include 
#include 
#include 

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    QString localHostName = QHostInfo::localHostName();     // 获取本地主机名称
    QHostInfo info = QHostInfo::fromName(localHostName);    // 根据主机名获取QHostInfo对象
    qDebug() << "localHostName: " << localHostName << endl << "IP Address: " << info.addresses();

    // info.addresses()返回一个QHostAddress对象的列表(从一个主机名获取的IP地址可能不止一个)
    foreach(QHostAddress address, info.addresses())
    {
        if(address.protocol() == QAbstractSocket::IPv4Protocol)
            qDebug() << address.toString();
    }
}

MainWindow::~MainWindow()
{
    delete ui;
}

这样,可以查看本地主机的IP信息。
Qt 网络编程_第4张图片

  除了如上面那样使用fromName()来获取IP地址外,还可以使用lookupHost()函数,它需要指定一个主机名,一个QObject指针和一个槽。该函数可以执行名称查找,完成后会调用指定的QObject对象的槽,查找工作是在其他线程中进行的,即它是异步执行的。

下面先到mainwindow.h文件中添加类的前置声明:

class QHostInfo;

然后声明一个私有槽:

private slots:
	void lookedUp(const QHostInfo &host);

下面到mainwindow.cpp文件中,添加对该槽的定义:

void MainWindow::lookedUp(const QHostInfo &host)
{
	if(host.error() != QHostInfo:NoError) {	// 返回错误信息
		qDebug() << "Lookup failed: " << host.errorString();
		return;
	}
	// 遍历IP列表并输出
	foreach(const QHostAddress &address, host.address())
		qDebug() << "Found address: " << address.toString();
}

做好准备工作后,我们就可以在构造函数中使用:

// 查找百度的IP地址,查找完后调用刚才定义的lookedUp()槽
QHostInfo::lookupHost("www.baidu.com", this, SLOT(lookedUp(QHostInfo)));

Qt 网络编程_第5张图片

  looupHost()函数返回一个整型ID值,可以调用abortHostLookup()函数通过这个ID值来终止查找。另外,还可以将这个函数中的第一个参数设置为一个IP地址,从而查找该IP地址对应的域名。

UDP

  UDP是一个轻量级的,不可靠的,面向数据包,无连接的协议,用于可靠性不是非常重要的情况。UDP一般分为发送端和接收端,UDP数据传输示意图如下:
Qt 网络编程_第6张图片

  QUdpSocket支持IPv4广播。广播一般用来实现网络发现协议。例如,查找网络上哪个主机拥有最多的硬盘空间,一个主机向网络中广播一个数据报,然后所有其他的主机都接收这个数据报,每一个主机接收到一个请求,然后发送应答给发送端,告知其当前的可用磁盘空间。这样一来,发送端就一直在等待,直到它接收完所有主机的答复,然后可以选择拥有最多空闲空间的服务器来存储数据。要广播一个数据报,则只需要发送它到一个特殊的地址QHostAddress::Broadcast(即IP 255.255.255.255),或者是本地网络的广播地址。

下面通过一个例子来讲解怎样进行UDP编程。

先编写发送端程序。

&ems; 新建Qt Widgets应用,项目名称udpsender,基类选择QDialog,类名设置为Sender,完成后向udpsender.pro文件中添加"QT += network"一行代码,并保存该文件。

到sender.h文件中添加前置声明:

class QUdpSocket;

再声明一个私有对象:

QUdpSocket * sender;

  下面双击sender.ui文件进入设计模式,向界面上拖入一个PushButton按钮部件,将其显示文本更改为"进行广播"。然后转到其信号槽:

void Sender::on_pushButton_clicked()
{
	QByteArray datagram = "hello world!";
	// QHostAddress::Broadcast表示广播,向所有主机发送
	sender->writeDatagram(datagram.data(), datagram.size(), QHostAddress::Broadcast, 45454);
}

上面用于发送数据报的writeDatagram()函数的原型:

// 发送size大小的数据报data到地址为address的主机的port端口,并返回成功发送的字节数;如果发送失败,则返回-1
qint64 QUdpSocket::writeDatagram(const char * data, qint64 size, const QHostAddress &address, quint16 port)

  数据报总是作为一整块写入的,它的最大大小根据平台的不同而不同。如果数据报过大,这个函数将会返回-1,而且error()函数会返回DatagramToolLargeError错误信息。

  一般不建议发送大于512字节的数据报,即便被发送成功,它们很可能是在到达最终目的地以前就在IP层分割了。这里使用了枚举值QHostAddress::Broadcast来表示广播地址,它等价于QHostAddress(“255.255.255.255”)。端口号是可以随意指定的,但是一般建议使用1024以上的端口号,因为1024以下的端口号通常用于保留端口号(例如FTP使用的21端口),端口最大为65535。

  需要注意的是,这里,UDP的接收端程序使用了45454端口,那么在接收端也要使用这个端口号。

  下面在sender.cpp文件中添加头文件,然后在构造函数中添加如下一行代码:

sender = new QUdpSocket(this);

这样,就构建好了UDP的发送端,下面再来构建UDP信息的接收端。

  新建Qt Widgets应用,项目名称udpreceiver,基类选择QDialog,类名设置为Receiver,完成后向UDPReceiver.pro文件中添加"QT += network"一行代码,并保存该文件。

然后到receiver.h文件中,添加类的前置声明:

class QUdpSocket;

再声明一个私有对象:

QUdpSocket * receiver;

然后添加一个私有槽声明:

private slots:
	void processPendingDatagram();

  下面双击receiver.ui进入设计模式,向界面上拖入一个Label,将其显示文本更改为"等待接收数据!"。

  然后进入receiver.cpp文件中,添加头文件,然后在构造函数中添加如下代码:

receiver = new QUdpSocket(this);
receiver->bind(45454, QUdpSocket::ShareAddress);	// 先使用bind()绑定发送信息的IP地址和端口号,QUdpSocket::ShareAddress是绑定模式,这个模式表示允许其他服务器绑定到相同的地址和端口上

// 每当有数据报到来时,QUdpSocket都会发射readyRead()信号
connect(receiver, &QUdpSocket::readyRead, this, &Receiver::processPendingDatagram);

下面实现processPendingDatagram()槽的定义:

void Receiver::processPendingDatagram()
{
	// 持续检查是否有数据报发送过来
	while(receiver->hasPendingDatagrams())
	{
		QByteArray datagram;
		// 让datagram的大小为等待处理的数据报的大小,这样才能接收到完整的数据
		datagram.resize(receiver->pendingDatagramSize());	// pendingDatagramSize()获取当前数据报的大小

		// 接收数据报,将其存放到datagram中(使用readDatagram()接收不大于指定大小的数据报,并将其存储到QByteArray变量中)
		receiver->readDatagram(datagram.data(), datagram.size());
		ui->label->setText(datagram);	// 将数据报的内容显示在label部件中
	}
}

Qt 网络编程_第7张图片

TCP

  TCP是一个用于数据传输的底层网络协议,多个互联网协议(包括HTTP和FTP)都是基于都是TCP协议的。TCP是一个面向数据流和可靠的传输协议。QTcpSocket类为TCP提供了一个接口,该类也继承自QAbstractSocket。

  可以使用QTcpSocket来实现POP3,SMTP和NNTP等标准的网络协议,也可以实现自定义的网络协议。与QUdpSocket传输的数据报不同,QTcpSocket传输的是连续的数据流,尤其适合连续的数据传输。TCP编程一般可分为客户端和服务器端,也就是所谓的C/S(Client/Server)模型。

TCP数据传输示意图:
Qt 网络编程_第8张图片

  在任何数据传输之前,必须建立一个TCP连接到远程的主机和端口上。一旦连接被建立,peer(对使用TCP协议连接在一起的主机的通称)的IP地址和端口就可以分别使用QTcpSocket::peerAddress()和QTcpSocket::peerPort()来获取。在任何时间,peer都可以关闭连接,这样数据传输就会立即停止。

  QTcpSocket是异步进行工作的,通过发射信号来报告状态改变和错误信息,就像前面介绍的QNetworkAccessManager一样;它依靠事件循环来检测到来的数据,并且自动刷新输出的数据。

  可以使用QTcpSocket::write()来写入数据,使用QTcpSocket::read()来读取数据。QTcpSocket代表了两个独立的数据流:一个用于读取,一个用来写入。

  因为QTcpSocket继承自QIODevice,所以可以使用QTextStream和QDataStream。当从一个QTcpSocket中读取数据前,必须先调用QTcpSocket::bytesAvailable()函数来确保已经有足够的数据可用。

  如果要处理到来的TCP连接,则可以使用QTcpServer类。调用listen()来设置服务器,然后关联newConnection()信号,每当有客户端连接时都会发送该信号。槽中调用nextPendingConnection()来接收这个连接,然后使用该函数返回的QTcpSocket对象与客户端进行通信。

  下面来讲解一个例子,其实现的功能是:服务器一直监听一个端口,一旦有客户端连接请求,便建立连接,并向客户端发送一个字符串,然后客户端接收该字符串并显示出来。

  新建Qt Widgets应用,项目名称为tcpserver,基类选择Dialog,类名为Server,项目创建完成后向tcpserver.pro文件中添加"QT += network"一行代码,并保存该文件。然后进入server.ui文件中,向界面上拖入一个Label部件,并更改其显示文本为"等待连接"。

首先进入server.h,先添加类前置声明:

class QTcpServer;

然后添加私有对象:

QTcpServer * tcpServer;

最后添加一个私有槽声明:

private slots:
	void sendMessage();

  下面转到server.cpp文件中,先添加头文件,然后在构造函数中添加如下代码:

tcpServer = new QTcpServer(this);
// QHostAddress::LocalHost表示Ipv4的本地主机地址,等价于QHostAddress("127.0.0.1"),使用本地主机可以使客户端和服务器端在同一台计算机上进行通信
// 使用listen()监听从这个端口到来的连接
if(!tcpServer->listen(QHostAddress::LocalHost, 6666)) {
	qDebug() << tcpServer->errorString();
	close();
}
// 一旦有客户端连接到服务器,则发射newConnection()信号
connect(tcpServer, &QTcpServer::newConnection, this, &Server::sendMessage);

下面添加sendMessage()槽的定义:

void Server::sendMessage()
{
	// 使用QByteArray数组来暂存要发送的数据
	QByteArray block;
	// 使用数据流将要发送的数据写入QByteArray对象中
	QDataStream out(&block, QIODevice::WriteOnly);
	// 设置数据流的版本,客户端和服务器端使用的版本要相同
	out.setVersion(QDataStream::Qt_5_5);
	// 数据流最开始的两个字节要用于填写待发送数据的大小(可能在写完数据之前我们并不知道整个数据流的大小,所以这里要先占位)
	out << (quint16)0;	// 强制类型转换(quint16类型的值占两个字节,这里用于占位)
	out << tr("hello TCP!!!");
	out.device()->seek(0);	// 写入指针跳转到数据流的开头,下面向数据流的开头写入数据块大小信息
	out << (quint16)(block.size() - sizeof(quint16));	// 数据块总大小减去数据块开头两个字节的大小,就是实际数据的大小

	// 获取已经建立的连接的套接字对象
	QTcpSocket *clientConnection = tcpServer->nextPendingConnection();
	// 当连接断开时删除该套接字
	connect(clientConnection, &QTcpSocket::disconnected, clientConnection, &QTcpSocket::deleteLater);
	clientConnection->write(block);	// 使用write()函数将block()中存储的数据发送出去
	clientConnection->disconnectFromHost();		// disconnectFromHost()会一直等待套接字将所有数据发送完毕,然后再关闭套接字,并发射disconnection()信号,这样上面的信号槽就被激活了
	// 发送数据成功后,显示提示
	ui->label->setText(tr("发送数据成功!!!"));
}

  可以看到,编写TCP服务器端程序时可以使用QTcpServer类,然后调用它的listen()函数来进行监听。要对连接过来的客户端进行操作,可以通过关联newConnection()信号在槽中进行。可以使用nextPendingConnection()来获取连接的套接字。

  新建Qt Widgets应用,项目名称tcpclient,基类选择QDialog,类名设置为Client,完成后向tcpclient.pro文件中添加"QT += network"一行代码,并保存该文件。

  然后进入client.ui文件中,往界面上拖入3个Label,两个LineEdit和一个PushButton,界面改成下面这样:
Qt 网络编程_第9张图片

  将"主机"标签后的LineEdit的objectName更改为hostLineEdit;"端口"标签后的LineEdit的objectName更改为portLineEdit;将"接收到的信息"标签的objectName更改为messageLabel;将"连接"按钮的objectName更改为connectButton。

进入client.h,先添加头文件和前置声明:

class QTcpSocket;

然后添加私有对象和变量:

QTcpSocket *tcpSocket;
QString message;
quint16 blockSize;	// 用来存放数据的大小信息

再添加几个私有槽的声明:

private slots:
	void newConnect();
	void readMessage();
	void displayError(QAbstractSocket::SocketError);

  下面再1转到client.cpp文件中,先添加头文件,然后在构造函数中添加如下代码:

tcpSocket = new QTcpSocket(this);
connect(tcpSocket, &QTcpSocket::readyRead, this, &Client::readMessage);
connect(tcpSocket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(displayError(QAbstractSocket::SocketError)));

下面添加newConnect()槽的定义:

void Client::newConnect()
{
	blockSize = 0;	// 初始化数据大小信息为0
	// 取消已有的连接
	tcpSocket->abort();
	// 从界面上两个lineEdit中取得主机名和端口号
	tcpSocket->connectToHost(ui->hostLineEdit->text(), ui->portLineEdit->text().toInt());
}

  这个槽中先初始化了存储接收数据的大小信息的变量为0,然后使用abort()函数取消了当前已经存在的连接,并重置套接字。最后调用connectToHost()函数里连接到指定主机的指定端口。

void Client::readMessage()
{
	QDataStream in(tcpSocket);	// 定义输入流
	// 设置数据流版本,这个要和服务器端的相同
	in.setVersion(QDataStream::Qt_5_5);
	// 如果是刚开始就接收数据
	/if(blockSize == 0) {
		// 判断接收的数据是否大于两字节,也就是文件的大小信息所占的空间
		if(tcpSocket->bytesAvailable() < (int)sizeof(quint16))
			return;
		in >> blockSize;
	}
	// 如果没有得到全部的数据,则返回,继续接收数据
	if(tcpSocket->bytesAvailable() < blockSize)
		return;
	in >> message;	// 将接收到的数据存放到变量中
	ui->messageLabel->setText(message);	// 在label中显示接收到的数据
}

  读取数据时,先读取了数据的大小信息,然后使用该大小信息来判断是否已经读取到了所有的数据。下面添加显示错误信息的槽的定义:

void Client::displayError(QAbstractSocket::SocketError)
{
	qDebug() << tcpSocket->errorString();
}

最后进入设计模式转到"连接"按钮的单击信号对应的槽

void Client::on_connectButton_clicked()
{
	newConnect();
}

Qt 网络编程_第10张图片

上面程序中整个TCP连接的过程是这样的:
  启动服务器端,开始监听窗口。在客户端确定主机和端口后点击"连接"按钮,调用newConnect()槽,然后客户端向服务端发送连接请求,这时服务端便调用sendMessage()槽,向发送字符串。客户端接收到字符串后,调用readMessage()槽将字符串显示在label中。

  下面再介绍一个稍微复杂点的例子,其实现了大型文件的传输,而且还可以显示传输进度。和上面例子中的服务端向客户端发送字符串不同,这次是客户端进行数据的发送,而服务器端进行数据的接收。

  新建Qt Widgets应用,项目名称tcp_client,基类选择QDialog,类名为Client,完成后向tcpclient.pro文件中添加"QT += network"一行代码,并保存该文件。

  然后进入client.ui文件中,往界面上拖入3个Label,两个LineEdit,两个PushButton和一个ProgressBar。将界面设置成下面这样:
Qt 网络编程_第11张图片

  然后设置这些部件的属性,将"主机"后的LineEdit的objectName改为hostLineEdit;"端口"后的LineEdit的objectName改为portLineEdit;ProgressBar的objectName改为clientProgressBar;其value属性设为0;"状态"Label的objectName改为clientStatusLabel;"打开"按钮的objectName改为openButton;"发送"按钮的objectName改为sendButton。

下面进入client.h文件,添加头文件和类的前置声明:

#include 
class QTcpSocket;
class QFile;

再添加几个私有对象和变量:

private:
	QTcpSocket *tcpClient;
	QFile * localFile;		// 要发送的文件
	qint64 totalBytes;		// 发送数据的总大小
	qint64 bytesWritten;	// 已经发送数据的大小
	qint64 bytesToWrite;	// 剩余数据的大小
	qint64 payloadSize;		// 每次发送数据的大小
	QString fileName;		// 保存文件路径
	QByteArray outBlock;	// 数据缓冲区,用于存放每次要发送的数据块

然后添加几个私有槽声明:

private slots:
	void openFile();
	void send();
	void startTransfer();
	void updateClientProgress(qint64);
	void displayError(QAbstractSocket::SocketError);

在转到client.cpp文件中添加头文件包含:

#include 
#include 

然后到构造函数中添加如下代码:

payloadSize = 64 * 1024;	// 64kb
totalBytes = 0;
bytesWritten = 0;
bytesToWrite = 0;
tcpClient = new QTcpSocket(this);
// 连接服务器成功,会发出connect()信号,开始传送文件
connect(tcpClient, SIGNAL(connected()), this, SLOT(startTransfer()));
connect(tcpClient, SIGNAL(bytesWritten(qint64)), this, SLOT(updateClientProgress(qint64)));
connect(tcpClient, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(displayError(QAbstractSocket::SocketError)));
ui->sendButton->setEnabled(false);

上面只是对变量进行了初始化,然后关联了几个信号槽。

下面添加打开文件槽的定义:

void Client::openFile()
{
	fileName = QFileDialog::getOpenFileName(this);
	if(!fileName.isEmpty()) {
		ui->sendButton->setEnabled(true);
		ui->clientStatusLabel->setText(tr("打开文件 %1 成功!").arg(fileName));
	}
}

这里使用QFileDialog来打开一个本地的文件,下面添加连接服务器槽的定义:

void Client::send()
{
	ui->sendButton->setEnabled(false);
	// 初始化已发送字节为0
	bytesWritten = 0;
	ui->clientStatusLabel->setText(tr("连接中..."));
	// 使用connectHost()来连接服务器
	tcpClient->connectToHost(ui->hostLineEdit->text(), ui->portLineEdit->text().toInt());
}

  在实际的文件传输之前,需要将整个传输的数据的大小,文件名大小文件名等信息放在待发送数据的开头,这里可以把它们统称为文件头结构。

  发送文件头结构的方法与前面例子中发送数据大小信息的方法是相似的,只不过这里会使用qint64,它可以表示更大的数据。

在startTransfer()槽中发送头结构,下面做出它的定义:

void Client::startTransfer()
{
	localFile = new QFile(fileName);
	if(!localFile->open(QFile::ReadOnly)) {
		qDebug() << "client: open file error!";
		return;
	}
	// 获取文件大小
	totalBytes = localFile->size();	// 这里totalBytes表示待发送数据的总大小(包括文件结构和实际文件的大小)
	QDataStream sendOut(&outBlock, QIODevice::WriteOnly);
	sendOut.setVersion(QDataStream::Qt_5_5);
	QString currentFileName = fileName.right(fileName.size() - fileName.lastIndexOf("/") - 1);
	// 保留总大小信息空间,文件名大小信息空间,然后输入文件名
	sendOut << qint64(0) << qint64(0) << currentFileName;
	// 这里的总大小是总大小信息,文件名大小信息,文件名和实际文件大小的总和
	totalBytes += outBlock.size();
	sendOut.device()->seek(0);
	// 使用seek(0)返回outBlock的开始,用实际的大小信息代替前面占位的qint64()的两个空间
	sendOut << totalBytes << qint64((outBlock.size() - sizeof(qint64) * 2));
	bytesToWrite = totalBytes - tcpClient->write(outBlock);
	ui->clientStatusLabel->setText(tr("已连接"));
	outBlock.resize(0);	// outBlock只是一个用于暂存数据的缓冲区数组,最后要将其清空
}

  totalBytes要放在数据流的最开始,占用第一个qint64(0)的空间;然后是文件名的大小,它放在totalBytes之后,占用第二个qint64(0)的空间;再往后是文件名。

  执行完startTransfer()函数后就将文件头结构发送完毕了,下面就是发送实际的文件了,这个过程是在updateClientProgress()槽中实现的。

void Client::updateClientProgress(qint64 numBytes)
{
	// 已经发送数据的大小
	bytesWritten += (int)numBytes;
	// 如果已经发送了数据
	if(bytesToWrite > 0) {
		// 每次发送payloadSize大小的数据,这里设置为64kb,如果剩余的数据不足64kb,那么就发送剩余数据的大小
		outBlock = localFile->read(qMin(bytesToWrite, payloadSize));
		// 发送完一次数据后还剩余数据的大小
		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();
	}
}

  数据的发送是分成多个数据块进行发送的,每次发送的数据块的大小为payloadSize指定的大小,这里为64kb。如果剩余的数据不足64kb,则发送剩余的数据,这就是qMin()函数的作用。每当有数据发送时就更新进度条如果数据发送完毕,则关闭本地文件和客户端套接字。

下面对错误槽displayError()做出定义:

void Client::displayError(QAbstractSocket::SocketError)
{
	qDebug() << tcpClient->errorString();
	tcpClient->close();
	ui->clientProgressBar->reset();
	ui->clientStatusLabel->setText(tr("客户端就绪"));
	ui->sendButton->setEnabled(true);
}

  这里输出了一些错误信息,然后进行了一些重置工作。下面进入设计模式,然后分别进入"打开"按钮和"发送"按钮的单击信号对应的槽,更改如下:

// 打开按钮
void Client::on_openButton_clicked()
{
	ui->clientProgressBar->reset();
	ui->clientStatusLabel->setText(tr("状态: 等待打开文件!"));
	openFile();
}

// 发送按钮
void Client::on_sendButton_clicked()
{
	send();
}

写到这里,客户端的程序就写完了。下面开始写服务器端程序。

  新建Qt Widgets应用,项目名称tcp_server,基类选择QDialog,类名Server,完成后向tcp_Server.pro文件中添加"Qt += network"一行代码,并保存该文件。

  然后进入server.ui文件中,往界面上拖入一个Label,一个PushButton和一个ProgressBar。将"服务器端"Label的objectName改为serverStatusLabel;进度条ProgressBar的objectName改为serverProgressBar,设置其value属性为0;"开始监听"按钮的objectName改为startButton。

将界面设计为下图所示:
Qt 网络编程_第12张图片

下面进入server.h文件,先添加头文件和类前置声明:

#include 
#include 
class QTcpSocket;
class QFile;

然后添加几个私有对象和变量:

QTcpServer tcpServer;
QTcpSocket * tcpServerConnection;
qint64 totalBytes;		// 存放总大小信息
qint64 bytesReceived;	// 已收到数据的大小
qint64 fileNameSize;	// 文件名的大小信息
QString fileName;		// 存放文件名
QFile * localFile;		// 本地文件
QByteArray inBlock;		// 数据缓冲区

再声明几个私有槽:

private slots:
	void start();
	void acceptConnection();
	void updateServerProgress();
	void displayError(QAbstractSocket::SocketError socketError);

  再转到server.cpp文件中,先添加头文件,然后在构造函数中添加如下代码:

connect(&tcpServer, SIGNAL(newConnection()), this, SLOT(acceptConnection()));

上面声明了很多槽,下面来逐一实现。

实现开启监听槽start()

void Server::start()
{
	if(!tcpServer.listen(QHostAddress::LocalHost, 6666)) {
		qDebug() << tcpServer.errorString();
		close();
		return;
	}
	ui->startButton->setEnabled(false);
	totalBytes = 0;
	bytesReceived = 0;
	fileNameSize = 0;
	ui->serverStatusLabel->setText(tr("监听"));
	ui->serverProgressBar->reset();
}

  start()开启了服务器对端口的监听,然后进行了一些初始化工作。因为每次接收完文件后,都要重新开始监听,所以将初始化放在了这里。

下面添加接收连接槽的实现:

void Server::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();
}

  这里接收了到来的连接请求并获取其套接字,然后进行了信号和槽的关联。主要,最后使用close()函数关闭了服务器,不再监听端口。

下面添加更新进度条的实现,文件的实际接收工作也定义在这个槽中:

void Server::updateServerProgress()
{
    QDataStream in(tcpServerConnection);
    in.setVersion(QDataStream::Qt_4_0);

    // 如果接收到的数据小于16个字节,保存到来的文件头结构
    if (bytesReceived <= sizeof(qint64)*2) {
        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() << "server: 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));
    }
}

  这里分别接收了数据总大小,文件名大小以及文件名等文件头等文件头结构的信息,然后接收实际的文件。后面更新了进度条。如果接收完成,那么关闭套接字和本地文件,并进行提示。

下面添加显示错误槽的实现:

void Server::displayError(QAbstractSocket::SocketError socketError)
{
    qDebug() << tcpServerConnection->errorString();
    tcpServerConnection->close();
    ui->serverProgressBar->reset();
    ui->serverStatusLabel->setText(tr("服务端就绪"));
    ui->startButton->setEnabled(true);
}

下面转到"开始监听"按钮的单击信号槽,更改如下:

// 开始监听按钮
void Server::on_startButton_clicked()
{
    start();
}

Qt 网络编程_第13张图片

你可能感兴趣的:(从零开始学Qt,网络,qt,c++)