018-Qt 网络编程

auther: abinng date: 2026-05-17 18:05 createDate:2026-05-17 18:05

复习路线

这篇笔记要回答的问题是:Qt 如何封装传统的 socket 编程,让我们用信号槽的方式完成 UDP/TCP 通信?

先记住 UDP 和 TCP 两条主线:

1
2
3
UDP:  bind() 绑定端口 → readyRead 信号 → readDatagram() 读取 → writeDatagram() 发送
TCP: listen() 开始监听 → newConnection 信号 → nextPendingConnection() 获取 socket
→ readyRead 信号 → readAll() 读取 → write() 回复

下次忘记时,可以按这个顺序复习:

  1. 先看“引入与配置“,回顾传统 socket 编程的痛点,理解 Qt 的封装思路。
  2. 看“UDP 网络编程“,先理解 QUdpSocket 的通信模型,再对照完整代码走通发送和接收两条路径。
  3. 看“TCP 服务端编程“,先理解 QTcpServer + QTcpSocket 的协作关系,再对照完整代码走通多客户端连接的流程。

1. 引入与配置

1.1 传统 socket 编程的痛点

学习本篇前,建议先学习传统 C 网络编程(socket / bind / listen / accept / send / recv 那一套),并在 Linux 上写一写 CS 架构的小 demo。本篇不会重点讲解 TCP/UDP 的协议原理,而是聚焦在 Qt 如何封装

传统 socket 编程的几个痛点:

  • 同步阻塞模式下,accept()recv() 会卡住整个程序,必须自己开线程处理。
  • 异步模式下,要用 select / poll / epoll 管理大量文件描述符,代码复杂度高。
  • 跨平台差异需要自己处理(Windows 要 WSAStartup,Linux 要处理信号中断等)。

Qt 的封装思路很直接:把 socket 事件映射为 Qt 信号。有数据到达?发射 readyRead()。有新客户端连接?发射 newConnection()。你只需要写好槽函数,剩下的交给 Qt 的事件循环。

1.2 CMake 配置

1
2
3
# CMake
find_package(Qt6 COMPONENTS REQUIRED Network)
target_link_libraries(mytarget PRIVATE Qt6::Network)
1
2
# qmake
QT += network

只需要加一行 Network 模块,所有相关类都可用。

1.3 Qt 网络编程的类分工

主要涉及三个类:

职责
QUdpSocket UDP 通信:无连接,每次发送需指定目标地址和端口
QTcpServer TCP 服务端:监听端口,接收客户端连接请求
QTcpSocket TCP 的 socket 连接:实现与客户端的双向数据流

UDP 只用 QUdpSocket 一个类就能同时完成收发。TCP 则需要 QTcpServer(负责接客)和 QTcpSocket(负责与每个客户端通信)两个类配合。

2. UDP 网络编程

2.1 通信模型

UDP 是一种无连接的传输协议,不需要预先建立持久的 socket 连接,每次发送数据报(datagram)时都需指定目标的 IP 地址和端口号。

这在 Qt 中的映射非常直观:

1
2
3
4
5
6
7
发送方                       接收方
│ │
│ writeDatagram(data, │ bind(QHostAddress::AnyIPv4, port)
│ targetIP, targetPort) │ ↓
│ ─────────────────────→ │ readyRead 信号触发
│ │ ↓
│ │ readDatagram() 读取数据报
  • 发送:直接调用 writeDatagram(),指定目标地址和端口,发射后不管。
  • 接收:先用 bind() 绑定本地端口,当数据报到达时 Qt 发射 readyRead() 信号,在槽函数里用 readDatagram() 读取。

QUdpSocket 同时承担发送和接收角色,一个对象搞定。

2.2 用到的接口

启动监听

1
2
m_udp = new QUdpSocket(this);
m_udp->bind(QHostAddress::AnyIPv4, port); // 绑定本机所有IPv4地址 + 指定端口
  • bind(QHostAddress::AnyIPv4, port) — 绑定到本机所有 IPv4 网卡的指定端口。成功后,到达该端口的数据报会触发 readyRead() 信号。

