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)。大家可以自行百度。