Qt之进程通信-IPC(QLocalServer,QLocalSocket 含源码+注释)

这篇具有很好参考价值的文章主要介绍了Qt之进程通信-IPC(QLocalServer,QLocalSocket 含源码+注释)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、IPC通信示例图

1.1 设置关键字并连接的示例图

如下,分别在各个界面的关键字控件中填入key,依次连接。
Qt之进程通信-IPC(QLocalServer,QLocalSocket 含源码+注释),qt,开发语言

1.2 进程间简单的数据通信示例图

如下,简单演示了server与全部、指定socket通信及接收socket发送的数据。
Qt之进程通信-IPC(QLocalServer,QLocalSocket 含源码+注释),qt,开发语言

1.3 断开连接的示例图

1.3.1 由Server主动断开连接

如下,演示了单独断开一个及断开全部的操作,其中断开操作是由server发送数据通知socket断开,server这边则等待断开返回。
Qt之进程通信-IPC(QLocalServer,QLocalSocket 含源码+注释),qt,开发语言

1.3.2 由Socket主动断开连接

如下演示了socket程序主动断开的操作
Qt之进程通信-IPC(QLocalServer,QLocalSocket 含源码+注释),qt,开发语言

1.4 Server停止监听后的效果

如下,演示了server停止监听后仍可以与已经连接过的socket的通信的效果。
Qt之进程通信-IPC(QLocalServer,QLocalSocket 含源码+注释),qt,开发语言

二、个人理解与一些心得

  1. 若要使用QLocalServer/QLocalSocket,需要在 pro添加network模块(添加这一行QT += network)。
  2. 在我个人使用中发现,在同一进程中,调用socket的write是不会触发当前进程的readyRead信号链接的信号槽。
  3. 在QLocalServer停止监听后不会影响已经连接好的Socket对象,因为QLocalServer的close仅负责停止监听,并不断开。

三、一些疑问(求教 家人们😂)

  1. 在帮助中又下方的帮助代码,但是在本地测试发现不能先调用disconnectFromServer,后面的waitForDisconnected总是拿不到状态。
	socket->disconnectFromServer();
  	if (socket->waitForDisconnected(1000))
      qDebug("Disconnected!");
  1. 以及在个人理解中的第2点也存在一些疑问

四、源码

CMainWindowServer

CMainWindowServer.h

#ifndef CMAINWINDOWSERVER_H
#define CMAINWINDOWSERVER_H

#include <QMainWindow>
#include <QLocalServer>

namespace Ui {
class CMainWindowServer;
}

class QLocalSocket;
class CMainWindowServer : public QMainWindow
{
    Q_OBJECT

public:
    explicit CMainWindowServer(QWidget *parent = nullptr);
    ~CMainWindowServer();

private:
    /**
     * @brief disconnectSocketByStr 指定socket断开函数(复用)
     * @param socketStr 指定的socket套接字字符串
     */
    void disconnectSocketByStr(const QString &socketStr);

private slots:
    /**
     * @brief on_btnListen_clicked 开始监听按钮
     */
    void on_btnListen_clicked();

    /**
     * @brief on_btnStopListen_clicked 停止监听按钮
     */
    void on_btnStopListen_clicked();

    /**
     * @brief on_newConnection 新连接槽函数
     */
    void on_newConnection();

    /**
     * @brief on_socketReadyRead 数据接收槽函数
     */
    void on_socketReadyRead();

    /**
     * @brief on_btnDisconnectSocket_clicked 断开socket槽函数
     */
    void on_btnDisconnectSocket_clicked();

    /**
     * @brief on_btnSend_clicked 数据发送按钮
     */
    void on_btnSend_clicked();

private:
    Ui::CMainWindowServer *ui;

    QLocalServer            m_localServer;          // 通信服务对象

    QList<QLocalSocket *>   m_listLocalSockets;     //  本地套接字列表
};

#endif // CMAINWINDOWSERVER_H

CMainWindowServer.cpp

#include "CMainWindowServer.h"
#include "ui_CMainWindowServer.h"