接收数据

1
2
3
4
5
6
7
8
9
10
11
12
connect(m_udp, &QUdpSocket::readyRead, this, &UDPWin::dataReceive);

void dataReceive() {
while (m_udp->hasPendingDatagrams()) { // 可能有多个数据报在排队
QByteArray datagram;
datagram.resize(m_udp->pendingDatagramSize()); // 先确认大小再分配空间
QHostAddress peerAddr;
quint16 peerPort;
m_udp->readDatagram(datagram.data(), datagram.size(), &peerAddr, &peerPort);
// datagram 里是数据,peerAddr/peerPort 是发送方地址
}
}
  • readyRead 信号 — 有新数据报到达时发射。注意处理时要用 while(hasPendingDatagrams()),因为可能一次收到多个数据报。
  • hasPendingDatagrams() — 检查是否还有未读的数据报。
  • pendingDatagramSize() — 获取下一个待读数据报的字节数。
  • readDatagram(data, maxSize, &peerAddr, &peerPort) — 读取数据报内容,同时获取发送方的地址和端口。

发送数据

1
2
QHostAddress targetAddr(targetIP);
m_udp->writeDatagram(msg.toUtf8(), targetAddr, targetPort);
  • writeDatagram(data, targetAddr, targetPort) — 向指定地址和端口发送数据报。

错误处理与关闭

1
2
3
4
5
6
connect(m_udp, &QUdpSocket::errorOccurred, this, [=]() {
qDebug() << "socket error:" << m_udp->errorString();
});

// 关闭
m_udp->close();
  • errorOccurred 信号 — socket 发生错误时发射。
  • errorString() — 获取错误的描述文字。
  • close() — 关闭 socket,停止接收。

2.3 完整代码

点击展开
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
#ifndef UDP_WIN_H
#define UDP_WIN_H
#include <QWidget>
#include <QUdpSocket>

QT_BEGIN_NAMESPACE
namespace Ui {
class UDPWin;
}
QT_END_NAMESPACE

class UDPWin : public QWidget {
Q_OBJECT
public:
explicit UDPWin(QWidget *parent = nullptr);
~UDPWin() override;
public slots:
void on_btnStart_clicked();
void dataReceive();
void on_btnSend_clicked();
private:
Ui::UDPWin *ui;
QUdpSocket *m_udp;
bool m_running;
};

#endif //UDP_WIN_H
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include "udp_win.h"
#include "ui_udp_win.h"
#include <QMessageBox>

UDPWin::UDPWin(QWidget *parent) :
QWidget(parent), ui(new Ui::UDPWin)
{
ui->setupUi(this);
m_udp = new QUdpSocket(this);
m_running = false;

connect(m_udp, &QUdpSocket::readyRead, this, &UDPWin::dataReceive);
connect(m_udp, &QUdpSocket::errorOccurred, this, [=]() {
qDebug() << "socket error:" << m_udp->errorString();
});
}

UDPWin::~UDPWin()
{
m_udp->close();
delete ui;
}

void UDPWin::on_btnStart_clicked()
{
if (!m_running) {
bool ok = false;
quint16 port = ui->bindPortEdit->text().toInt(&ok);
if (!ok) {
QMessageBox::critical(this, "端口错误", "端口格式错误");
return;
}
ok = m_udp->bind(QHostAddress::AnyIPv4, port);
if (!ok) {
QMessageBox::critical(this, "服务启动错误", m_udp->errorString());
return;
}
QMessageBox::information(this, "服务状态", "UDP服务启动成功");
ui->btnStart->setText("关闭服务");
m_running = true;
} else {
m_udp->close();
ui->btnStart->setText("开启服务");
m_running = false;
}
}

