欢迎光临
我们一直在努力

【Qt】《Qt 5.9 C++开发指南》笔记-Chapter14-网络编程

第14章 网络编程

Qt网络模块提供了用于编写TCP/IP客户端和服务器端程序的各种类,如用于TCP通信的QTcpSocket和QTcpServer,用于UDP通信的QUdpSocket,还有用于实现HTTP、FTP等普通网络协议的高级类如QNetworkRequest,QNetworkReply和QNetworkAccessManager。Qt网络模块还提供用于网络代理、网络承载管理的类,提供基于安全套接字层(Secure Sockets Layer,SSL)协议的安全网络通信的类。

本章主要介绍基本的TCP和UDP网络通信类的使用,基于HTTP的网络下载管理的实现。要在程序中使用Qt网络模块,需要在项目配置文件中增加一条配置语句:

Qt += network

14.1 主机信息查询

14.1.1 QHostInfo和QNetworkInterface类

查询一个主机的MAC地址或IP地址是网络应用程序中经常用到的功能,Qt提供了QHostInfo和QNetworkInterface类可以用于此类信息的查询。

QHostInfo的静态函数localHostName()可获取本机的主机名,静态函数fromName()可以通过主机名获取IP地址,静态函数lookupHost()可以通过一个主机名,以异步方式查找这个主机的IP地址。表14-1是QHostInfo类主要的功能函数(省略了函数中的const关键字)。

表14-1 QHostInfo类的主要函数

类别函数原型作用
公共函数 QList addresses() 返回与hostName()主机关联的IP地址列表
HostInfoError error() 如果主机查找失败,返回失败类型
QString errorString() 如果主机查找失败,返回错误描述字符串
QString hostName() 返回通过IP查找的主机的名称
int lookupId() 返回本次查找的ID
静态函数 void abortHostLookup(int id) 中断主机查找
QHostInfo fromName(QString &name) 返回指定的主机名的IP地址
QString localDomainName() 返回本机DNS域名
QString localHostName() 返回本机主机名
int lookupHost(QString &name, QObject receiver, char member) 以异步方式根据主机名查找主机的IP地址,并返回一个表示本次查找的ID,可用于abortHostLookup()

QNetworkInterface可以获得运行应用程序的主机的所有IP地址和网络接口列表。静态函数allInterfaces()返回主机上所有的网络接口的列表,一个网络接口可能包括多个的IP地址,每个IP地址与掩码或广播地址关联。如果无需知道子网掩码和广播的IP地址,使用静态函数allAddresses()可以获得主机上的所有IP地址列表。表14-2是QNetworkInterface类的主要功能函数。

表14-2 QNetworkInterface类的主要函数

类别函数原型作用
公共函数 QList addressEntries() 返回该网络接口(包括子网掩码和广播地址)的IP地址列表
QString hardwareAddress() 返回该接口的低级硬件地址,以太网里就是MAC地址
QString humanReadableName() 返回可以读懂的接口名称,如果名称不确定,得到的就是name()函数的返回值
bool isValid() 如果接口信息有效就返回true
QString name() 返回网络接口名称
静态函数 QList  allAddresses() 返回主机上所有IP地址的列表
QList  allInterfaces() 返回主机上所有接口的网络列表

为演示这两个类的主要功能,创建一个窗口基于QDialog的应用程序samp14_1,实例运行时界面如原书的图14-1所示。对话框界面由UI设计器设计,主要代码都是各按钮的clicked()信号的槽函数。

14.1.2 QHostInfo的使用

1.显示本机地址信息

原书的图14-1窗口上的“QHostInfo 获取本机主机名和IP地址”按钮的响应代码如下:

void Dialog::on_btnGetHostInfo_clicked()
{//QHostInfo获取主机信息
QString hostName=QHostInfo::localHostName();//本地主机名
ui->plainTextEdit->appendPlainText("本机主机名:"+hostName+"\\n");
QHostInfo hostInfo=QHostInfo::fromName(hostName); //本机IP地址

QList<QHostAddress> addList=hostInfo.addresses();//IP地址列表
if (!addList.isEmpty())
for (int i=0;i<addList.count();i++)
{
QHostAddress aHost=addList.at(i); //每一项是一个QHostAddress
bool show=ui->chkOnlyIPv4->isChecked();//只显示IPv4
if (show)
show=(QAbstractSocket::IPv4Protocol==aHost.protocol()); //IPv4协议
else
show=true;
if (show) {
ui->plainTextEdit->appendPlainText("协 议:"+ protocolName(aHost.protocol()));
ui->plainTextEdit->appendPlainText("本机IP地址:"+aHost.toString());
ui->plainTextEdit->appendPlainText("");
}
}
}

这段代码先通过静态函数QHostInfo::localHostName()获取本机主机名hostName,然后再使用主机名作为参数,用静态函数QHostInfo::fromName(hostName)获取主机的信息hostInfo。

hostInfo是QHostInfo类的实例,通过其函数addresses()获取主机的IP地址列表。

QList<QHostAddress> addList=hostInfo.addresses();

返回的addList是QHostAddress类的列表,QHostAddress类提供一个IP地址的信息,包括IPv4地址和IPv6地址。QHostAddress有以下两个主要的函数。

  • protocol() 返回QAbstractSocket::NetworkLayerProtocol类型变量,表示当前IP地址的协议类型。QAbstractSocket::NetworkLayerProtocol枚举类型的取值见表14-3。

表14-3 QAbstractSocket::NetworkLayerProtocol枚举类型取值

枚举值表示的协议类型
QAbstractSocket::IPv4Protocol IPv4
QAbstractSocket::IPv6Protocol IPv6
QAbstractSocket::AnyIPProtocol IPv4 或 IPv6
QAbstractSocket::UnknownNetworkLayerProtocol 其他类型
  • toString () 返回IP地址的字符串,表示程序中显示了IP地址列表中每个IP地址的协议类型和IP地址字符串,为根据protocol()返回的QAbstractSocket::NetworkLayerProtocol枚举值显示协议名称字符串,自定义了一个函数protocolName(),代码如下:

QString Dialog::protocolName(QAbstractSocket::NetworkLayerProtocol protocol)
{//通过协议类型返回协议名称
switch(protocol)
{
case QAbstractSocket::IPv4Protocol:
return "IPv4 Protocol";
case QAbstractSocket::IPv6Protocol:
return "IPv6 Protocol";
case QAbstractSocket::AnyIPProtocol:
return "Any IP Protocol";
default:
return "Unknown Network Layer Protocol";
}
}

单击“QHostInfo 获取本机主机名和IP地址”按钮,如果勾选了“只显示 IPv4协议地址”复选框,就只显示本机的IPv4地址,否则显示所有IP地址信息。

2.查找主机地址信息

QHostInfo的静态函数lookupHost()可以根据主机名、域名或IP地址查找主机的地址信息,lookupHost()函数原型如下:

int QHostInfo::lookupHost(const QString &name, QObject *receiver, const char *member)

输入参数name是表示主机名的字符串,可以是一个主机名、一个域名,或者是一个IP地址。

lookupHost()以异步方式查找主机地址,参数receiver和member指定一个响应槽函数的接收者和槽函数名称。执行lookupHost()后,程序可能需要花一定时间来查找主机地址,但不会阻塞程序的运行。当查找到主机地址后,通过信号通知设定的槽函数,在槽函数里读取查找的结果。函数返回一个表示查找的ID。

