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() 回复
|
下次忘记时,可以按这个顺序复习:
- 先看“引入与配置“,回顾传统 socket 编程的痛点,理解 Qt
的封装思路。
- 看“UDP 网络编程“,先理解 QUdpSocket
的通信模型,再对照完整代码走通发送和接收两条路径。
- 看“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
| find_package(Qt6 COMPONENTS REQUIRED Network) target_link_libraries(mytarget PRIVATE Qt6::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);
|
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); } }
|
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
|
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);
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(); 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
|
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); }
|