#include <QLocalServer>
#include <QMessageBox>
#include <QLocalSocket>
#include <QDebug>
#include <QTimer>

CMainWindowServer::CMainWindowServer(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::CMainWindowServer)
{
    ui->setupUi(this);
    // 关联套接字连接槽函数
    connect(&m_localServer, &QLocalServer::newConnection, this, &CMainWindowServer::on_newConnection);
}

CMainWindowServer::~CMainWindowServer()
{
    delete ui;
}

void CMainWindowServer::disconnectSocketByStr(const QString &socketStr)
{
    // 强转当前指针字符串或者socket指针对象
    QLocalSocket *socket = (QLocalSocket *)socketStr.toUInt();
    // 判断是否存在于socket容器中
    if(m_listLocalSockets.contains(socket)) {
        // 发送关闭提示给socket
        socket->write(u8"服务器断开!");
        // 等待3000毫秒接收断开链接的信息
        if(!socket->waitForDisconnected(3000)) {
            QMessageBox::information(this, u8"提示", "断开超时");
        }
        else {
            // 移除当前位置的控件
            QMessageBox::information(this, u8"提示", "断开成功");
            // 移除当前指定的
            ui->comboBoxSockets->removeItem(ui->comboBoxSockets->findText(socketStr));
            m_listLocalSockets.removeOne(socket);
        }
    }
    else {
        QMessageBox::information(this, u8"提示", socketStr + u8"地址无记录");
    }
}

void CMainWindowServer::on_btnListen_clicked()
{
    QString listenKey = ui->lineEditListenKey->text();
    // 获取是否监听成功
    bool flag =  m_localServer.listen(listenKey);
    if(!flag) {
        QMessageBox::information(this, u8"提示", m_localServer.errorString());
    }
    else {
        QMessageBox::information(this, u8"提示", u8"监听成功");
        // 监听后‘开始监听’按钮禁用,‘停止监听’按钮启用
        ui->btnListen->setEnabled(false);
        ui->btnStopListen->setEnabled(true);
    }
}

void CMainWindowServer::on_btnStopListen_clicked()
{
    m_localServer.close();
    if(!m_localServer.isListening()) {
        QMessageBox::information(this, u8"提示", u8"停止监听成功");
        // 停止监听后‘开始监听’按钮启用,‘停止监听’按钮禁用
        ui->btnListen->setEnabled(true);
        ui->btnStopListen->setEnabled(false);
    }
    else {
        QMessageBox::information(this, u8"提示", u8"停止监听失败");
    }
}

void CMainWindowServer::on_newConnection()
{
    // 判断是否存在新的socket连接
    if(m_localServer.hasPendingConnections()) {
        // 获取套接字对象
        QLocalSocket *socketTmp = m_localServer.nextPendingConnection();
        // 套接字对象添加到套接字容器中
        m_listLocalSockets.append(socketTmp);
        // 套接字地址转为数值
        QString socketStr = QString::number((uint64_t)socketTmp);
        // 套接字文本添加到下拉列表中并在界面做出连接提示
        ui->comboBoxSockets->addItem(socketStr);
        ui->textEdit->append(socketStr + "加入连接!");
        // 关联新数据的信号槽
        connect(socketTmp, &QLocalSocket::readyRead, this, &CMainWindowServer::on_socketReadyRead);
    }
}

void CMainWindowServer::on_socketReadyRead()
{
    // 获取发送信号的对象
    QLocalSocket *curSocket = dynamic_cast<QLocalSocket *>(sender());
    // 将数据直接读取并添加到多行文本框中
    ui->textEdit->append(QString::number((uint64_t)curSocket) + ":" + curSocket->readAll());
}

void CMainWindowServer::on_btnDisconnectSocket_clicked()
{
    // 获取将要断开的文本并弹出断开提示
    QString socketStr = ui->comboBoxSockets->currentText();
    QMessageBox::StandardButton flag = QMessageBox::information(this, u8"提示", u8"是否断开" + socketStr + "?");
    if(QMessageBox::Ok != flag) {
        return;
    }

    // 根据断开文本做不不同断开操作
    if(0 == socketStr.compare(u8"全部")) {
        foreach(QLocalSocket *socket, m_listLocalSockets) {
            disconnectSocketByStr(QString::number((uint64_t)socket));
        }
    }
    else {
        disconnectSocketByStr(socketStr);
    }
}