原书的图14-1中的“QHostInfo查找域名的IP地址”按钮的槽函数及lookupHost()函数关联槽函数代码如下:

void Dialog::on_btnLookup_clicked()
{//查找主机信息
QString hostname=ui->editHost->text(); //主机名
ui->plainTextEdit->appendPlainText("正在查找查找主机信息:"+hostname);
QHostInfo::lookupHost(hostname,this,SLOT(lookedUpHostInfo(QHostInfo)));
}
void Dialog::lookedUpHostInfo(const QHostInfo &host)
{//查找主机信息的槽函数
QList<QHostAddress> addList=host.addresses();
if (!addList.isEmpty())
for (int i=0;i<addList.count();i++)
{
QHostAddress aHost=addList.at(i);
bool show=ui->chkOnlyIPv4->isChecked();//只显示IPv4
if (show)
show=QAbstractSocket::IPv4Protocol==aHost.protocol();
else
show=true;
if (show) {
ui->plainTextEdit->appendPlainText("协 议:"+ protocolName(aHost.protocol()));
ui->plainTextEdit->appendPlainText(aHost.toString());
}
}
}

14.1.3 QNetworkInterface的使用

QNetworkInterface可以获得应用程序所在主机的所有网络接口,包括其子网掩码和广播地址等。

静态函数QNetworkInterface::allInterfaces()获取所有网络接口的列表,函数原型为:

QList<QNetworkInterface> QNetworkInterface::allInterfaces()

其返回结果是一个QNetworkInterface类的列表。

界面上“QNetworkInterface::allInterfaces()”按钮的响应代码如下:

void Dialog::on_btnALLInterface_clicked()
{//QNetworkInterface::allInterfaces()函数的使用
QList<QNetworkInterface> list=QNetworkInterface::allInterfaces();
for(int i=0;i<list.count();i++)
{
QNetworkInterface aInterface=list.at(i);
if (!aInterface.isValid())
continue;

ui->plainTextEdit->appendPlainText("设备名称:"+ aInterface.humanReadableName());
ui->plainTextEdit->appendPlainText("硬件地址:"+ aInterface.hardwareAddress());
QList<QNetworkAddressEntry> entryList=aInterface.addressEntries();
for(int j=0;j<entryList.count();j++)
{
QNetworkAddressEntry aEntry=entryList.at(j);
ui->plainTextEdit->appendPlainText(" IP 地址:"+ aEntry.ip().toString());
ui->plainTextEdit->appendPlainText(" 子网掩码:"+ aEntry.netmask().toString());
ui->plainTextEdit->appendPlainText(" 广播地址:"+ aEntry.broadcast(). toString()+"\\n");
}
ui->plainTextEdit->appendPlainText("\\n");
}
}

通过QNetworkInterface::allInterfaces()获取网络接口列表list之后,显示每个接口的humanReadable Name()和hardwareAddress()。每个接口又有一个QNetworkAddressEntry类型的地址列表,通过addressEntries()获得这个列表。

QNetworkAddressEntry包含了一个网络接口的IP地址、子网掩码和广播地址,分别用ip()、netmask()和broadcast()函数返回。

QNetworkInterface::allInterfaces()返回的网络接口的信息很多,如果无需知道子网掩码和广播地址等信息,可以使用QNetworkInterface::allAddresses()只获取IP地址。

界面上“QNetworkInterface::allAddresses()”按钮的响应代码如下:

void Dialog::on_btnDetail_clicked()
{//QNetworkInterface::allAddresses()的使用
QList<QHostAddress> addList=QNetworkInterface::allAddresses();
if (!addList.isEmpty())
for (int i=0;i<addList.count();i++)
{
QHostAddress aHost=addList.at(i);
bool show=ui->chkOnlyIPv4->isChecked();//只显示IPv4
if (show)
show=QAbstractSocket::IPv4Protocol==aHost.protocol();
else
show=true;
if (show) {
ui->plainTextEdit->appendPlainText("协 议:"+ protocolName(aHost.protocol()));
ui->plainTextEdit->appendPlainText("IP地址:"+aHost.toString());
ui->plainTextEdit->appendPlainText("");
}
}
}

QNetworkInterface ::allAddresses()的功能与QHostInfo::addresses()函数功能相似,都是返回一个QHostAddress的列表。只是QNetworkInterface会返回更多地址,包括表示本机的127.0.0.1,而QHostInfo不会返回这个IP地址。

14.2 TCP通信

14.2.1 TCP通信概述

TCP(Transmission Control Protocol)是一种被大多数Internet网络协议(如HTTP 和FTP)用于数据传输的低级网络协议,它是可靠的、面向流、面向连接的传输协议,特别适合用于连续数据传输。

TCP通信必须先建立TCP连接,通信端分为客户端和服务器端(如原书的图14-2所示)。Qt提供QTcpSocket类和QTcpServer类用于建立TCP通信应用程序。服务器端程序必须使用QTcpServer用于端口监听,建立服务器;QTcpSocket用于建立连接后使用套接字(Socket)进行通信。

QTcpServer是从QObject继承的类,它主要用于服务器端建立网络监听,创建网络Socket连接。QTcpServer类的主要接口函数见表14-4(省略了函数中的const关键字,省略了缺省参数)。

表14-4 QTcpServer类的主要接口函数

类型函数功能
公共函数 void close() 关闭服务器,停止网络监听
bool listen() 在给定IP地址和端口上开始监听,若成功就返回true
bool isListening() 返回true表示服务器处于监听状态
QTcpSocket * nextPendingConnection() 返回下一个等待接入的连接
QHostAddress serverAddress() 如果服务器处于监听状态,返回服务器地址
quint16 serverPort() 如果服务器处于监听状态,返回服务器监听端口
bool waitForNewConnection() 以阻塞方式等待新的连接
信号 void acceptError( QAbstractSocket::SocketError socketError ) 当接受一个新的连接发生错误时发射此信号,参数socketError描述了错误信息
void newConnection() 当有新的连接时发射此信号
保护函数 void incomingConnection(qintptr socketDescriptor) 当有一个新的连接可用时,QTcpServer内部调用此函数,创建一个QTcpSocket对象,添加到内部可用新连接列表,然后发射newConnection()信号。用户若从QTcpServer继承定义类,可以重定义此函数,但必须调用addPendingConnection()
void addPendingConnection(QTcpSocket *socket) 由incomingConnection()调用,将创建的QTcpSocket添加到内部新可用连接列表

服务器端程序首先需要用QTcpServer::listen()开始服务器端监听,可以指定监听的IP地址和端口,一般一个服务程序只监听某个端口的网络连接。

当有新的客户端接入时,QTcpServer内部的incomingConnection()函数会创建一个与客户端连接的QTcpSocket对象,然后发射信号newConnection()。在newConnection()信号的槽函数中,可以用nextPendingConnection()接受客户端的连接,然后使用QTcpSocket与客户端通信。

所以在客户端与服务器建立TCP连接后,具体的数据通信是通过QTcpSocket完成的。QTcpSocket类提供了TCP协议的接口,可以用QTcpSocket类实现标准的网络通信协议如POP3、SMTP和NNTP,也可以设计自定义协议。

