Qt网络通信骨架解析,QtClient QtServer QtSerialPort

概述

老生常谈,为什么写这个骨架?通讯部分的代码繁杂,条理性差,是最主要的原因。平常的工程师对这块注意的不够。Qt当中通信组件就这么几个,他们都使用信号和槽机制代替了线程,接收的while循环就被信号和槽掩盖了。那么现在梳理下,这块怎么做。权当过去对Qt网络功能认识的升级版。经典的哦。

通信骨架

不使用Qt的时候,我们网络工程师经常缠绕在通讯件/协议/报文格式/网络功能定义之中,转着圈子不清晰的来回。现在这个骨架就是为了解决这个不明了不清晰。这里只介绍Tcp通信,Udp通信照本宣科。
骨架分析图如下
1. 通讯组件
2. 通讯协议
- 报文格式
- 功能定义
- 功能时序

通讯协议不依赖于通信组件。
通讯协议包含三个要素
遵循这几个特点制订了这套通讯骨架。

通讯组件

QtScocket,QtServer,QtSerialPort三个。他们的通信过程是同样的。
通过读数据槽接收流数据或者报文数据,对数据进行切分和分发。发送数据非常容易,write即可。

通讯协议

QtProtocol,虚基类提供协议定义,在所有的通信组件中都可以使用。这个虚基类定义了一组接口,splitter/dispatacher/maxlength/minlength/write信号等提供通讯组件设定的所需的协议接口。这个协议的突出特点是不依赖通讯组件。

通讯功能

用户通过此协议基类派生出特定的协议实现特定的功能。通讯组件安装特定协议,实现特定的通讯。

通讯报文

QtMessage,虚基类定义了报文格式,提供了packer和parser两个虚接口,通过派生报文,来提供通讯组件使用的报文格式。这个报文的突出特点是不依赖协议,从属于协议。

通讯结构体

这是用户最终发送和接收时使用的结构体,应用层直接使用结构体进行设定和读取操作。

工作过程

通讯组件安装通讯协议,
通讯协议提供通讯功能,自己选定使用的报文。

通讯例程

QtServer

qteserver.h

#ifndef QTESERVER_H
#define QTESERVER_H

#include <QTcpServer>
#include "qteprotocol.h"
#include "qteclient.h"

class QteServer : public QTcpServer
{
    Q_OBJECT
public:
    explicit QteServer(QObject *parent = 0);
    ~QteServer();

    void installProtocol(QteProtocol* stack);
    void uninstallProtocol(QteProtocol* stack);
    QteProtocol* installedProtocol();

signals:
    // QTcpServer interface
protected:
    void incomingConnection(int handle);
private:
    QteProtocol* m_protocol;
};

qteserver.cpp

#include "qteserver.h"


QteServer::QteServer(QObject *parent) :
    QTcpServer(parent)
{
}

QteServer::~QteServer()
{
    close();
}

void QteServer::incomingConnection(int handle)
{
    QteClient* clientSocket = new QteClient(this);
    clientSocket->setSocketDescriptor(handle);
    connect(clientSocket, SIGNAL(disconnected()), clientSocket, SLOT(deleteLater()));
    clientSocket->installProtocol(m_protocol);
}

void QteServer::installProtocol(QteProtocol *stack)
{
    if(m_protocol)
        return;

    m_protocol = stack;
    connect(m_protocol, SIGNAL(write(const QByteArray&)), this, SLOT(write(const QByteArray&)));
}

void QteServer::uninstallProtocol(QteProtocol *stack)
{
    if(!m_protocol)
        return;

    disconnect(m_protocol, SIGNAL(write(const QByteArray&)), this, SLOT(write(const QByteArray&)));
    m_protocol = NULL;
}

QteProtocol *QteServer::installedProtocol()
{
    return m_protocol;
}

QtClient

qteclient.h

#ifndef QTE_CLIENT_H
#define QTE_CLIENT_H

#include <QTcpSocket>
#include "qteprotocol.h"
#include "QStringList"

#define _TCP_BLOCKDATA_SIZE 0x400
#define _TCP_RECVBUFF_SIZE 0x800

/** * @brief 客户端决定和协议的交互关系;只跟协议打交道; */
class QteClient : public QTcpSocket
{
    Q_OBJECT
public:
    explicit QteClient(QObject *parent = 0);
    virtual ~QteClient();

