当前位置: 首页 > article >正文

【Qt网络编程】Tcp多线程并发服务器和客户端通信

目录

一、编写思路

1、服务器

(1)总体思路widget.c(主线程)

(2)详细流程widget.c(主线程)

(1)总体思路chat_thread.c(处理聊天逻辑线程)

(2)详细流程chat_thread.h(处理聊天逻辑线程)

2、客户端

(1)总体思路widget.c(主线程)

(2)详细思路widget.c(主线程)

(1)总体思路chat_thread.c(处理聊天逻辑线程)

(2)详细流程chat_thread.c(处理聊天逻辑线程)

二、实现效果

1、服务器

2、客户端


完整代码请到指定链接下载:Qt网络编程-Tcp多线程并发服务器和客户端通信: 【Qt网络编程】Tcp多线程并发服务器和客户端通信

一、编写思路

1、服务器

(1)总体思路widget.c(主线程)

  1. 初始化界面

    创建窗口、输入框、按钮等基本UI元素。

  2. 创建服务器对象

    实现 My_tcp_server 并监听客户端连接。

  3. 处理新客户端连接

    当有新客户端连接时,创建新的 Chat_thread 线程来处理通信。

  4. 绑定信号槽

    确保主线程与客户端处理线程间的信号槽连接,使用 Qt::QueuedConnection 处理跨线程通信。

  5. 处理消息传递

    接收和发送消息,并在界面上更新显示。

  6. 服务器启动与关闭

    通过按钮控制服务器的启动和关闭,管理所有客户端线程的安全退出。

(2)详细流程widget.c(主线程)

  1. 创建 Qt 界面及设置窗口属性: 首先通过 ui->setupUi(this); 来初始化用户界面,并设置窗口标题、大小等基本属性。这是 Qt 项目的常见步骤,通过 .ui 文件生成的类进行界面管理。

    Widget::Widget(QWidget *parent)
        : QWidget(parent)
        , ui(new Ui::Widget)  // 初始化UI对象
    {
        ui->setupUi(this);  // 设置UI界面
        this->setWindowTitle("--服务器--");  // 设置窗口标题
        this->resize(1024, 960);  // 设置窗口大小
    ​
        ui->le_ip->setText("127.0.0.1");
        ui->le_port->setText("9999");
    }

  2. 初始化服务器对象 My_tcp_server 并处理客户端连接

    • 创建 tcp_server 对象以处理客户端的连接。

    • 使用 connect 函数连接 tcp_servernew_descriptor 信号和匿名槽函数,确保一旦有新客户端连接,便创建一个 Chat_thread 来处理该客户端。

    this->tcp_server = new My_tcp_server(this);
    ​
    connect(tcp_server, &My_tcp_server::new_descriptor, this, [=](qintptr socketDescriptor){
        QMessageBox::information(this, "提示", "新的客户端连接!", QMessageBox::Ok, QMessageBox::Information);
    ​
        ui->btn_send->setEnabled(true);  // 启用“发送消息”按钮
    ​
        // 创建新线程处理客户端
        Chat_thread *chat_thread = new Chat_thread(socketDescriptor);
        chat_thread->moveToThread(chat_thread);  // 将线程和对象绑定到同一线程,防止冲突
    ​
        thread_list.append(chat_thread);
        
        // 启动线程处理客户端通信
        chat_thread->start();
    });
  3. 管理客户端线程 Chat_thread

    • 每当有新客户端连接时,创建一个 Chat_thread 并启动它处理客户端通信。通过 moveToThreadChat_thread 的执行线程与该对象保持一致,避免跨线程冲突。

    • 使用 connect 绑定线程中的信号(如连接断开、接收消息)和主界面槽函数,确保客户端状态能够正确显示。

    Chat_thread *chat_thread = new Chat_thread(socketDescriptor);
    chat_thread->moveToThread(chat_thread);  // 将线程与对象绑定在同一线程
    ​
    thread_list.append(chat_thread);
    ​
    // 连接信号和槽
    connect(chat_thread, &Chat_thread::break_connect, this, [=](){
        ui->te_receive->append(currentTime + "\n【状态】客户端断开连接...");
        ui->btn_send->setEnabled(false);  // 禁用“发送消息”按钮
    });
    ​
    connect(chat_thread, &Chat_thread::recv_info, this, [=](QString data){
        currentTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
        ui->te_receive->append(currentTime + "form client\n    【数据】 " + data);  // 在文本框中显示消息
    });
    ​
    chat_thread->start();  // 启动线程
  4. 处理启动和关闭服务器的按钮事件

    • on_btn_connect_clicked() 处理连接按钮点击事件,启动或关闭服务器。

    • 启动时,检查 IP 地址和端口的有效性,成功后开始监听客户端连接。

    • 关闭服务器时,停止监听,并确保所有已连接客户端线程安全退出。

    void Widget::on_btn_connect_clicked()
    {
        if (!is_server_running)
        {
            // 启动服务器
            QString ip_address = ui->le_ip->text().trimmed();
            QString port_text = ui->le_port->text().trimmed();
            
            if (!tcp_server->listen(QHostAddress(ip_address), port_text.toUInt()))
            {
                QMessageBox::warning(this, "warning", "服务器监听失败");
                return;
            }
            
            is_server_running = true;
            ui->btn_connect->setText("关闭服务器");
            ui->te_receive->append(currentTime + "\n【状态】服务器开始监听...");
        }
        else
        {
            // 停止服务器并关闭所有客户端线程
            tcp_server->close();
            for (Chat_thread *thread : qAsConst(thread_list))
            {
                thread->exit();
                thread->wait();
                thread->deleteLater();
            }
            thread_list.clear();
            is_server_running = false;
            ui->btn_connect->setText("创建服务器");
                       ui->te_receive->append(currentTime + "\n【状态】服务器已停止监听...");
           }
       }
  1. 处理发送消息按钮的点击事件

    • 当点击“发送消息”按钮时,触发 send_request 信号,利用信号槽机制将输入的消息发送给客户端。需要确保主线程和子线程的信号槽通信是异步进行的(通过 Qt::QueuedConnection)。

    void Widget::on_btn_send_clicked()
    {
        QString data = ui->te_send->toPlainText().toUtf8();
        emit send_request(data);  // 发出 send_request 信号
    }

  2. 服务器监听客户端的状态和信息传递

    • 服务器通过 recv_infosend_info 信号接收客户端消息并在界面上显示。

    • 在客户端连接成功或断开时,更新界面显示状态。

    connect(chat_thread, &Chat_thread::recv_info, this, [=](QString data){
        currentTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
        ui->te_receive->append(currentTime + "form client\n    【数据】 " + data);  // 显示接收的客户端数据
    });
    ​
    connect(chat_thread, &Chat_thread::send_info, this, [=](QString data){
        currentTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
        ui->te_receive->append(currentTime + "to client\n    【数据】 " + data);  // 显示发送给客户端的数据
    });

