Qt开发总结(25)——网络编程

Qt网络通信模块提供了用于编写TCP/IP客户端和服务器端程序的各种类。包括一些低级功能类,比如QTcpSocket, QTcpServer 和 QUdpSocket实现一些底层TCP或UDP通信,也包括一些高级类,如QNetworkRequest, QNetworkReply 和 QNetworkAccessManager等实现一些常见的Http、Ftp网络通讯。Qt网络模块还提供用于网络代理、网络承载管理的类,提供基于安全套接字层(SSL)协议的安全网络通信类。本篇笔记简要总结Qt网络编程中TCP、UDP以及HTTP和FTP上传下载的实现。

概述

网络通信模块是Qt的一个独立模块,开发时需要在工程中添加该模块支持:

QT += network

在使用时还需要添加头文件:

#include <QtNetwork>

主要的类如下:

描述

QHttpMultiPart

Resembles a MIME multipart message to be sent over HTTP

QHttpPart

Holds a body part to be used inside a HTTP multipart MIME message

QNetworkAccessManager

Allows the application to send network requests and receive replies

QNetworkCookie

Holds one network cookie

QNetworkReply

Contains the data and headers for a request sent with QNetworkAccessManager

QNetworkRequest

Holds a request to be sent with QNetworkAccessManager

QNetworkConfigurationManager

Manages the network configurations provided by the system

QNetworkConfiguration

Abstraction of one or more access point configurations

QNetworkSession

Control over the system's access points and enables session management for cases when multiple clients access the same access point

QHostAddress

IP address

QHostInfo

Static functions for host name lookups

QNetworkDatagram

The data and metadata of a UDP datagram

QNetworkAddressEntry

Stores one IP address supported by a network interface, along with its associated netmask and broadcast address

QNetworkInterface

Listing of the host's IP addresses and network interfaces

QNetworkProxy

Network layer proxy

QNetworkProxyFactory

Fine-grained proxy selection

QNetworkProxyQuery

Used to query the proxy settings for a socket

QAbstractSocket

The base functionality common to all socket types

QLocalServer

Local socket based server

QLocalSocket

Local socket

QSctpServer

SCTP-based server

QSctpSocket

SCTP socket

QTcpServer

TCP-based server

QTcpSocket

TCP socket

QUdpSocket

UDP socket

TCP通信

TCP (Transmission Control Protocol)是一种被大多数Internet网络协议(如HTTP和FTP)用于数据传输的低级网络协议。它是可靠的、面向流、面向链接的传输协议,特别适合用于连续数据传输。TCP通信必须先建立TCP连接,通信端分为客户端和服务器端,Qt提供了QTcpSocket类和QTcpServer类用于建立TCP通信应用程序。服务器端必须用QTcpServer监听端口,建立服务器,QTcpSocket用于建立连接之后使用套接字进行通信。下面是建立Server的大致流程:

 

      tcpServer = new QTcpServer(this);
      if (!tcpServer->listen()) {
          QMessageBox::critical(this, tr("TCP Server"),
                                tr("Unable to start the server: %1.")
                                .arg(tcpServer->errorString()));
          return;
      }
      QString ipAddress;
      QList<QHostAddress> ipAddressesList = QNetworkInterface::allAddresses();
      // use the first non-localhost IPv4 address
      for (int i = 0; i < ipAddressesList.size(); ++i) {
          if (ipAddressesList.at(i) != QHostAddress::LocalHost &&
              ipAddressesList.at(i).toIPv4Address()) {
              ipAddress = ipAddressesList.at(i).toString();
              break;
          }
      }
      // if we did not find one, use IPv4 localhost
      if (ipAddress.isEmpty())
          ipAddress = QHostAddress(QHostAddress::LocalHost).toString();
      statusLabel->setText(tr("The server is running on\n\nIP: %1\nport: %2\n\n"
                              "Run the Fortune Client example now.")
                           .arg(ipAddress).arg(tcpServer->serverPort()));