    void setServerIPAddress(QStringList ip) { m_serverIP = ip; }
    void setServerPort(quint32 p = 7000) { m_PORT = p; }

    void installProtocol(QteProtocol* stack);
    void uninstallProtocol(QteProtocol* stack);
    QteProtocol* installedProtocol();

    void SendConnectMessage();
    int SendDisConnectFromHost();

signals:
    void signalConnecting();
    void signalConnectSucc();
    void signalConnectFail();//
    void signalDisConnectSucc();//maybe
    void signalDisConnectFail();//?
    void signalUpdateProgress(int value);


private slots:
    void domainHostFound();
    void socketStateChanged(QAbstractSocket::SocketState);
    void socketErrorOccured(QAbstractSocket::SocketError);
    void socketConnected();
    void socketDisconnect();
    void updateProgress(qint64);

protected slots:
    void readyReadData();

private:
    void connectToSingelHost();

    //TODO:如果文件传输影响到了UI线程,那么需要将QTcpSocket局部变量化
    //阻塞UI不必考虑此处
    //非阻塞UI,UI却工作很慢,考虑此处。
    //QTcpSocket* m_sock;

    QteProtocol* m_protocol;
    quint32 eConType;
    QStringList m_serverIP;
    quint32 m_PORT;
};
*qteclient.cpp*
#include "qteclient.h"
#include "qtelinux.h"
#include <QTcpSocket>
#include <QHostInfo>

QteClient::QteClient(QObject *parent) :
    QTcpSocket(parent)
{
    connect(this, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(socketStateChanged(QAbstractSocket::SocketState)) );
    // connected
    connect(this, SIGNAL(connected()), this, SLOT(socketConnected()) );
    // disconnected
    connect(this, SIGNAL(disconnected()), this, SLOT(socketDisconnect()) );
    // domain
    connect(this, SIGNAL(hostFound()), this, SLOT(domainHostFound()));
    // error
    connect(this, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(socketErrorOccured(QAbstractSocket::SocketError)) );

    connect(this, SIGNAL(readyRead()), this, SLOT(readyReadData()));

    connect(this, SIGNAL(bytesWritten(qint64)), this, SLOT(updateProgress(qint64)));

    connect(this, SIGNAL(signalSendData()), this, SLOT(sendUploadFileData()), Qt::QueuedConnection);
    connect(this, SIGNAL(signalDownData()), this, SLOT(sendDownFileData()), Qt::QueuedConnection);
    connect(this, SIGNAL(signalDownSucc()), this, SLOT(sendDownFileSuccess()), Qt::QueuedConnection );

    setSocketOption(QAbstractSocket::LowDelayOption, 0);
    setSocketOption(QAbstractSocket::KeepAliveOption, 0);
    setReadBufferSize(_TCP_RECVBUFF_SIZE);

    m_PORT = 0;
    //启动连接
    eConType = 0;
    m_protocol = NULL;
}

QteClient::~QteClient()
{
}

void QteClient::installProtocol(QteProtocol *stack)
{
    if(m_protocol)
        return;

    m_protocol = stack;
    connect(m_protocol, SIGNAL(write(const QByteArray&)), this, SLOT(write(const QByteArray&)));
}

void QteClient::uninstallProtocol(QteProtocol *stack)
{
    if(!m_protocol)
        return;

    disconnect(m_protocol, SIGNAL(write(const QByteArray&)), this, SLOT(write(const QByteArray&)));
    m_protocol = NULL;
}

QteProtocol *QteClient::installedProtocol()
{
    return m_protocol;
}

void QteClient::SendConnectMessage()
{
    pline() << isValid() << isOpen() << state();

    if(!isValid() && !isOpen())
    {
        connectToSingelHost();
        return;
    }

    if(state() == HostLookupState ||
            state() == ConnectingState)
    {
        emit signalConnecting();
        return;
    }

    if(state() == ConnectedState)
        emit signalConnectSucc();

    return;
}


int QteClient::SendDisConnectFromHost()
{
    pline() << isValid() << isOpen() << state();

    if(isValid() || isOpen() )
    {
        shutdown(this->socketDescriptor(), SHUT_RDWR);
        disconnectFromHost();
        waitForDisconnected();
        close();
        emit signalDisConnectSucc();
    }

    return true;
}