QTcpSocket是从QIODevice间接继承的类,所以具有流读写的功能。QTcpSocket和下一节要讲到的QUdpSocket的类继承关系如原书的图14-3所示。

QTcpSocket类除了构造函数和析构函数,其他函数都是从QAbstractSocket继承或重定义的。QAbstractSocket用于TCP通信的主要接口函数见表14-5(省略了函数中的const关键字,省略了缺省参数)。

表14-5 QAbstractSocket类的主要接口函数

类型函数功能
公共函数 void connectToHost(QHostAddress &address, quint16 port,) 以异步方式连接到指定IP地址和端口的TCP服务器,连接成功后会发射connected()信号
void disconnectFromHost() 断开socket,关闭成功后发射disconnected()信号
bool waitForConnected() 等待直到建立socket连接
bool waitForDisconnected() 等待直到断开socket连接
QHostAddress localAddress() 返回本socket的地址
quint16 localPort() 返回本socket的端口
QHostAddress peerAddress() 在已连接状态下,返回对方socket的地址
QString peerName() 返回connectToHost()连接到的对方的主机名
quint16 peerPort() 在已连接状态下,返回对方socket的端口
qint64 readBufferSize() 返回内部读取缓冲区的大小,该大小决定了read()和readAll()函数能读出的数据的大小
void setReadBufferSize(qint64 size) 设置内部读取缓冲区大小
qint64 bytesAvailable() 返回需要读取的缓冲区的数据的字节数
bool canReadLine() 如果有行数据要从socket缓冲区读取,就返回true
SocketState state() 返回socket当前的状态
信号 void connected() connectToHost()成功连接到服务器后发射此信号
void disconnected() 当socket断开连接后发射此信号
void error(QAbstractSocket::SocketError socketError) 当socket发生错误时发射此信号
void hostFound() 调用connectToHost()找到主机后发射此信号
void stateChanged(QAbstractSocket::SocketState socketState) 当socket的状态变化时发射此信号,参数socketState表示了socket当前的状态
void readyRead() 当缓冲区有新数据需要读取时发射此信号,在此信号的槽函数里读取缓冲区的数据

TCP客户端使用QTcpSocket与TCP服务器建立连接并通信。

客户端的QTcpSocket实例首先通过connectToHost()尝试连接到服务器,需要指定服务器的IP地址和端口。connectToHost()是异步方式连接服务器,不会阻塞程序运行,连接后发射connected()信号。

如果需要使用阻塞方式连接服务器,则使用waitForConnected()函数阻塞程序运行,直到连接成功或失败。例如:

socket->connectToHost("192.168.1.100", 1340);
if (socket->waitForConnected(1000))
qDebug("Connected!");

与服务器端建立socket连接后,就可以向缓冲区写数据或从接收缓冲区读取数据,实现数据的通信。当缓冲区有新数据进入时,会发射readyRead()信号,一般在此信号的槽函数里读取缓冲区数据。

QTcpSocket是从QIODevice间接继承的,所以可以使用流数据读写功能。一个QTcpSocket实例既可以接收数据也可以发送数据,且接收与发射是异步工作的,有各自的缓冲区。

作为演示TCP通信的实例,创建了一个TCPClient程序和一个TCPServer程序,两个程序运行时界面如原书的图14-4和图14-5所示。

TCPServer程序具有如下的功能:

  • 根据指定IP地址(本机地址)和端口打开网络监听,有客户端连接时创建socket连接;

  • 采用基于行的数据通信协议,可以接收客户端发来的消息,也可以向客户端发送消息;

  • 在状态栏显示服务器监听状态和socket的状态。

TCPClient程序程序具有如下的功能:

  • 通过IP地址和端口号连接到服务器;

  • 采用基于行的数据通信协议,与服务器端收发消息;

  • 处理QTcpSocket的StateChange()信号,在状态栏显示socket的状态。

14.2.2 TCP服务器端程序设计

1.主窗口定义与构造函数

TCPServer是一个窗口基于QMainWindow的应用程序,界面由UI设计器设计,MainWindow类的定义如下(忽略了UI设计器自动生成的actions和按钮的槽函数):

class MainWindow : public QMainWindow
{
Q_OBJECT
private:
QLabel *LabListen;//状态栏标签
QLabel *LabSocketState;//状态栏标签
QTcpServer *tcpServer; //TCP服务器
QTcpSocket *tcpSocket;//TCP通信的Socket
QString getLocalIP();//获取本机IP地址
protected:
void closeEvent(QCloseEvent *event);
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private slots:
//自定义槽函数
void onNewConnection();//QTcpServer的newConnection()信号
void onSocketStateChange(QAbstractSocket::SocketState socketState);
void onClientConnected(); //Client Socket connected
void onClientDisconnected();//Client Socket disconnected
void onSocketReadyRead();//读取socket传入的数据
private:
Ui::MainWindow *ui;
};

MainWindow中定义了私有变量tcpServer用于建立TCP服务器,定义了tcpSocket用于与客户端进行socket连接和通信。

定义了几个槽函数,用于与QTcpServer和QTcpSocket的相关信号连接,实现相应的处理。MainWindow构造函数代码如下:

MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
LabListen=new QLabel("监听状态:");
LabListen->setMinimumWidth(150);
ui->statusBar->addWidget(LabListen);

LabSocketState=new QLabel("Socket状态:");
LabSocketState->setMinimumWidth(200);
ui->statusBar->addWidget(LabSocketState);

QString localIP=getLocalIP();//本机IP
this->setWindowTitle(this->windowTitle()+"—-本机IP:"+localIP);
ui->comboIP->addItem(localIP);
tcpServer=new QTcpServer(this);
connect(tcpServer,SIGNAL(newConnection()),this,SLOT(onNewConnection()));
}

QString MainWindow::getLocalIP()
{//获取本机IPv4地址
QString hostName=QHostInfo::localHostName();//本地主机名
QHostInfo hostInfo=QHostInfo::fromName(hostName);
QString localIP="";
QList<QHostAddress> addList=hostInfo.addresses();

if (!addList.isEmpty())
for (int i=0;i<addList.count();i++)
{
QHostAddress aHost=addList.at(i);
if (QAbstractSocket::IPv4Protocol==aHost.protocol())
{ localIP=aHost.toString(); break; }
}
return localIP;
}

MainWindow的构造函数创建状态栏上的标签用于信息显示,调用自定义函数getLocalIP()获取本机IP地址,并显示到标题栏上。创建QTcpServer实例tcpServer,并将其newConnection()信号与onNewConnection()槽函数关联。

2.网络监听与socket连接的建立

作为TCP服务器,QTcpServer类需要调用listen()在本机某个IP地址和端口上开始TCP监听,以等待TCP客户端的接入。单击主窗口上“开始监听”按钮可以开始网络监听,其代码如下:

void MainWindow::on_actStart_triggered()
{//开始监听
QString IP=ui->comboIP->currentText();//IP地址
quint16 port=ui->spinPort->value();//端口
QHostAddress addr(IP);
tcpServer->listen(addr,port);//开始监听
ui->plainTextEdit->appendPlainText("**开始监听…");
ui->plainTextEdit->appendPlainText("**服务器地址:"
+tcpServer->serverAddress().toString());
ui->plainTextEdit->appendPlainText("**服务器端口:"
+QString::number(tcpServer->serverPort()));
ui->actStart->setEnabled(false);
ui->actStop->setEnabled(true);
LabListen->setText("监听状态:正在监听");
}

