16. 网络通信

Qt提供许多用于高级和低级网络通信的类、用于web集成的类以及用于进程间通信的类。

Qt Network模块,为所使用的操作提供了一个抽象层,只显示高级类和函数,还可以处理较低级别的协议。 如用于TCP通讯的QTcpSocket和用于UDP通讯的QUdpSocket,这些类使开发人员能够使用TCP或UDP协议发送和接收消息。

关于HTTP,主要通过QNetworkRequest,QNetworkAccessManager和QNetworkReply类。 简而言之,QNetworkRequest类似于HTTP请求,该请求被传递给QNetworkAccessManager以在线发送请求, 此类返回一个QNetworkReply,它可以解析HTTP答复。

Qt Network还有用于网络承载管理的类,以及基于SSL协议进行安全通信的类,除了QSslSocket外还提供了很多具备辅助功能的类, 例如QSslCertificate,QSslConfiguration和QSslError。Qt中唯一受支持的SSL后端是OpenSSL,需要单独安装。

本章主要讲在Qt中HTTP、TCP、UDP相关类的使用和应用编程。

16.1. TCP

TCP(传输控制协议,transmission control protocol)是大多数Internet协议(包括HTTP和FTP)用于数据传输的底层网络协议。 它是一种可靠的,面向流,面向连接的传输协议,特别适合连续数据传输。

16.1.1. TCP通讯相关的类

QTcpServer 类主要用于创建socket连接,在服务端进行网络监听,提供一个基于TCP的服务器。

QTcpServer主要的接口函数

函数

描述

bool listen(const QHostAddress &address = QHostAddress::Any, quint16 port = 0)

公共函数,开始监听设置的IP和端口

void close()

公共函数,关闭,停止监听

virtual QTcpSocket * nextPendingConnection()

公共函数,返回下一个等待的连接

QHostAddress serverAddress() const

公共函数,如果服务器处于监听状态,返回服务器地址

quint16 serverPort() const

公共函数,如果服务器处于监听状态,返回监听的端口

void newConnection()

信号,当有新的连接时,信号被发射

QTcpServer类允许接受传入的TCP连接,可以指定端口或让QTcpServer自动选择一个端口,监听特定地址或所有机器地址。 使用listen函数监听,当有客户端连接时发射newConnection()信号,我们可以关联相关槽函数,在槽函数中使用nextPendingConnection()函数接受客户端的连接,使用QTcpSocket与之通讯。

当客户端和服务端连接后,可以使用函数serverPort()和serverAddress()返回监听的端口和服务器地址,使用QTcpSocket对象进行通讯。

QTcpSocket 使用必须先建立与远程主机和端口的TCP连接,然后才能开始任何数据传输。 建立连接后,可通过QTcpSocket::peerAddress()和QTcpSocket::peerPort()获得连接方的IP地址和端口。

QTcpSocket是QAbstractSocket的子类,允许建立TCP连接和传输数据流,QTcpSocket间接继承于QIODevice,因此有流数据读写能力,可以将其与QTextStream和QDataStream一起使用。

QAbstractSocket相关接口函数

函数

描述

virtual void connectToHost()

公共函数,异步方式连接指定的IP和端口

virtual bool waitForConnected(int msecs = 30000)

公共函数,阻塞等待建立socket连接,默认30秒

virtual void disconnectFromHost()

公共函数,断开连接

void disconnected()

信号,disconnectFromHost断开连接后,信号被发射”

void connected()

信号,connectToHost连接成功,信号被发射

TCP客户端发起的TCP连接,使用QTcpSocket对象与服务端连接。QTcpSocket使用connectToHost尝试异步的方式连接到服务器, 连接成功会发射connected()信号。如果是需要阻塞的方式连接到服务端,使用waitForConnected()函数,直到发出connect()信号为止。

建立socket连接后,QTcpSocket对象可以向数据缓冲区写或者接收数据,且接收和发送是异步工作的,有各自的缓冲区。

16.2. UDP

UDP(用户数据报协议,User Datagram Protocol)是一种轻量级,不可靠,面向数据报的无连接协议,当可靠性不重要时可以使用它。 UDP是面向数据报传输,不需要建立持久的socket连接:

core01.png

UDP通讯不区分客户端和服务端,都是客户端。两个UDP客户端通讯时需要指定目的地址和端口。

16.2.1. UDP通讯相关的类

QUdpSocket 类提供socket,允许您发送和接收UDP数据包。它继承了QAbstractSocket,因此它共享QTcpSocket的大部分接口。 主要区别在于QUdpSocket将数据作为数据报而不是连续的数据流进行传输。

通常使用UDP接收信息,需要bind()绑定到地址和端口,用于传入的数据报。当有数据报传入时,会发射readyRead()信号,然后使用 readDatagram()/rereceiveDatagram()读取接收的数据报。 需要注意的是,当收到readyRead()信号时,应该读取传入的数据报,否则该信号下一个数据报接收到时,将不会发出信号。