void CMainWindowServer::on_btnSend_clicked()
{
    // 获取将要接收数据的识别文本
    QString socketStr = ui->comboBoxSockets->currentText();
    // 获取将要发送的数据
    QString data = ui->textEditSendData->toPlainText();

    // 根据识别文本做出不同的操作
    if(0 == socketStr.compare(u8"全部")) {
        foreach(QLocalSocket *socket, m_listLocalSockets) {
            socket->write(data.toUtf8());
        }
    }
    else {
        // 直接将当前文本强转为套接字对象(因为该文本为指针地址强转而来)
        QLocalSocket *socket = (QLocalSocket *)socketStr.toUInt();
        if(m_listLocalSockets.contains(socket)) {
            socket->write(data.toUtf8());
        }
        else {
            QMessageBox::information(this, u8"提示", socketStr + "地址找不到");
        }
    }

}

CMainWindowServer.ui

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>CMainWindowServer</class>
 <widget class="QMainWindow" name="CMainWindowServer">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>340</width>
    <height>420</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>CMainWindow</string>
  </property>
  <widget class="QWidget" name="centralWidget">
   <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,3,1,0">
    <item>
     <layout class="QHBoxLayout" name="horizontalLayout">
      <item>
       <widget class="QLineEdit" name="lineEditListenKey"/>
      </item>
      <item>
       <widget class="QPushButton" name="btnListen">
        <property name="text">
         <string>监听</string>
        </property>
       </widget>
      </item>
      <item>
       <widget class="QPushButton" name="btnStopListen">
        <property name="enabled">
         <bool>false</bool>
        </property>
        <property name="text">
         <string>停止监听</string>
        </property>
       </widget>
      </item>
     </layout>
    </item>
    <item>
     <layout class="QHBoxLayout" name="horizontalLayout_3">
      <item>
       <widget class="QComboBox" name="comboBoxSockets">
        <item>
         <property name="text">
          <string>全部</string>
         </property>
        </item>
       </widget>
      </item>
      <item>
       <widget class="QPushButton" name="btnDisconnectSocket">
        <property name="text">
         <string>断开当前链接选项</string>
        </property>
       </widget>
      </item>
     </layout>
    </item>
    <item>
     <widget class="QTextEdit" name="textEdit"/>
    </item>
    <item>
     <widget class="QTextEdit" name="textEditSendData"/>
    </item>
    <item>
     <layout class="QHBoxLayout" name="horizontalLayout_2">
      <item>
       <spacer name="horizontalSpacer">
        <property name="orientation">
         <enum>Qt::Horizontal</enum>
        </property>
        <property name="sizeHint" stdset="0">
         <size>
          <width>40</width>
          <height>20</height>
         </size>
        </property>
       </spacer>
      </item>
      <item>
       <widget class="QPushButton" name="btnSend">
        <property name="text">
         <string>发送</string>
        </property>
       </widget>
      </item>
     </layout>
    </item>
   </layout>
  </widget>
  <widget class="QMenuBar" name="menuBar">
   <property name="geometry">
    <rect>
     <x>0</x>
     <y>0</y>
     <width>340</width>
     <height>23</height>
    </rect>
   </property>
  </widget>
  <widget class="QToolBar" name="mainToolBar">
   <attribute name="toolBarArea">
    <enum>TopToolBarArea</enum>
   </attribute>
   <attribute name="toolBarBreak">
    <bool>false</bool>
   </attribute>
  </widget>
  <widget class="QStatusBar" name="statusBar"/>
 </widget>
 <layoutdefault spacing="6" margin="11"/>
 <resources/>
 <connections/>
</ui>

CMainWindowSocket

CMainWindowSocket.h

#ifndef CMAINWINDOWSOCKET_H
#define CMAINWINDOWSOCKET_H

