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的服务器。
函数 |
描述 |
---|---|
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一起使用。
函数 |
描述 |
---|---|
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连接:
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,监听接受客户端的请求。
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。
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的状态:
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,只编译。
16.4.3. 运行结果¶
运行测试需要一个客户端一个服务端,这里我们将在虚拟机中运行客户端,鲁班猫板卡运行服务端(也可以单独一台设备使用127.0.0.1测试)。
在虚拟机中点击交叉编译,部署到鲁班猫板卡,然后使用命令运行程序。然后设置监听地址和端口,点击“开始监听”按钮:
虚拟机中,直接点击编译并运行程序,然后点击设置服务器地址(鲁班猫设备IP)和端口,然后点击“连接”按钮:
然后发送消息,测试TCP socket通讯。