void QteClient::domainHostFound()
{
    pline();
}

/** * @brief QteClient::socketStateChanged * @param eSocketState * 状态函数 */
void QteClient::socketStateChanged(QAbstractSocket::SocketState eSocketState)
{
    pline() << eSocketState;
    switch(eSocketState)
    {
    case QAbstractSocket::HostLookupState:
    case QAbstractSocket::ConnectingState:
        break;
    case QAbstractSocket::ConnectedState:
        break;
    case QAbstractSocket::ClosingState:
        break;
    case QAbstractSocket::UnconnectedState:
        eConType++;
        break;
    default:
        break;
    }
}

/** * @brief QteClient::socketErrorOccured * @param e * 状态函数 */
void QteClient::socketErrorOccured(QAbstractSocket::SocketError e)
{
    //在错误状态下重新连接其他热点,直到确定连接类型,写入配置文件
    pline() << e;
    switch(e)
    {
    case QAbstractSocket::RemoteHostClosedError:
        break;
    case QAbstractSocket::HostNotFoundError:
    default:
        emit signalConnectFail();
        break;
    }
}

/** * @brief QteClient::socketConnected * 功能接口 */
void QteClient::socketConnected()
{
    pline() << peerName() << peerAddress().toString() << peerPort();
    //这个步骤,socket重建,资源重新开始
    emit signalConnectSucc();
}

/** * @brief QteClient::socketDisconnect * 功能接口 */
void QteClient::socketDisconnect()
{
    pline();
}

void QteClient::updateProgress(qint64 bytes)
{
    //pline() << bytes;
}

void QteClient::connectToSingelHost()
{
    int contype = eConType % m_serverIP.size();
    QString ip = m_serverIP.at(contype);
    connectToHost(QHostAddress(ip), m_PORT);

    pline() << peerName() << m_PORT;
}


void QteClient::readyReadData()
{
    // queued conn and queued package;
    // direct conn and direct package;

    static QByteArray m_blockOnNet;
    m_blockOnNet += readAll();
    //qint64 aaa = bytesAvailable();
    //pline() << aaa;

    do{
        quint16 nBlockLen = m_protocol->splitter(m_blockOnNet);

        pline() << m_blockOnNet.size() << "..." << nBlockLen;

        //收到数据不足或者解析包长小于最小包长
        if(m_blockOnNet.length() < nBlockLen)
        {
            return;
        }
        //粘包
        else if(m_blockOnNet.length() > nBlockLen)
        {
            //还没有处理完毕,数据已经接收到,异步信号处理出现这种异常
            //疑问:如果异步调用这个函数绘出现什么问题?正常情况,同步获取数据,异步处理;检测异步获取并且处理会有什么状况
            pline() << "stick package" << m_blockOnNet.length() << nBlockLen;
            QByteArray netData;
            netData.resize(nBlockLen);
            m_blockOnNet >> netData;
            m_protocol->dispatcher(netData);
            continue;
        }
        //正常分发
        m_protocol->dispatcher(m_blockOnNet);
        break;
    }while(1);

    m_blockOnNet.clear();
}

QtSerialPort

qteserialport.h

#ifndef SERIALPORT_H
#define SERIALPORT_H

#include <QtSerialPort/QSerialPort>
#include "qteprotocol.h"

class QteSerialPort : public QSerialPort
{
    Q_OBJECT
public:
    explicit QteSerialPort(QObject *parent = 0);
    ~QteSerialPort();

    void installProtocol(QteProtocol* stack);
    void uninstallProtocol(QteProtocol* stack);
    QteProtocol* installedProtocol();

private slots:
    void readyReadData();
    QteProtocol* m_protocol;
};

#endif // SERIALPORT_H

qteserialport.cpp

#include "qteserialport.h"

QteSerialPort::QteSerialPort(QObject *parent) :
    QSerialPort(parent)
{
    //connect(this, SIGNAL(bytesWritten(qint64)), this, SLOT(updateProgress(qint64)) );
    connect(this, SIGNAL(readyRead()), this, SLOT(readyReadData()) );
    //connect(this, SIGNAL(aboutToClose()), this, SLOT(aboutToClose()));
    //connect(this, SIGNAL(readChannelFinished()), this, SLOT(readChannelFinished()));
}

