• QT5 网络通讯


    QT5 TCP网络通讯

    • 服务器与客户端建立连接listen() - connectToHost();  触发newPendingConnect信号
    • 实时数据通讯write(); read();  触发readyRead信号

    通讯主要使用的类:

    QTcpServer  Class

    QTcpServer类提供了一个基于TCP的服务器。
    这个类可以接受传入的TCP连接。您可以指定端口或让QTcpServer自动选择一个端口。您可以收听特定地址或所有机器的地址。
    调用listen()让服务器侦听传入的连接。每次客户端连接到服务器时,都会发出newConnection()信号。

    QTcpSocket Class

    QTcpSocket类提供了一个TCP套接字。
    TCP(传输控制协议)是一种可靠的,面向流的,面向连接的传输协议。 它特别适合连续传输数据。
    QTcpSocket是QAbstractSocket的一个方便的子类,它允许你建立一个TCP连接并传输数据流。

    建立连接:

    服务器端以监听的方式监听客服端是否有连接请求

    客户端以调用connectToHost()函数主动连接服务器端

    tcp协议服务器端实现流程

    建立服务器对象

    listen服务器, 通过建立的服务器 监听指定地址/端口的客服端;判断是否有客户连接有连接就触发newConnection();

    通过connect处理newConnection()信号;

        server = new QTcpServer(this); //建立一个服务器对象
        server->listen(QHostAddress::Any, 8000);//通过建立的服务器监听指定ip地址及端口号的客服端,如不指定端口号,系统会随机分配
        connect(server, QTcpServer::newConnection,
        [=]()
        {
            qDebug() << "有连接进来";
        }
        );

    tcp协议客户端实现流程

    建立QTcpSocket套节字(ip,端口)

    通过套节字connectToHost()函数主动连接服务器;连接成功则触发服务器QTcpServer::newConnection信号;并发送套节字到服务器端;

    关闭连接;

    QTcpSocket Sc(this);
    Sc.connectToHost("127.0.0.1", 8888);//实际代码中参数要进行类型转化
    Sc.close();

    实时通讯:

    • 客户端到服务器端通讯
    1. 当客户端与服务器端建立连接后;
    2. 客户端与服务器端通讯在客户端通过套节字对象调用write()函数发送上传内容;
    3. 服务器端有客户端数据写入时服务器端会自动调用readyread信号
    4. 服务器端在connect中处理readyread信号,并由nextPendingConnection()函数接收客户端发送的套节字;
    5. 服务器端对接收的套节字进行相应处理,即完成一次客户端到服务器端的通讯
    • 服务器端到客户端的通讯
    1. 当客户端与服务器端建立连接后;
    2. 服务器通过套节字对象调用write()函数发送上传内容;客户端会触发readyread信号
    3. 客户端在connect中处理readyread信号

    客户端到服务器端实现代码:

    //服务器端   
     connect(server, QTcpServer::newConnection,
                [=]()
        {
           QTcpSocket socket = server->nextPendingConnection();
            connect(socket, &QTcpSocket::readyRead, [=]()
            {
                tp = socket->readAll();
                ui->textrc->append(tp);
            });
        }
        );
    //客户端
    void socket::on_buttonsend_clicked()
    {
        QString temp = ui->textrc->toPlainText();
        if(!temp.isEmpty())sock->write(temp.toUtf8());
    
    }

    服务器端客户端实现代码:

    //服务器端
    void Widget::on_buttonsend_clicked()
    {
        socket->write(ui->textEdit->toPlainText().toUtf8());
    }
    //客户端
        connect(sock, &QTcpSocket::readyRead,
                [=]()
        {
           ui->textdis->append(sock->readAll());
        });

    完整代码:

    服务器ui设计:

    服务器端头文件widget.h

    #ifndef WIDGET_H
    #define WIDGET_H
    
    #include <QWidget>
    #include <QTcpServer>
    #include <QTcpSocket>
    namespace Ui {
    class Widget;
    }
    
    class Widget : public QWidget
    {
        Q_OBJECT
    
    public:
        explicit Widget(QWidget *parent = 0);
        ~Widget();
    
    private slots:
        void on_buttonsend_clicked();
    
    private:
        Ui::Widget *ui;
    
        QTcpServer *server; //建立服务器对象
        QTcpSocket *socket; //套节字对象
        QByteArray tp;   //
    };
    
    #endif // WIDGET_H

    服务器端cpp文件 widget.cpp

    #include "widget.h"
    #include "ui_widget.h"
    #include <QDebug>
    
    Widget::Widget(QWidget *parent) :
        QWidget(parent),
        ui(new Ui::Widget)
    {
        ui->setupUi(this);
        setWindowTitle("服务器");
        tp = nullptr;
    
        server = new QTcpServer(this);
        server->listen(QHostAddress::Any, 8000);
        connect(server, QTcpServer::newConnection,
                [=]()
        {
            socket = server->nextPendingConnection();
            connect(socket, &QTcpSocket::readyRead, [=]()
            {
                tp = socket->readAll();
                ui->testdis->append(tp);
            });
        }
        );
    
    }
    
    Widget::~Widget()
    {
        delete ui;
    }
    
    void Widget::on_buttonsend_clicked()
    {
        socket->write(ui->textEdit->toPlainText().toUtf8());
    }

    客户端ui:

    客户端头文件socket.h:

    #ifndef SOCKET_H
    #define SOCKET_H
    
    #include <QWidget>
    #include <QTcpSocket>
    #include <QHostAddress>
    
    namespace Ui {
    class socket;
    }
    
    class socket : public QWidget
    {
        Q_OBJECT
    
    public:
        explicit socket(QWidget *parent = 0);
        ~socket();
    
    private slots:
    
        void on_buttonLink_clicked();
    
        void on_buttonsend_clicked();
    
        void on_serverclose_clicked();
    
    private:
        Ui::socket *ui;
    
        QTcpSocket *sock;
        QHostAddress adrs;
        quint16 port;
    };
    
    #endif // SOCKET_H

    客户端cpp文件socket.cpp

    #include "socket.h"
    #include "ui_socket.h"
    
    socket::socket(QWidget *parent) :
        QWidget(parent),
        ui(new Ui::socket)
    {
        ui->setupUi(this);
        sock = new QTcpSocket(this);
        setWindowTitle("张三");
        connect(sock, &QTcpSocket::readyRead,
                [=]()
        {
           ui->textdis->append(sock->readAll());
        });
    }
    
    socket::~socket()
    {
        delete ui;
    }
    
    void socket::on_buttonLink_clicked()
    {
        QString ip = ui->serverIP->text();
        QString p = ui->serverPort->text();
        sock->connectToHost(ip, p.toUShort());
    }
    
    void socket::on_buttonsend_clicked()
    {
        QString temp = ui->textEdit->toPlainText();
        if(!temp.isEmpty())sock->write(temp.toUtf8());
    }
    
    void socket::on_serverclose_clicked()
    {
        sock->close();
    }

    main.cpp文件

    #include "widget.h"
    #include <QApplication>
    #include "socket.h"
    
    int main(int argc, char *argv[])
    {
        QApplication a(argc, argv);
        Widget w;
        w.show();
        socket w1;
        w1.show();
    
        return a.exec();
    }

    最终运行效果:

     当然在具体的实现过程中还有很多很多的细节需要优化;

    QT5对tcp协议基本的通讯总结:                             

    • QTcpServer *p = new QTcpServer(this);//建立服务器对象               QTcpSocket *q = new QTcpSocket(this); //客户机建立套节字对象                 
    • p.listen(监听的客户ip , 监听端口port);//监听客户机           q.conncetToHost(要连接的服务器ip, 要连接的服务器端口);
    • connect(p, &QTcpServer::newConnection, )连接成功触发信号   q.write();//发送数剧 到服务器
    • QTcpSocket skt = p.nextPendingConnection();//获取客户机套节字     connect(q, &QTcpSocket::readyRead, )//服务端发送数据客户端触发信号
    • connect(skt, &QTcpSocket::readyRead, )//客户发送数据触发信号    q.readall();//读取客户端发送的数据;  
    • skt.readall();//读取客户端发送的数据;                                                       客户端处理数据
    • 服务器端处理数据 

    QT5 UDP网络通讯 

    UDP没有服务器与客户端之分;单纯通过writeDatagram发( 参数1, 参数2,参数3)送指定的内容(参数1)到指定的ip(参数2),端口(参数3)上;

    当收取到网络中的数据发送,就会触发自己的readyRead信号;readDatagram(参数1, 参数2,参数3),保存读取的内容(参数1);保存对方ip(参数2);保存对方端口(参数3)

    具体实现过程:

    • 建立QUdpSocket套节字                                                                                   QUdpSocket* p = new QUdpSocket(this);
    • 绑定本程序端口号 bind()                                                                                    p.bind(8000);
    • 通过writeDatagram()发送数据到指定目标                                                      p.writeDatagram();
    • 当有readyRead信号发生通过readDatagram()函数读取保存数据        p.readDatagram();

     实现代码:

        QUdpSocket *udp = new QUdpSocket(this);
        udp->bind(8000);
        connect(udp, &QUdpSocket::readyRead, [=]()
        {
    
           char temp[1024] = {0};
           QHostAddress q;
           quint16 p;
           udp->readDatagram(temp, sizeof(temp), &q, &p);
           ui->textdis->append(temp);
        });
    
    /......./
    
    //按键确定发送
    void Widget::on_buttonlink_clicked()
    {
        udp->writeDatagram(ui->textsend->toPlainText().toUtf8(), QHostAddress(ui->ip->text()), ui->port->text().toUShort());
    }

    整体代码:

    ui设计:

    widget.h头文件

    #ifndef WIDGET_H
    #define WIDGET_H
    
    #include <QWidget>
    #include <QUdpSocket>
    namespace Ui {
    class Widget;
    }
    
    class Widget : public QWidget
    {
        Q_OBJECT
    
    public:
        explicit Widget(QWidget *parent = 0);
        ~Widget();
    
    private slots:
        void on_buttonlink_clicked();
    
    private:
        Ui::Widget *ui;
        QUdpSocket *udp;
    };
    
    #endif // WIDGET_H

    widget.cpp

    #include "widget.h"
    #include "ui_widget.h"
    #include <QHostAddress>
    #include <QDialog>
    
    Widget::Widget(QWidget *parent) :
        QWidget(parent),
        ui(new Ui::Widget)
    {
        ui->setupUi(this);
        setWindowTitle("8000");
        udp = new QUdpSocket(this);
        udp->bind(8000);
        udp->joinMulticastGroup(QHostAddress(""));
        connect(udp, &QUdpSocket::readyRead, [=]()
        {
    
           char temp[1024] = {0};
           QHostAddress q;
           quint16 p;
           udp->readDatagram(temp, sizeof(temp), &q, &p);
           ui->textdis->append(temp);
        });
    }
    
    Widget::~Widget()
    {
        delete ui;
    }
    
    void Widget::on_buttonlink_clicked()
    {
        udp->writeDatagram(ui->textsend->toPlainText().toUtf8(), QHostAddress(ui->ip->text()), ui->port->text().toUShort());
    }

    main.cpp文件

    #include "widget.h"
    #include <QApplication>
    
    int main(int argc, char *argv[])
    {
        QApplication a(argc, argv);
        Widget w;
        w.show();
    
        return a.exec();
    }

    以便测试:我们先编译生成一份客户端;再在源文件中改变bind端口号为8888再生成一份客户端;最终就会有两份客户端以便相互通信测试

    运行测试结果:

    由于UDP不需要服务器,所以,UDP发送的数据,只要能接收到你的ip及端口的客户端就殾能收到信息;所以在局域网内,ip地址栏可输入

    255.255.255.255 即整个局域网内的客户端都能收到信息;

    UDP通讯组包

    为了满足,发送的信息指定ip段内的客户收到信息可以用函数JoinMulticastGroup(IPAddress)加入到组;根据msdn记载,没错是同样的功能;

     

    目前个人理解也不深;详细数据可查msdn;可用leaveMulticastGroup()函数离开组翻;

    Tcp 与 Udp的比较:

    Udp不需要服务器,只管发送数据,不对数据进行检查,也不对接收者检测;速度快,易丢包,做即时数据传送比较好;

     Tcp方式总结:

    服务器端:QTcpServer

    1】基本用法:
    创建一个QTcpServer,然后调用listen函数监听相应的地址和端口。当有客户端链接到服务器时,会有信号newConnection()产生。
    调用nextPendingConnection()接受一个挂起的TcpSocket连接,该函数返回一个指向QTcpSocket的指针,同时进入到QAbstractSocket::ConnectedState状态。这样就可以和客户端进行通信了。如果错误发生,可以用函数serverError()返回错误类型,用errorString()返回错误提示字符串。
    调用close使得QTcpServer停止监听连接请求。尽管QTcpServer使用了事件循环,但是可以不这么使用。利用waitForNewConnection(),该函数阻塞直到有连接可用或者时间超时。
    2】重要函数:
    void incomingConnection (int socketDescriptor);
    当一个连接可以用时,QTcpServer调用该函数。其基本过程是现创建一个QTcpSocket,设置描述符和保存到列表,最后发送newConnection() 事件消息。
     
    QTcpSocket*ss  QTcpServer::nextPendingConnection();
    返回下一个将要连接的QTcpSocket对象,该返回对象是QTcpServer的子对象,意味着如果删除了QTcpSServer,则删除了该对象。也可以在你不需要该对象时,将他删除掉,以免占用内存。

           ss->peerAddress();ss->peerName();ss->peerPort(); 当连接成功可能通过函数获取客户端ip,name,port;

     
    客户端:QTcpSocket,QAbstractSocket
     
    1】基本用法:
    在客户端创建一个QTcpSocket,然后用connectToHost函数向对应的主机和端口建立连接。
    任何时候,可以用state()查询状态,初始为UnconnectedState,然后调用连接函数之后,HostLookupState,如果连接成功进入ConnectedState,并且发送hostFound()信号。
    当连接建立,发送connected(),在任何状态下如果在错误发生error()信号发送。状态改变发送stateChanged()信号。如果QTcpSocket准备好可读可写,则isValid() 函数范围为真。
     
    用read()和write()来读写,或者使用readLine()和readAll.当有数据到来的时候,系统会发送readyRead()信号。
    bytesAvailable()返回包的字节数,如果你不是一次性读完数据,新的数据包到来的时候将会附加到内部读缓存后面。setReadBufferSize()可以设置读缓存的大小。
     
    用disconnectFromHost()关闭连接,进入ClosingState。当所有数据写入到socket,QAbstractSocket会关闭该台socket,同时发送disconnected()消息。
    如果想立即终止一个连接,放弃数据发送,调用abort().
    如果远程主机关闭连接,QAbstractSocket发送QAbstractSocket::RemoteHostClosedError错误,但是状态还停留在ConnectedState,然后发送disconnected()信号。
     
    QAbstractSocket提供几个函数用来挂起调用线程,知道一定的信号发送,这些函数可以用来阻塞socket:
    waitForConnected() 阻塞知道一个连接建立。
    waitForReadyRead() 阻塞知道有新的数据可以读取。
    waitForBytesWritten() 阻塞直到发送数据写道socket中。
    waitForDisconnected() 阻塞知道链接关闭。
     

    QT5 TCP网络通讯综合案例之文件传送

    最终效果图:

     1.功能设计

    服务器端:

    1. 有客户端连接进入就提示连接客户ip端口号等基本信息
    2. [选择文件]按钮被激活可在系统选取任意文件,显示选取的文件路径
    3. [发送]按钮被激活,点击[发送]向客户端发送文件请求;
    4. 文件发送完成后 显示发送完成提示

    客户端:

    1. 连接服务器ip,port编辑框
    2. 按钮[连接服务器]成功后显示提示并激活断开服务器按钮
    3. 可选文件接收保存路径
    4. 当收到服务器发送文件请求,显示所要接收的文件名,文件大小等基本信息
    5. 仅当收到服务器发送的文件请求与选择了有效文件路径前提下,激活[接收文件]按钮
    6. 点击[接收文件]开始从服务器接收文件,文件接收进度条提示功能,并显示文件传送完成提示

    实现原代码:

    //main主函数
    #include "widget.h"
    #include "client.h"
    #include <QApplication>
    
    int main(int argc, char *argv[])
    {
        QApplication a(argc, argv);
        Widget w;
        w.show();
        client w1;
        w1.show();
        return a.exec();
    }
    //服务器端头文件
    #ifndef WIDGET_H
    #define WIDGET_H
    
    #include <QWidget>
    #include <QTcpServer>
    #include <QTcpSocket>
    #include <QTimer>
    #include <QFile>
    
    namespace Ui {
    class Widget;
    }
    class Widget : public QWidget
    {
        Q_OBJECT
    public:
        explicit Widget(QWidget *parent = 0);
        ~Widget();
    private slots:
        void on_buttoncheck_clicked();
        void on_buttonsend_clicked();
    private:
        Ui::Widget *ui;
        QTcpServer *sv;
        QTcpSocket *ss;
    
        QString sendFileName;
        qint64 sendFileSize;
        qint64 sendedSize;
        QFile sendFile;
    };
    
    #endif // WIDGET_H
    //服务器cpp
    #include "widget.h"
    #include "ui_widget.h"
    #include <QFileDialog>
    #include <QFile>
    #include <QMessageBox>
    
    Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
    {
        ui->setupUi(this);
        setWindowTitle("服务端-端口:9527");
        sv = new QTcpServer(this);
        sv->listen(QHostAddress::Any, 9527);
        ui->buttonsend->setEnabled(false);
        ui->buttoncheck->setEnabled(false);
        sendedSize = 0;
        connect(sv, &QTcpServer::newConnection, [=]()
        {
            ss = sv->nextPendingConnection();
            if(ss->isValid())
            {
                ui->buttoncheck->setEnabled(true);
                QString pAd = ss->peerAddress().toString().remove(0, 7);
                QString pNa = ss->peerName();
                quint16 pPo = ss->peerPort();
                ui->textdis->setText(QString("已成功连接到用户IP:%1:%2:Name:%3").arg(pAd).arg(pPo).arg(pNa));
            }else
            {
                ui->textdis->setText("连接套节字无效");
            }
            connect(ss, &QTcpSocket::readyRead,[=]()
            {
               if(ss->readAll() == "ok")
               {
                   qint64 lenW = 0;
                   if(sendFile.open(QIODevice::ReadOnly))
                   {
                       do{
                           char tempdata[1024] = {0};
                           lenW = sendFile.read(tempdata, sizeof(tempdata));
                           ss->write(tempdata, lenW);
                           sendedSize += lenW;
                       }while(lenW > 0);
                       if(sendedSize >= sendFileSize)
                       {
                           ui->textdis->append("发送完成");
                           ss->disconnectFromHost();
                           sendFile.close();
                       }
                   }else
                   {
                       QMessageBox *abot = new QMessageBox(this);
                       abot->setText("打开文件失败");
                   }
               }
            });
    
        });
    }
    
    Widget::~Widget()
    {
        delete ui;
    }
    
    void Widget::on_buttoncheck_clicked()
    {
        QString File = QFileDialog::getOpenFileName(this, QString("选择文件"), QString("../"));
        if(!File.isEmpty())
        {
            ui->buttoncheck->setEnabled(false);
            ui->textdis->append(File);
            sendFile.setFileName(File);
            QFileInfo info(File);
            sendFileName = info.fileName();
            sendFileSize = info.size();
            ui->buttonsend->setEnabled(true);
        }
    }
    
    void Widget::on_buttonsend_clicked()
    {
        QString temp = QString("head@@%1@@%2").arg(sendFileName).arg(sendFileSize);
        ss->write(temp.toUtf8());
        ui->buttonsend->setEnabled(false);
    }
    //客户端头文件
    #ifndef CLIENT_H
    #define CLIENT_H
    
    #include <QWidget>
    #include <QTcpSocket>
    #include <QFile>
    
    namespace Ui {
    class client;
    }
    class client : public QWidget
    {
        Q_OBJECT
    
    public:
        explicit client(QWidget *parent = 0);
        ~client();
    
    private slots:
        void on_Buttonlink_clicked();
        void on_Buttonclose_clicked();
        void on_ButtonSelect_clicked();
        void on_ButtonSave_clicked();
    private:
        Ui::client *ui;
        QTcpSocket userSocket;
    
        QFile acpFile;
        QString headFile;
        QString savePath;
        bool head;
        bool openFile;
        QString _name;
        QString _size;
        qint64 nowsize;
    
    };
    
    #endif // CLIENT_H
    //客户端cpp
    #include "client.h"
    #include "ui_client.h"
    #include <QFile>
    #include <QFileDialog>
    #include <QMessageBox>
    
    client::client(QWidget *parent) :
        QWidget(parent),
        ui(new Ui::client)
    {
        ui->setupUi(this);
        setWindowTitle("客户端");
        head = true;
        openFile =false;
        nowsize = 0;
        ui->progressBar->setMinimum(0);
        ui->progressBar->setValue(0);
        ui->ButtonSave->setEnabled(false);
        ui->Buttonclose->setEnabled(false);
        savePath = nullptr;
        connect(&userSocket, &QTcpSocket::connected,
                [=](){
            ui->Buttonlink->setEnabled(false);
            ui->Buttonclose->setEnabled(true);
            ui->lineStatus->setText("连接成功");});
        connect(&userSocket, &QTcpSocket::readyRead,
                [=](){
            if(head)
            {
                headFile = QString(userSocket.readAll());
                if(!headFile.isEmpty())
                {
                    head = false;
                    _name = headFile.section("@@", 1, 1);
                    _size = headFile.section("@@", 2, 2);
                    ui->progressBar->setMaximum(_size.toLongLong());
                    ui->lineStatus->setText(QString("文件名:[%1]总大小:[%2]等待接收").arg(_name).arg(_size));
                    acpFile.setFileName(QString("%1/%2").arg(savePath).arg(_name));
                    if(!savePath.isEmpty())
                    {
                        ui->ButtonSave->setEnabled(true);
                    }
                }
            }
            else
            {
                if(openFile)
                {
                    nowsize += acpFile.write(userSocket.readAll());
                    ui->progressBar->setValue(nowsize);
                }
                else
                {
                    QMessageBox *abot = new QMessageBox(this);
                    abot->setText("打开文件失败");
                }
                if(acpFile.size() >= _size.toLongLong())
                {
                    nowsize = 0;
                    acpFile.close();
                    ui->lineStatus->setText("文件传送完成");
                    head = true;
                }
            }
    
        });
    }
    
    client::~client()
    {
        delete ui;
    }
    
    void client::on_Buttonlink_clicked()
    {
        QString linkIp = ui->lineIp->text();
        quint16 linkPort = ui->linePort->text().toUShort();
        userSocket.connectToHost(linkIp, linkPort);
    }
    
    void client::on_Buttonclose_clicked()
    {
        userSocket.disconnectFromHost();
        userSocket.close();
        ui->lineStatus->setText("断开连接");
        head = true;
        ui->Buttonlink->setEnabled(true);
        ui->Buttonclose->setEnabled(false);
        ui->progressBar->setValue(0);
    }
    
    void client::on_ButtonSelect_clicked()
    {
        savePath = QFileDialog::getExistingDirectory(this, QString("保存路径"),QString("../"));
        if(!savePath.isEmpty())
        {
            ui->linePath->setText(savePath);
            if(!head)ui->ButtonSave->setEnabled(true);
        }
    }
    
    void client::on_ButtonSave_clicked()
    {
        ui->ButtonSave->setEnabled(false);
        userSocket.write("ok");
        openFile = acpFile.open(QIODevice::WriteOnly);
    }

    主要运用函数:

    • QString QFileDialog::getExistingDirectory() //静态函数 获取文件路径

    • QString QFileDialog::getOpenFileName()//静态函数 获取要打开文件的文件路径文件名

    • QTcpSocket -> isValid();            //如果套节字有效返回true,否则返回false;

    • void setenable();  //设置控件激活状态

    注意事项:

    在服务器端发送文件,采取的是分段读取文件,边读边发的方式发送,而在客户端采取每次接收每次发送的全部内容方式保存文件

    起初有采取过在服务器端all = readAall();write(all);一次性读取发送;但在测试中发现,实际发送大文件时还是会自动被分成多次(只是简单测试,不排除受其他因素影响)

     

  • 相关阅读:
    Valgrind使用转载 Sanny.Liu
    Caffe模型读取 Sanny.Liu
    JNI动态库生成、编译、查看相关简易资料 Sanny.Liu
    GDB调试,转载一位大牛的东西 Sanny.Liu
    Android处理图片工具(转载) Sanny.Liu
    添加可点击的imagebottom,有个点击动画效果 Sanny.Liu
    去OpenCVManager,大部分为转载,仅当自己学习使用 Sanny.Liu
    转载: vim使用技巧 Sanny.Liu
    结构体数组初始化三种方法,转载 Sanny.Liu
    AsyncTask机制学习 Sanny.Liu
  • 原文地址:https://www.cnblogs.com/flowingwind/p/8348519.html
Copyright © 2020-2023  润新知