目录
[1. UDP Socket](#1. UDP Socket)
[1.1 核心 API 概览](#1.1 核心 API 概览)
[1.2 回显服务器](#1.2 回显服务器)
[1.3 回显客户端](#1.3 回显客户端)
[2. TCP Socket](#2. TCP Socket)
[2.1 核心 API 概览](#2.1 核心 API 概览)
[2.2 回显服务器](#2.2 回显服务器)
[2.3 回显客户端](#2.3 回显客户端)
[3. HTTP Client](#3. HTTP Client)
[3.1 核心 API](#3.1 核心 API)
[3.2 代码示例](#3.2 代码示例)
在进行网络编程之前, 需要在项目中的.pro 文件中添加 network 模块.
添加之后要手动编译一下项目, 使 Qt Creator 能够加载对应模块的头文件
1. UDP Socket
1.1 核心 API 概览
主要的类有两个. QUdpSocket 和 QNetworkDatagram
QUdpSocket 表示一个 UDP 的 socket 文件.

QNetworkDatagram 表示一个 UDP 数据报.

1.2 回显服务器
- 创建界面, 包含一个 QListWidget 用来显示消息.

- 创建 QUdpSocket 成员
修改 widget.h
cpp
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private:
Ui::Widget *ui;
QUdpSocket* socket;
};
修改 widget.cpp, 完成 socket 后续的初始化
一般来说, 要先连接信号槽, 再绑定端口.
如果顺序反过来, 可能会出现端口绑定好了之后, 请求就过来了. 此时还没来得及连接信号槽. 那么这个请求就有可能错过了.
cpp
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
// 1. 设置窗口标题
this->setWindowTitle("服务器");
// 2. 实例化 socket
socket = new QUdpSocket(this);
// 3. 连接信号槽, 处理收到的请求
connect(socket, &QUdpSocket::readyRead, this, &Widget::processRequest);
// 4. 绑定端口
bool ret = socket->bind(QHostAddress::Any, 9090);
if (!ret)
{
QMessageBox::critical(nullptr, "服务器启动出错", socket->errorString());
return;
}
}
- 实现 processRequest , 完成处理请求的过程
• 读取请求并解析
• 根据请求计算响应
• 把响应写回到客户端
cpp
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);
// 显示打印日志
QString log = "[" + requestDatagram.senderAddress().toString() + ":" + QString::number(requestDatagram.senderPort())+ "] req: " + request + ", resp: " + response;
ui->listWidget->addItem(log);
}
- 实现 process 函数
由于我们此处是实现回显服务器. 所以 process 方法中并没有包含实质性的内容.
cpp
QString Widget::process(const QString& request)
{
return request;
}
"根据请求处理响应" 是服务器开发中的最核心的步骤.
此时, 服务器程序编写完毕.
但是直接运行还看不出效果. 还需要搭配客户端来使用.
1.3 回显客户端
- 创建界面. 包含一个 QLineEdit , QPushButton , QListWidget
• 先使用水平布局把 QLineEdit 和 QPushButton 放好, 并设置这两个控件的垂直方向的
sizePolicy 为 Expanding
• 再使用垂直布局把 QListWidget 和上面的水平布局放好.
• 设置垂直布局的 layoutStretch 为 5, 1 (当然这个尺寸比例根据个人喜好微调).

- 在 widget.cpp 中, 先创建两个全局常量, 表示服务器的 IP 和 端口
cpp
// 提前定义好服务器的 IP 和 端口
const QString& SERVER_IP = "127.0.0.1";
const quint16 SERVER_PORT = 9090;
- 创建 QUdpSocket 成员
修改 widget.h, 定义成员
cpp
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private:
Ui::Widget *ui;
// 创建 socket 成员
QUdpSocket* socket;
};
修改 widget.cpp, 初始化 socket
cpp
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
// 1. 设置窗口名字
this->setWindowTitle("客户端");
// 2. 实例化 socket
socket = new QUdpSocket(this);
}
- 给发送按钮 slot 函数, 实现发送请求
cpp
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 的构造函数, 通过信号槽, 来处理服务器的响应.
cpp
connect(socket, &QUdpSocket::readyRead, this, [=]() {
const QNetworkDatagram responseDatagram = socket->receiveDatagram();
QString response = responseDatagram.data();
ui->listWidget->addItem(QString("服务器说: ") + response);
});
最终执行效果
启动多个客户端都可以正常工作.

2. TCP Socket
2.1 核心 API 概览
核心类是两个: QTcpServer 和 QTcpSocket
QTcpServer 用于监听端口, 和获取客户端连接.

QTcpSocket 用户客户端和服务器之间的数据交互.

QByteArray 用于表示一个字节数组. 可以很方便的和 QString 进行相互转换.
例如:
• 使用 QString 的构造函数即可把 QByteArray 转成 QString.
• 使用 QString 的 toUtf8 函数即可把 QString 转成 QByteArray.
2.2 回显服务器
- 创建界面. 包含一个 QListWidget , 用于显示收到的数据.

- 创建 QTcpServer 并初始化
修改 widget.h, 添加 QTcpServer 指针成员.
cpp
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private:
Ui::Widget *ui;
// 创建 QTcpServer
QTcpServer* tcpServer;
};
修改 widget.cpp, 实例化 QTcpServer 并进行后续初始化操作.
• 设置窗口标题
• 实例化 TCP server. (父元素设为当前控件, 会在父元素销毁时被一起销毁).
• 通过信号槽, 处理客户端建立的新连接.
• 监听端口
cpp
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
// 1. 设置窗口标题
this->setWindowTitle("服务器");
// 2. 实例化 TCP server
tcpServer = 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(1);
}
}
- 继续修改 widget.cpp, 实现处理连接的具体方法 processConnection
• 获取到新的连接对应的 socket.
• 通过信号槽, 处理收到请求的情况
• 通过信号槽, 处理断开连接的情况
cpp
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, [=](){
// a) 读取请求
QString request = clientSocket->readAll();
// b) 根据请求处理响应
const QString& response = process(request);
// c) 把响应写回客户端
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);
// 删除 clientSocket
clientSocket->deleteLater();
});
}
- 实现 process 方法, 实现根据请求处理响应.
由于我们此处是实现回显服务器. 所以 process 方法中并没有包含实质性的内容.
cpp
QString Widget::process(const QString &request)
{
return request;
}
此时, 服务器程序编写完毕.
但是直接运行还看不出效果. 还需要搭配客户端来使用.
2.3 回显客户端
- 创建界面. 包含一个 QLineEdit , QPushButton , QListWidget
• 先使用水平布局把 QLineEdit 和 QPushButton 放好, 并设置这两个控件的垂直方向的
sizePolicy 为 Expanding
• 再使用垂直布局把 QListWidget 和上面的水平布局放好.
• 设置垂直布局的 layoutStretch 为 5, 1 (当然这个尺寸比例根据个人喜好微调).