QteSerialPort::~QteSerialPort()
{
    close();
}

void QteSerialPort::installProtocol(QteProtocol *stack)
{
    if(m_protocol)
        return;

    m_protocol = stack;
    connect(m_protocol, SIGNAL(write(const QByteArray&)), this, SLOT(write(const QByteArray&)));
}

void QteSerialPort::uninstallProtocol(QteProtocol *stack)
{
    if(!m_protocol)
        return;

    disconnect(m_protocol, SIGNAL(write(const QByteArray&)), this, SLOT(write(const QByteArray&)));
    m_protocol = NULL;
}

QteProtocol *QteSerialPort::installedProtocol()
{
    return m_protocol;
}

void QteSerialPort::readyReadData()
{
    // queued conn and queued package;
    // direct conn and direct package;

    static QByteArray m_blockOnNet;
    m_blockOnNet += readAll();
    //qint64 aaa = bytesAvailable();
    //pline() << aaa;

    do{
        quint16 nBlockLen = m_protocol->splitter(m_blockOnNet);

        pline() << m_blockOnNet.size() << "..." << nBlockLen;

        //收到数据不足或者解析包长小于最小包长
        if(m_blockOnNet.length() < nBlockLen || nBlockLen < m_protocol->minlength())
        {
            return;
        }
        //数据包长超过了最大长度
        else if(nBlockLen > m_protocol->maxlength())
        {
            m_blockOnNet.clear();
            pline() << "forbidden package" << m_blockOnNet.length() << nBlockLen;
            return;
        }
        //粘包
        else if(m_blockOnNet.length() > nBlockLen)
        {
            //还没有处理完毕,数据已经接收到,异步信号处理出现这种异常
            //疑问:如果异步调用这个函数绘出现什么问题?正常情况,同步获取数据,异步处理;检测异步获取并且处理会有什么状况
            pline() << "stick package" << m_blockOnNet.length() << nBlockLen;
            QByteArray netData;
            netData.resize(nBlockLen);
            m_blockOnNet >> netData;
            m_protocol->dispatcher(netData);
            continue;
        }
        //正常分发
        m_protocol->dispatcher(m_blockOnNet);
        break;
    }while(1);

    m_blockOnNet.clear();
}

QtProtocol

qteprotocol.h

#ifndef QTEPROTOCOL_H
#define QTEPROTOCOL_H

#include <QObject>
#include "qtemessage.h"

class QteProtocol : public QObject
{
    Q_OBJECT
public:
    explicit QteProtocol(QObject *parent = 0);

signals:
    qint64 write(const QByteArray& l);

public:
    virtual quint16 minlength() {}
    /** * @brief 最大包长 * @return */
    virtual quint16 maxlength() {}
    /** * @brief 语法解析器 从流中解析报文长度 * @param 接收到的数据段 * @return 按照协议解析到的数据长度 可用,继续接收,丢弃,粘包。 */
    virtual quint16 splitter(const QByteArray &s){}
    /** * @brief 语义解析器 * @param 数据包 * @return 0 no dispatched(others) 1 dispatched(own) */
    virtual bool dispatcher(const QByteArray &m){}
};

#endif // QTEPROTOCOL_H

qteprotocol.cpp

#include "qteprotocol.h"

QteProtocol::QteProtocol(QObject *parent) : QObject(parent)
{

}

QtNetworkProtocol

自行实现

QtC3SerialProtocol

自行实现

QtC0SerialProtocol

自行实现

QtThirdpartyProtocol

qtethirdpartyprotocol.h

#ifndef QTEAPROTOCOL_H
#define QTEAPROTOCOL_H

#include "qteprotocol.h"
#define __LOGIN 0x00BB
class QteLogin : public QteThirdpartyMessage
{
    Q_OBJECT
public:
    explicit QteLogin(QObject *parent = 0) : QteThirdpartyMessage(parent){}

    void pack(QByteArray& l)
    {
    setCmd(__LOGIN);
    translate();
    QteThirdpartyMessage::packer(l);
}
};
class QteLoginAck : public QteThirdpartyMessage
{
    Q_OBJECT
public:
    explicit QteLoginAck(QObject *parent = 0) : QteThirdpartyMessage(parent){}

