🧠 一、TCP 协议到底是什么?
可以把 TCP(Transmission Control Protocol) 想象成:
两个之间寄快递的过程
------每个包裹都必须按顺序送达、不能丢、不能重复,并且送达后要有签收。
TCP 的三大特点:
✅ 1. 面向连接
双方要先握手 → 才能发送数据
就像寄快递前,你必须确认对方是否愿意收快递。
这就是著名的三次握手:
cpp
客户端:我想连接你!(SYN)
服务器:我收到了,你能来。(SYN+ACK)
客户端:好的,我来了!(ACK)
✅ 2. 可靠传输
TCP 会确保:
-
包不丢失(丢了就重发)
-
包不乱序(按序号排队)
-
包不重复
就是快递公司那种"必须按顺序送到"的服务。
✅ 3. 面向字节流
TCP 不关心"你发的一条消息多长"。
它把数据当作"流水管里的水",连成一串流。
你一次 send(100 字节),可能对方一次读到 50 + 50,也可能一次 100。
这就是"流式协议"的本质。
🧱 二、TCP 的基础知识点(你在 Qt 中也会用到)
⭐ 1. 套接字(Socket)
套接字 = 网络通信的插口 。
程序要通过它发送/接收数据。
TCP 套接字分成两类:
👉 服务端套接字(监听)
-
在 Qt 中对应:
QTcpServer -
像"快递点",等待客户端来连接
👉 客户端套接字(通信)
-
在 Qt 中对应:
QTcpSocket -
用来发送/接收数据
🖥️ 三、Qt 中如何使用 TCP?
Qt 提供两个主要类:
🔸 1. 服务端:QTcpServer
流程:
-
创建服务器对象
-
监听端口
-
有客户端连接时,得到一个新的
QTcpSocket -
用 socket 收发数据
Qt 伪代码结构:
cpp
QTcpServer server;
server.listen(QHostAddress::Any, 8888);
connect(&server, &QTcpServer::newConnection, [&](){
QTcpSocket *client = server.nextPendingConnection();
// 使用 client 进行读写
});
🔸 2. 客户端:QTcpSocket
流程:
-
创建 socket
-
connectToHost("服务器IP", 端口)
-
连接成功后即可 write()
-
有数据时 read()
cpp
QTcpSocket socket;
socket.connectToHost("127.0.0.1", 8888);
connect(&socket, &QTcpSocket::readyRead, [&](){
QByteArray data = socket.readAll();
});
/**********************************************************************************************************/
Gemini回答:
你好!很高兴为你讲解。TCP 协议是网络编程的基石,虽然底层很复杂,但 Qt 把它封装得非常优雅,使用起来其实很像是在"写信"或"打电话"。
为了让你彻底理解,我们分三步走:生动比喻 、核心概念 、Qt 实战代码。
第一部分:生动形象理解 TCP ------ "极其靠谱的打电话"
想象一下,TCP(传输控制协议)就是两个人之间一次严谨的通话。
1. 建立连接(著名的"三次握手")
在传输数据前,必须先打通电话,确认双方都在线。
-
第一次(SYN): 你拨通电话问:"喂?听得到吗?"
-
第二次(SYN+ACK): 对方回答:"听到了。你能听到我说话吗?"
-
第三次(ACK): 你回答:"我也听到了。那我们开始聊正事吧。"
- 只有这三步都完成了,连接才算建立,大家才开始真正传输数据。
2. 传输数据(可靠传输)
TCP 最牛的地方在于**"靠谱"**。
-
确认机制: 你每说一句话,对方必须回一句"嗯,记下了"。如果你没听到"嗯",你会认为对方没听清,于是重说一遍(重传机制)。
-
顺序控制: 如果你语速太快,句子乱了,TCP 会负责把它们重新排好序,保证对方听到的是连贯的故事,而不是乱码。
3. 断开连接("四次挥手")
聊完了,不能直接挂断,要礼貌告别。
-
第一次(FIN): 你说:"我说完了,我想挂了。"
-
第二次(ACK): 对方说:"好的,我知道你想挂了。(但我还有最后两句要补充)"
-
第三次(FIN): 等了一会儿,对方补充完了,说:"好了,我也没话说了,挂吧。"
-
第四次(ACK): 你说:"好的,拜拜。" ------ 然后电话挂断。
第二部分:核心知识点(必须要懂的"黑话")
在写代码之前,有几个词你一定会遇到:
-
套接字 (Socket)
-
形象理解: 它是电话机插座。
-
解释: 在程序里,Socket 就是网络通信的"句柄"或"入口"。无论是服务器还是客户端,想要联网,手里都得攥着一个 Socket 对象。
-
-
IP 地址 (IP Address)
-
形象理解: 它是房子地址(比如:xx市xx路100号)。
-
解释: 定位网络上的一台电脑。
-
-
端口号 (Port)
-
形象理解: 它是分机号 或柜台窗口。
-
解释: 一台电脑上同时运行着微信、浏览器、QQ。数据到了电脑(IP)后,该给谁?靠端口区分。比如 Web 常用 80,我们自己写程序可以用 12345。
-
-
服务器 (Server) vs 客户端 (Client)
-
服务器: 像客服中心。它一直开机监听(Listen),等待别人打进来。
-
客户端: 像顾客。它主动拨号(Connect)去连接服务器。
-
第三部分:在 Qt 中如何简单使用
Qt 的网络模块非常强大,核心类只有两个:
-
QTcpServer:用于服务器,负责监听端口,派发 Socket。 -
QTcpSocket:用于通信,负责读写数据(客户端用它,服务器接到连接后也会生成一个用来专门对付这个客户端)。
准备工作:
在你的 Qt 工程文件 (.pro) 中必须添加一行,否则无法识别网络代码:
代码段
cpp
QT += network
场景举例:简单的聊天室
目标: 客户端发送"你好",服务器收到后回复"收到,我是服务器"。
1. 服务器端代码 (Server)
服务器的逻辑是:监听 -> 发现有人连接 -> 获取那个连接的 Socket -> 接收/发送数据。
C++
cpp
#include <QTcpServer>
#include <QTcpSocket>
#include <QDebug>
// 假设这是在你的 MainWindow 类或者专门的 Server 类中
void startServer() {
// 1. 创建服务器对象
QTcpServer *server = new QTcpServer(this);
// 2. 监听端口 (Any 表示监听本机所有网卡,12345 是端口号)
if (!server->listen(QHostAddress::Any, 12345)) {
qDebug() << "服务器启动失败!";
return;
}
qDebug() << "服务器正在监听端口 12345...";
// 3. 关联信号:当有新连接进来时,触发 newConnection 信号
connect(server, &QTcpServer::newConnection, [=](){
// 4. 取出这个新建立的连接(Socket)
// 注意:这个 socket 是专门用来跟刚才连进来的那个客户端说话的
QTcpSocket *clientSocket = server->nextPendingConnection();
qDebug() << "有新客户端连接:" << clientSocket->peerAddress().toString();
// 5. 监听这个 Socket 的数据读取信号
connect(clientSocket, &QTcpSocket::readyRead, [=](){
// 读取所有发来的数据
QByteArray data = clientSocket->readAll();
qDebug() << "服务器收到:" << data;
// 6. 给客户端回话
clientSocket->write("收到,我是服务器!");
});
// 7. 处理断开连接(可选,为了不内存泄漏,建议断开后删除对象)
connect(clientSocket, &QTcpSocket::disconnected, clientSocket, &QTcpSocket::deleteLater);
});
}
2. 客户端代码 (Client)
客户端的逻辑是:连接服务器 -> 连接成功 -> 发送数据 -> 接收回信。
C++
cpp
#include <QTcpSocket>
#include <QHostAddress>
#include <QDebug>
// 假设这是在客户端的某个按钮点击事件中
void connectAndSend() {
// 1. 创建 Socket 对象(就像拿起了电话)
QTcpSocket *socket = new QTcpSocket(this);
// 2. 连接服务器 (IP是本机,端口必须和服务器一致)
socket->connectToHost("127.0.0.1", 12345);
// 3. 监听连接成功信号
connect(socket, &QTcpSocket::connected, [=](){
qDebug() << "成功连接到服务器!";
// 4. 连接一建立,马上发送一条消息
socket->write("你好,服务器!我是客户端。");
});
// 5. 监听服务器的回信
connect(socket, &QTcpSocket::readyRead, [=](){
QByteArray response = socket->readAll();
qDebug() << "收到服务器回信:" << response;
});
// 6. 监听断开
connect(socket, &QTcpSocket::disconnected, [=](){
qDebug() << "服务器断开了连接。";
socket->deleteLater();
});
}
总结与要点速查
| 概念 | 类比 | Qt 类/函数 | 作用 |
|---|---|---|---|
| TCP | 打电话 | QT += network |
提供可靠的连接 |
| Server | 接线员 | QTcpServer |
监听并接受连接 |
| Client | 拨号者 | QTcpSocket |
发起连接 |
| Listen | 守在电话旁 | server->listen() |
服务器开始工作 |
| Connect | 拨号 | socket->connectToHost() |
客户端寻找服务器 |
| Socket | 通话线路 | QTcpSocket |
最重要! 读写数据都靠它 |
| Read/Write | 听说 | readAll() / write() |
收发字节数据 |
一、获取本机的网络信息
Qt提供了 QHostInfo 和 QNetworkInterface 类 可以用于网络信息查询。
本例例程:本例目的:了解如何通过QHostInfo和 QNetworkInterface类获取本地网络所有接口的信息。
cpp
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QPushButton>
#include <QTextBrowser>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QTimer>
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
/* 点击获取和清空文本按钮 */
QPushButton *pushButton[2];
/* 文本浏览框用于显示本机的信息 */
QTextBrowser *textBrowser;
/* 水平Widget容器和垂直Widget容器*/
QWidget *hWidget;
QWidget *vWidget;
/* 水平布局和垂直布局 */
QHBoxLayout *hBoxLayout;
QVBoxLayout *vBoxLayout;
/* 定时器 */
QTimer *timer;
/* 获取本机的网络的信息,返回类型是QString */
QString getHostInfo();
private slots:
/* 定时器槽函数,点击按钮后定时触发 */
void timerTimeOut();
/* 显示本机信息 */
void showHostInfo();
/* 启动定时器 */
void timerStart();
/* 清空textBrowser的信息 */
void clearHostInfo();
};
#endif // MAINWINDOW_H
cpp
#include "mainwindow.h"
#include <QNetworkInterface>
#include <QHostInfo>
#include <QThread>
#include <QDebug>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
/* 设置位置与大小 */
this->setGeometry(0, 0, 800, 480);
/* 点击获取本地信息按钮和清空文本按钮 */
pushButton[0] = new QPushButton();
pushButton[1] = new QPushButton();
pushButton[0]->setText("获取本机信息");
pushButton[1]->setText("清空文本信息");
/* 按钮的大小根据文本自适应,
* 注意setSizePolicy需要在布局中使用 */
pushButton[0]->setSizePolicy(QSizePolicy::Fixed,
QSizePolicy::Fixed);
pushButton[1]->setSizePolicy(QSizePolicy::Fixed,
QSizePolicy::Fixed);
/* 水平Widget和垂直Widget用于添加布局 */
hWidget = new QWidget();
vWidget = new QWidget();
/* 水平布局和垂直布局 */
hBoxLayout = new QHBoxLayout();
vBoxLayout = new QVBoxLayout();
/* 文本浏览框 */
textBrowser = new QTextBrowser();
/* 添加到水平布局 */
hBoxLayout->addWidget(pushButton[0]);
hBoxLayout->addWidget(pushButton[1]);
/* 将水平布局设置为hWidget的布局 */
hWidget->setLayout(hBoxLayout);
/* 将文本浏览框和hWidget添加到垂直布局 */
vBoxLayout->addWidget(textBrowser);
vBoxLayout->addWidget(hWidget);
/* 将垂直布局设置为vWidget的布局 */
vWidget->setLayout(vBoxLayout);
/* 设置vWidget为中心部件 */
setCentralWidget(vWidget);
/* 定时器初始化 */
timer = new QTimer();
/* 信号槽连接 */
connect(pushButton[0], SIGNAL(clicked()),
this, SLOT(timerStart()));
connect(pushButton[1], SIGNAL(clicked()),
this, SLOT(clearHostInfo()));
connect(timer, SIGNAL(timeout()),
this, SLOT(timerTimeOut()));
}
MainWindow::~MainWindow()
{
}
void MainWindow::timerStart()
{
/* 清空文本 */
textBrowser->clear();
/* 定时1s */
timer->start(1000);
}
void MainWindow::timerTimeOut()
{
/* 显示本机信息 */
showHostInfo();
/* 停止定时器 */
timer->stop();
}
QString MainWindow::getHostInfo()
{
/* 通过QHostInfo的localHostName函数获取主机名称 */
QString str = "主机名称:" + QHostInfo::localHostName() + "\n";
/* 获取所有的网络接口,
* QNetworkInterface类提供主机的IP地址和网络接口的列表 */
QList<QNetworkInterface> list
= QNetworkInterface::allInterfaces();
/* 遍历list */
foreach (QNetworkInterface interface, list) {
str+= "网卡设备:" + interface.name() + "\n";
str+= "MAC地址:" + interface.hardwareAddress() + "\n";
/* QNetworkAddressEntry类存储IP地址子网掩码和广播地址 */
QList<QNetworkAddressEntry> entryList
= interface.addressEntries();
/* 遍历entryList */
foreach (QNetworkAddressEntry entry, entryList) {
/* 过滤IPv6地址,只留下IPv4 */
if (entry.ip().protocol() ==
QAbstractSocket::IPv4Protocol) {
str+= "IP 地址:" + entry.ip().toString() + "\n";
str+= "子网掩码:" + entry.netmask().toString() + "\n";
str+= "广播地址:" + entry.broadcast().toString() + "\n\n";
}
}
}
/* 返回网络信息 */
return str;
}
void MainWindow::showHostInfo()
{
/* 获取本机信息后显示到textBrowser */
textBrowser->insertPlainText(getHostInfo());
}
void MainWindow::clearHostInfo()
{
/* 判断textBrowser是否为空,如果不为空则清空文本 */
if (!textBrowser->toPlainText().isEmpty())
/* 清空文本 */
textBrowser->clear();
}