(1)总体思路chat_thread.c(处理聊天逻辑线程)

  1. 构造函数

    初始化 socketDescriptor 以供后续线程使用。

  2. 线程启动与套接字初始化

    run() 函数中创建 QTcpSocket,并关联 socketDescriptor

  3. 获取客户端信息

    通过 peerAddress()peerPort() 获取客户端 IP 地址和端口号,并进行错误处理。

  4. 信号槽机制连接

    将套接字状态、接收数据、错误处理等信号连接到相应的槽函数。

  5. 处理连接状态变化

    通过 handler_client_changed() 处理客户端的连接或断开,并发出相应的信号。

  6. 处理接收消息

    receive_message() 函数中处理客户端发送的消息,并发出信号 recv_info

  7. 发送消息

    send_message() 函数中,检查连接状态并发送消息,发出 send_info 信号。

  8. 错误处理

    处理客户端连接中的错误,删除资源并退出线程。

(2)详细流程chat_thread.h(处理聊天逻辑线程)

  1. 构造函数初始化

    • Chat_thread 构造函数接受一个 socketDescriptor 参数,并将其存储为类的成员变量,以供 run() 函数中使用。注意,QTcpSocket 对象将在 run() 函数中创建,以确保在新线程中创建并使用。

    Chat_thread::Chat_thread(qintptr socketDescriptor, QObject *parent)
        : QThread{parent}
        , socketDescriptor(socketDescriptor)
    {
        // socketDescriptor 存储为成员变量
    }
  2. 线程启动和套接字初始化

    • run() 函数中创建 QTcpSocket 对象,并通过 setSocketDescriptor() 将套接字描述符与 QTcpSocket 关联。这允许线程使用此套接字与客户端通信。

    • 如果套接字初始化失败,进行错误处理并返回。

    void Chat_thread::run()
    {
        // 创建 QTcpSocket 对象,用于处理与客户端的通信
        this->socket = new QTcpSocket();
    ​
        // 将套接字描述符与 QTcpSocket 关联
        if (!socket->setSocketDescriptor(socketDescriptor))
        {
            qDebug() << "Error: Failed to get new socketDescriptor.";
            return;
        }
    ​
        // 错误处理:检查是否成功获取客户端连接
        if (socket == nullptr)
        {
            qDebug() << "Error: Failed to get new client connection.";
            return;  // 如果获取失败,直接返回
        }
    }
  3. 获取客户端信息

    • 在成功创建套接字后,获取客户端的 IP 地址和端口号。

    • 如果获取失败,进行错误处理并断开连接。

    // 获取客户端的IP地址和端口号
    QString ip_addr = socket->peerAddress().toString();
    quint16 port = socket->peerPort();
    ​
    // 错误处理:检查是否成功获取IP地址和端口号
    if (ip_addr.isEmpty() || port == 0)
    {
        qDebug() << "Error: Failed to get client's IP address or port.";
        socket->disconnectFromHost();  // 断开连接
        socket->deleteLater();  // 删除客户端套接字对象
        return;  // 如果获取失败,直接返回
    }
  4. 信号槽机制的连接

    • 连接套接字的状态改变信号 stateChanged 到槽函数 handler_client_changed,以便监控客户端连接状态的变化。

    • 连接 QTcpSocketreadyRead 信号到 receive_message 槽函数,用于处理接收数据。

    • 处理套接字错误时,连接 errorOccurred 信号到 handle_socket_error 槽函数。

    // 处理连接状态变化的槽函数
    connect(socket, &QTcpSocket::stateChanged, this, &Chat_thread::handler_client_changed);
    ​
    // 错误处理:处理客户端的异常断开情况
    connect(socket, &QTcpSocket::errorOccurred, this, &Chat_thread::handle_socket_error);
    ​
    // 处理接收数据的槽函数
    connect(socket, &QTcpSocket::readyRead, this, &Chat_thread::receive_message);

  5. 处理客户端连接状态变化

    • handler_client_changed() 槽函数中,根据客户端的连接状态(如断开、已连接)做相应处理并发出信号,通知其他部分更新状态。

    void Chat_thread::handler_client_changed(QAbstractSocket::SocketState socket_state)
    {
        socket = (QTcpSocket*)sender();  // 获取发信的客户端套接字
        if(!socket) return;
    ​
        switch (socket_state)
        {
        case QAbstractSocket::UnconnectedState:  // 客户端断开连接
            emit break_connect();
            break;
    ​
        case QAbstractSocket::ConnectedState:  // 客户端已连接
            emit complete_connect();
            break;
    ​
        default:
            break;
        }
    }
  6. 接收消息的处理

    • receive_message() 槽函数中,通过 socket->readAll() 读取客户端发送的所有数据,并发出信号 recv_info 通知上层处理。

    void Chat_thread::receive_message()
    {
        if (socket)
        {
            QString data = socket->readAll();  // 读取客户端发送的所有数据
            emit recv_info(data);  // 发出信号,通知收到消息
        }
    }
  7. 发送消息

    • send_message() 函数中,检查客户端是否处于连接状态,如果是则发送消息,否则输出警告信息。

    • 发送完成后,发出 send_info 信号。

    void Chat_thread::send_message(QString data)
    {
        if (socket->state() == QAbstractSocket::ConnectedState)
        {
            socket->write(data.toUtf8());  // 发送数据
        }
        else
        {
            qDebug() << "warning:   客户端未连接,无法发送消息";  // 输出警告
        }
    ​
        emit send_info(data);  // 发出信号,通知发送消息
    }
  8. 错误处理

    • handle_socket_error() 函数中处理 QTcpSocket 的错误。如果出现错误,打印错误信息,并退出线程。

    • 删除套接字对象并退出线程事件循环。

    void Chat_thread::handle_socket_error(QAbstractSocket::SocketError socketError)
    {
        qDebug() << "Client connection error, error code: " << socketError;
    ​
        this->exit();  // 退出线程
        this->wait();  // 等待线程完全退出
        socket->deleteLater();  // 删除客户端套接字对象
    ​
        // 停止线程事件循环
        quit();
    }