void UDPWin::dataReceive()
{
while (m_udp->hasPendingDatagrams()) {
QByteArray datagram;
datagram.resize(m_udp->pendingDatagramSize());
QHostAddress peerAddr;
quint16 peerPort;
m_udp->readDatagram(datagram.data(), datagram.size(), &peerAddr, &peerPort);
if (datagram.size() <= 0) {
qDebug() << "datagram empty";
return;
}
QString log = QString("[From: %1:%2]# %3")
.arg(peerAddr.toString())
.arg(peerPort)
.arg(datagram.data());
ui->listWidget->addItem(log);
}
}

void UDPWin::on_btnSend_clicked()
{
QString targetIP = ui->IPEdit->text();
quint16 targetPort = ui->PortEdit->text().toUInt();
QString msg = ui->msgEdit->toPlainText();
QHostAddress targetAddr(targetIP);
m_udp->writeDatagram(msg.toUtf8(), targetAddr, targetPort);
}

3. TCP 服务端编程

3.1 通信模型

TCP 是面向连接的传输协议,通信前需要经过三次握手建立连接。这在 Qt 中由两个类分工完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
客户端1 ──连接──→ ┌─────────────┐
客户端2 ──连接──→ │ QTcpServer │ 监听端口,接收连接
客户端3 ──连接──→ │ (接客) │
└──────┬──────┘
│ newConnection 信号
│ nextPendingConnection()

┌──────────────┐
│ QTcpSocket │ 每个客户端一个独立的 socket
│ (与客户端1) │ 通过 readyRead 收发数据
└──────────────┘
┌──────────────┐
│ QTcpSocket │
│ (与客户端2) │
└──────────────┘
  • QTcpServer 扮演“前台“角色:调用 listen() 开始监听端口。当有新客户端连接时,自动创建 QTcpSocket 并发射 newConnection() 信号。
  • QTcpSocket 扮演“专员“角色:每个连接的客户端都有一个对应的 QTcpSocket,通过它来收发数据、检测断开。

3.2 用到的接口

启动服务器

1
2
3
4
5
m_tcpServer = new QTcpServer(this);
connect(m_tcpServer, &QTcpServer::newConnection, this, &TCPWin::new_connect_handle);

// 开始监听本机所有IPv4地址的指定端口
m_tcpServer->listen(QHostAddress::AnyIPv4, port);
  • listen(QHostAddress::AnyIPv4, port) — 开始监听指定端口。返回 true 表示监听成功。
  • newConnection 信号 — 有新客户端连接时发射。

接收新客户端连接

1
2
3
4
5
6
7
8
9
10
11
12
void new_connect_handle()
{
QTcpSocket *new_client = m_tcpServer->nextPendingConnection(); // 获取新客户端的 socket
// 记录并保存
QString log = QString("[%1:%2]客户端上线")
.arg(new_client->peerAddress().toString())
.arg(new_client->peerPort());
m_clients.append(new_client);
// 为这个客户端绑定数据接收和断开的处理
connect(new_client, &QTcpSocket::readyRead, ...);
connect(new_client, &QTcpSocket::disconnected, ...);
}
  • nextPendingConnection() — 获取下一个待处理的客户端连接,返回一个 QTcpSocket*。这个 socket 就是你和该客户端通信的通道。
  • peerAddress().toString() / peerPort() — 获取客户端的 IP 地址和端口。
  • readyRead 信号 — 该客户端发来数据时触发。
  • disconnected 信号 — 该客户端断开时触发。

收发数据

1
2
3
4
5
connect(new_client, &QTcpSocket::readyRead, this, [=]() {
QString request = new_client->readAll(); // 读取客户端发来的全部数据
QString response = process(request); // 处理业务逻辑
new_client->write(response.toUtf8()); // 回复客户端
});
  • readAll() — 读取缓冲区中所有数据,返回 QByteArray
  • write(data) — 向客户端发送数据。

关闭服务器

1
2
3
4
5
6
7
8
9
10
11
// 停止监听
if (m_tcpServer->isListening()) {
m_tcpServer->close();
}
// 断开所有客户端
for (QTcpSocket *socket : m_clients) {
if (socket->state() == QAbstractSocket::ConnectedState) {
socket->disconnectFromHost(); // 优雅挥手
}
}
m_clients.clear();
  • isListening() — 检查服务器是否正在监听。
  • close() — 关闭服务器,停止接受新连接。
  • state() — 检查 socket 当前的连接状态(ConnectedState 等)。
  • disconnectFromHost() — 主动断开与客户端的连接(发送 FIN)。