首先new一个QTcpServer对象,调用listen()接口,来监听所有的地址。当然也可以监听某一指定的IP和端口。当有新的客户端接入时,QTcpServer内部的incomingConnection()函数会创建一个与客户端连接的QTcpSocket对象,然后发射信号newConnection()。在该信号的槽函数中可以用nextPendingConnection()函数接受客户端的连接,然后使用QTcpSocket与客户端通信。

 

  connect(tcpServer, &QTcpServer::newConnection, this, &Server::sendData);

  void Server::sendData()
  {
      QByteArray block;
      QDataStream out(&block, QIODevice::WriteOnly);
      out.setVersion(QDataStream::Qt_4_0);

      out << fortunes.at(qrand() % fortunes.size());

      QTcpSocket *clientConnection = tcpServer->nextPendingConnection();
      connect(clientConnection, &QAbstractSocket::disconnected,
              clientConnection, &QObject::deleteLater);
      clientConnection->write(block);
      clientConnection->disconnectFromHost();
  }

我们在发送完数据数据后调用了QTcpSocket::disconnectFromHost()关闭连接。但是由于QTcpSocket是异步工作的,数据会在这个函数返回后继续发送,直到发送完毕后才回到Qt的事件回环中,这时,socket会关闭,我们也调用了deleteLater()函数来删除它。

另外,上述代码中涉及了QNetworkInterface,该类可以获得应用程序所在主机的所有网络接口,包括其子网掩码和广播地址等。可以通过调用其静态接口allAddresses()和allInterfaces()获取所有信息。上述代码也显示了挑选非LocalHost的ipv4地址和挑选LocalHost的方法。

对于TCP客户端,使用QTcpSocket与TCP服务器建立连接并通信。客户端的QTcpSocket对象首先通过QTcpSocket::connectToHost()尝试连接到服务器,这里需要指定服务器的IP和端口。connectToHost()是异步方式连接服务器,不会阻塞程序运行,连接成功后发射connected()信号。如果要使用阻塞的方式连接服务器,则使用QTcpSocket::waitForConnected()函数,直到连接成功或失败。下面首先是常用的异步实现方式:

 

     QTcpSocket *tcpSocket = new QTcpSocket(this);
	QString serverIpAddr = "192.168.0.10";
	int serverPort = 8080;
	tcpSocket->connectToHost(serverIpAddr,serverPort);
	connect(tcpSocket, &QIODevice::readyRead, this, &NetworkCom::readCmdData);
	connect(tcpSocket, QOverload<QAbstractSocket::SocketError>::of(&QAbstractSocket::error),
		this, &NetworkCom::networkError);
	connect(tcpSocket, &QAbstractSocket::connected, this, &NetworkCom::onConnected);
	connect(tcpSocket, &QAbstractSocket::disconnected, this, &NetworkCom::onDisconnected);

若为阻塞方式则可以在connectToHost后增加:

    if (tcpSocket->waitForConnected())
 		qDebug("Connected!");
 	else
 	{
 		qDebug("Connected failed!");
 	}

与服务器建立连接后,就可以向缓冲区写数据或从缓冲区读取数据,实现数据通信。当缓冲区有新数据进入时,会发射readyRead()信号,一般就在此信号的槽函数内读取缓冲区数据。socket同时也可以用来发送数据,接收和发送时异步工作的,有各自的缓冲区,不必担心冲突问题。

void Client::readCmdData()
{
	QByteArray buffer = tcpSocket->readAll();
	if (buffer.isEmpty())
	{
		return;
	}
	//使用buffer
	...
	//发送数据
	QByteArray data;
	tcpSocket->write(data);
}
void Client::networkError(QAbstractSocket::SocketError socketError)
{
	switch (socketError) {
	case QAbstractSocket::RemoteHostClosedError:
		break;
	case QAbstractSocket::HostNotFoundError:
		QMessageBox::information(nullptr,tr("Network Client"),
			tr("The host was not found. Please check the "
				"host name and port settings."));
		break;
	case QAbstractSocket::ConnectionRefusedError:
		QMessageBox::information(nullptr, tr("Network Client"),
			tr("The connection was refused by the peer. "
				"Make sure the Network server is running, "
				"and check that the host name and port "
				"settings are correct."));
		break;
	default:
		QMessageBox::information(nullptr, tr("Network Client"),
			tr("The following error occurred: %1.")
			.arg(tcpSocket->errorString()));
	}
}
void Client::onConnected()
{
	qDebug() << "Tcp Connected!";
}
void Client::onDisconnected()
{
	qDebug() << "Tcp Disconnected!";
}

UDP通信