2、客户端

(1)总体思路widget.c(主线程)

  1. 初始化界面

    设置窗口属性并初始化用户输入的默认值。

  2. 创建线程和通信任务对象

    实现异步通信,使用 QThread 和自定义 Chat_thread 处理服务器交互。

  3. 信号槽机制的建立

    连接 UI 和工作线程之间的信号槽,确保各操作异步处理。

  4. 线程管理

    在析构函数中确保线程安全退出,释放资源。

  5. 处理连接与断开

    通过按钮触发连接和断开操作,并更新 UI 显示。

  6. 消息传递与显示

    处理消息的发送与接收,并在 UI 界面上更新显示结果。

(2)详细思路widget.c(主线程)

  1. 初始化界面

    • 使用 ui->setupUi(this) 初始化用户界面,并设置窗口标题和窗口大小。

    • 初始化 IP 地址和端口号的默认值。

    Widget::Widget(QWidget *parent)
        : QWidget(parent)
        , ui(new Ui::Widget)
        , is_connected(false)  // 初始化连接状态为未连接
    {
        ui->setupUi(this);  // 设置UI界面
        this->setWindowTitle("-客户端-");  // 设置窗口标题
        this->resize(1024, 960);  // 设置窗口大小
    ​
        ui->le_ip->setText("127.0.0.1");  // 设置默认IP地址
        ui->le_port->setText("8888");  // 设置默认端口号
    }

  2. 创建线程和通信任务对象

    • 创建 QThread 对象以进行异步通信任务。

    • 创建 Chat_thread 对象负责与服务器进行通信操作。

    • 使用 moveToThread 将通信任务对象移到新的线程中执行,并启动该线程。

    // 创建线程对象
    thread = new QThread;
    ​
    // 创建任务对象,负责与服务器的通信
    Chat_thread *worker = new Chat_thread;
    ​
    worker->moveToThread(thread);  // 将任务对象移至线程
    thread->start();  // 启动工作线程

  3. 信号槽机制的建立

    • 使用信号槽连接 UI 和工作线程之间的交互。例如,连接服务器、发送消息、断开连接等操作通过信号槽机制进行。

    • 信号从 UI 线程发出,工作线程的槽函数接收信号并执行相关操作。

    // 信号槽连接:从UI线程发出连接信号,worker线程接收并执行连接操作
    connect(this, &Widget::connect_server, worker, &Chat_thread::start_connected);
    connect(this, &Widget::send_info, worker, &Chat_thread::start_send);
    connect(this, &Widget::quit_connect, worker, &Chat_thread::break_connected);
    ​
    // 连接断开信号槽,worker线程通知UI线程更新UI
    connect(worker, &Chat_thread::connect_cancel, this, &Widget::submit_connect_cancel);
    connect(worker, &Chat_thread::connected, this, &Widget::submit_connect_info);
    connect(worker, &Chat_thread::transfer_recv_info, this, &Widget::submit_recv_info);

  4. 管理线程的生命周期

    • 在析构函数中,确保工作线程在窗口关闭时被正确停止,并释放相关资源。

    • 如果线程正在运行,需要先请求线程退出,然后等待其完全退出后再删除。

    Widget::~Widget()
    {
        if (thread->isRunning())
        {
            thread->quit();  // 请求线程退出
            thread->wait();  // 等待线程结束
        }
        delete worker;  // 删除任务对象
        delete thread;  // 删除线程对象
        delete ui;  // 删除UI对象
    }

  5. 处理连接成功或断开连接的槽函数

    • 当客户端成功连接到服务器时,工作线程发出 connected 信号,UI 界面通过槽函数 submit_connect_info() 来更新显示状态,并启用“发送消息”按钮。

    • 断开连接时,UI 界面通过槽函数 submit_connect_cancel() 来禁用“发送消息”按钮,并更新状态显示。

    // 连接成功时的槽函数,更新UI显示信息
    void Widget::submit_connect_info()
    {
        currentTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
        ui->te_receive->append(currentTime + "\n【状态】已成功连接到服务器");
        ui->btn_connect->setText("断开服务器");
        ui->btn_send->setEnabled(true);   // 启用发送按钮
        is_connected = true;
    }
    ​
    // 断开连接时的槽函数,更新UI显示信息
    void Widget::submit_connect_cancel()
    {
        is_connected = false;
    }

  6. 处理消息的发送与接收

    • 当用户点击“发送消息”按钮时,获取文本框中的消息,发出 send_info 信号,将消息发送到服务器。

    • 当从服务器接收到消息时,工作线程发出 transfer_recv_info 信号,UI 界面更新显示接收到的消息。

    // 当用户点击发送按钮时,读取输入框中的内容并发送给服务器
    void Widget::on_btn_send_clicked()
    {
        currentTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
        QString message = ui->te_send->toPlainText().toUtf8();  // 获取用户输入的消息
        ui->te_receive->append(currentTime + " to server\n    【数据】" + message + "\n");
        emit send_info(message);  // 发出信号,通知工作线程发送消息
    }
    ​
    // 当接收到服务器发送的消息时,更新UI显示接收到的消息
    void Widget::submit_recv_info(QString message)
    {
        currentTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
        ui->te_receive->append(currentTime + " form server\n    【数据】" + message + "\n");  // 显示服务器的消息
    }

  7. 处理连接与断开的按钮事件

    • 当点击“连接”按钮时,获取 IP 地址和端口号,检查输入的有效性后发出 connect_server 信号,通知工作线程与服务器建立连接。

    • 当点击“断开服务器”按钮时,发出 quit_connect 信号,通知工作线程断开连接。

    // 当用户点击"连接"按钮时触发该槽函数
    void Widget::on_btn_connect_clicked()
    {
        if (!is_connected)
        {
            QString ip_address = ui->le_ip->text().trimmed();
            QString port_text = ui->le_port->text().trimmed();
    ​
            QHostAddress address;
            if (!address.setAddress(ip_address))  // 检查IP地址的有效性
            {
                QMessageBox::warning(this, "warning", "无效的IP地址,请重新输入!");
                return;
            }
    ​
            bool ok;
            unsigned int port = port_text.toUInt(&ok);
            if (!ok || port == 0 || port > 65535)  // 检查端口号的有效性
            {
                QMessageBox::warning(this, "warning", "无效的端口号,请输入1到65535之间的数值!");
                return;
            }
    ​
            emit connect_server(ip_address, port);  // 发出连接服务器的信号
        }
        else
        {
            currentTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
            ui->te_receive->append(currentTime + "\n【状态】已断开与服务器的连接");
            ui->btn_send->setEnabled(false);  // 禁用发送按钮
            is_connected = false;
            emit quit_connect();  // 发出断开连接的信号
        }
    }

