Qt的网络编程
使用 Qt 的网络编程 API 需要先在 .pro 文件中添加 network 模块。Qt 的模块提供了静态库和动态库两个版本。
不默认包含网络等其他模块,是为了使 Qt 生成的可执行程序更加轻量化。
#.pro
QT +=core gui network
#添加到如上位置
Qt 封装了自己的网络编程接口,将 Qt 的信号和槽的机制运用到了 Qt 的网络编程接口上,这些接口的使用方法与 C 语言原生的套接字编程接口区别较大,尤其是信号和槽的机制使得 Qt 服务器设计可很少使用甚至不使用多线程,也能实现正常的服务器功能。但由于服务器一般都不会做成图形化界面,或者说不会使用 Qt 进行编写,也就无所谓 Qt 的服务器设计了。
1. UDP Socket
Qt 提供了两个类,用 QUdpSocket
来表示一个 UDP 的 socket 文件,用 QNetworkDatagram
来表示一个 UDP 数据报。
QUdpSocket:
声明 | 类型 | 说明 | 对标原生 API |
---|---|---|---|
bind(constQHostAddress&, quint16) | 方法 | 绑定指定的端口号。 | bind() |
QNetworkDatagram receiveDatagram() | 方法 | 读取一个 UDP 数据报。 | recvfrom() |
writeDatagram(const QNetworkDatagram&) | 方法 | 发送一个 UDP 数据报。 | sendto |
readyRead | 信号 | 在收到数据并准备就绪后触发。 | 无 |
quint16
是 Qt 提供的短整型类型,由于 C++ 并未规定short int
的大小,于是 Qt 为了解耦自创了自己的短整型类型。
QNetworkDatagram:
声明 | 类型 | 说明 | 对标原生 API |
---|---|---|---|
QNetworkDatagram(const QByteArray&,const QHostAddress&,quint16) | 构造函数 | 构造一个 UDP 数据报,通常用于发送数据时使用。 | 无 |
QByteArray data() | 方法 | 获取数据报内部持有的数据。 | 无 |
senderAddress() | 方法 | 获取数据报中包含的对端 IP 地址。 | 无 |
senderPort() | 方法 | 获取数据报中包含的对端端口号。 | 无 |
QString
提供了一个QByteArray
的构造,可以使用QString
直接赋值。
1.1 UDP回显服务器
注意 Qt 的信号和槽提供了一种类似于多路复用 IO 的机制,当对端发送数据到来,本机准备读取时,就会触发 readyRead
信号并调用相关槽函数(自定义)。
1.1.1 服务器
mainwindow.h:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QNetworkDatagram>
#include <QUdpSocket>
QT_BEGIN_NAMESPACE
namespace Ui {
class MainWindow;
}
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
Ui::MainWindow *ui;
QUdpSocket* socket;
void processRequest();
QString process(const QString& request);
};
#endif // MAINWINDOW_H
mainwindow.cpp:
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QMessageBox>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
socket = new QUdpSocket(this);
this->setWindowTitle("服务器");
connect(socket,&QUdpSocket::readyRead,this,&MainWindow::processRequest);
bool ret = socket->bind(QHostAddress::Any,6666);
if(!ret)
{
QMessageBox::critical(this,"端口号绑定失败",socket->errorString());
}
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::processRequest()
{
const QNetworkDatagram& requestDatagram = socket->receiveDatagram();
QString request = requestDatagram.data();
const QString& response = process(request);
QNetworkDatagram responseDatagram(response.toUtf8(),requestDatagram.senderAddress(),requestDatagram.senderPort());
socket->writeDatagram(responseDatagram);
QString log = "[" + requestDatagram.senderAddress().toString()+ ":" +QString::number(requestDatagram.senderPort())+"] req: "
+request+"resp: "+response;
ui->listWidget->addItem(log);
}
QString MainWindow::process(const QString& request)
{
return request;
}
1.1.2 客户端
mainwindow.h:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QNetworkDatagram>
#include <QUdpSocket>
QT_BEGIN_NAMESPACE
namespace Ui {
class MainWindow;
}
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
void on_pushButton_clicked();
private:
Ui::MainWindow *ui;
QUdpSocket* socket;
void processResponse();
};
#endif // MAINWINDOW_H
mainwindow.cpp:
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
socket = new QUdpSocket(this);
this->setWindowTitle("客户端");
connect(socket,&QUdpSocket::readyRead,this,&MainWindow::processResponse);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::processResponse()
{
const QNetworkDatagram& responseDatagram = socket->receiveDatagram();
QString response = responseDatagram.data();
ui->listWidget->addItem("服务器:"+response);
}
void MainWindow::on_pushButton_clicked()
{
if(ui->lineEdit->text()=="")
{
return;
}
const QString& text = ui->lineEdit->text();
QNetworkDatagram requestDatagram(text.toUtf8(),QHostAddress("127.0.0.1"),6666);
socket->writeDatagram(requestDatagram);
ui->listWidget->addItem("客户端:"+text);
ui->lineEdit->setText("");
}
2. TCP Socket
Qt 提供了两个类,用 QTcpServer
来监听端口,获取客户端连接。用 QTcpSocket
来实现客户端和服务器之间的数据交换。
QTcpServer:
声明 | 类型 | 说明 | 对标原生 API |
---|---|---|---|
listen(constQHostAddress&, quint16 port) | 方法 | 绑定指定的地址和端口号,并开始监听。 | bind() 和 listen() |
nextPendingConnection() | 方法 | 从系统中获取一个已经建立好的 TCP 连接。返回一个 QTcpSocket 表示这个客户端的连接,通过这个 socket 对象完成和客户端之间的通信。 |
accept() |
newConnection | 信号 | 有新的客户端建立连接好之后触发。 | 无,类似于多路复用 |
QTcpSocket
声明 | 类型 | 说明 | 对标原生 API |
---|---|---|---|
QBytearray readAll() | 方法 | 读取当前接收缓冲区中的所有数据,返回 QBytearray 对象。 |
read() |
write(const QByteArray&) | 方法 | 把数据写入 socket 中。 | write() |
deleteLater | 方法 | 暂时把 socket 对象标记为无效。Qt 会在下个事件循环中析构释放该对象。 | 无 |
readyRead | 信号 | 有数据到达并准备就绪时就触发。 | 无 |
disconnected | 信号 | 连接断开时触发。 | 无 |
2.1 TCP回显服务器
注意,回显服务器的设计没有考虑到粘包问题,在实际的服务器设计中,应当自定义应用层协议,在报文里写好本次发送的数据包总大小,并使用一个足够大的字节数组作为缓冲区来接收,然后按照协议的方法解析报文。
2.1.1 服务器
mainwindow.h
注意服务器是使用 QTcpServer
类型作为成员变量监听套接字,当接收到新的连接之后,再创建 QTcpSocket
变量作为通信的套接字使用。
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QTcpServer>
QT_BEGIN_NAMESPACE
namespace Ui {
class MainWindow;
}
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
Ui::MainWindow *ui;
QTcpServer* tcpServer;
void processConnection();
QString process(const QString request);
};
#endif // MAINWINDOW_H
mainwindow.cpp:
每有一个连接来到,服务器就会有一个 QTcpSocket*
对象,随着客户端越来越多,如果不释放就会释放就会导致严重的内存泄漏问题。但是手动 delete 并不可行,因为槽函数都是依赖这个对象来进行操作的,最好使用 Qt 提供的 deleteLater()
接口,它会在下次事件循环时,将对象释放掉。
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QMessageBox>
#include <QTcpSocket>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
this->setWindowTitle("服务器");
tcpServer = new QTcpServer(this);
connect(tcpServer,&QTcpServer::newConnection,this,&MainWindow::processConnection);
bool ret = tcpServer->listen(QHostAddress::Any,6666);
if(!ret)
{
QMessageBox::critical(this,"服务器启动失败!",tcpServer->errorString());
exit(1);
}
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::processConnection()
{
QTcpSocket* clientSocket = tcpServer->nextPendingConnection();
QString log ="[" + clientSocket->peerAddress().toString()+ ":" + QString::number(clientSocket->peerPort())+"]客户端上线!";
ui->listWidget->addItem(log);
connect(clientSocket,&QTcpSocket::readyRead,this,[=]()
{
QString request = clientSocket->readAll();
const QString& response = process(request);
clientSocket->write(response.toUtf8());
QString log = "[" + clientSocket->peerAddress().toString()+":"+QString::number(clientSocket->peerPort())+"] req: "+request
+",resp: "+response;
ui->listWidget->addItem(log);
});
connect(clientSocket,&QTcpSocket::disconnected,this,[=]()
{
QString log = "[" + clientSocket->peerAddress().toString()+":"+QString::number(clientSocket->peerPort())+"]客户端下线!";
ui->listWidget->addItem(log);
//手动释放 clientSocket,使用Qt提供的方法,在下一次事件循环时释放
clientSocket->deleteLater();
});
}
QString MainWindow::process(const QString request)
{
return request;
}
2.1.2 客户端
mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QTcpSocket>
QT_BEGIN_NAMESPACE
namespace Ui {
class MainWindow;
}
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
void on_pushButton_clicked();
private:
Ui::MainWindow *ui;
QTcpSocket* socket;
};
#endif // MAINWINDOW_H
mainwindow.cpp
connectToHost()
是在进行三次握手。
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QMessageBox>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
this->setWindowTitle("客户端");
socket = new QTcpSocket(this);
socket->connectToHost("127.0.0.1",6666);
connect(socket,&QTcpSocket::readyRead,this,[=]()
{
QString response = socket->readAll();
ui->listWidget->addItem(" 服务器:"+response);
});
bool ret=socket->waitForConnected();
if(!ret)
{
QMessageBox::critical(this,"服务器连接出错",socket->errorString());
exit(1);
}
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_pushButton_clicked()
{
const QString& text = ui->lineEdit->text();
if(text=="")
{
return;
}
socket->write(text.toUtf8());
ui->listWidget->addItem("客户端:"+text);
ui->lineEdit->setText("");
}
3. HTTP
Qt 提供了 HTTP 客户端的库,但没有提供服务器的库。Qt 的 HTTP 库并没有像浏览器那样解析 HTML ,进行类似于网页浏览的操作。一般用于通过 HTTP 从服务器获取数据或向服务器提交数据。
Qt 的 API 主要是三个类包含的,QNetworkAccessManager
、QNetworkRequest
、QNetworkReply
。
QNetworkAccessManager 提供了 HTTP 的核心操作:
方法 | 说明 |
---|---|
QNetworkReply get(constQNetworkRequest&) | 发起一个 HTTP GET 请求。 |
post(const QNetworkRequest&,const QByteArray&) | 发起一个 HTTP POST 请求。 |
QNetworkRequest 表示一个 HTTP 请求:
注意 QNetworkRequest
是不包含 body 的,body 需要通过 setHeader()
来包含。
方法 | 说明 |
---|---|
QNetworkRequest(constQUrl&) | 通过 URL 构造一个 HTTP 请求。 |
setHeader(QNetworkRequest::KnownHeaders header,const QVariant& value) | 设置请求头。 |
QVariant
表示一个类型可变的值,类似于 C 语言中的void*
。
QNetworkRequest::KnownHeaders
是一个枚举变量,常用取值:
取值 | 说明 |
---|---|
ContentTypeHeader | 描述 body 的类型。 |
ContentLengthHeader | 描述 body 的长度。 |
LocationHeader | 用于重定向报文中指定重定向地址。 |
CookieHeader | 设置 cookie。 |
UserAgentHeader | 设置 User-Agent。 |
QNetworkReply 表示一个 HTTP 响应:
方法 | 说明 |
---|---|
error() | 获取出错状态。 |
errorString() | 获取出错原因的文本。 |
readAll() | 读取响应 body。 |
header(QNetworkRequest::KnownHeaders header) | 读取指定 header 的值。 |
注意
QNetworkReply
的方法一般都是非阻塞的,即执行流不会等待方法获取请求或响应完成后再执行剩下的代码,Qt 通过提供了finished
信号,这个信号会在客户端收到完整的响应数据之后触发。