二、TCP 通信
TCP 协议(Transmission Control Protocol)全称是传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议。
TCP 通信必须先建立TCP 连接,通信端分为客户端和服务端。服务端通过监听某个端口来监听是否有客户端连接到来 ,如果有连接到来,则建立新的socket 连接;客户端通过ip 和port 连接服务端,当成功建立连接之后,就可进行数据的收发了。需要注意的是,在Qt 中,Qt 把socket 当成输入输出流来对待的,数据的收发是通过read()和write()来进行的,需要与我们常见的send()与recv()进行区分。

示例:
本例 大体流程首先获取本地 IP地址。创建 一个 tcpSocket套接字,一个 tcpServer服务端。点击监听即监听本地的主机 IP地址和端口,同时等待服务端的连接。此程序需要结合客户端一起使用。
服务端:
cpp
/******************************************************************
Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName 08_tcpserver
* @brief mainwindow.h
* @author Deng Zhimao
* @email 1252699831@qq.com
* @net www.openedv.com
* @date 2021-04-13
*******************************************************************/
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QTcpServer>
#include <QTcpSocket>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
#include <QTextBrowser>
#include <QLabel>
#include <QComboBox>
#include <QSpinBox>
#include <QHostInfo>
#include <QLineEdit>
#include <QNetworkInterface>
#include <QDebug>
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
/* tcp服务器 */
QTcpServer *tcpServer;
/* 通信套接字 */
QTcpSocket *tcpSocket;
/* 按钮 */
QPushButton *pushButton[4];
/* 标签文本 */
QLabel *label[2];
/* 水平容器 */
QWidget *hWidget[3];
/* 水平布局 */
QHBoxLayout *hBoxLayout[3];
/* 垂直容器 */
QWidget *vWidget;
/* 垂直布局 */
QVBoxLayout *vBoxLayout;
/* 文本浏览框 */
QTextBrowser *textBrowser;
/* 用于显示本地ip */
QComboBox *comboBox;
/* 用于选择端口 */
QSpinBox *spinBox;
/* 文本输入框 */
QLineEdit *lineEdit;
/* 存储本地的ip列表地址 */
QList<QHostAddress> IPlist;
/* 获取本地的所有ip */
void getLocalHostIP();
private slots:
/* 客户端连接处理槽函数 */
void clientConnected();
/* 开始监听槽函数 */
void startListen();
/* 停止监听槽函数 */
void stopListen();
/* 清除文本框时的内容 */
void clearTextBrowser();
/* 接收到消息 */
void receiveMessages();
/* 发送消息 */
void sendMessages();
/* 连接状态改变槽函数 */
void socketStateChange(QAbstractSocket::SocketState);
};
#endif // MAINWINDOW_H
cpp
/******************************************************************
Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName 08_tcpserver
* @brief mainwindow.cpp
* @author Deng Zhimao
* @email 1252699831@qq.com
* @net www.openedv.com
* @date 2021-04-13
*******************************************************************/
#include "mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
/* 设置主窗体的位置与大小 */
this->setGeometry(0, 0, 800, 480);
/* 实例化tcp服务器与tcp套接字 */
tcpServer = new QTcpServer(this);
tcpSocket = new QTcpSocket(this);
/* 开始监听按钮 */
pushButton[0] = new QPushButton();
/* 停止监听按钮 */
pushButton[1] = new QPushButton();
/* 清空聊天文本按钮 */
pushButton[2] = new QPushButton();
/* 发送消息按钮 */
pushButton[3] = new QPushButton();
/* 水平布局一 */
hBoxLayout[0] = new QHBoxLayout();
/* 水平布局二 */
hBoxLayout[1] = new QHBoxLayout();
/* 水平布局三 */
hBoxLayout[2] = new QHBoxLayout();
/* 水平布局四 */
// hBoxLayout[3] = new QHBoxLayout();
/* 水平容器一 */
hWidget[0] = new QWidget();
/* 水平容器二 */
hWidget[1] = new QWidget();
/* 水平容器三 */
hWidget[2] = new QWidget();
vWidget = new QWidget();
vBoxLayout = new QVBoxLayout();
/* 标签实例化 */
label[0] = new QLabel();
label[1] = new QLabel();
lineEdit = new QLineEdit();
comboBox = new QComboBox();
spinBox = new QSpinBox();
textBrowser = new QTextBrowser();
label[0]->setText("监听IP地址:");
label[1]->setText("监听端口:");
/* 设置标签根据文本文字大小自适应大小 */
label[0]->setSizePolicy(QSizePolicy::Fixed,
QSizePolicy::Fixed);
label[1]->setSizePolicy(QSizePolicy::Fixed,
QSizePolicy::Fixed);
/* 设置端口号的范围,注意不要与主机的已使用的端口号冲突 */
spinBox->setRange(10000, 99999);
pushButton[0]->setText("开始监听");
pushButton[1]->setText("停止监听");
pushButton[2]->setText("清空文本");
pushButton[3]->setText("发送消息");
/* 设置停止监听状态不可用 */
pushButton[1]->setEnabled(false);
/* 设置输入框默认的文本 */
lineEdit->setText("www.openedv.com正点原子论坛");
/* 水平布局一添加内容 */
hBoxLayout[0]->addWidget(pushButton[0]);
hBoxLayout[0]->addWidget(pushButton[1]);
hBoxLayout[0]->addWidget(pushButton[2]);
/* 设置水平容器一的布局为水平布局一 */
hWidget[0]->setLayout(hBoxLayout[0]);
/* 水平布局二添加内容 */
hBoxLayout[1]->addWidget(label[0]);
hBoxLayout[1]->addWidget(comboBox);
hBoxLayout[1]->addWidget(label[1]);
hBoxLayout[1]->addWidget(spinBox);
/* 设置水平容器二的布局为水平布局二 */
hWidget[1]->setLayout(hBoxLayout[1]);
/* 水平布局三添加内容 */
hBoxLayout[2]->addWidget(lineEdit);
hBoxLayout[2]->addWidget(pushButton[3]);
/* 设置水平容器三的布局为水平布局三 */
hWidget[2]->setLayout(hBoxLayout[2]);
/* 垂直布局添加内容 */
vBoxLayout->addWidget(textBrowser);
vBoxLayout->addWidget(hWidget[1]);
vBoxLayout->addWidget(hWidget[0]);
vBoxLayout->addWidget(hWidget[2]);
/* 设置垂直容器的布局为垂直布局 */
vWidget->setLayout(vBoxLayout);
/* 居中显示 */
setCentralWidget(vWidget);
/* 获取本地ip */
getLocalHostIP();
/* 信号槽连接 */
connect(pushButton[0], SIGNAL(clicked()),
this, SLOT(startListen()));
connect(pushButton[1], SIGNAL(clicked()),
this, SLOT(stopListen()));
connect(pushButton[2], SIGNAL(clicked()),
this, SLOT(clearTextBrowser()));
connect(pushButton[3], SIGNAL(clicked()),
this, SLOT(sendMessages()));
connect(tcpServer, SIGNAL(newConnection()),
this, SLOT(clientConnected()));
}
MainWindow::~MainWindow()
{
}
/* 新的客户端连接 */
void MainWindow::clientConnected()
{
/* 获取客户端的套接字 */
tcpSocket = tcpServer->nextPendingConnection();
/* 客户端的ip信息 */
QString ip = tcpSocket->peerAddress().toString();
/* 客户端的端口信息 */
quint16 port = tcpSocket->peerPort();
/* 在文本浏览框里显示出客户端的连接信息 */
textBrowser->append("客户端已连接");
textBrowser->append("客户端ip地址:"
+ ip);
textBrowser->append("客户端端口:"
+ QString::number(port));
connect(tcpSocket, SIGNAL(readyRead()),
this, SLOT(receiveMessages()));
connect(tcpSocket,
SIGNAL(stateChanged(QAbstractSocket::SocketState)),
this,
SLOT(socketStateChange(QAbstractSocket::SocketState)));
}
/* 获取本地IP */
void MainWindow::getLocalHostIP()
{
// /* 获取主机的名称 */
// QString hostName = QHostInfo::localHostName();
// /* 主机的信息 */
// QHostInfo hostInfo = QHostInfo::fromName(hostName);
// /* ip列表,addresses返回ip地址列表,注意主机应能从路由器获取到
// * IP,否则可能返回空的列表(ubuntu用此方法只能获取到环回IP) */
// IPlist = hostInfo.addresses();
// qDebug()<<IPlist<<endl;
// /* 遍历IPlist */
// foreach (QHostAddress ip, IPlist) {
// if (ip.protocol() == QAbstractSocket::IPv4Protocol)
// comboBox->addItem(ip.toString());
// }
/* 获取所有的网络接口,
* QNetworkInterface类提供主机的IP地址和网络接口的列表 */
QList<QNetworkInterface> list
= QNetworkInterface::allInterfaces();
/* 遍历list */
foreach (QNetworkInterface interface, list) {
/* QNetworkAddressEntry类存储IP地址子网掩码和广播地址 */
QList<QNetworkAddressEntry> entryList
= interface.addressEntries();
/* 遍历entryList */
foreach (QNetworkAddressEntry entry, entryList) {
/* 过滤IPv6地址,只留下IPv4 */
if (entry.ip().protocol() ==
QAbstractSocket::IPv4Protocol) {
comboBox->addItem(entry.ip().toString());
/* 添加到IP列表中 */
IPlist<<entry.ip();
}
}
}
}
/* 开始监听 */
void MainWindow::startListen()
{
/* 需要判断当前主机是否有IP项 */
if (comboBox->currentIndex() != -1) {
qDebug()<<"start listen"<<endl;
tcpServer->listen(IPlist[comboBox->currentIndex()],
spinBox->value());
/* 设置按钮与下拉列表框的状态 */
pushButton[0]->setEnabled(false);
pushButton[1]->setEnabled(true);
comboBox->setEnabled(false);
spinBox->setEnabled(false);
/* 在文本浏览框里显示出服务端 */
textBrowser->append("服务器IP地址:"
+ comboBox->currentText());
textBrowser->append("正在监听端口:"
+ spinBox->text());
}
}
/* 停止监听 */
void MainWindow::stopListen()
{
qDebug()<<"stop listen"<<endl;
/* 停止监听 */
tcpServer->close();
/* 如果是连接上了也应该断开,如果不断开客户端还能继续发送信息,
* 因为socket未断开,还在监听上一次端口 */
if (tcpSocket->state() == tcpSocket->ConnectedState)
tcpSocket->disconnectFromHost();
/* 设置按钮与下拉列表框的状态 */
pushButton[1]->setEnabled(false);
pushButton[0]->setEnabled(true);
comboBox->setEnabled(true);
spinBox->setEnabled(true);
/* 将停止监听的信息添加到文本浏览框中 */
textBrowser->append("已停止监听端口:"
+ spinBox->text());
}
/* 清除文本浏览框里的内容 */
void MainWindow::clearTextBrowser()
{
/* 清除文本浏览器的内容 */
textBrowser->clear();
}
/* 服务端接收消息 */
void MainWindow::receiveMessages()
{
/* 读取接收到的消息 */
QString messages = "客户端:" + tcpSocket->readAll();
textBrowser->append(messages);
}
/* 服务端发送消息 */
void MainWindow::sendMessages()
{
if(NULL == tcpSocket)
return;
/* 如果已经连接 */
if(tcpSocket->state() == tcpSocket->ConnectedState) {
/* 发送消息 */
tcpSocket->write(lineEdit->text().toUtf8().data());
/* 在服务端插入发送的消息 */
textBrowser->append("服务端:" + lineEdit->text());
}
}
/* 服务端状态改变 */
void MainWindow::socketStateChange(QAbstractSocket::SocketState state)
{
switch (state) {
case QAbstractSocket::UnconnectedState:
textBrowser->append("scoket状态:UnconnectedState");
break;
case QAbstractSocket::ConnectedState:
textBrowser->append("scoket状态:ConnectedState");
break;
case QAbstractSocket::ConnectingState:
textBrowser->append("scoket状态:ConnectingState");
break;
case QAbstractSocket::HostLookupState:
textBrowser->append("scoket状态:HostLookupState");
break;
case QAbstractSocket::ClosingState:
textBrowser->append("scoket状态:ClosingState");
break;
case QAbstractSocket::ListeningState:
textBrowser->append("scoket状态:ListeningState");
break;
case QAbstractSocket::BoundState:
textBrowser->append("scoket状态:BoundState");
break;
default:
break;
}
}