(1)总体思路chat_thread.c(处理聊天逻辑线程)

  1. 构造函数

    初始化 Chat_thread 对象。

  2. 接收消息

    通过 readyRead 信号槽接收服务器发送的数据,并将其转发给主线程。

  3. 处理连接状态变化

    监控与服务器的连接状态,并打印调试信息。

  4. 断开连接

    关闭套接字连接并释放资源,发出连接断开信号。

  5. 启动连接

    通过指定的 IP 和端口号连接服务器,并处理连接成功、失败、断开、接收数据等事件。

  6. 发送消息

    检查连接状态并发送消息。如果未连接,则发出未连接信号。

(2)详细流程chat_thread.c(处理聊天逻辑线程)

  1. 构造函数

    • 构造函数 Chat_thread::Chat_thread(QObject *parent) 初始化 Chat_thread 对象。在这个阶段不需要任何复杂的逻辑,主要是确保对象正常创建。

    Chat_thread::Chat_thread(QObject *parent)
        : QObject{parent}
    {}

  2. 接收消息处理

    • receive_message() 是一个槽函数,用于接收从服务器发送的数据。当 QTcpSocket 对象有数据可读取时,信号 readyRead 会被触发,调用此槽函数。读取数据后,通过 transfer_recv_info 信号将接收到的消息发送出去。

    void Chat_thread::receive_message()
    {
        QString message = socket->readAll();  // 从服务器读取数据
        emit transfer_recv_info(message);  // 发出信号,通知接收到的数据
    }

  3. 处理连接状态变化

    • state_changed() 函数是一个槽函数,用于处理客户端与服务器的连接状态变化。根据不同的 QAbstractSocket::SocketState 枚举值,打印调试信息并处理相应状态的变化。

    void Chat_thread::state_changed(QAbstractSocket::SocketState socketstate)
    {
        QString stateStr;  // 用于保存状态的字符串
        switch (socketstate)
        {
        case QAbstractSocket::UnconnectedState:
            qDebug()<< "\n【状态】与服务器断开连接...";
            stateStr = "UnconnectedState";
            break;
    ​
        case QAbstractSocket::ConnectedState:
            stateStr = "ConnectedState";
            qDebug()<< "【状态】与服务器建立连接...";
            break;
    ​
        case QAbstractSocket::HostLookupState:
            stateStr = "HostLookupState";
            qDebug()<< "【状态】正在查找主机...";
            break;
    ​
        case QAbstractSocket::ConnectingState:
            stateStr = "ConnectingState";
            qDebug()<< "【状态】正在连接服务器...";
            break;
    ​
        case QAbstractSocket::ClosingState:
            stateStr = "ClosingState";
            qDebug()<< "【状态】正在关闭连接...";
            break;
    ​
        default:
            stateStr = "UnknownState";
            qDebug()<< "未知的错误, 当前状态: " + stateStr;
            break;
        }
    }

  4. 断开连接处理

    • break_connected() 用于处理断开与服务器的连接。当套接字连接断开时,关闭并释放资源,并发出 connect_cancel 信号通知主线程。

    void Chat_thread::break_connected()
    {
        socket->close();  // 关闭套接字
        socket->deleteLater();  // 延迟删除套接字,释放资源
        emit connect_cancel();  // 发出连接断开信号
    }

  5. 开始连接服务器

    • start_connected() 用于发起连接服务器的请求。创建 QTcpSocket 对象并尝试连接到指定的 IP 和端口。连接成功、失败、断开、接收数据等事件都会通过信号槽机制进行处理。

    void Chat_thread::start_connected(QString IP, unsigned short PORT)
    {
        socket = new QTcpSocket;  // 创建套接字对象
    ​
        socket->connectToHost(QHostAddress(IP), PORT);  // 连接到服务器
    ​
        // 连接成功时,发送 connected 信号通知主线程上传消息
        connect(socket, &QTcpSocket::connected, this, &Chat_thread::connected);
    ​
        // 连接失败时处理
        connect(socket, &QTcpSocket::errorOccurred, this, [=](QAbstractSocket::SocketError socketError){
            qDebug() << "连接失败";
            QMessageBox::critical(nullptr, "连接失败", "连接失败,错误代码:" + QString::number(socketError));
        });
    ​
        // 连接断开时处理
        connect(socket, &QTcpSocket::disconnected, this, &Chat_thread::break_connected);
    ​
        // 监听数据接收
        connect(socket, &QTcpSocket::readyRead, this, &Chat_thread::receive_message);
    }

  6. 发送消息

    • start_send() 用于发送消息到服务器。首先检查套接字是否处于连接状态,如果已连接,则发送消息。如果未连接,则发出 not_connected 信号。

    void Chat_thread::start_send(QString message)
    {
        if (socket && socket->state() == QAbstractSocket::ConnectedState)
        {
            socket->write(message.toUtf8());  // 发送消息
        }
        else
        {
            emit not_connected();  // 如果未连接,发出未连接信号
        }
    }