#include <QMainWindow>
#include <QLocalSocket>

namespace Ui {
class CMainWindowSocket;
}

class CMainWindowSocket : public QMainWindow
{
    Q_OBJECT

public:
    explicit CMainWindowSocket(QWidget *parent = nullptr);
    ~CMainWindowSocket();

private slots:
    /**
     * @brief on_btnConnect_clicked 连接按钮信号槽
     */
    void on_btnConnect_clicked();

    /**
     * @brief on_btnSend_clicked 发送按钮信号槽
     */
    void on_btnSend_clicked();

    /**
     * @brief on_btnDisConnected_clicked 断开连接信号槽
     */
    void on_btnDisConnected_clicked();

    /**
     * @brief on_socketReadyRead 数据接收信号槽
     */
    void on_socketReadyRead();

private:
    Ui::CMainWindowSocket *ui;

    QLocalSocket    m_localSocket;  // 套接字对象
};

#endif // CMAINWINDOWSOCKET_H

CMainWindowSocket.cpp

#include "CMainWindowSocket.h"
#include "ui_CMainWindowSocket.h"

#include <QMessageBox>
#include <QTimer>

CMainWindowSocket::CMainWindowSocket(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::CMainWindowSocket)
{
    ui->setupUi(this);
    // 关联数据接收信号槽
    connect(&m_localSocket, &QLocalSocket::readyRead, this, &CMainWindowSocket::on_socketReadyRead);
}

CMainWindowSocket::~CMainWindowSocket()
{
    delete ui;
}

void CMainWindowSocket::on_btnConnect_clicked()
{
    // 根据key连接服务
    m_localSocket.connectToServer(ui->lineEditConnectKey->text());
    // 等待一秒是否连接成功
    if(m_localSocket.waitForConnected(1000)) {
        QString tip = u8"连接成功";
        // 连接成功后打开读写通道
        if(!m_localSocket.open(QIODevice::ReadWrite)) {
            tip.append(QString(u8",但Socket读写打开失败(%1)").arg(m_localSocket.errorString()));
        }
        QMessageBox::information(this, u8"提示", tip);
        // 连接后‘连接’按钮禁用,‘断开连接’按钮启用
        ui->btnConnect->setEnabled(false);
        ui->btnDisConnected->setEnabled(true);

    }
    else {
        QMessageBox::information(this, u8"提示", u8"连接失败");
    }
}

void CMainWindowSocket::on_btnSend_clicked()
{
    // 写入数据
    m_localSocket.write(ui->textEditSendData->toPlainText().toUtf8());
    // 等待写入信号,若未写入成功弹出提示
    if(!m_localSocket.waitForBytesWritten(100)) {
        QMessageBox::information(this, u8"提示", m_localSocket.errorString());
    }
}

void CMainWindowSocket::on_btnDisConnected_clicked()
{
    if(QLocalSocket::ConnectedState == m_localSocket.state()) {
        m_localSocket.write(QString(u8"%1已断开!").arg((uint64_t)this).toUtf8());
        m_localSocket.disconnectFromServer();
        // 断开连接后‘连接’按钮启用,‘断开连接’按钮禁用
        ui->btnConnect->setEnabled(true);
        ui->btnDisConnected->setEnabled(false);
    }
    else {
        QMessageBox::information(this, u8"提示", u8"断开失败,当前并非连接状态!" );
    }
}

void CMainWindowSocket::on_socketReadyRead()
{
    // 读取索引数据
    QString data = m_localSocket.readAll();
    // 识别数据文本,当复合条件是断开连接
    if(0 == data.compare(u8"服务器断开!")) {
        on_btnDisConnected_clicked();
    }
    ui->textEdit->append(data);
}

