Qt 网络编程 TCP通信

🧠 一、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

流程:

  1. 创建服务器对象

  2. 监听端口

  3. 有客户端连接时,得到一个新的 QTcpSocket

  4. 用 socket 收发数据

Qt 伪代码结构:

cpp 复制代码
QTcpServer server;
server.listen(QHostAddress::Any, 8888);

connect(&server, &QTcpServer::newConnection, [&](){
    QTcpSocket *client = server.nextPendingConnection();
    // 使用 client 进行读写
});

🔸 2. 客户端:QTcpSocket

流程:

  1. 创建 socket

  2. connectToHost("服务器IP", 端口)

  3. 连接成功后即可 write()

  4. 有数据时 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): 你说:"好的,拜拜。" ------ 然后电话挂断。


第二部分:核心知识点(必须要懂的"黑话")

在写代码之前,有几个词你一定会遇到:

  1. 套接字 (Socket)

    • 形象理解: 它是电话机插座。

    • 解释: 在程序里,Socket 就是网络通信的"句柄"或"入口"。无论是服务器还是客户端,想要联网,手里都得攥着一个 Socket 对象。

  2. IP 地址 (IP Address)

    • 形象理解: 它是房子地址(比如:xx市xx路100号)。

    • 解释: 定位网络上的一台电脑。

  3. 端口号 (Port)

    • 形象理解: 它是分机号柜台窗口

    • 解释: 一台电脑上同时运行着微信、浏览器、QQ。数据到了电脑(IP)后,该给谁?靠端口区分。比如 Web 常用 80,我们自己写程序可以用 12345。

  4. 服务器 (Server) vs 客户端 (Client)

    • 服务器:客服中心。它一直开机监听(Listen),等待别人打进来。

    • 客户端:顾客。它主动拨号(Connect)去连接服务器。


第三部分:在 Qt 中如何简单使用

Qt 的网络模块非常强大,核心类只有两个:

  1. QTcpServer:用于服务器,负责监听端口,派发 Socket。

  2. 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线程)里用,界面会卡死(未响应),直到函数返回。


总结:一个标准的"一来一回"流程用到的函数

  1. 服务器: server->listen() (开始营业)

  2. 客户端: socket->connectToHost() (拨号)

  3. 服务器: 收到信号 newConnection -> 调用 server->nextPendingConnection() (拿起电话)

  4. 客户端: 收到信号 connected -> 调用 socket->write("Hello") (说话)

  5. 服务器: 收到信号 readyRead -> 调用 socket->readAll() (听到话) -> socket->write("Hi back") (回话)

  6. 客户端: 收到信号 readyRead -> 调用 socket->readAll() (听到回复)

  7. 客户端: socket->disconnectFromHost() (挂断)

相关推荐
开始了码1 小时前
QT::鼠标事件简单介绍
qt
T***u3331 小时前
PHP在电商中的会员管理
开发语言·wireshark·php·ue4·jina
张丶大帅1 小时前
JS案例合集
开发语言·javascript·笔记
2301_795167202 小时前
Python 高手编程系列八:缓存
开发语言·python·缓存
8***29312 小时前
Go基础之环境搭建
开发语言·后端·golang
Yue丶越2 小时前
【C语言】自定义类型:联合体与枚举
c语言·开发语言
csbysj20203 小时前
DOM 节点
开发语言
小尧嵌入式3 小时前
C++基础语法总结
开发语言·c++·stm32·单片机·嵌入式硬件·算法
@游子4 小时前
Python学习笔记-Day2
开发语言·python