    void parse(const QByteArray& l)
    {
        QByteArray _l = l;
        _l >> aid;
    }
    quint8 aid;
};

class QteThirdpartyProtocol : public QteProtocol
{
public:
    explicit QteThirdpartyProtocol(QObject *parent = 0);

    void recvLogin(const QByteArray& data)
    {
        QteLoginAck ack;
        ack.parse(data);
        qDebug() << ack.mid;
    }
    // QteProtocol interface
public:
    quint16 minlength() override;
    quint16 maxlength() override;
    quint16 splitter(const QByteArray &s) override;
    bool dispatcher(const QByteArray &m) override;
};

#endif // QTEAPROTOCOL_H

qtethirdpartyprotocol.cpp

#include "qtelanprotocol.h"
#include "qtethirdpartymessage.h"
QteThirdpartyProtocol::QteThirdpartyProtocol(QObject *parent) :
    QteProtocol(parent)
{

}


quint16 QteThirdpartyProtocol::minlength()
{
    return 0x0A;
}

quint16 QteThirdpartyProtocol::maxlength()
{
    return 23456;
}

quint16 QteThirdpartyProtocol::splitter(const QByteArray &s)
{
    QByteArray l = s;
    quint16 b0 = 0, b1 = 0;
    l >> b0 >> b1;
    return b1;
}