客户端:
cpp
/******************************************************************
Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName 09_tcpclient
* @brief mainwindow.h
* @author Deng Zhimao
* @email 1252699831@qq.com
* @net www.openedv.com
* @date 2021-04-13
*******************************************************************/
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QTcpServer>
#include <QTcpSocket>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
#include <QTextBrowser>
#include <QLabel>
#include <QComboBox>
#include <QSpinBox>
#include <QHostInfo>
#include <QLineEdit>
#include <QNetworkInterface>
#include <QDebug>
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
/* 通信套接字 */
QTcpSocket *tcpSocket;
/* 按钮 */
QPushButton *pushButton[4];
/* 标签文本 */
QLabel *label[2];
/* 水平容器 */
QWidget *hWidget[3];
/* 水平布局 */
QHBoxLayout *hBoxLayout[3];
/* 垂直容器 */
QWidget *vWidget;
/* 垂直布局 */
QVBoxLayout *vBoxLayout;
/* 文本浏览框 */
QTextBrowser *textBrowser;
/* 用于显示本地ip */
QComboBox *comboBox;
/* 用于选择端口 */
QSpinBox *spinBox;
/* 文本输入框 */
QLineEdit *lineEdit;
/* 存储本地的ip列表地址 */
QList<QHostAddress> IPlist;
/* 获取本地的所有ip */
void getLocalHostIP();
private slots:
/* 连接 */
void toConnect();
/* 断开连接 */
void toDisConnect();
/* 已连接 */
void connected();
/* 已断开连接 */
void disconnected();
/* 清除文本框时的内容 */
void clearTextBrowser();
/* 接收到消息 */
void receiveMessages();
/* 发送消息 */
void sendMessages();
/* 连接状态改变槽函数 */
void socketStateChange(QAbstractSocket::SocketState);
};
#endif // MAINWINDOW_H
cpp
/******************************************************************
Copyright © Deng Zhimao Co., Ltd. 1990-2021. All rights reserved.
* @projectName 09_tcpclient
* @brief mainwindow.cpp
* @author Deng Zhimao
* @email 1252699831@qq.com
* @net www.openedv.com
* @date 2021-04-13
*******************************************************************/
#include "mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
/* 设置主窗体的位置与大小 */
this->setGeometry(0, 0, 800, 480);
/* tcp套接字 */
tcpSocket = new QTcpSocket(this);
/* 开始监听按钮 */
pushButton[0] = new QPushButton();
/* 停止监听按钮 */
pushButton[1] = new QPushButton();
/* 清空聊天文本按钮 */
pushButton[2] = new QPushButton();
/* 发送消息按钮 */
pushButton[3] = new QPushButton();
/* 水平布局一 */
hBoxLayout[0] = new QHBoxLayout();
/* 水平布局二 */
hBoxLayout[1] = new QHBoxLayout();
/* 水平布局三 */
hBoxLayout[2] = new QHBoxLayout();
/* 水平布局四 */
hBoxLayout[3] = new QHBoxLayout();
/* 水平容器一 */
hWidget[0] = new QWidget();
/* 水平容器二 */
hWidget[1] = new QWidget();
/* 水平容器三 */
hWidget[2] = new QWidget();
vWidget = new QWidget();
vBoxLayout = new QVBoxLayout();
/* 标签实例化 */
label[0] = new QLabel();
label[1] = new QLabel();
lineEdit = new QLineEdit();
comboBox = new QComboBox();
spinBox = new QSpinBox();
textBrowser = new QTextBrowser();
label[0]->setText("服务器地址:");
label[1]->setText("服务器端口:");
/* 设置标签根据文本文字大小自适应大小 */
label[0]->setSizePolicy(QSizePolicy::Fixed,
QSizePolicy::Fixed);
label[1]->setSizePolicy(QSizePolicy::Fixed,
QSizePolicy::Fixed);
/* 设置端口号的范围,注意不要与主机的已使用的端口号冲突 */
spinBox->setRange(10000, 99999);
pushButton[0]->setText("连接服务器");
pushButton[1]->setText("断开连接");
pushButton[2]->setText("清空文本");
pushButton[3]->setText("发送消息");
/* 设置停止监听状态不可用 */
pushButton[1]->setEnabled(false);
/* 设置输入框默认的文本 */
lineEdit->setText("广州星翼电子科技有限公司");
/* 水平布局一添加内容 */
hBoxLayout[0]->addWidget(pushButton[0]);
hBoxLayout[0]->addWidget(pushButton[1]);
hBoxLayout[0]->addWidget(pushButton[2]);
/* 设置水平容器的布局为水平布局一 */
hWidget[0]->setLayout(hBoxLayout[0]);
hBoxLayout[1]->addWidget(label[0]);
hBoxLayout[1]->addWidget(comboBox);
hBoxLayout[1]->addWidget(label[1]);
hBoxLayout[1]->addWidget(spinBox);
/* 设置水平容器的布局为水平布局二 */
hWidget[1]->setLayout(hBoxLayout[1]);
/* 水平布局三添加内容 */
hBoxLayout[2]->addWidget(lineEdit);
hBoxLayout[2]->addWidget(pushButton[3]);
/* 设置水平容器三的布局为水平布局一 */
hWidget[2]->setLayout(hBoxLayout[2]);
/* 垂直布局添加内容 */
vBoxLayout->addWidget(textBrowser);
vBoxLayout->addWidget(hWidget[1]);
vBoxLayout->addWidget(hWidget[0]);
vBoxLayout->addWidget(hWidget[2]);
/* 设置垂直容器的布局为垂直布局 */
vWidget->setLayout(vBoxLayout);
/* 居中显示 */
setCentralWidget(vWidget);
/* 获取本地ip */
getLocalHostIP();
/* 信号槽连接 */
connect(pushButton[0], SIGNAL(clicked()),
this, SLOT(toConnect()));
connect(pushButton[1], SIGNAL(clicked()),
this, SLOT(toDisConnect()));
connect(pushButton[2], SIGNAL(clicked()),
this, SLOT(clearTextBrowser()));
connect(pushButton[3], SIGNAL(clicked()),
this, SLOT(sendMessages()));
connect(tcpSocket, SIGNAL(connected()),
this, SLOT(connected()));
connect(tcpSocket, SIGNAL(disconnected()),
this, SLOT(disconnected()));
connect(tcpSocket, SIGNAL(readyRead()),
this, SLOT(receiveMessages()));
connect(tcpSocket,
SIGNAL(stateChanged(QAbstractSocket::SocketState)),
this,
SLOT(socketStateChange(QAbstractSocket::SocketState)));
}
MainWindow::~MainWindow()
{
}
void MainWindow::toConnect()
{
/* 如果连接状态还没有连接 */
if (tcpSocket->state() != tcpSocket->ConnectedState) {
/* 指定IP地址和端口连接 */
tcpSocket->connectToHost(IPlist[comboBox->currentIndex()],
spinBox->value());
}
}
void MainWindow::toDisConnect()
{
/* 断开连接 */
tcpSocket->disconnectFromHost();
/* 关闭socket*/
tcpSocket->close();
}
void MainWindow::connected()
{
/* 显示已经连接 */
textBrowser->append("已经连上服务端");
/* 设置按钮与下拉列表框的状态 */
pushButton[0]->setEnabled(false);
pushButton[1]->setEnabled(true);
comboBox->setEnabled(false);
spinBox->setEnabled(false);
}
void MainWindow::disconnected()
{
/* 显示已经断开连接 */
textBrowser->append("已经断开服务端");
/* 设置按钮与下拉列表框的状态 */
pushButton[1]->setEnabled(false);
pushButton[0]->setEnabled(true);
comboBox->setEnabled(true);
spinBox->setEnabled(true);
}
/* 获取本地IP */
void MainWindow::getLocalHostIP()
{
// /* 获取主机的名称 */
// QString hostName = QHostInfo::localHostName();
// /* 主机的信息 */
// QHostInfo hostInfo = QHostInfo::fromName(hostName);
// /* ip列表,addresses返回ip地址列表,注意主机应能从路由器获取到
// * IP,否则可能返回空的列表(ubuntu用此方法只能获取到环回IP) */
// IPlist = hostInfo.addresses();
// qDebug()<<IPlist<<endl;
// /* 遍历IPlist */
// foreach (QHostAddress ip, IPlist) {
// if (ip.protocol() == QAbstractSocket::IPv4Protocol)
// comboBox->addItem(ip.toString());
// }
/* 获取所有的网络接口,
* QNetworkInterface类提供主机的IP地址和网络接口的列表 */
QList<QNetworkInterface> list
= QNetworkInterface::allInterfaces();
/* 遍历list */
foreach (QNetworkInterface interface, list) {
/* QNetworkAddressEntry类存储IP地址子网掩码和广播地址 */
QList<QNetworkAddressEntry> entryList
= interface.addressEntries();
/* 遍历entryList */
foreach (QNetworkAddressEntry entry, entryList) {
/* 过滤IPv6地址,只留下IPv4 */
if (entry.ip().protocol() ==
QAbstractSocket::IPv4Protocol) {
comboBox->addItem(entry.ip().toString());
/* 添加到IP列表中 */
IPlist<<entry.ip();
}
}
}
}
/* 清除文本浏览框里的内容 */
void MainWindow::clearTextBrowser()
{
/* 清除文本浏览器的内容 */
textBrowser->clear();
}
/* 客户端接收消息 */
void MainWindow::receiveMessages()
{
/* 读取接收到的消息 */
QString messages = tcpSocket->readAll();
textBrowser->append("服务端:" + messages);
}
/* 客户端发送消息 */
void MainWindow::sendMessages()
{
if(NULL == tcpSocket)
return;
if(tcpSocket->state() == tcpSocket->ConnectedState) {
/* 客户端显示发送的消息 */
textBrowser->append("客户端:" + lineEdit->text());
/* 发送消息 */
tcpSocket->write(lineEdit->text().toUtf8().data());
}
}
/* 客户端状态改变 */
void MainWindow::socketStateChange(QAbstractSocket::SocketState state)
{
switch (state) {
case QAbstractSocket::UnconnectedState:
textBrowser->append("scoket状态:UnconnectedState");
break;
case QAbstractSocket::ConnectedState:
textBrowser->append("scoket状态:ConnectedState");
break;
case QAbstractSocket::ConnectingState:
textBrowser->append("scoket状态:ConnectingState");
break;
case QAbstractSocket::HostLookupState:
textBrowser->append("scoket状态:HostLookupState");
break;
case QAbstractSocket::ClosingState:
textBrowser->append("scoket状态:ClosingState");
break;
case QAbstractSocket::ListeningState:
textBrowser->append("scoket状态:ListeningState");
break;
case QAbstractSocket::BoundState:
textBrowser->append("scoket状态:BoundState");
break;
default:
break;
}
}