UDP (User Datagram Protocol)用户数据报协议是轻量的、不可靠的、面向数据报的无连接协议,它可以用于对可靠性要求不高的场合。与TCP通信不同,两个程序进行UDP通信无须先建立持久的Socket连接,UDP每次发送数据都需要制定目标地址和端口。QUdpSocket类用于实现UDP通信,它和QTcpSocket都是继承于QAbstractSocket基类,所以与QTcpSocket共享大部分的接口函数。主要区别就是发送数据时调用的接口为writeDatagram(),数据报的长度不能大于512Kb。UDP在接收数据时,要用QUdpSocket::bind()函数绑定一个端口,当有数据传入时就会触发readyRead()信号,然后调用QUdpSocket::readDatagram()来读取数据。

UDP消息传送有单播、广播和组播三种方式。

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

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

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

不论是何种传输方式,UDP程序都是对等的,没有服务端和客户端的区分,实现方式也基本相同,只是数据报的目标IP地址设置不同,下面首先来看UDP单播和广播的通讯:

//Sender:
QTimer *timer = new QTimer(this);
QUdpSocket *udpSocket = new QUdpSocket(this);
connect(timer, SIGNAL(timeout()), this, SLOT(broadcastDatagram()));
  ...
void Sender::broadcastDatagram()
{
    QByteArray datagram = "Broadcast message " + QByteArray::number(messageNo);
    udpSocket->writeDatagram(datagram.data(), datagram.size(),
                               QHostAddress::Broadcast, 45454);
    ++messageNo;
}
//receiver:
QUdpSocket *udpSocket = new QUdpSocket(this);
udpSocket->bind(45454, QUdpSocket::ShareAddress);
connect(udpSocket, SIGNAL(readyRead()), this, SLOT(processPendingDatagrams()));
...
void Receiver::processPendingDatagrams()
{
    while (udpSocket->hasPendingDatagrams()) {
        QByteArray datagram;
        datagram.resize(udpSocket->pendingDatagramSize());
        udpSocket->readDatagram(datagram.data(), datagram.size());
    }
}

对于组播:

//Sender:
QHostAddress groupAddress;
groupAddress = QHostAddress("239.255.43.21");
QUdpSocket *udpSocket = new QUdpSocket(this);
void Sender::sendDatagram()
{
    QByteArray datagram = "Multicast message " + QByteArray::number(messageNo);
    udpSocket->writeDatagram(datagram.data(), datagram.size(),
                               groupAddress, 45454);
    ++messageNo;
}
//Receiver:
groupAddress = QHostAddress("239.255.43.21");
udpSocket = new QUdpSocket(this);
udpSocket->bind(QHostAddress::AnyIPv4, 45454, QUdpSocket::ShareAddress);
udpSocket->joinMulticastGroup(groupAddress);
connect(udpSocket, SIGNAL(readyRead()), this, SLOT(processPendingDatagrams()));
void Receiver::processPendingDatagrams()
{
    while (udpSocket->hasPendingDatagrams()) {
        QByteArray datagram;
        datagram.resize(udpSocket->pendingDatagramSize());
        udpSocket->readDatagram(datagram.data(), datagram.size());
    }
}

这里首先要注意组播地址不是任意的。组播地址是D类IP地址,有特定的地址段。若是在家庭或办公室局域网内测试UDP组播功能,可以使用的组播地址为239.0.0.0~239.255.255.255.UDP主机可以在任意时间加入和离开组播,用joinMulticastGroup()函数加入,leaveMulticastGroup()离开。

高级网络通讯HTTP和FTP

Qt网络模块也实现了中高层网络传输协议,比如通过HTTP和FTP下载和上传数据。QNetworkRequest类通过一个URL地址发起网络协议请求,也保存网络请求的信息,目前支持HTTP,FTP和局部文件URLs的下载和上传。QNetworkAccessManager类用于协调网络操作。在QNetworkRequest发起一个网络请求后,QNetworkAccessManager负责发送网络请求,创建网络响应。QNetworkReply类用于表示网络请求的响应。QNetworkAccessManager发送网络请求后创建的一个网络响应,它提供finished() readyRead()和downloadProgress()信号,用以监测网络响应的执行情况,执行响应操作。

HTTP和FTP上传下载的区别在于地址不同,对于HTTP其地址为http://ip:port,对于FTP其地址为ftp://ip:port.在用QNetworkAccessManager实现时,并无其他差别。下面为典型的下载和上传的例子:

void NetFileTrans::download(QUrl url, QString dstDir)
{
	if (!url.isValid())
	{
		return;
	}
	if (dstDir.isEmpty())
	{
		return;
	}
	QString fullFileName = dstDir + m_url.fileName();
	if (QFile::exists(fullFileName))
	{
		QFile::remove(fullFileName);
	}
	m_file = new QFile(fullFileName);
	if (!m_file->open(QIODevice::WriteOnly))
	{
		return;
	}
	QNetworkAccessManager  *manager = new QNetworkAccessManager(this);
	m_reply = manager->get(QNetworkRequest(url));	//下载
	connect(m_reply, SIGNAL(readyRead()), this, SLOT(onReadyRead()));
	connect(m_reply, SIGNAL(finished()), this, SLOT(onDownloadFinished()));
	connect(m_reply, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(onProgress(qint64, qint64)));
	connect(m_reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(slotError(QNetworkReply::NetworkError)));
#ifndef QT_NO_SSL
	connect(m_reply, SIGNAL(sslErrors(QList<QSslError>)), this, SLOT(slotSslErrors(QList<QSslError>)));
#endif
}
void NetFileTrans::upload(QUrl url , QString srcFile)
{
	m_file = new QFile(srcFile);
	if (!m_file->open(QIODevice::ReadOnly))
	{
		return;
	}
	if (!url.isValid())
	{
		return;
	}
	//从文件读取数据
	QByteArray fileData = m_file->readAll();
	//从ftp或http的发送数据方式不同
	QNetworkAccessManager  *manager = new QNetworkAccessManager(this);
	if (url.scheme() == "ftp")
	{
		m_reply = manager->put(QNetworkRequest(url), fileData);
	}
	else if (url.scheme() == "http")
	{
		QNetworkRequest request(url);
		request.setHeader(QNetworkRequest::ContentTypeHeader, "multipart/form-data");
		request.setRawHeader("filename", "3.xml");
		m_reply = manager->post(request, fileData);
	}
	else
	{
		return;
	}
	

	connect(m_reply, SIGNAL(uploadProgress(qint64, qint64)), this, SLOT(onProgress(qint64, qint64)));
	connect(m_reply, SIGNAL(finished()), this, SLOT(onUploadFinished()));
	connect(m_reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(slotError(QNetworkReply::NetworkError)));
#ifndef QT_NO_SSL
	connect(m_reply, SIGNAL(sslErrors(QList<QSslError>)), this, SLOT(slotSslErrors(QList<QSslError>)));
#endif
}
void NetFileTrans::onReadyRead()
{
	m_file->write(m_reply->readAll());
}
//传输完成
void NetFileTrans::onDownloadFinished()
{
	if (m_reply->error() == QNetworkReply::NoError)
	{
		QString oldFileName = m_file->fileName();
		m_reply->deleteLater();
		m_file->flush();
		m_file->close();
		m_reply = Q_NULLPTR;
		m_file = Q_NULLPTR;
	}
	
}
void NetFileTrans::onUploadFinished()
{
	if (m_reply->error() == QNetworkReply::NoError)
	{
		m_reply->deleteLater();
		m_file->close();
		m_reply = Q_NULLPTR;
		m_file = Q_NULLPTR;
	}
}
//传输过程监控
void NetFileTrans::onProgress(qint64 byteRead, qint64 totalBytes)
{
	//use byteRead and totalBytes
	qint64 reading = byteRead;
	qint64 total = totalBytes;
}
//传输错误
void NetFileTrans::slotError(QNetworkReply::NetworkError error)
{
	qDebug() << "DownLoad Error! error code:" << error << ": " << m_reply->errorString();
}
//ssl 错误
void NetFileTrans::slotSslErrors(QList<QSslError> &sslErrors)
{
#ifndef QT_NO_SSL
	foreach(const QSslError &error, sslErrors)
		LOGI << "SSL error: " << error.errorString();
#else
	Q_UNUSED(sslErrors);
#endif
}

上述代码自测时可以自建HTTP服务器和FTP服务器,这里安利两个比较好用的第三方软件:HFS(Http Files Server)fileZilla(FTP Server)。大家可以自行百度。

 

发布了76 篇原创文章 · 获赞 63 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/bjtuwayne/article/details/104692825