程序读取窗口上设置的监听地址和监听端口,然后调用QTcpServer的listen()函数开始监听。TCP服务器在本机上监听,所以IP地址可以是表示本机的“127.0.0.1”,或是本机的实际IP,亦或是常量QHostAddress::LocalHost,即在本机上监听某个端口也可以写成:

tcpServer->listen(QHostAddress::LocalHost,port);

tcpServer开始监听后,TCPClient就可以通过IP地址和端口连接到此服务器。当有客户端接入时,tcpServer会发射newConnection()信号,此信号关联的槽函数onNewConnection()的代码如下:

void MainWindow::onNewConnection()
{
tcpSocket = tcpServer->nextPendingConnection(); //获取socket
//connect(tcpSocket, SIGNAL(connected()), this, SLOT(onClientConnected()));
onClientConnected();
connect(tcpSocket, SIGNAL(disconnected()),
this, SLOT(onClientDisconnected()));
connect(tcpSocket,SIGNAL(stateChanged(QAbstractSocket::SocketState)),
this,SLOT(onSocketStateChange(QAbstractSocket::SocketState)));
onSocketStateChange(tcpSocket->state());
connect(tcpSocket,SIGNAL(readyRead()), this,SLOT(onSocketReadyRead()));
}

程序首先通过nextPendingConnection()函数获取与接入连接进行通信的QTcpSocket对象实例tcpSocket,然后将tcpSocket的几个信号与相应的槽函数连接起来。QTcpSocket的这几个信号的作用是:

  • connected()信号,客户端socket连接建立时发射此信号;

  • disconnected()信号,客户端socket连接断开时发射此信号;

  • stateChanged(),本程序的socket状态变化时发射此信号;

  • readyRead(),本程序的socket的读取缓冲区有新数据时发射此信号。

涉及状态变化的几个信号的槽函数代码如下:

void MainWindow::onClientConnected()
{//客户端接入时
ui->plainTextEdit->appendPlainText("**client socket connected");
ui->plainTextEdit->appendPlainText("**peer address:"+
tcpSocket->peerAddress().toString());
ui->plainTextEdit->appendPlainText("**peer port:"+
QString::number(tcpSocket->peerPort()));
}
void MainWindow::onClientDisconnected()
{//客户端断开连接时
ui->plainTextEdit->appendPlainText("**client socket disconnected");
tcpSocket->deleteLater();
}

void MainWindow::onSocketStateChange(QAbstractSocket::SocketState socketState)
{//socket状态变化时
switch(socketState)
{
case QAbstractSocket::UnconnectedState:
LabSocketState->setText("scoket状态:UnconnectedState"); break;
case QAbstractSocket::HostLookupState:
LabSocketState->setText("scoket状态:HostLookupState"); break;
case QAbstractSocket::ConnectingState:
LabSocketState->setText("scoket状态:ConnectingState"); break;
case QAbstractSocket::ConnectedState:
LabSocketState->setText("scoket状态:ConnectedState"); break;
case QAbstractSocket::BoundState:
LabSocketState->setText("scoket状态:BoundState"); break;
case QAbstractSocket::ClosingState:
LabSocketState->setText("scoket状态:ClosingState"); break;
case QAbstractSocket::ListeningState:
LabSocketState->setText("scoket状态:ListeningState");
}
}

TCP服务器停止监听,只需调用QTcpServer的close()函数即可。窗口上的“停止监听”响应代码如下:

void MainWindow::on_actStop_triggered()
{//停止监听
if (tcpServer->isListening()) //tcpServer正在监听
{
tcpServer->close();//停止监听
ui->actStart->setEnabled(true);
ui->actStop->setEnabled(false);
LabListen->setText("监听状态:已停止监听");
}
}

3.与TCPClient的数据通信

TCP服务器端和客户端之间通过QTcpSocket通信时,需要规定两者之间的通信协议,即传输的数据内容如何解析。QTcpSocket间接继承于QIODevice,所以支持流读写功能。

Socket之间的数据通信协议一般有两种方式,基于行的或基于数据块的。

基于行的数据通信协议一般用于纯文本数据的通信,每一行数据以一个换行符结束。canReadLine()函数判断是否有新的一行数据需要读取,再用readLine()函数读取一行数据,例如:

while(tcpClient->canReadLine())
ui->plainTextEdit->appendPlainText("[in] "+tcpClient->readLine());

基于块的数据通信协议用于一般的二进制数据的传输,需要自定义具体的格式。

实例程序TCPServer和TCPClient只是进行字符串的信息传输,类似于一个简单的聊天程序,程序采用基于行的数据通信协议。

单击窗口上的“发送消息”,将文本框里的字符串发送给客户端,其实现代码如下:

void MainWindow::on_btnSend_clicked()
{//发送一行字符串,以换行符结束
QString msg=ui->editMsg->text();
ui->plainTextEdit->appendPlainText("[out] "+msg);
ui->editMsg->clear();
ui->editMsg->setFocus();

QByteArray str=msg.toUtf8();
str.append('\\n');//添加一个换行符
tcpSocket->write(str);
}

从上面的代码中可以看到,读取文本框中的字符串到msg后,先将其转换为QByteArray类型字节数组str,然后在str最后面添加一个换行符,用QIODevice的write()函数写入缓冲区,这样就向客户端发送一行文字。

QTcpSocket接收到数据后,会发射readyRead()信号,在onNewConnection()槽函数中已经建立了这个信号与槽函数onSocketReadyRead()的连接。

槽函数onSocketReadyRead()实现缓冲区数据的读取,其代码如下:

void MainWindow::onSocketReadyRead()
{//读取缓冲区行文本
while(tcpSocket->canReadLine())
ui->plainTextEdit->appendPlainText("[in] "+tcpSocket->readLine());
}

这样,TCPServer就可以与TCPClient之间进行双向通信了,且这个连接将一直存在,直到某一方的QTcpSocket对象调用disconnectFromHost()函数断开socket连接。

14.2.3 TCP客户端程序设计

1.主窗口定义与构造函数

客户端程序TCPClient只需要使用一个QTcpSocket对象,就可以和服务器端程序TCPServer进行通信。

TCPClient也是一个窗口基于QMainWindow的应用程序,其主窗口的定义如下:

class MainWindow : public QMainWindow
{
Q_OBJECT
private:
QTcpSocket *tcpClient; //socket
QLabel *LabSocketState; //状态栏显示标签
QString getLocalIP();//获取本机IP地址
protected:
void closeEvent(QCloseEvent *event);
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private slots:
//自定义槽函数
void onConnected();
void onDisconnected();
void onSocketStateChange(QAbstractSocket::SocketState socketState);
void onSocketReadyRead();//读取socket传入的数据
private:
Ui::MainWindow *ui;
};

这里只定义了一个用于socket连接和通信的QTcpSocket变量tcpClient,自定义了几个槽函数,用于与tcpClient的相关信号关联。

下面是MainWindow的构造函数,主要功能是创建tcpClient,并建立信号与槽函数的关联。

MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
tcpClient=new QTcpSocket(this); //创建socket变量
LabSocketState=new QLabel("Socket状态:");//状态栏标签
LabSocketState->setMinimumWidth(250);
ui->statusBar->addWidget(LabSocketState);
QString localIP=getLocalIP();//本机IP
this->setWindowTitle(this->windowTitle()+"—-本机IP:"+localIP);
ui->comboServer->addItem(localIP);

connect(tcpClient,SIGNAL(connected()),this,SLOT(onConnected()));
connect(tcpClient,SIGNAL(disconnected()),this,SLOT(onDisconnected()));
connect(tcpClient,SIGNAL(stateChanged(QAbstractSocket::SocketState)),
this,SLOT(onSocketStateChange(QAbstractSocket::SocketState)));
connect(tcpClient,SIGNAL(readyRead()),
this,SLOT(onSocketReadyRead()));
}

2.与服务器端建立socket连接

在窗口上设置服务器IP地址和端口后,调用QTcpSocket的函数connectToHost()连接到服务器,也可以使用disconnectFromHost()函数断开与服务器的连接。

下面是两个按钮的响应代码,以及两个相关槽函数的代码:

void MainWindow::on_actConnect_triggered()
{//“连接到服务器”按钮
QString addr=ui->comboServer->currentText();
quint16 port=ui->spinPort->value();
tcpClient->connectToHost(addr,port);
}
void MainWindow::on_actDisconnect_triggered()
{//“断开连接”按钮
if (tcpClient->state()==QAbstractSocket::ConnectedState)
tcpClient->disconnectFromHost();
}
void MainWindow::onConnected()
{ //connected()信号槽函数
ui->plainTextEdit->appendPlainText("**已连接到服务器");
ui->plainTextEdit->appendPlainText("**peer address:"+
tcpClient->peerAddress().toString());
ui->plainTextEdit->appendPlainText("**peer port:"+
QString::number(tcpClient->peerPort()));
ui->actConnect->setEnabled(false);
ui->actDisconnect->setEnabled(true);
}
void MainWindow::onDisconnected()
{//disConnected()信号槽函数
ui->plainTextEdit->appendPlainText("**已断开与服务器的连接");
ui->actConnect->setEnabled(true);
ui->actDisconnect->setEnabled(false);
}

槽函数onSocketStateChange()的功能和代码与TCPServer中的完全一样,这里不再赘述。

3.与TCPServer的数据收发

TCPClient与TCPServer之间采用基于行的数据通信协议。单击“发送消息”按钮将发送一行字符串。在readyRead()信号的槽函数里读取行字符串,其相关代码如下:

void MainWindow::on_btnSend_clicked()
{//发送数据
QString msg=ui->editMsg->text();
ui->plainTextEdit->appendPlainText("[out] "+msg);
ui->editMsg->clear();
ui->editMsg->setFocus();
QByteArray str=msg.toUtf8();
str.append('\\n');
tcpClient->write(str);
}
void MainWindow::onSocketReadyRead()
{//readyRead()信号槽函数
while(tcpClient->canReadLine())
ui->plainTextEdit->appendPlainText("[in] "+tcpClient->readLine());
}

实例TCPServer和TCPClient只是简单演示了TCP通信的基本原理,TCPServer只允许一个TCPClient客户端接入。而一般的TCP服务器程序允许多个客户端接入,为了使每个socket连接独立通信互不影响,一般采用多线程,即为一个socket连接创建一个线程。

实例TCPServer和TCPClient之间的数据通信采用基于行的通信协议,只能传输字符串数据。QTcpSocket间接继承于QIODevice,可以使用数据流的方式传输二进制数据流,例如传输图片、任意格式文件等,但是这涉及到服务器端和客户端之间通信协议的定义,本书不具体介绍了。

14.3 QUdpSocket实现UDP通信

14.3.1 UDP通信概述

UDP (User Datagram Protocol,用户数据报协议)是轻量的、不可靠的、面向数据报(datagram)、无连接的协议,它可以用于对可靠性要求不高的场合。与TCP通信不同,两个程序之间进行UDP通信无需预先建立持久的socket连接,UDP每次发送数据报都需要指定目标地址和端口(如原书的图14-6所示)。

QUdpSocket类用于实现UDP通信,它从QAbstractSocket类继承,因而与QTcpSocket共享大部分的接口函数。主要区别是QUdpSocket以数据报传输数据,而不是以连续的数据流。发送数据报使用函数QUdpSocket::writeDatagram(),数据报的长度一般少于512字节,每个数据报包含发送者和接收者的IP地址和端口等信息。

要进行UDP数据接收,要用QUdpSocket::bind()函数先绑定一个端口,用于接收传入的数据报。当有数据报传入时会发射readyRead()信号,使用readDatagram()函数来读取接收到的数据报。

UDP消息传送有单播、广播、组播三种模式,其示意图如原书的图14-7所示。

  • 单播(unicast)模式:一个UDP客户端发出的数据报只发送到另一个指定地址和端口的UDP客户端,是一对一的数据传输。

  • 广播(broadcast)模式:一个UDP客户端发出的数据报,在同一网络范围内其他所有的UDP客户端都可以收到。QUdpSocket支持IPv4广播。广播经常用于实现网络发现的协议。要获取广播数据只需在数据报中指定接收端地址为QHostAddress::Broadcast,一般的广播地址是255.255.255.255。

  • 组播(multicast)模式:也称为多播。UDP客户端加入到另一个组播IP地址指定的多播组,成员向组播地址发送的数据报组内成员都可以接收到,类似于QQ群的功能。QUdpSocket::joinMulticastGroup()函数实现加入多播组的功能,加入多播组后,UDP数据的收发与正常的UDP数据收发方法一样。

使用广播和多播模式,UDP可以实现一些比较灵活的通信功能,而TCP通信只有单播模式,没有广播和多播模式。所以,UDP通信虽然不能保证数据传输的准确性,但是具有灵活性,一般的即时通信软件都是基于UDP通信的。

QUdpSocket类从QAbstractSocket继承而来,但是又定义了较多新的功能函数用于实现UDP特有的一些功能,如数据报读写和多播通信功能。QUdpSocket没有定义新的信号。QUdpSocket的主要功能函数见表14-6(包括从QAbstractSocket继承的函数,省略了函数中的const关键字,省略了缺省参数)。

表14-6 QUdpSocket类的主要接口函数

函数功能
bool bind(quint16 port = 0) 为UDP通信绑定一个端口
qint64 writeDatagram(QByteArray &datagram, QHostAddress &host, quint16 port) 向目标地址和端口的UDP客户端发送数据报,返回成功发送的字节数
bool hasPendingDatagrams() 当至少有一个数据报需要读取时,返回true
qint64 pendingDatagramSize() 返回第一个待读取的数据报的大小
qint64 readDatagram(char *data, qint64 maxSize) 读取一个数据报,返回成功读取的数据报的字节数
bool joinMulticastGroup(QHostAddress &groupAddress) 加入一个多播组
bool leaveMulticastGroup(QHostAddress &groupAddress) 离开一个多播组

在单播、广播和多播模式下,UDP程序都是对等的,不像TCP通信那样分为客户端和服务器端。多播和广播的实现方式基本相同,只是数据报的目标IP地址设置不同,多播模式需要加入多播组,实现方式有较大差异。

