Qt 系统相关 - 网络
Qt 网络
针对网络编程,都是操作系统提供的一组 API(Socket API),Qt 为了支持跨平台,对网络编程的 API 也进行了重新封装。
但是对于 C++ 标准库中,并没有提供网络编程的 API 的封装!
咱们接下来的课程内容中重点介绍 Qt 的网络相关的 API 的使用。
注意:
实际 Qt 开发中进行网络编程,也不一定使用 Qt 封装的网络 API,也有一定可能使用的是系统原生 API 或者其他第三方框架的 API。
在进行网络编程之前,需要在项目中的 .pro
文件中添加 network
模块。
添加之后要手动编译一下项目,使 Qt Creator 能够加载对应模块的头文件。
之前学过的 Qt 的各种控件,各种内容,都是包含在 QtCore 模块中的,这是默认就添加的!
UDP Socket
核心 API 概览
主要的类有两个:QUdpSocket 和 QNetworkDatagram
QUdpSocket 表示一个 UDP 的 socket 文件。
QUdpSocket 核心 API
名称 | 类型 | 说明 | 对标原生 API |
---|---|---|---|
bind | 方法 | 绑定指定的端口号。 | bind |
receiveDatagram | 方法 | 返回 QNetworkDatagram ,读取一个 UDP 数据报。 | recvfrom |
writeDatagram | 方法 | 发送一个 UDP 数据报。 | sendto |
readyRead | 信号 | 在收到数据并准备就绪后触发。 | 无 (类似于 IO 多路复用的通知机制) |
readyRead --- 当 socket 收到请求的时候,QUdpSocket 就会触发这个信号,此时就可以在槽函数里完成读取请求的操作了! --- 基于信号槽,就天然达成了“事件驱动”这样的一种网络编程的方式!
QNetworkDatagram 表示一个 UDP 数据报。
QNetworkDatagram 核心 API
名称 | 类型 | 说明 | 对标原生 API |
---|---|---|---|
QNetworkDatagram | 构造函数 | 通过 QByteArray 、目标 IP 地址、目标端口号构造一个 UDP 数据报。通常用于发送数据时。 | 无 |
data() | 方法 | 获取数据报内部持有的数据,返回 QByteArray | 无 |
senderAddress() | 方法 | 获取数据报中包含的对端的 IP 地址。 | 无,recvfrom 包含了该功能。 |
senderPort() | 方法 | 获取数据报中包含的对端的端口号。 | 无,recvfrom 包含了该功能。 |
这些API是Qt网络编程中常用的函数,可以帮助开发者更好地管理和控制网络通信。
回显服务器
1. 创建界面,包含一个 QListWidget
用来显示消息。
- 创建
QUdpSocket
成员 - 修改
widget.h
class Widget : public QWidget
{Q_OBJECT
public:Widget(QWidget *parent = nullptr);~Widget();
private:Ui::Widget *ui;QUdpSocket *socket;
};
修改 widget.cpp
,完成 socket
后续的初始化
一般来说,要先连接信号槽,再绑定端口。一旦绑定窗口了,就意味着请求就可以被收到了!
如果相反,在完成绑定之后,在连接信号槽之前,有客户端将请求发送过来了,此时就可能读不到这个请求!
Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);// 1. 设置窗口标题this->setWindowTitle("服务器");// 2. 实例化 socketsocket = new QUdpSocket(this);// 3. 连接信号槽,处理收到的请求connect(socket, &QUdpSocket::readyRead, this, &Widget::processRequest);// 4. 绑定端口bool ret = socket->bind(QHostAddress::Any, 9990);if (!ret) {QMessageBox::critical(nullptr, "服务器启动出错", socket->errorString());return;}
}
实现 processRequest
,完成处理请求的过程
读取请求并解析
根据请求计算响应
把响应写回到客户端
void Widget::processRequest()
{// 1. 读取请求const QNetworkDatagram& requestDatagram = socket->receiveDatagram();QString request = requestDatagram.data();// 2. 根据请求计算响应const QString response = process(request);// 3. 把响应写回到客户端QNetworkDatagram responseDatagram(response.toUtf8(),requestDatagram.senderAddress(), requestDatagram.senderPort());socket->writeDatagram(responseDatagram);
}
实现 process
函数
由于我们此处是实现回显服务器,所以 process
方法中并没有包含实质性的内容。
QString Widget::process(const QString &request)
{return request;
}
💡 “根据请求处理响应” 是服务器开发中的最核心的步骤。
一个商业服务器程序,这里的逻辑可能是几万行几十万行代码量级的。
此时,服务器程序编写完毕。
但是直接运行还看不出效果,还需要搭配客户端来使用。
回显客户端
创建界面,包含一个 QLineEdit
,QPushButton
,QListWidget
先使用水平布局把
QLineEdit
和QPushButton
放好,并设置这两个控件的垂直方向的sizePolicy
为Expanding
再使用垂直布局把
QListWidget
和上面的水平布局放好。设置垂直布局的
layoutStretch
为 5, 1(当然这个尺寸比例根据个人喜好微调)。
在 widget.cpp
中,先创建两个全局常量,表示服务器的 IP 和 端口
// 提前定义好服务器的 IP 和 端口
const QString SERVER_IP = "127.0.0.1";
const quint16 SERVER_PORT = 9990;
- 创建
QUdpSocket
成员 - 修改
widget.h
,定义成员
class Widget : public QWidget
{Q_OBJECT
public:Widget(QWidget *parent = nullptr);~Widget();
private:Ui::Widget *ui;QUdpSocket *socket;
};
修改 widget.cpp
,初始化 socket
Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);// 1. 设置窗口名字this->setWindowTitle("客户端");// 2. 实例化 socketsocket = new QUdpSocket(this);
}
给发送按钮 slot 函数,实现发送请求
void Widget::on_pushButton_clicked()
{// 1. 获取到输入框的内容const QString text = ui->lineEdit->text();// 2. 构造请求数据QNetworkDatagram requestDatagram(text.toUtf8(), QHostAddress(SERVER_IP),SERVER_PORT);// 3. 发送请求socket->writeDatagram(requestDatagram);// 4. 信息添加到列表框中ui->listWidget->addItem("客户端说:" + text);// 5. 清空输入框ui->lineEdit->setText("");
}
再次修改 Widget
的构造函数,通过信号槽,来处理服务器的响应。
connect(socket, &QUdpSocket::readyRead, this, [=]() {const QNetworkDatagram responseDatagram = socket->receiveDatagram();QString response = responseDatagram.data();ui->listWidget->addItem(QString("服务器说:") + response);
});
最终执行效果
启动多个客户端都可以正常工作。
TCP Socket
核心 API 概览
核心类是两个:QTcpServer 和 QTcpSocket
QTcpServer 用于监听端口,和获取客户端连接。
名称 | 类型 | 说明 | 对标原生 API |
---|---|---|---|
listen(const QHostAddress&, quint16 port) | 方法 | 绑定指定的地址和端口号,并开始监听。 | bind 和 listen |
nextPendingConnection() | 方法 | 从系统中获取到一个已经建立好的 tcp 连接。返回一个 QTcpSocket,表示这个客户端的连接。通过这个 socket 对象完成和客户端之间的通信。 | accept |
newConnection | 信号 | 有新的客户端建立连接好之后触发。 | 无(但是类似于 IO 多路复用中的通知机制) |
QTcpSocket 用户客户端和服务器之间的数据交互。
名称 | 类型 | 说明 | 对标原生 API |
---|---|---|---|
readAll() | 方法 | 读取当前接收缓冲区中的所有数据。返回 QByteArray 对象。 | read |
write(const QByteArray&) | 方法 | 把数据写入 socket 中。 | write |
deleteLater | 方法 | 暂时把 socket 对象标记为无效。Qt 会在下个事件循环中析构释放该对象。 | 无(但是类似于 "半自动化的垃圾回收") |
readyRead | 信号 | 有数据到达并准备就绪时触发。 | 无(但是类似于 IO 多路复用中的通知机制) |
disconnected | 信号 | 连接断开时触发。 | 无(但是类似于 IO 多路复用中的通知机制) |
QByteArray 用于表示一个字节数组。可以很方便的和 QString 进行相互转换。
例如:
使用 QString 的构造函数即可把 QByteArray 转换成 QString。
使用 QString 的
toUtf8
函数即可把 QString 转换成 QByteArray。
回显服务器
创建界面。包含一个 QListWidget,用于显示收到的数据。
创建 QTcpServer 并初始化
修改 widget.h
,添加 QTcpServer 指针成员。
class Widget : public QWidget
{Q_OBJECTpublic:Widget(QWidget *parent = nullptr);~Widget();private:Ui::Widget *ui;// 创建 QTcpServerQTcpServer* tcpServer;
};
修改 widget.cpp
,实例化 QTcpServer 并进行后续初始化操作。
设置窗口标题
实例化 TCP server(父元素设为当前控件,会在父元素销毁时被一起销毁)。
通过信号槽,处理客户端建立的新连接。
监听端口
Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);this->setWindowTitle("服务器端");// 2. 实例化 TCP servertcpServer = new QTcpServer(this);// 3. 通过信号槽,处理客户端建立的新连接。connect(tcpServer, &QTcpServer::newConnection, this, &Widget::processConnection);// 4. 监听端口bool ret = tcpServer->listen(QHostAddress::Any, 9090);if (!ret) {QMessageBox::critical(nullptr, "服务器启动失败!", tcpServer->errorString());exit(0);}
}
继续修改 widget.cpp
,实现处理连接的具体方法 processConnection
获取到新的连接对应的 socket。
通过信号槽,处理收到请求的情况
通过信号槽,处理断开连接的情况。
void Widget::processConnection()
{// 1. 获取到新的连接对应的 socket.QTcpSocket* clientSocket = tcpServer->nextPendingConnection();QString log = QString("[") + clientSocket->peerAddress().toString()+ ":" + QString::number(clientSocket->peerPort()) + "] 客户端上线!";ui->listWidget->addItem(log);// 2. 通过信号槽,处理收到请求的情况.connect(clientSocket, &QTcpSocket::readyRead, this, [=]() {QString request = clientSocket->readAll();QString requestStr = process(request);QString response = requestStr;clientSocket->write(response.toUtf8());QString log = QString("[") + clientSocket->peerAddress().toString()+ ":" + QString::number(clientSocket->peerPort()) + "] req: "+ request + ", resp: " + response;ui->listWidget->addItem(log);});// 3. 通过信号槽,处理断开连接的情况connect(clientSocket, &QTcpSocket::disconnected, this, [=]() {QString log = QString("[") + clientSocket->peerAddress().toString()+ ":" + QString::number(clientSocket->peerPort()) + "] 客户端下线!";ui->listWidget->addItem(log);// 删除 clientSocketclientSocket->deleteLater();});
}
实现 process
方法,实现根据请求处理响应。
由于我们此处是实现回显服务器,所以 process
方法中并没有包含实质性的内容。
QString Widget::process(const QString &request)
{return request;
}
回显客户端
创建界面。包含一个 QLineEdit,QPushButton,QListWidget
先使用水平布局把 QLineEdit 和 QPushButton 放好,并设置这两个控件的垂直方向的
sizePolicy
为Expanding
再使用垂直布局把 QListWidget 和上面的水平布局放好。
设置垂直布局的
layoutStretch
为 5, 1(当然这个尺寸比例根据个人喜好微调)。
创建 QTcpSocket 并实例化
修改 widget.h
,创建成员。
class Widget : public QWidget
{Q_OBJECTpublic:Widget(QWidget *parent = nullptr);~Widget();private:Ui::Widget *ui;// 新增 QTcpSocketQTcpSocket* socket;
};
修改 widget.cpp
,对 QTcpSocket 进行实例化。
设置窗口标题
Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);// 1. 设置窗口标题.this->setWindowTitle("客户端");// 2. 实例化 socket 对象.socket = new QTcpSocket(this);// 3. 和服务器建立连接.socket->connectToHost("127.0.0.1", 9090);// 4. 等待并确认连接是否出错.if (!socket->waitForConnected()) {QMessageBox::critical(nullptr, "连接服务器出错!", socket->errorString());exit(0);}
}
修改 widget.cpp
,给按钮增加点击的 slot 函数,实现发送请求给服务器。
void Widget::on_pushButton_clicked()
{// 获取输入框的内容const QString text = ui->lineEdit->text();// 清空输入框内容ui->lineEdit->setText("");// 把消息显示到界面上ui->listWidget->addItem(QString("客户端说:") + text);// 发送消息给服务器socket->write(text.toUtf8());
}
修改 widget.cpp
中的 Widget 构造函数,通过信号槽,处理收到的服务器的响应。
// 处理服务器返回的响应.
connect(socket, &QTcpSocket::readyRead, this, [=]() {QString response = socket->readAll();qDebug() << response;ui->listWidget->addItem(QString("服务器说:") + response);
});
先启动服务器,再启动客户端(可以启动多个),最终执行效果:
之前在学习 Linux 时写的 TCP 的回显服务器的时候,遇到了一个问题~~ 多个客户端同时访问的时候,就只有一个生效~~
后来引入了多线程,每个客户端安排一个单独的线程,问题才得到改善~~
之前写的那个程序,之所以出现上述问题,和 TCP,和多线程 都没啥关系…… 从来没有说法,说 TCP 服务器必须使用多线程编写!!!
之前存在这个问题的本质原因,是写双重循环,里层循环没有及时结束,导致外层循环不能快速的第二次调用到 accept,导致第二个客户端无法进行处理了。
引入多线程,本质上就是把双重循环,化简成两个独立的循环。
注意:也是思考点:
服务器通常需要不断地监听客户端的连接请求和数据传输,这在代码中通常表现为一个持续运行的循环(while循环)。然而,在现代的事件驱动编程模型中,这种持续的轮询或循环检查不再是必须的,因为操作系统和编程框架提供了更为高效的事件处理机制。
在代码中,使用了Qt框架来实现服务器。Qt是一个跨平台的应用程序和用户界面框架,它提供了事件驱动的编程模型。在这种模型中,服务器不需要显式地编写一个while循环来不断检查是否有新的连接或数据。相反,Qt框架会在后台处理这些事件,并在适当的时候触发相应的信号(signals)。
在代码中,QTcpServer
对象负责监听端口并接受新的连接。当有新的客户端连接时,QTcpServer
会发出listen
信号,然后通过processConnection
槽函数(slot)来处理新的连接。这是通过连接(connect)QTcpServer::listen
信号到Widget::processConnection
槽函数实现的:
connect(tcpServer, &QTcpServer::listen, this, &Widget::processConnection);
当新的数据到达时,QTcpSocket
对象会发出readyRead
信号,然后通过一个lambda表达式来读取数据并处理请求。这是通过连接QTcpSocket::readyRead
信号到一个lambda表达式实现的:
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);
});
同样,当客户端断开连接时,QTcpSocket
会发出disconnected
信号,然后通过一个lambda表达式来处理断开连接的事件:
connect(clientSocket, &QTcpSocket::disconnected, this, [=](){QString log = "[" + clientSocket->peerAddress().toString() + ":" +QString::number(clientSocket->peerPort()) + "] 客户端下线";ui->listWidget->addItem(log);clientSocket->deleteLater();//下一轮事件进行delete!
});
这种事件驱动的模型使得服务器能够高效地处理多个客户端的连接和数据,而不需要显式地编写一个while循环。Qt框架会自动管理这些事件,并在适当的时候调用相应的处理函数。
HTTP Client
- HTTP 使用场景比 TCP/UDP 更多一些。
- Qt 中提供了 HTTP 客户端相关支持。
- HTTP 协议本质上是基于 TCP 协议实现的,实现一个 HTTP 客户端 / 服务器,本质上是基于 TCP socket 进行封装。
- 需要注意的是,Qt 只是提供了 HTTP 客户端,而没有提供 HTTP 服务器的库。
进行 Qt 开发时,和服务器之间的通信很多时候也会用到 HTTP 协议。
通过 HTTP 从服务器获取数据。
通过 HTTP 向服务器提交数据。
核心 API
关键类主要是三个:QNetworkAccessManager,QNetworkRequest,QNetworkReply。
方法 | 说明 |
---|---|
get(const QNetworkRequest&) | 发起一个 HTTP GET 请求,返回 QNetworkReply 对象。 |
post(const QNetworkRequest&, const QByteArray&) | 发起一个 HTTP POST 请求,返回 QNetworkReply 对象。 |
QNetworkRequest 表示一个 HTTP 请求(不含 body)。
如果需要发送一个带有 body 的请求(比如 post),会在 QNetworkAccessManager 的 post 方法中通过单独的参数来传入 body。
方法 | 说明 |
---|---|
QNetworkRequest(const QUrl&) | 通过 URL 构造一个 HTTP 请求。 |
setHeader(QNetworkRequest::KnownHeaders header, const QVariant &value) | 设置请求头。 |
其中 QNetworkRequest::KnownHeaders
是一个枚举类型,常用取值:
取值 | 说明 |
---|---|
ContentTypeHeader | 描述 body 的类型。 |
ContentLengthHeader | 描述 body 的长度。 |
LocationHeader | 用于重定向报文中指定重定向地址(响应中使用,请求用不到) |
CookieHeader | 设置 cookie |
UserAgentHeader | 设置 UserAgent |
QNetworkReply 表示一个 HTTP 响应。这个类同时也是 QIODevice 的子类。
方法 | 说明 |
---|---|
error() | 获取出错状态。 |
errorString() | 获取出错原因的文本。 |
readAll() | 读取响应 body。 |
header(QNetworkRequest::KnownHeaders header) | 读取响应指定 header 的值。 |
此外,QNetworkReply 还有一个重要的信号 finished
会在客户端收到完整的响应数据之后触发。
代码示例
给服务器发送一个 GET 请求。
创建界面。包含一个 QLineEdit,QPushButton
先使用水平布局把 QLineEdit 和 QPushButton 放好,并设置这两个控件的垂直方向的
sizePolicy
为 Expanding再使用垂直布局把 QPlainTextEdit 和上面的水平布局放好。(QPlainTextEdit 的
readOnly
设置为true
)设置垂直布局的
layoutStretch
为 5, 1(当然这个尺寸比例根据个人喜好微调)。
💡 此处建议使用 QPlainTextEdit 而不是 QTextEdit,主要因为 QTextEdit 要进行富文本解析,如果得到的 HTTP 响应体积很大,就会导致界面渲染缓慢甚至被卡住。
修改 widget.h
,创建 QNetworkAccessManager 属性
class Widget : public QWidget
{Q_OBJECTpublic:Widget(QWidget *parent = nullptr);~Widget();private slots:void on_pushButton_clicked();private:Ui::Widget *ui;// 新增属性QNetworkAccessManager* manager;
};
修改 widget.cpp
,创建实例
Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);// 实例化属性manager = new QNetworkAccessManager(this);
}
编写按钮的 slot 函数,实现发送 HTTP 请求功能
void Widget::on_pushButton_clicked()
{// 1. 获取到输入框中的 url,构造 Url 对象QUrl url(ui->lineEdit->text());// 2. 构造 HTTP 请求对象QNetworkRequest request(url);// 3. 发送 GET 请求QNetworkReply* response = manager->get(request);// 4. 通过信号槽来处理响应connect(response, &QNetworkReply::finished, this, [=]() {if (response->error() == QNetworkReply::NoError) {// 响应正确QString html(response->readAll());ui->plainTextEdit->setPlainText(html);qDebug() << html;} else {// 响应出错ui->plainTextEdit->setPlainText(response->errorString());}response->deleteLater();});
}
执行程序,观察效果
发送 POST 请求代码也是类似。使用 manager->post()
即可。此处不再演示。
其他模块
Qt 中还提供了 FTP, DNS, SSL 等网络相关的组件工具。此处不再一一展开介绍。有需要的同学可以自行翻阅官方文档学习相关 API 的使用。