- 创建 QTcpSocket 并实例化
修改 widget.h, 创建成员.
cpp
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private:
Ui::Widget *ui;
// 新增 QTcpSocket
QTcpSocket* socket;
};
修改 widget.cpp, 对 QTcpSocket 进行实例化.
• 设置窗口标题
• 实例化 socket 对象 (父元素设为当前控件, 会在父元素销毁时被一起销毁).
• 和服务器建立连接.
• 等待并确认连接是否出错.
cpp
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(1);
}
}
- 修改 widget.cpp, 给按钮增加点击的 slot 函数, 实现发送请求给服务器.
cpp
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 构造函数, 通过信号槽, 处理收到的服务器的响应
cpp
// 处理服务器返回的响应.
connect(socket, &QTcpSocket::readyRead, this, [=]() {
QString response = socket->readAll();
qDebug() << response;
ui->listWidget->addItem(QString("服务器说: ") + response);
});
先启动服务器, 再启动客户端(可以启动多个), 最终执行效果:
由于我们使用信号槽处理同一个客户端的多个请求, 不涉及到循环, 也就不会使客户端之间相互影响了.

3. HTTP Client
进行 Qt 开发时, 和服务器之间的通信很多时候也会用到 HTTP 协议.
• 通过 HTTP 从服务器获取数据.
• 通过 HTTP 向服务器提交数据.
3.1 核心 API
关键类主要是三个. QNetworkAccessManager , QNetworkRequest , QNetworkReply .
QNetworkAccessManager 提供了 HTTP 的核心操作.

QNetworkRequest 表示一个 HTTP 请求(不含 body).
如果需要发送一个带有 body 的请求(比如 post), 会在 QNetworkAccessManager 的 post 方法中通过单独的参数来传入 body.

其中的 QNetworkRequest::KnownHeaders 是一个枚举类型, 常用取值:

QNetworkReply 表示一个 HTTP 响应. 这个类同时也是 QIODevice 的子类.

此外, QNetworkReply 还有一个重要的信号 finished 会在客户端收到完整的响应数据之后触发.
3.2 代码示例
给服务器发送一个 GET 请求.
- 创建界面. 包含一个 QLineEdit , QPushButton
• 先使用水平布局把 QLineEdit 和 QPushButton 放好, 并设置这两个控件的垂直方向的
sizePolicy 为 Expanding
• 再使用垂直布局把 QPlainTextEdit 和上面的水平布局放好. ( QPlainTextEdit 的
readOnly 设为 true )
• 设置垂直布局的 layoutStretch 为 5, 1 (当然这个尺寸比例根据个人喜好微调).

💡 此处建议使用 QPlainTextEdit 而不是 QTextEdit . 主要因为 QTextEdit 要进行富文本解析, 如果得到的 HTTP 响应体积很大, 就会导致界面渲染缓慢甚至被卡住.
- 修改 widget.h, 创建 QNetworkAccessManager 属性
cpp
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private slots:
void on_pushButton_clicked();
private:
Ui::Widget *ui;
// 新增属性
QNetworkAccessManager* manager;
};
- 修改 widget.cpp, 创建实例
cpp
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
// 实例化属性
manager = new QNetworkAccessManager(this);
}
- 编写按钮的 slot 函数, 实现发送 HTTP 请求功能
cpp
void Widget::on_pushButton_clicked()
{
// 1. 获取到输入框中的 URL, 构造 QUrl 对象
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() 即可. 此处不再演示.
本篇完!