Qt多进程(四)QTcpSocket

前言

本节将介绍进程间通信的第三种方法------Tcp。对于IPC通信而言,它既可以实现本机的进程间通信,也可以实现跨机器的通信。

然而相比于IPC,Tcp本质上是为网络通信设计的,它是网络通信中绕不开的经典话题,也是实际项目开发中经常使用的通信技术,主要应用在局域网内的数据传输。

如果深入网络通信相关的内容,我认为不大属于进程间通信的范畴,比方说Tcp的粘包问题。因此,本节只简单介绍Tcp在Qt中的使用,并演示如何实现进程间的连接和通信。

一、TCP基础知识

1.TCP 是什么?

Transmission Control Protocol(传输控制协议)

面向连接、可靠、有序、基于字节流的传输层协议。

保证数据不丢失、不重复、按序到达。
2. TCP 用于 IPC 的前提

虽然 TCP 主要用于网络通信,但本机进程可通过 127.0.0.1(IPv4)或 ::1(IPv6)进行通信。

操作系统会将发往 127.0.0.1 的数据包绕过物理网卡,直接在内核协议栈中回环处理,称为 loopback(回环)。

因此,本地 TCP 通信是合法且有效的 IPC 方式,尽管不是最高效的。
3.Qt 对 TCP 的封装:QTcpSocket 与 QTcpServer

QTcpServer 监听 TCP 连接请求的服务端,socket() + bind() + listen() + accept()

QTcpSocket 客户端连接 或 服务端 accepted 的连接,connect() 或 accept() 返回的 socket

核心特性:

继承自 QIODevice,支持 read() / write() / readyRead() 等流式接口。

支持异步事件驱动(信号槽)和同步阻塞(waitForConnected() 等)两种模式。

自动处理字节序、缓冲、重连等细节(但需注意粘包问题)。
4.TCP 作为 IPC 方式的优缺点

✅ 优点

跨平台一致 Windows/Linux/macOS 行为完全一致

开发简单 API 成熟,调试方便(可用 Wireshark、netstat)

天然支持多连接 一个 QTcpServer 可同时服务多个客户端进程

防火墙友好 仅限 localhost,通常无需额外配置