bool QteThirdpartyProtocol::dispatcher(const QByteArray &m)
{
        QteThirdpartyMessage qMsg;
    qMsg.parser(m);
    switch(qMsg.cmd())
    {
    case __LOGIN:
        recvLogin(qMsg.data());
        break;
        default:
        break;
}

QtC788Protocol

自行实现

QtMessage

qtemessage.h

#ifndef QTEMESSAGE_H
#define QTEMESSAGE_H

#include <QObject>


/** * @brief 语法类 定义报文格式 */
class QteMessage : public QObject
{
    Q_OBJECT
public:
    explicit QteMessage(QObject *parent = 0);

protected:
    /**
     * @brief 从流中解析报文
     * @param m
     * @param l
     */
    virtual void parser(const QByteArray &l) = 0;
    /** * @brief 将报文组装为流 * @param l * @param m */
    virtual void packer(QByteArray& l) = 0;
    /** * @brief 最小包长 * @return */

signals:

public slots:

private:
};


#endif // QTEMESSAGE_H

qtemessage.cpp


#include "qtemessage.h"

QteMessage::QteMessage(QObject *parent) : QObject(parent)
{

}

QtNetworkMessage

自行实现

QtSerialMessage

自行实现

QtThirdpartyMessage

qtethirdpartymessage.h

#ifndef QTETHIRDPARTYMESSAGE_H
#define QTETHIRDPARTYMESSAGE_H

#include "qtemessage.h"


#define __HEAD 0xEEFF
#define __TAIL 0xFFEE

class QteThirdpartyMessage : public QteMessage
{
public:
    explicit QteThirdpartyMessage(QObject* parent = 0);

    const quint16& head() const;
    void setHead(quint16 head);
    const quint16& size() const;
    void setSize(quint16 size);
    //user
    const quint16& cmd() const;
    void setCmd(quint16 cmd);
    //user
    const QByteArray& data() const;
    void setData(QByteArray& data);
    const quint16& sum() const;
    void setSum(quint16 sum);
    const quint16& tail() const;
    void setTail(quint16 tail);
    void translate();

protected:
    void parser(const QByteArray &l) override;
    void packer(QByteArray &l) override;
private:
    quint16 m_Head;
    quint16 m_Size;
    quint16 m_Cmd;
    QByteArray m_Data;
    quint16 m_Sum;
    quint16 m_Tail;
};

qtethirdpartymessage.cpp

#include "qtethirdpartymessage.h"

QteThirdpartyMessage::QteThirdpartyMessage(QObject *parent) :
    QteMessage(parent)
{
    m_Head = __HEAD;
    m_Size = m_Cmd = m_Sum = 0;
    m_Data.clear();;
    m_Tail = __TAIL;
}

const quint16 &QteThirdpartyMessage::head() const { return m_Head; }

void QteThirdpartyMessage::setHead(quint16 head) { m_Head = head; }

const quint16 &QteThirdpartyMessage::size() const { return m_Size; }

void QteThirdpartyMessage::setSize(quint16 size) { m_Size = size; }

const quint16 &QteThirdpartyMessage::cmd() const { return m_Cmd; }

void QteThirdpartyMessage::setCmd(quint16 cmd) { m_Cmd = cmd; }

const QByteArray &QteThirdpartyMessage::data() const { return m_Data; }

void QteThirdpartyMessage::setData(QByteArray &data) { m_Data = data; }

const quint16 &QteThirdpartyMessage::sum() const { return m_Sum; }

void QteThirdpartyMessage::setSum(quint16 sum) { m_Sum = sum; }

const quint16 &QteThirdpartyMessage::tail() const { return m_Tail; }

void QteThirdpartyMessage::setTail(quint16 tail) { m_Tail = tail; }

void QteThirdpartyMessage::translate()
{
    m_Size = m_Data.length() + 0x0A;
    QByteArray qbaVerify;
    qbaVerify << m_Size << m_Cmd << m_Data;
    m_Sum = 0;
    for(int i = 0; i < qbaVerify.length(); i++)
        m_Sum += quint8(qbaVerify.at(i)) + 34;
}

void QteThirdpartyMessage::parser(const QByteArray &netData)
{
    QByteArray l = netData;
    quint16 b0 = 0, b1 = 0, b2 = 0, b4 = 0, b5 = 0;
    QByteArray b3;
    l >> b0 >> b1 >> b2;
    b3.resize(b1-0x0A);
    l >> b3 >> b4 >> b5;
    setHead(b0);
    setSize(b1);
    setCmd(b2);
    setData(b3);
    setSum(b4);
    setTail(b5);
}

void QteThirdpartyMessage::packer(QByteArray &l)
{
    l << head();
    l << size();
    l << cmd();
    l << data();
    l << sum();
    l << tail();
}

工具函数


QByteArray &operator<<(QByteArray &l, const quint8 r)
{
    return l.append(r);
}


QByteArray &operator<<(QByteArray &l, const quint16 r)
{
    return l<<quint8(r>>8)<<quint8(r);
}


QByteArray &operator<<(QByteArray &l, const quint32 r)
{
    return l<<quint16(r>>16)<<quint16(r);
}


QByteArray &operator<<(QByteArray &l, const QByteArray &r)
{
    return l.append(r);
}


QByteArray &operator>>(QByteArray &l, quint8 &r)
{
    r = l.left(sizeof(quint8))[0];
    return l.remove(0, sizeof(quint8));
}


QByteArray &operator>>(QByteArray &l, quint16 &r)
{
    quint8 r0 = 0, r1 = 0;
    l >> r0 >> r1;
    r = ( r0 << 8 ) | r1;
    return l;
}


QByteArray &operator>>(QByteArray &l, quint32 &r)
{
    quint8 r0 = 0, r1 = 0, r2 = 0, r3 = 0;
    l >> r0 >> r1 >> r2 >> r3;
    r = ( r0 << 24 ) | ( r1 << 16 ) | ( r2 << 8 ) | r3;
    return l;
}


QByteArray &operator>>(QByteArray &l, QByteArray &r)
{
    r = l.left(r.size());
    return l.remove(0, r.size());
}

QByteArray &operator<<(QByteArray &l, const qint8 r)
{
    quint8 ubyte = quint8(r);
    l << ubyte;
    return l;
}

QByteArray &operator<<(QByteArray &l, const qint16 r)
{
    quint16 ubyte = quint16(r);
    l << ubyte;
    return l;
}

QByteArray &operator<<(QByteArray &l, const qint32 r)
{
    quint32 ubyte = quint32(r);
    l << ubyte;
    return l;
}

QByteArray &operator>>(QByteArray &l, qint8 r)
{
    quint8 ubyte = 0;
    l >> ubyte;
    r = qint8(ubyte);
    return l;
}

QByteArray &operator>>(QByteArray &l, qint16 r)
{
    quint16 ubyte = 0;
    l >> ubyte;
    r = qint16(ubyte);
    return l;
}

QByteArray &operator>>(QByteArray &l, qint32 r)
{
    quint32 ubyte = 0;
    l >> ubyte;
    r = qint32(ubyte);
    return l;
}

转载于:https://my.oschina.net/tianduanrui/blog/1536601

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值