客户端下线处理

1
2
3
4
connect(new_client, &QTcpSocket::disconnected, this, [=]() {
new_client->close();
new_client->deleteLater(); // 安排删除,避免内存泄漏
});
  • deleteLater() — 延迟删除,等事件循环处理完与该 socket 相关的所有事件后再销毁对象。比直接 delete 安全,避免在处理事件的过程中对象被析构。

3.3 完整代码

点击展开
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
#ifndef TCP_WIN_H
#define TCP_WIN_H

#include <QWidget>
#include <QTcpServer>
#include <QTcpSocket>
#include <QList>

QT_BEGIN_NAMESPACE
namespace Ui {
class TCPWin;
}
QT_END_NAMESPACE

class TCPWin : public QWidget {
Q_OBJECT

public:
explicit TCPWin(QWidget *parent = nullptr);
~TCPWin() override;

public slots:
void new_connect_handle();
void on_btnStart_clicked();
void on_btnClose_clicked();

private:
Ui::TCPWin *ui;
QTcpServer *m_tcpServer = nullptr;
QList<QTcpSocket *> m_clients;
QString process(QString request);
};

#endif //TCP_WIN_H
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#include "tcp_win.h"
#include "ui_tcp_win.h"
#include <QMessageBox>

TCPWin::TCPWin(QWidget *parent) :
QWidget(parent), ui(new Ui::TCPWin)
{
ui->setupUi(this);
ui->btnClose->setEnabled(false);
ui->btnStart->setEnabled(true);

m_tcpServer = new QTcpServer(this);
connect(m_tcpServer, &QTcpServer::newConnection,
this, &TCPWin::new_connect_handle);
}

TCPWin::~TCPWin()
{
delete ui;
}

void TCPWin::new_connect_handle()
{
qDebug() << "Have a new connection";
QTcpSocket *new_client = m_tcpServer->nextPendingConnection();
QString log = QString("[%1:%2]客户端上线")
.arg(new_client->peerAddress().toString())
.arg(new_client->peerPort());
ui->listWidget->addItem(log);
m_clients.append(new_client);

connect(new_client, &QTcpSocket::readyRead, this, [=]() {
QString request = new_client->readAll();
QString response = process(request);
new_client->write(response.toUtf8());
});
connect(new_client, &QTcpSocket::disconnected, this, [=]() {
new_client->close();
new_client->deleteLater(); // 清理资源
QString log = QString("[%1:%2]客户端下线")
.arg(new_client->peerAddress().toString())
.arg(new_client->peerPort());
ui->listWidget->addItem(log);
});
}

void TCPWin::on_btnStart_clicked()
{
bool ok = false;
quint16 port = ui->portEdit->text().toInt(&ok);
if (!ok) {
QMessageBox::critical(this, "无效参数", "请输入正确的端口号");
return;
}
ok = m_tcpServer->listen(QHostAddress::AnyIPv4, port);
if (!ok) {
QMessageBox::critical(this, "服务启动失败",
m_tcpServer->errorString());
return;
}

ui->btnClose->setEnabled(true);
ui->btnStart->setEnabled(false);
ui->listWidget->addItem(
QString("TCP服务器开启成功,监听%1端口").arg(port));
}

void TCPWin::on_btnClose_clicked()
{
// 停止监听
if (m_tcpServer->isListening()) {
m_tcpServer->close();
}
// 遍历已有的客户端连接,关闭
for (QTcpSocket *socket : m_clients) {
if (socket->state() == QAbstractSocket::ConnectedState) {
socket->disconnectFromHost(); // 挥手
}
}
m_clients.clear();
ui->btnClose->setEnabled(false);
ui->btnStart->setEnabled(true);
}

QString TCPWin::process(QString request)
{
return QString("&& %1 &&").arg(request);
}