语言无关 任何支持 TCP 的语言(Python/C#/Rust)都能与 Qt 进程通信

❌ 缺点

性能开销大 数据需经过完整 TCP/IP 协议栈(校验和、缓冲、ACK 等)

延迟较高 相比 QLocalSocket 或共享内存,延迟高 2~5 倍

端口管理麻烦 需避免端口冲突(如多个实例运行)

粘包问题 TCP 是字节流,无消息边界,需自行设计协议(如长度前缀)

资源占用 每个连接消耗文件描述符/句柄,大量连接时需优化
5.粘包问题

这个问题没有自行解决过,在面试中曾经回答不上来。其实想要解决粘包问题,主要是通过协议头和数据缓冲来实现的。协议头包含了数据的长度信息,而数据缓冲提供了完整包数据的合并和读取,以此来解决单次接受读取到的信息并不完整的问题。

实际工作中其实曾经遇到过,程序中通过tcp来接受摄像头数据,一帧图像的大小往往比较大,不像简短的文本信息,因此单次接收可能并不完整。因此协议层中做了不少设计,比如协议头、数据帧大小等等。
6.和LocalSocket的关系

上一节也说过了,LocalSocket不属于Tcp的范畴,但概念上同样属于Socket通信的一种。Qt官方对LocalSocket封装时,故意将接口类似TcpSocket的样子,以降低开发者学习成本。所以,QTcpSocket的使用和上一节会很像。

二、代码实例

同样的,我们为QTcpSocket设计单独的测试窗口类:

cpp 复制代码
#ifndef TCPWINDOW_H
#define TCPWINDOW_H

#include <QWidget>
#include <QTextEdit>
#include <QLineEdit>
#include <QPushButton>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QSpinBox>
#include <QTcpServer>
#include <QTcpSocket>
#include <QProcess>
#include <QDateTime>

class TCPWindow : public QWidget
{
    Q_OBJECT

public:
    explicit TCPWindow(const QString &role, QWidget *parent = nullptr);

private slots:
    void onSendMessage();
    void onPortChanged(int port);
    void onAcceptConnection();
    void onReadyRead();
    void onDisconnected();
    void onStartServerClicked();
    void onConnectClicked();

private:
    void appendLog(const QString &msg);
    void setupServer();
    void setupClient();

    QString m_role;
    int m_port;
    QTextEdit *m_logView;
    QLineEdit *m_inputEdit;
    QPushButton *m_sendButton;
    QPushButton *m_startServerButton;
    QPushButton *m_connectButton;
    QSpinBox *m_portSpinBox;
    QTcpServer *m_server;
    QTcpSocket *m_socket;
    QProcess *m_workerProcess;
};

#endif // TCPWINDOW_H
cpp 复制代码
#include "tcpwindow.h"
#include <QLabel>
#include <QApplication>
#include <QTimer>

TCPWindow::TCPWindow(const QString &role, QWidget *parent)
    : QWidget(parent)
    , m_role(role)
    , m_port(9876)
    , m_server(nullptr)
    , m_socket(nullptr)
    , m_workerProcess(nullptr)
{
    setWindowTitle("TCP Socket - " + role);
    resize(600, 500);

    m_logView = new QTextEdit();
    m_logView->setReadOnly(true);
    m_inputEdit = new QLineEdit();
    m_inputEdit->setPlaceholderText("Enter message to send...");
    m_sendButton = new QPushButton("Send Message");
    m_sendButton->setEnabled(false);

    QHBoxLayout *portLayout = new QHBoxLayout();
    QLabel *portLabel = new QLabel("Port:");
    m_portSpinBox = new QSpinBox();
    m_portSpinBox->setRange(1024, 65535);
    m_portSpinBox->setValue(m_port);
    m_startServerButton = new QPushButton("Start Server");
    m_connectButton = new QPushButton("Connect Server");

    portLayout->addWidget(portLabel);
    portLayout->addWidget(m_portSpinBox);
    portLayout->addWidget(m_startServerButton);
    portLayout->addWidget(m_connectButton);

    QVBoxLayout *mainLayout = new QVBoxLayout();
    mainLayout->addWidget(m_logView);
    mainLayout->addLayout(portLayout);
    mainLayout->addWidget(m_inputEdit);
    mainLayout->addWidget(m_sendButton);

    setLayout(mainLayout);

    connect(m_sendButton, &QPushButton::clicked, this, &TCPWindow::onSendMessage);
    connect(m_portSpinBox, QOverload<int>::of(&QSpinBox::valueChanged), this, &TCPWindow::onPortChanged);
    connect(m_startServerButton, &QPushButton::clicked, this, &TCPWindow::onStartServerClicked);
    connect(m_connectButton, &QPushButton::clicked, this, &TCPWindow::onConnectClicked);

    if (m_role == "Server") {
        setupServer();
        m_connectButton->hide();
    } else {
        setupClient();
        m_startServerButton->hide();
    }

    appendLog("TCP " + m_role + " initialized on port " + QString::number(m_port));
}

void TCPWindow::setupServer()
{
    m_server = new QTcpServer(this);
    connect(m_server, &QTcpServer::newConnection, this, &TCPWindow::onAcceptConnection);

}

void TCPWindow::setupClient()
{
    m_socket = new QTcpSocket(this);
    connect(m_socket, &QTcpSocket::connected, this, [this]() {
        appendLog("Connected to server!");
        m_sendButton->setEnabled(true);
        m_inputEdit->setPlaceholderText("Enter message to send to server...");
    });

    connect(m_socket, &QTcpSocket::readyRead, this, &TCPWindow::onReadyRead);
    connect(m_socket, &QTcpSocket::disconnected, this, &TCPWindow::onDisconnected);
}

void TCPWindow::onAcceptConnection()
{
    if (m_socket) {
        m_socket->disconnectFromHost();
        m_socket->deleteLater();
    }

    m_socket = m_server->nextPendingConnection();
    if (!m_socket) return;

    connect(m_socket, &QTcpSocket::readyRead, this, &TCPWindow::onReadyRead);
    connect(m_socket, &QTcpSocket::disconnected, this, &TCPWindow::onDisconnected);

    appendLog("Client connected!");
    m_sendButton->setEnabled(true);
    m_inputEdit->setPlaceholderText("Enter message to send to client...");
}

void TCPWindow::onReadyRead()
{
    if (!m_socket) return;

    QByteArray data = m_socket->readAll();
    appendLog("Received: " + QString::fromUtf8(data));
}

void TCPWindow::onDisconnected()
{
    appendLog("Disconnected from peer");
    m_sendButton->setEnabled(false);
}

void TCPWindow::onStartServerClicked()
{
    if(m_server){
        if (!m_server->listen(QHostAddress::LocalHost, m_port)) {
            appendLog("Failed to start server: " + m_server->errorString());
            return;
        }
        appendLog("Server listening on port " + QString::number(m_port));
    }
}

void TCPWindow::onSendMessage()
{
    if (!m_socket || m_socket->state() != QAbstractSocket::ConnectedState) {
        appendLog("Not connected!");
        return;
    }

    QString msg = m_inputEdit->text();
    if (msg.isEmpty()) return;

    m_socket->write(msg.toUtf8());
    m_socket->flush();
    appendLog("Sent: " + msg);
    m_inputEdit->clear();
}

void TCPWindow::onPortChanged(int port)
{
    m_port = port;
}

void TCPWindow::onConnectClicked()
{
    if (!m_socket) return;

    m_socket->connectToHost("127.0.0.1", m_port);
    appendLog("Attempting to connect to 127.0.0.1:" + QString::number(m_port));
}

void TCPWindow::appendLog(const QString &msg)
{
    m_logView->append(QDateTime::currentDateTime().toString("hh:mm:ss") + " | " + msg);
}

具体代码就不啰嗦了,确实比较简单,而且和上一节非常相似。

使用上,注意要使用127.0.0.1的ip,端口的话可以在规则范围内合理选择。

贴上运行效果图:

三、总结

个人认为,在跨机器的场景中,Tcp还是非常实用的。当然,在网络通信当中,用以实现通信的技术和第三方库很多,比如我使用过的MQTT。需要记住的是,在本机场景中,选择LocalSocket的效率其实是更高的。

与Tcp相对的,还有Udp这种通信方式。在IPC通信中,一般不会使用Udp,因为它属于广播/主播,特点是无连接和不可靠,因此进程间通信的安全性和可靠性无法确保。

但在某些场景下,比如一对多(1-30)的消息统一分发中,Udp还是非常实用的。

既然已经写了Tcp,下一节就顺便记录和实现一下Udp好了。

相关推荐
LinHenrY12272 小时前
初识C语言(预处理详解)
c语言·开发语言
CC.GG2 小时前
【Qt】常用控件----QWidget属性
java·数据库·qt
hqwest2 小时前
码上通QT实战02--登录设计
开发语言·qt·登录·ui设计·qt控件·qt布局·qt登录
superman超哥2 小时前
仓颉Actor模型的实现机制深度解析
开发语言·后端·python·c#·仓颉
superman超哥2 小时前
仓颉内存管理深度探索:引用计数的实现原理与实战
c语言·开发语言·c++·python·仓颉
资生算法程序员_畅想家_剑魔2 小时前
Java常见技术分享-13-多线程安全-锁机制-底层核心实现机制
java·开发语言
TechPioneer_lp2 小时前
27届暑期实习内推:网易美团京东快手等
数据结构·c++·人工智能·笔记·机器学习·面试
shix .2 小时前
spiderdemo 2-混淆
开发语言·python
lsx2024062 小时前
Bootstrap 页面标题:设计指南与最佳实践
开发语言