/************************************************************************************************************/
总结:
在 Qt 的 TCP 编程中,核心就是 QTcpServer (服务器大管家)和 QTcpSocket(通信员)这两个类。
这里按常用函数按**"动作阶段"**分类整理,并附带了通俗易懂的解释。
一、 服务器端专属:QTcpServer 类
角色: 像公司的前台,负责坐在那里等人来访,不做具体的聊天工作。
| 函数名 | 关键程度 | 含义与通俗解释 |
|---|---|---|
listen(address, port) |
⭐⭐⭐⭐⭐ | "开门营业"。 启动监听。参数通常填 QHostAddress::Any(监听本机所有网卡)和 端口号。返回 true 表示启动成功。 |
nextPendingConnection() |
⭐⭐⭐⭐⭐ | "接待这位客人"。 当有新连接时调用。它会返回一个 QTcpSocket* 指针。以后你要和这个特定的客户端发消息,全靠这个返回的指针。 |
hasPendingConnections() |
⭐⭐⭐ | "还有人在排队吗?"。 如果有等待处理的连接请求,返回 true。通常配合 nextPendingConnection 使用。 |
close() |
⭐⭐⭐ | "打烊了"。 停止监听端口,不再接受新的连接,但已经连上的通常不会立即断开(取决于实现)。 |
serverAddress() / serverPort() |
⭐⭐ | "查自己的户口"。 获取服务器当前监听的 IP 和端口号。 |
errorString() |
⭐⭐ | "刚才报错说了啥?"。 如果监听失败,调用这个函数获取人类可读的错误信息(比如"端口被占用")。 |
🔔 常用信号(Signal):
newConnection():最重要! 每当有新客户端连上来,服务器就会发出这个信号。你必须连接这个信号到槽函数里去写nextPendingConnection()。
二、 通信专属:QTcpSocket 类
角色: 像电话听筒。服务器拿到它跟客户端聊,客户端己自创建它跟服务器聊。它是真正干活的。
1. 发送与接收(读写数据)
| 函数名 | 关键程度 | 含义与通俗解释 |
|---|---|---|
write(data) |
⭐⭐⭐⭐⭐ | "说话"。 发送数据给对方。参数通常是 QByteArray 类型或字符串。 |
readAll() |
⭐⭐⭐⭐⭐ | "听完所有话"。 一次性读取缓冲区里所有收到的数据。最常用。 |
read(maxSize) |
⭐⭐⭐ | "只听几句"。 只读取指定长度的数据(处理定长数据包时有用)。 |
bytesAvailable() |
⭐⭐⭐ | "有多少话在嘴边?"。 返回当前缓冲区里有多少字节的数据等待读取。 |
2. 连接控制(客户端常用,服务器端断开时也用)
| 函数名 | 关键程度 | 含义与通俗解释 |
|---|---|---|
connectToHost(ip, port) |
⭐⭐⭐⭐⭐ | "拨号"。 (客户端专用)尝试连接指定的服务器 IP 和端口。 |
disconnectFromHost() |
⭐⭐⭐⭐ | "挂电话"。 礼貌地断开连接。它会发送断开通知,等待缓冲区数据发完后再断开。 |
abort() |
⭐⭐ | "拔网线"。 立即强制重置套接字,丢弃缓冲区数据,立即关闭。 |
3. 信息查询
| 函数名 | 关键程度 | 含义与通俗解释 |
|---|---|---|
peerAddress() |
⭐⭐⭐⭐ | "对方是谁?"。 获取连接另一端(对方)的 IP 地址。 |
peerPort() |
⭐⭐⭐⭐ | "对方哪个分机?"。 获取连接另一端(对方)的端口号。 |
state() |
⭐⭐⭐ | "电话通了吗?"。 查看当前状态。常见状态有 UnconnectedState(没连)、ConnectingState(正在连)、ConnectedState(通了)。 |
🔔 常用信号(Signal):
-
readyRead():最重要! 表示"有新数据到了,快来读!"。你必须连接这个信号,在槽函数里调用readAll()。 -
connected():(客户端用) "拨号成功,电话接通了"。 -
disconnected():(双方都用) "对方挂断了"。通常在这里做清理工作,比如socket->deleteLater()。
三、 进阶:同步(阻塞)函数
Qt 网络默认是异步 的(非阻塞,不会卡住界面),但在多线程里,有时候我们想写成"死等"的代码。这时候会用到 waitFor... 系列函数。
-
waitForConnected(msecs):挂起程序,死等直到连接成功(或超时)。 -
waitForReadyRead(msecs):挂起程序,死等直到有数据发过来(或超时)。
⚠️ 警告: 只有在子线程里才推荐用这些函数!如果在主线程(GUI线程)里用,界面会卡死(未响应),直到函数返回。
总结:一个标准的"一来一回"流程用到的函数
-
服务器:
server->listen()(开始营业) -
客户端:
socket->connectToHost()(拨号) -
服务器: 收到信号
newConnection-> 调用server->nextPendingConnection()(拿起电话) -
客户端: 收到信号
connected-> 调用socket->write("Hello")(说话) -
服务器: 收到信号
readyRead-> 调用socket->readAll()(听到话) ->socket->write("Hi back")(回话) -
客户端: 收到信号
readyRead-> 调用socket->readAll()(听到回复) -
客户端:
socket->disconnectFromHost()(挂断)