CMainWindowSocket.ui

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>CMainWindowSocket</class>
 <widget class="QMainWindow" name="CMainWindowSocket">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>300</width>
    <height>420</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>CMainWindowSocket</string>
  </property>
  <widget class="QWidget" name="centralWidget">
   <layout class="QVBoxLayout" name="verticalLayout" stretch="0,3,1,0">
    <item>
     <layout class="QHBoxLayout" name="horizontalLayout">
      <item>
       <widget class="QLineEdit" name="lineEditConnectKey"/>
      </item>
      <item>
       <widget class="QPushButton" name="btnConnect">
        <property name="text">
         <string>连接</string>
        </property>
       </widget>
      </item>
      <item>
       <widget class="QPushButton" name="btnDisConnected">
        <property name="enabled">
         <bool>false</bool>
        </property>
        <property name="text">
         <string>断开连接</string>
        </property>
       </widget>
      </item>
     </layout>
    </item>
    <item>
     <widget class="QTextEdit" name="textEdit"/>
    </item>
    <item>
     <widget class="QTextEdit" name="textEditSendData"/>
    </item>
    <item>
     <layout class="QHBoxLayout" name="horizontalLayout_2">
      <item>
       <spacer name="horizontalSpacer">
        <property name="orientation">
         <enum>Qt::Horizontal</enum>
        </property>
        <property name="sizeHint" stdset="0">
         <size>
          <width>40</width>
          <height>20</height>
         </size>
        </property>
       </spacer>
      </item>
      <item>
       <widget class="QPushButton" name="btnSend">
        <property name="text">
         <string>发送</string>
        </property>
       </widget>
      </item>
     </layout>
    </item>
   </layout>
  </widget>
  <widget class="QMenuBar" name="menuBar">
   <property name="geometry">
    <rect>
     <x>0</x>
     <y>0</y>
     <width>300</width>
     <height>23</height>
    </rect>
   </property>
  </widget>
  <widget class="QToolBar" name="mainToolBar">
   <attribute name="toolBarArea">
    <enum>TopToolBarArea</enum>
   </attribute>
   <attribute name="toolBarBreak">
    <bool>false</bool>
   </attribute>
  </widget>
  <widget class="QStatusBar" name="statusBar"/>
 </widget>
 <layoutdefault spacing="6" margin="11"/>
 <resources/>
 <connections/>
</ui>

总结

在使用QLocalServer和QLocalSocket的过程中,发现QLocalSocket不是数据通道的持有对象,而是数据通道本身(如共享内存是通过data获取共享内存的地址,而QLocalSocket是直接调用write写入,当然和他的继承有关系),而相对来说QLocalServer更像使用者。不过IPC相对于共享内存来说可能有及时性的特点,因为数据一来IPC就直接读取,而共享内存则是需要定时读取数据。

相关文章

Qt之进程通信-共享内存(含源码+注释)
Qt之进程通信-QProcess(含源码+注释)

友情提示——哪里看不懂可私哦,让我们一起互相进步吧
(创作不易,请留下一个免费的赞叭 谢谢 ^o^/)

注:文章为作者编程过程中所遇到的问题和总结,内容仅供参考,若有错误欢迎指出。
注:如有侵权,请联系作者删除文章来源地址https://www.toymoban.com/news/detail-678787.html