使用UDP发送信息,不需要绑定地址和端口,调用writeDatagram()写数据包, 数据报通常小于512字节,除了要传输的数据外,还包含数据报的发送方和接收方的IP地址和端口。

使用QUdpSocket,还可以使用connectToHost()建立与UDP服务器的虚拟连接,然后使用标准QIODevices的read()和write()函数交换数据报,而无需为每个数据报指定接收器。

使用示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 初始化一个QUdpSocket,绑定地址和端口
void Server::initSocket()
{
    // 创建一个UDP套接字对象
    udpSocket = new QUdpSocket(this);

    // 绑定套接字到本地主机的IP地址和端口号
    udpSocket->bind(QHostAddress::LocalHost, 7755);

    // 将UDP套接字的readyRead信号连接到readPendingDatagrams槽函数
    connect(udpSocket, &QUdpSocket::readyRead, this, &Server::readPendingDatagrams);
}

void Server::readPendingDatagrams()
{
    // 循环读取待处理的广播数据报
    while (udpSocket->hasPendingDatagrams()) {
        // 接收一个广播数据报
        QNetworkDatagram datagram = udpSocket->receiveDatagram();

        // 处理接收到的广播数据报
        processTheDatagram(datagram);
    }
}

前面将的都是一对一的UDP通讯,叫做单播(unicast), UDP通讯还有广播(boardcast)和多播/组播(multicast),广播通常用于实现网络发现协议,例如查找网络上的哪个主机具有最大的可用硬盘空间。 要广播数据报,只需将其发送到特殊地址QHostAddress::Broadcast(255.255.255.255),或发送到本地网络的广播地址。

QUdpSocket还支持多播/组播(multicast)。UDP客户端加入一个有组播IP地址的多播组,向组播地址发送数据报,组内成员将都收到数据报。 使用joinMulticastGroup()和leaveMulticastGroup()函数加入或者离开一个多播组。

16.3. 高层网络协议(HTTP,FTP等)

Qt 网络模块还提供了一些类,用于高层的网络协议(例如HTTP,FTP),主要是QNetworkRequest、QNetworkAccessManage、QNetworkReply。

网络请求由 QNetworkRequest 类表示,该类也充当与请求相关联的信息(例如,任何标头信息和所使用的加密)的常规容器。 构造请求对象时指定的URL确定用于请求的协议,支持HTTP,FTP和本地文件URL进行上载和下载。

网络操作的协调由 QNetworkAccessManager 类执行。使用QNetworkRequest类创建请求后, 将使用QNetworkAccessManager类来分派请求并发出信号以报告其进度。QNetworkAccessManager还协调使用Cookie来存储客户端上的数据,身份验证请求以及代理的使用。

对网络请求的响应由 QNetworkReply 类表示。由QNetworkAccessManager在发送网络请求后创建网络响应(QNetworkReply), QNetworkReply提供的信号可用于单独监视每个答复,或者开发人员可以选择为此目的使用管理器的信号,而放弃对答复的引用。 由于QNetworkReply是QIODevice的子类,因此可以同步或异步地处理答复,即作为阻塞或非阻塞操作。

16.4. 例程说明

例程是一个简单的TCP socket通讯。

16.4.1. 代码简单讲解

在Qt Creator创建一个工程,包含UI界面,界面布局参考下配套例程。

在主函数中,创建创建一个QActionGroup,用于选择当作TCP客户端还是TCP服务端, 然后为TCP客户端创建一个socket(tcpClient),为TCP服务端创建一个QTcpServer,监听接受客户端的请求。

lubancat_qt_tutorial_code/QtNetwork/TcpCommunication/mainwindow.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);   // 初始化UI

    QActionGroup *tcpGroupAction = new QActionGroup(this);   // 创建一个QActionGroup
    tcpGroupAction->setExclusive(true);                      // 设置QActionGroup中Qaction互斥选中

    tcpGroupAction->addAction(ui->actionClient);   // 将客户端Qaction添加到动作组中
    tcpGroupAction->addAction(ui->actionServer);   // 将服务器Qaction添加到动作组中
    // 连接QActionGroup的triggered信号到updateTcpGroup槽函数
    connect(tcpGroupAction, &QActionGroup::triggered, this, &MainWindow::updateTcpGroup);

    ui->btn_connect->setEnabled(true);       // 启用连接按钮
    ui->btn_disconnect->setEnabled(false);   // 禁用断开连接按钮
    this->setWindowTitle("TCP客户端");        // 设置主窗口标题为"TCP客户端"

    QString localIP=getLocalIP();        // 获取本机IP地址
    ui->comboServer->addItem(localIP);   // 将本机IP地址添加到服务器下拉框中
    ui->comboClient->addItem(localIP);   // 将本机IP地址添加到客户端下拉框中

    // 创建TCP socket
    tcpClient=new QTcpSocket(this);   // 创建一个QTcpSocket对象,用于TCP客户端的连接和数据传输

    // 关联tcp客户端socket相关槽函数
    connect(tcpClient,SIGNAL(connected()),   this,SLOT(do_connected()));
    connect(tcpClient,SIGNAL(disconnected()),this,SLOT(do_disconnected()));
    connect(tcpClient,SIGNAL(readyRead()),   this,SLOT(do_tcpClient_ReadyRead()));
    connect(tcpClient,&QTcpSocket::stateChanged,this,&MainWindow::do_socketStateChange);

    // 创建TCP server
    tcpServer=new QTcpServer(this);   // 创建一个QTcpServer对象,用于监听和接受TCP客户端的连接请求
    // 当有新的连接请求时,执行do_newConnection()槽函数
    connect(tcpServer,SIGNAL(newConnection()),this,SLOT(do_newConnection()));
}