为分别演示这三种UDP通信模式,本节设计了两个实例。Samp14_3实例演示UDP单播和广播通信,Samp14_4实例演示UDP组播通信。

14.3.2 UDP单播和广播

1.UDP通信实例程序功能

实例程序samp14_3实现UDP单播和广播,其主窗口是继承自QMainWindow的类,界面用UI设计器设计。程序可以进行UDP数据报的发送和接收,samp14_3的两个运行实例之间可以进行UDP通信,这两个实例可以运行在同一台计算机上,也可以运行在不同的计算机上。原书的图14-8和图14-9是samp14_3两个实例在一台计算机上运行时通信的界面。

在同一台计算机上运行时,两个运行实例需要绑定不同的端口,例如实例A绑定端口1200,实例B绑定端口3355。实例A向实例B发送数据报时,需要指定实例B所在主机的IP地址、绑定端口作为目标地址和目标端口,这样实例B才能接收到数据报。

如果两个实例在不同计算机上运行,则可以使用相同的端口,因为IP地址不同了,不会导致绑定时发生冲突。一般的UDP通信程序都是在不同的计算机上运行的,约定一个固定的端口作为通信端口。

2.主窗口类定义和构造函数

主窗口是基于QMainWindow的类MainWindow,界面采用UI设计器设计。MainWindow类的定义如下(省略了UI设计器为actions和按钮生成的槽函数声明):

class MainWindow : public QMainWindow
{
Q_OBJECT
private:
QLabel *LabSocketState; //socket状态显示标签
QUdpSocket *udpSocket;
QString getLocalIP();//获取本机IP地址
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private slots:
//自定义槽函数
void onSocketStateChange(QAbstractSocket::SocketState socketState);
void onSocketReadyRead();//读取socket传入的数据
private:
Ui::MainWindow *ui;
};

QUdpSocket类型的私有变量udpSocket是用于UDP通信的socket。

定义了两个自定义槽函数,onSocketStateChange()与udpSocket的stateChange()信号关联,用于显示udpSocket当前的状态;onSocketReadyRead()信号与udpSocket的readyRead()信号关联,用于读取缓冲区的数据报。

MainWindow的构造函数主要完成udpSocket的创建、信号与槽函数的关联,代码如下:

MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
LabSocketState=new QLabel("Socket状态:");
LabSocketState->setMinimumWidth(200);
ui->statusBar->addWidget(LabSocketState);
QString localIP=getLocalIP();//本机IP
this->setWindowTitle(this->windowTitle()+"—-本机IP:"+localIP);
ui->comboTargetIP->addItem(localIP);

udpSocket=new QUdpSocket(this);
connect(udpSocket,SIGNAL(stateChanged(QAbstractSocket::SocketState)),
this,SLOT(onSocketStateChange(QAbstractSocket::SocketState)));
onSocketStateChange(udpSocket->state());
connect(udpSocket,SIGNAL(readyRead()),this,SLOT(onSocketReadyRead()));
}

槽函数onSocketStateChange()的功能与14.2节TCP通信程序里的完全一样,不再显示其具体代码。

3.UDP通信的实现

要实现UDP数据的接收,必须先用QUdpSocket::bind()函数绑定一个端口,用于监听传入的数据报,解除绑定则使用abort()函数。程序主窗口上的“绑定端口”和“解除绑定”按钮的响应代码如下:

void MainWindow::on_actStart_triggered()
{//绑定端口
quint16 port=ui->spinBindPort->value(); //本机UDP端口
if (udpSocket->bind(port))//绑定端口成功
{
ui->plainTextEdit->appendPlainText("**已成功绑定");
ui->plainTextEdit->appendPlainText("**绑定端口:"
+QString::number(udpSocket->localPort()));
ui->actStart->setEnabled(false);
ui->actStop->setEnabled(true);
}
else
ui->plainTextEdit->appendPlainText("**绑定失败");
}

void MainWindow::on_actStop_triggered()
{//解除绑定
udpSocket->abort(); //解除绑定
ui->actStart->setEnabled(true);
ui->actStop->setEnabled(false);
ui->plainTextEdit->appendPlainText("**已解除绑定");
}

绑定端口后,socket的状态变为已绑定状态“BoundState”,解除绑定后状态变为未连接状态“UnconnectedState”。

发送点对点消息和广播消息都使用QUdpSocket::writeDatagram()函数,窗口上“发送消息”和“广播消息”两个按钮的代码如下:

void MainWindow::on_btnSend_clicked()
{//发送消息 按钮
QString targetIP=ui->comboTargetIP->currentText(); //目标IP
QHostAddress targetAddr(targetIP);
quint16 targetPort=ui->spinTargetPort->value();//目标port
QString msg=ui->editMsg->text();//发送的消息内容
QByteArray str=msg.toUtf8();
udpSocket->writeDatagram(str,targetAddr,targetPort); //发出数据报
ui->plainTextEdit->appendPlainText("[out] "+msg);
ui->editMsg->clear();
ui->editMsg->setFocus();
}

void MainWindow::on_btnBroadcast_clicked()
{ //广播消息 按钮
quint16 targetPort=ui->spinTargetPort->value(); //目标端口
QString msg=ui->editMsg->text();
QByteArray str=msg.toUtf8();
udpSocket->writeDatagram(str,QHostAddress::Broadcast,targetPort);
ui->plainTextEdit->appendPlainText("[broadcast] "+msg);
ui->editMsg->clear();
ui->editMsg->setFocus();
}

使用writeDatagram()函数向一个目标用户发送消息时,需要指定目标地址和端口。

在广播消息时,只需将目标地址更换为一个特殊地址,即广播地址QHostAddress::Broadcast,一般是255.255.255.255。

QUdpSocket发送的数据报是QByteArray类型的字节数组,数据报的长度一般不超过512字节。数据报的内容可以是文本字符串,也可以自定义格式的二进制数据,文本字符串无需以换行符结束。

QUdpSocket接收到数据报后发射readyRead()信号,在关联的槽函数onSocketReadyRead()里读取缓冲区的数据报,代码如下:

void MainWindow::onSocketReadyRead()
{//读取收到的数据报
while(udpSocket->hasPendingDatagrams())
{
QByteArray datagram;
datagram.resize(udpSocket->pendingDatagramSize());
QHostAddress peerAddr;
quint16 peerPort;
udpSocket->readDatagram(datagram.data(), datagram.size(), &peerAddr,&peerPort);
QString str=datagram.data();
QString peer="[From "+peerAddr.toString() +":" +QString::number(peerPort)+"] ";
ui->plainTextEdit->appendPlainText(peer+str);
}
}

hasPendingDatagrams()表示是否有待读取的传入数据报。

pendingDatagramSize()返回待读取数据报的字节数。

readDatagram()函数用于读取数据报的内容,其函数原型为:

qint64 QUdpSocket::readDatagram(char *data, qint64 maxSize, QHostAddress *address = Q_NULLPTR, quint16 *port = Q_NULLPTR)

输入参数data和maxSize是必须的,表示最多读取maxSize字节的数据到变量data里。address和port变量是可选的,用于获取数据报来源的地址和端口。上面的代码中使用了完整的参数形式,从而可以获得数据报来源的地址peerAddr和端口peerPort。如果无需获取来源地址和端口,可以采用简略形式,即:

udpSocket->readDatagram(datagram.data(),datagram.size());

读取的数据报内容是QByteArray字节数组,因为本程序只是传输字符串,所以简单地将其转换为字符串即可。如果传输的是自定义格式的字符串或二进制数据,需要对接收到的数据进行解析。

14.3.3 UDP组播

1.UDP组播的特性

原书的图14-7的示意图简单表示了组播的原理。UDP组播是主机之间“一对一组”的通信模式,当多个客户端加入由一个组播地址定义的多播组之后,客户端向组播地址和端口发送的UDP数据报,组内成员都可以接收到,其功能类似于QQ群。

组播报文的目的地址使用D类IP地址,D类地址不能出现在IP报文的源IP地址字段。用同一个IP多播地址接收多播数据报的所有主机构成了一个组,称为多播组(或组播组)。所有的信息接收者都加入到一个组内,并且一旦加入之后,流向组地址的数据报立即开始向接收者传输,组中的所有成员都能接收到数据报。组中的成员是动态的,主机可以在任何时间加入和离开组。

所以,采用UDP组播必须使用一个组播地址。组播地址是D类IP地址,有特定的地址段。多播组可以是永久的也可以是临时的。多播组地址中,有一部分由官方分配,称为永久多播组。永久多播组保持不变的是它的IP地址,组中的成员构成可以发生变化。永久多播组中成员的数量可以是任意的,甚至可以为零。那些没有保留下来的供永久多播组使用的IP组播地址,可以被临时多播组利用。关于组播IP地址,有如下的一些约定:

  • 224.0.0.0~224.0.0.255为预留的组播地址(永久组地址),地址224.0.0.0保留不做分配,其他地址供路由协议使用;

  • 224.0.1.0~224.0.1.255是公用组播地址,可以用于Internet;

  • 224.0.2.0~238.255.255.255为用户可用的组播地址(临时组地址),全网范围内有效;

  • 239.0.0.0~239.255.255.255为本地管理组播地址,仅在特定的本地范围内有效。

所以,若是在家庭或办公室局域网内测试UDP组播功能,可以使用的组播地址范围是239.0.0.0~239.255.255.255。

QUdpSocket支持UDP组播,joinMulticastGroup()函数使主机加入一个多播组,leaveMulticast Group()函数使主机离开一个多播组,UDP组播的特点是使用组播地址,其他的端口绑定、数据报收发等功能的实现与单播UDP完全相同。

2.UDP组播实例程序的功能

设计一个UDP组播实例程序Samp14_4,在两台计算机上分别运行,进行组播通信。原书的图14-10是运行于主机192.168.1.104上的程序,原书的图14-11是运行于主机192.168.1.106上的程序。两个主机上的程序都加入地址为239.255.43.21的多播组,绑定端口35320进行通信。

从图14-10和图14-11可以看到,两个Samp14_4程序都可以发送和接收组播数据报,且在自己主机上发出的数据报,自己也可以接收到。

3.组播功能的程序实现

程序的主窗口是基于QMainWindow的类MainWindow,界面由UI设计器设计,其类定义如下(忽略UI设计器生成的槽函数):

class MainWindow : public QMainWindow
{
Q_OBJECT
private:
QLabel *LabSocketState;
QUdpSocket *udpSocket;
QHostAddress groupAddress; //组播地址
QString getLocalIP(); //获取本机IP地址
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private slots:
//自定义槽函数
void onSocketStateChange(QAbstractSocket::SocketState socketState);
void onSocketReadyRead();//读取socket传入的数据
private:
Ui::MainWindow *ui;
};

其中定义了一个QHostAddress类型变量groupAddress,用于记录组播地址。下面是MainWindow的构造函数的代码:

MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
LabSocketState=new QLabel("Socket状态:");
LabSocketState->setMinimumWidth(200);
ui->statusBar->addWidget(LabSocketState);
QString localIP=getLocalIP();//本地主机名
this->setWindowTitle(this->windowTitle()+"—-本机IP:"+localIP);
udpSocket=new QUdpSocket(this);
udpSocket->setSocketOption(QAbstractSocket::MulticastTtlOption,1);

connect(udpSocket,SIGNAL(stateChanged(QAbstractSocket::SocketState)),
this,SLOT(onSocketStateChange(QAbstractSocket::SocketState)));
onSocketStateChange(udpSocket->state());
connect(udpSocket,SIGNAL(readyRead()), this,SLOT(onSocketReadyRead()));
}

其中使用了QUdpSocket::setSocketOption()函数,对socket进行参数设置。

udpSocket->setSocketOption(QAbstractSocket::MulticastTtlOption,1);

将socket的QAbstractSocket::MulticastTtlOption值设置为1。MulticastTtlOption是UDP组播的数据报的生存期,数据报每跨1个路由会减1。缺省值为1,表示多播数据报只能在同一路由下的局域网内传播。

要进行UDP组播通信,UDP客户端必须先加入UDP多播组,也可以随时退出多播组。主窗口上的“加入组播”和“退出组播”按钮的代码如下:

void MainWindow::on_actStart_triggered()
{//加入组播
QString IP=ui->comboIP->currentText();
groupAddress=QHostAddress(IP);//多播组地址
quint16 groupPort=ui->spinPort->value();//端口
if (udpSocket->bind(QHostAddress::AnyIPv4, groupPort, QUdpSocket::ShareAddress))
{
udpSocket->joinMulticastGroup(groupAddress); //加入多播组
ui->plainTextEdit->appendPlainText("**加入组播成功");
ui->plainTextEdit->appendPlainText("**组播地址IP:"+IP);
ui->plainTextEdit->appendPlainText("**绑定端口:"+ QString::number(groupPort));
ui->actStart->setEnabled(false);
ui->actStop->setEnabled(true);
ui->comboIP->setEnabled(false);
}
else
ui->plainTextEdit->appendPlainText("**绑定端口失败");
}
void MainWindow::on_actStop_triggered()
{//退出组播
udpSocket->leaveMulticastGroup(groupAddress);//退出组播
udpSocket->abort(); //解除绑定
ui->actStart->setEnabled(true);
ui->actStop->setEnabled(false);
ui->comboIP->setEnabled(true);
ui->plainTextEdit->appendPlainText("**已退出组播,解除端口绑定");
}

加入组播之前,必须先绑定端口,绑定端口的语句是:

udpSocket->bind(QHostAddress::AnyIPv4, groupPort, QUdpSocket::ShareAddress)

这里指定地址为QHostAddress::AnyIPv4,端口为多播组统一的一个端口。

使用QUdpSocket::joinMulticastGroup()函数加入多播组,即:

udpSocket->joinMulticastGroup(groupAddress);

多播组地址groupAddress由界面上的组合框里输入。注意,局域网内的组播地址的范围是239.0.0.0~239.255.255.255,绝对不能使用本机地址作为组播地址。

退出多播组,使用QUdpSocket::leaveMulticastGroup()函数,即:

udpSocket->leaveMulticastGroup(groupAddress);

加入多播组后,发送组播数据报也是使用writeDatagram()函数,只是目标地址使用的是组播地址,在readyRead()信号的槽函数里用readDatagram()读取数据报。下面是发送和读取数据报的代码:

void MainWindow::on_btnMulticast_clicked()
{//发送组播消息
quint16 groupPort=ui->spinPort->value();
QString msg=ui->editMsg->text();
QByteArray datagram=msg.toUtf8();
udpSocket->writeDatagram(datagram,groupAddress,groupPort);
ui->plainTextEdit->appendPlainText("[multicst] "+msg);
ui->editMsg->clear();
ui->editMsg->setFocus();
}
void MainWindow::onSocketReadyRead()
{//读取数据报
while(udpSocket->hasPendingDatagrams())
{
QByteArray datagram;
datagram.resize(udpSocket->pendingDatagramSize());
QHostAddress peerAddr;
quint16 peerPort;
udpSocket->readDatagram(datagram.data(), datagram.size(), &peerAddr, &peerPort);
QString str=datagram.data();
QString peer="[From "+peerAddr.toString()+":" +QString::number(peerPort)+"] ";
ui->plainTextEdit->appendPlainText(peer+str);
}
}

14.4 基于HTTP协议的网络应用程序

14.4.1 实现高层网络操作的类

Qt网络模块提供一些类实现OSI 7层网络模型中高层的网络协议,如HTTP、FTP、SNMP等,这些类主要是QNetworkRequest、QNetworkReply和QNetworkAccessManager。

QNetworkRequest类通过一个URL地址发起网络协议请求,也保存网络请求的信息,目前支持HTTP、FTP和局部文件URLs的下载或上传。

QNetworkAccessManager类用于协调网络操作。在QNetworkRequest发起一个网络请求后,QNetworkAccessManager类负责发送网络请求,创建网络响应。

QNetworkReply类表示网络请求的响应。由QNetworkAccessManager在发送一个网络请求后创建一个网络响应。QNetworkReply提供的信号finished()、readyRead()和downloadProgress()可以监测网络响应的执行情况,执行相应操作。

QNetworkReply是QIODevice的子类,所以QNetworkReply支持流读写功能,也支持异步或同步工作模式。

14.4.2 基于HTTP协议的网络文件下载

基于上述三个类,设计一个基于HTTP协议的网络文件下载程序,实例程序名称samp14_5,原书的图14-12是程序运行下载文件时的界面。

在URL地址编辑框里输入一个网络文件URL地址,设置下载文件保存路径后,单击“下载”按钮就可以开始下载文件到设置的目录下。进度条可以显示文件下载进度,下载完成后还可以用缺省的软件打开下载的文件。URL里的HTTP地址可以是任何类型的文件,如html、pdf、doc、exe等。

实例samp14_5主界面是基于QMainWindow的窗口类MainWindow,使用UI设计器设计界面,删除了主窗口上的工具栏和状态栏。MainWindow类的定义如下:

class MainWindow : public QMainWindow
{
Q_OBJECT
private:
QNetworkAccessManager networkManager;//网络管理
QNetworkReply *reply; //网络响应
QFile *downloadedFile;//下载保存的临时文件
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private slots:
//自定义槽函数
void on_finished();
void on_readyRead();
void on_downloadProgress(qint64 bytesRead, qint64 totalBytes);
private:
Ui::MainWindow *ui;
};

要下载文件,先在窗口上的URL编辑框里输入下载地址(可以使用Ctrl+V组合键粘贴URL地址),再设置下载文件保存的目录。单击“缺省路径”按钮会在程序的当前目录下创建一个临时文件夹,代码如下:

void MainWindow::on_btnDefaultPath_clicked()
{//缺省路径 按钮
QString curPath=QDir::currentPath();
QDir dir(curPath);
QString sub="temp";
dir.mkdir(sub);
ui->editPath->setText(curPath+"/"+sub+"/");
}

输入这些设置后,单击“下载”按钮开始下载过程,“下载”按钮的响应代码如下:

void MainWindow::on_btnDownload_clicked()
{//开始下载
QString urlSpec = ui->editURL->text().trimmed();
if (urlSpec.isEmpty())
{ QMessageBox::information(this, "错误", "请指定需要下载的URL");
return;
}

QUrl newUrl = QUrl::fromUserInput(urlSpec);
if (!newUrl.isValid())
{ QMessageBox::information(this, "错误",
QString("无效URL: %1 \\n 错误信息: %2").arg(urlSpec, newUrl.errorString()));
return;
}

QString tempDir =ui->editPath->text().trimmed();
if (tempDir.isEmpty())
{ QMessageBox::information(this, "错误", "请指定保存下载文件的目录");
return;
}
QString fullFileName =tempDir+newUrl.fileName();
if (QFile::exists(fullFileName))
QFile::remove(fullFileName);

downloadedFile =new QFile(fullFileName);
if (!downloadedFile->open(QIODevice::WriteOnly))
{ QMessageBox::information(this, "错误","临时文件打开错误");
return;
}
ui->btnDownload->setEnabled(false);
reply = networkManager.get(QNetworkRequest(newUrl));
connect(reply, SIGNAL(finished()), this, SLOT(on_finished()));
connect(reply, SIGNAL(readyRead()), this, SLOT(on_readyRead()));
connect(reply, SIGNAL(downloadProgress(qint64,qint64)),
this, SLOT(on_downloadProgress(qint64,qint64)));
}

代码在读取URL地址后,将其转换为一个QUrl类变量newUrl,并检查其有效性,再检查临时文件目录,创建临时文件downloadedFile。

这些准备好之后,用QNetworkAccessManager发布网络请求,请求下载URL地址表示的文件,并创建网络响应,关键代码为:

reply = networkManager.get(QNetworkRequest(newUrl));

reply为网络响应,将其3个信号与相关的自定义槽函数相关联,实现相应的操作。这3个槽函数的代码如下:

void MainWindow::on_readyRead()
{//读取下载的数据
downloadedFile->write(reply->readAll());
}
void MainWindow::on_downloadProgress(qint64 bytesRead, qint64 totalBytes)
{//下载进程
ui->progressBar->setMaximum(totalBytes);
ui->progressBar->setValue(bytesRead);
}
void MainWindow::on_finished()
{//网络响应结束
QFileInfo fileInfo;
fileInfo.setFile(downloadedFile->fileName());
downloadedFile->close();
delete downloadedFile;
downloadedFile = Q_NULLPTR;
reply->deleteLater();
reply = Q_NULLPTR;
if (ui->checkOpen->isChecked())
QDesktopServices::openUrl(QUrl::fromLocalFile(
fileInfo.absoluteFilePath()));
ui->btnDownload->setEnabled(true);
}

在缓冲区有新下载的数据等待读取时,会发射readyRead()信号,槽函数on_readyRead()读取下载缓冲区的数据到临时文件。

downloadProgress()是表示网络操作进度的信号,传递bytesRead和totalBytes两个参数,表示已读取字节数和总的字节数;on_downloadProgress()槽函数将这两个参数用于进度条的显示,可以显示下载进度。

finished()信号在下载结束后发射,槽函数on_finished()的功能是关闭临时文件,删除文件变量和网络响应变量。然后用QDesktopServices::openUrl()函数调用缺省的应用软件打开下载的文件,例如,如果下载的是一个PDF文件,会自动用相关联的PDF阅读器软件打开此文件。

赞(0)
未经允许不得转载:171主机测评 » 【Qt】《Qt 5.9 C++开发指南》笔记-Chapter14-网络编程
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址