到了这里,关于Qt之进程通信-IPC(QLocalServer,QLocalSocket 含源码+注释)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【看表情包学Linux】IPC 进程间通信 | PIPE 管道 | 匿名管道 | 管道通信的原理 | 系统调用: pipe 接口

       🤣  爆笑 教程  👉 《看表情包学Linux》 🔥 CSDN 累计订阅量破千的火爆 C/C++ 教程的 2023 重制版,C 语言入门到实践的精品级趣味教程。 了解更多: 👉  \\\"不太正经\\\" 的专栏介绍  ← 试读第一章 订阅链接: 🔗 《C语言趣味教程》 ← 猛戳订阅! 目录 Ⅰ. 进程间通信(I

    2024年02月14日
    浏览(37)
  • electron+vue3全家桶+vite项目搭建【13.1】ipc通信的使用,主进程与渲染进程之间的交互

    electron项目常常由一个主进程和多个渲染进程构成,渲染进程之间是隔离的,而所有渲染进程都和主进程共享资源。 所有的渲染进程都是由主进程创建的 每个窗口都对应着一个渲染进程 所有的渲染进程共享一个主进程 我们主进程与渲染进程交互,渲染进程与渲染进程交互【

    2024年02月10日
    浏览(56)
  • C/C++ 进程间通信system V IPC对象超详细讲解(系统性学习day9)

    目录 前言 一、system V IPC对象图解 1.流程图解: ​编辑 2.查看linux内核中的ipc对象:  二、消息队列 1.消息队列的原理 2.消息队列相关的API 2.1 获取或创建消息队列(msgget)  实例代码如下: 2.2 发送消息到消息队列中  实例代码如下: 2.3 从消息队列中获取消息   实例代码如

    2024年02月08日
    浏览(35)
  • Qt之QSystemTrayIcon(托盘图标)的使用(含源码+注释)

    下方为消息通知操作,可能是因为录屏原因导致消息弹窗未弹出(无图标通知也会带icon,考虑是windows的原因)。 正常消息提示如下: 下方对托盘图标进行了基本的鼠标操作,值得一提的是,双击之前会先触发单击操作。 要做闪烁操作时,需要更新图标, 且我在测试时,更

    2023年04月16日
    浏览(35)
  • Qt之QPainter绘制多个矩形/圆形(含源码+注释)

    下图绘制的是矩形对象,但是将绘制矩形函数(drawRect)更改为绘制圆形(drawEllipse)即可绘制圆形。 绘制矩形需要自然要获取矩形数据,因此通过鼠标事件获取每个矩形的rect数据(鼠标按下为起始点,鼠标释放为结束点;每次移动时的当前位置做结束点,并实时刷新,实现

    2023年04月25日
    浏览(117)
  • Qt之QtDataVisualization各三维图表的简单使用(含源码+注释)

    下图演示了通过下拉列表框切换三维图表的操作。 下图演示了自动切换、通过spinBox控件切换和slider控件的缩放功能(三维图表自带有鼠标滚轮缩放功能)。 下图演示了主题奇幻,系列样式切换,选择模式切换的效果 下图演示了显示背景、显示网格、显示倒影和二维显示等功

    2024年01月16日
    浏览(44)
  • 跨进程通信设计:Qt 进程间通讯类全面解析

    进程间通信(Interprocess Communication,简称 IPC)是现代软件开发中不可或缺的一部分。在许多应用场景中,多个进程需要共享数据、协同工作,或在不同进程间传递消息。IPC 技术允许这些进程安全、高效地通信,从而实现复杂的功能和任务。 Qt 提供了一系列进程间通信类,以

    2023年04月18日
    浏览(36)
  • Qt之QTableView自定义排序/过滤(QSortFilterProxyModel实现,含源码+注释)

    本文过滤条件为行索引取余2等于0时返回true,且从下图中可以看到,奇偶行是各自挨在一起的。 下图添加两列条件(当前数据大于当前列条件才返回true,且多个列条件为且关系);下方添加条件分别为,”0列,条件值50“,”2列条件值40“,综合下来为0列值大于50且2列值大

    2024年02月05日
    浏览(36)
  • Qt之QGraphicsView实现截图(漏洞百出且BUG丛生版,部分源码+注释)

    下方一次绘制的图元为:矩形、圆形、箭头、画笔。 下方为添加文本操作,演示文本过多时,文本框便捷不超出编辑区边界。 下方为演示设置弹窗更新画笔颜色、画笔粗细更新后的绘制效果。 下方为截图图片拖动效果,仅支持未添加图元的情况。 下方操作为截图保存当前截

    2024年02月02日
    浏览(37)
  • Qt/QML编程学习之心得:D-BUS进程间通信(四)

    Qt/QML应用编程最适合于一些触摸的嵌入式界面设计,那么GUI界面怎么与底层的设备通信,怎么与一个系统内其他模块通信的呢?这就不得不说一个很重要的设计模式: d-bus 。  D-BUS是一个系统中消息总线,用于IPC/RPC。消息系统很简单而功能强大,可以在一些命令行实用程序的

    2023年04月20日
    浏览(46)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包