二、实现效果

1、服务器

2、客户端


http://www.kler.cn/a/311513.html

相关文章:

  • 项目模块详细说明
  • Oracle RAC的thread
  • 大数据新视界 -- 大数据大厂之 Impala 性能优化:优化数据加载的实战技巧(下)(16/30)
  • PCL 点云分割 基于CPC算法的分割
  • 「QT」几何数据类 之 QLine 整型直线类
  • 【ARM Coresight OpenOCD 系列 5 -- arp_examine 使用介绍】
  • Laravel邮件发送:从配置到发邮件的指南!
  • 从零开始讲DDR(2)——DDR的核心技术
  • 软考架构-架构风格
  • 全网最适合入门的面向对象编程教程:50 Python函数方法与接口-接口和抽象基类
  • 2-95 基于matlab的模板定位
  • 阿里云容器服务Kubernetes部署新服务
  • springboot实战章节小结
  • SpringSecurity原理解析(七):权限校验流程
  • TypeScript异常处理
  • Spring IoC 配置类 总结
  • LabVIEW多语言支持优化
  • [数据集][目标检测]不同颜色的安全帽检测数据集VOC+YOLO格式7574张5类别
  • 版本控制之Git
  • U盘显示未被格式化:深入解析、恢复策略与预防之道
  • 前端mock了所有……
  • firewalld实现NAT端口转发
  • 美国站群服务器优化技巧解析
  • Opencv边缘检测(四)
  • 梯度计算中的一些算子
  • 魔方财务迁移指南