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() (挂断)

相关推荐
用户805533698034 天前
不止三件套:QObject 属性系统全关键字与运行时反射!
c++·qt
xcyxiner4 天前
DicomViewer (vcpkg Windows和ubuntu编译)7
qt
Quz9 天前
QML Hello World 入门示例
qt
xcyxiner12 天前
DicomViewer (dcmtk读取dcm文件)5
qt
xcyxiner13 天前
DicomViewer (后台线程处理文件)4
qt
xcyxiner13 天前
DicomViewer (添加模型类)3
qt
xcyxiner14 天前
DicomViewer (目录调整) 2
qt
xcyxiner14 天前
dcmtk vtk vtk-dicom(gdcm) 编译(debug) v2
qt
LDR00616 天前
Type-C 快充全面升级!LDR6601 赋能个人护理便携电机,重塑剃须刀 / 理发器新体验
c语言·开发语言
雪碧聊技术16 天前
Tree.js是什么?一文讲透
开发语言·javascript·ecmascript