用做TCP服务端时,设置服务端的IP的端口,点击监听按钮,就会 当接受到一个连接,执行do_newConnection()槽函数,在该函数中创建一个Socket用于通信。 当该Socket接受到数据时,执行do_tcpSocket_ReadyRead函数,读取缓冲区数据,并显示在plainTextEdit。

lubancat_qt_tutorial_code/QtNetwork/TcpCommunication/mainwindow.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// QTcpServer  newConnection的槽函数
void MainWindow::do_newConnection()
{
    ui->plainTextEdit->appendPlainText("一个客户端连接");
    tcpSocket = tcpServer->nextPendingConnection();   // 创建TCP服务端通信socket

    // 关联tcpSocket相关槽函数
    connect(tcpSocket, SIGNAL(connected()),this, SLOT(do_clientConnected()));
    connect(tcpSocket, SIGNAL(disconnected()),this, SLOT(do_clientDisconnected()));
    connect(tcpSocket,SIGNAL(readyRead()),  this,SLOT(do_tcpSocket_ReadyRead()));
    connect(tcpSocket,&QTcpSocket::stateChanged,this,&MainWindow::do_socketStateChange);
}

// 读取缓冲区
void MainWindow::do_tcpSocket_ReadyRead()
{
    // tcp 服务端socket读取
    while(tcpSocket->canReadLine())
        ui->plainTextEdit->appendPlainText("接收:"+tcpSocket->readLine());
}

用做TCP客户端时,设置服务端的监听地址和端口,点击连接按钮,就会执行on_btn_connect_clicked槽函数, 使用connectToHost(ipaddr,port)函数连接服务端,连接成功会显示socket的状态:

lubancat_qt_tutorial_code/QtNetwork/TcpCommunication/mainwindow.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// TCP客户端连接服务器
void MainWindow::on_btn_connect_clicked()
{
    QString ipaddr=ui->comboServer->currentText();
    quint16 port=ui->spinPort->value();
    tcpClient->connectToHost(ipaddr,port);      // 连接到服务端
}

// socket状态变化
void MainWindow::do_socketStateChange(QAbstractSocket::SocketState socketState)
{
    switch(socketState)
    {
    case QAbstractSocket::UnconnectedState:
        ui->statusbar->showMessage("tcpsocket状态:UnconnectedState");
        break;
    case QAbstractSocket::HostLookupState:
        ui->statusbar->showMessage("tcpsocket状态:HostLookupState");
        break;
    case QAbstractSocket::ConnectingState:
        ui->statusbar->showMessage("tcpsocket状态:ConnectingState");
        break;
    case QAbstractSocket::ConnectedState:
        ui->statusbar->showMessage("tcpsocket状态:ConnectedState");
        break;
    case QAbstractSocket::BoundState:
        ui->statusbar->showMessage("tcpsocket状态:BoundState");
        break;
    case QAbstractSocket::ClosingState:
        ui->statusbar->showMessage("tcpsocket状态:ClosingState");
        break;
    case QAbstractSocket::ListeningState:
        ui->statusbar->showMessage("tcpsocket状态:ListeningState");
    }
}

16.4.2. 编译构建

以鲁班猫Debian10带桌面的系统为例,打开工程,配置两个编译套件:

  • Ubuntu 选择 Desktop Qt 5.15.2 GCC 64bit 套件,编译运行:

  • LubanCat 选择 Lubancat_rk_debian10,只编译。

build001

16.4.3. 运行结果

运行测试需要一个客户端一个服务端,这里我们将在虚拟机中运行客户端,鲁班猫板卡运行服务端(也可以单独一台设备使用127.0.0.1测试)。

在虚拟机中点击交叉编译,部署到鲁班猫板卡,然后使用命令运行程序。然后设置监听地址和端口,点击“开始监听”按钮:

result001

虚拟机中,直接点击编译并运行程序,然后点击设置服务器地址(鲁班猫设备IP)和端口,然后点击“连接”按钮:

result001

然后发送消息,测试TCP socket通讯。