一、流程

详细流程可参考本博客:https://blog.csdn.net/weixin_38739598/article/details/106571074

二、具体实现

CHttpDownLoadFile头文件

#pragma once#include <QWidget>//网络相关头文件
#include <QtNetwork/QNetworkAccessManager>
#include <QtNetwork/QNetworkRequest>
#include <QtNetwork/QNetworkReply>
//文件相关头文件
#include <QFile>
#include <QFileInfo>
#include <QDir>
#include <QDesktopServices>class CHttpDownLoadFile : public QWidget
{Q_OBJECTpublic:CHttpDownLoadFile(const QString &url, const QString &fileName, const QString &dir,QWidget *parent);~CHttpDownLoadFile();void DownLoadFile();void DestroyData();signals://文件下载结束void DownloadFinishedSignal();   //文件下载进度void DownloadProcess(QString, qint64, qint64);public slots:void ReplyNewDataArrived();//响应m_netReply有新的数据到达void ReplyFinished();//响应数据接收完成void ReplyDownloadProgress(qint64 bytesReceived, qint64 bytesTotal);  //响应文件下载进度public:QNetworkAccessManager *m_netAccessManager;//网络参数QNetworkReply *m_netReply;QUrl m_urlAdress;//网络地址QString m_strFileName;//需要下载的文件名QString m_strDir;//文件的存储位置QFile *m_file;//下载的文件qint64 m_nReceived;//下载文件时,已经接收的文件大小和总共大小qint64 m_nTotal;bool m_bIsFinished;
};

CHttpDownLoadFile源文件

#include "CHttpDownLoadFile.h"CHttpDownLoadFile::CHttpDownLoadFile(const QString &url, const QString &fileName, const QString &dir, QWidget *parent): QWidget(parent),m_urlAdress(url),m_strFileName(fileName),m_strDir(dir),m_bIsFinished(false)
{m_netAccessManager = new QNetworkAccessManager(this);
}CHttpDownLoadFile::~CHttpDownLoadFile()
{}void CHttpDownLoadFile::DownLoadFile()
{m_bIsFinished = false;QNetworkRequest request(m_urlAdress);request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);m_netReply = m_netAccessManager->get(request);connect(m_netReply, SIGNAL(readyRead()), this, SLOT(ReplyNewDataArrived()));//当有新数据到达时就会触发此信号connect(m_netReply, SIGNAL(finished()), this, SLOT(ReplyFinished()));//完成数据接收后发送此信号connect(m_netReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(ReplyError(QNetworkReply::NetworkError)));//出现错误时发送此信号;connect(m_netReply, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(ReplyDownloadProgress(qint64, qint64)));//用来提示文件下载进度if (m_strFileName.isEmpty())//文件名{QFileInfo fileInfo(m_urlAdress.path());m_strFileName = fileInfo.fileName();}if (!m_strDir.isEmpty())//文件夹{QDir directory(m_strDir);if (!directory.exists())//没有此文件夹,则创建{directory.mkpath(m_strDir);}m_strFileName = m_strDir + m_strFileName;//添加/是为了防止用户名没有加/,因为对于文件夹来说两个/都会当成一个/}if (QFile::exists(m_strFileName))//如果文件已经存在,那么删除{QFile::remove(m_strFileName);}m_file = new QFile(m_strFileName);if (!m_file->open(QIODevice::WriteOnly | QIODevice::Text)){m_file->close();delete m_file;m_file = NULL;return;}
}void CHttpDownLoadFile::DestroyData()
{m_netAccessManager->deleteLater();m_netReply->deleteLater();m_file->close();m_file->deleteLater();
}void CHttpDownLoadFile::ReplyNewDataArrived()//响应m_netReply有新的数据到达
{if (m_file){// 写文件-形式为追加QFile file(m_strFileName);if (file.open(QIODevice::Append))file.write(m_netReply->readAll());file.close();}else{qDebug() << m_netReply->readAll();}
}void CHttpDownLoadFile::ReplyFinished()//响应数据接收完成
{m_bIsFinished = true;m_netAccessManager->deleteLater();m_netReply->deleteLater();m_file->close();m_file->deleteLater();emit DownloadFinishedSignal();
}void CHttpDownLoadFile::ReplyDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
{QFileInfo fileInfo(m_urlAdress.path());QString strFileName = fileInfo.fileName();emit DownloadProcess(strFileName, bytesReceived, bytesTotal);
}

AutoUpdate 头文件

#pragma once#include <QtWidgets/QDialog>
#include <QtWidgets/QMainWindow>
#include <QUrl>
#include <QDesktopServices>
//网络相关头文件
#include <QtNetwork/QNetworkAccessManager>
#include <QtNetwork/QNetworkRequest>
#include <QtNetwork/QNetworkReply>
//JSON相关头文件
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QMessageBox>
#include <QFile>
#include <QFileInfo>
#include <QDir>
#include <QTime>
#include <QProgressBar>
#include <QTextCodec>#include <QProcess>
#include "zlib.h"
#include "zconf.h"
#include "unzip.h"
#include <direct.h>#include "ui_AutoUpdate.h"
#include "CHttpDownLoadFile.h"
#include <QList>class AutoUpdate : public QDialog
{Q_OBJECTpublic:AutoUpdate(QWidget *parent = Q_NULLPTR);private:int ParseJson(const QString &str);      //解析数据函数的声明
private:QNetworkAccessManager * m_NetManager;     //定义网络请求对象QNetworkAccessManager * m_NetManagerDown;     //定义网络请求对象QNetworkReply *         m_pReply;QString                    m_CurVerison;    //定义当前软件的版本号QUrl                   m_url;QString                   m_strUrl;QString                    m_strFileName;QTime                 m_downloadTime;int                      m_nTime;QString                 m_lastVerison;  private:CHttpDownLoadFile *m_httpXML;QList<QString> m_listFileDir;QList<QString> m_listFileName;QString m_strTip;QString m_strXmlName;void DownLoadXML();QString GetElementVersion(const QString &xml, const QString &name);bool CheckVersion(const QString &v1, const QString &v2);int CheckUpdateFiles(const QString &xml1, const QString &xml2);void DownLoadUpdateFiles();void ExitApp(const QString &name);private slots:void slotUpdateNow();void slotClose();void replyFinished(QNetworkReply *reply);   //json 文件下载结束void onDownloadProgress(QString fileName, qint64 bytesReceived, qint64 bytesTotal);    //下载进度void ReplyHttpFinished();
private:Ui::AutoUpdate ui;
};

AutoUpdate源文件

# include <stdio.h>
# include <inttypes.h>
# include <Windows.h>
# include <WinInet.h>
#include "AutoUpdate.h"
#include "configer.h"
#include "Format.h"
#include <QDomDocument>AutoUpdate::AutoUpdate(QWidget *parent): QDialog(parent),m_NetManager(NULL),m_NetManagerDown(NULL),m_nTime(0)
{ui.setupUi(this);ui.progressBar->setValue(0);m_strFileName = QString(QString::fromLocal8Bit(Configer::GetInstance()->FilePath().c_str())) + "\\Test.zip";m_CurVerison = QString::fromStdString(Configer::GetInstance()->GetVersion());setWindowIcon(QIcon(QString::fromStdString(Configer::GetInstance()->GetWindowIcon())));setWindowFlags(Qt::FramelessWindowHint | Qt::WindowMinimizeButtonHint | Qt::WindowStaysOnTopHint);m_NetManager = new QNetworkAccessManager(this);          //新建QNetworkAccessManager对象connect(m_NetManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(replyFinished(QNetworkReply*)));//关联信号和槽QNetworkRequest quest;quest.setUrl(QUrl(Configer::GetInstance()->GetCfgQurl().c_str())); //包含最新版本软件的下载地址//quest.setHeader(QNetworkRequest::UserAgentHeader, "RT-Thread ART");QNetworkReply *reply = m_NetManager->get(quest);    //发送get网络请求connect(reply, SIGNAL(error), this, SLOT(slotError));
}
int AutoUpdate::ParseJson(const QString &str)
{QJsonParseError err_rpt;QJsonDocument  root_Doc = QJsonDocument::fromJson(str.toUtf8(), &err_rpt);//字符串格式化为JSONif (err_rpt.error != QJsonParseError::NoError){QMessageBox::critical(this, QString::fromLocal8Bit("检查失败"), QString::fromLocal8Bit("服务器地址错误或JSON格式错误!"));return -1;}if (root_Doc.isObject()){QJsonObject  root_Obj = root_Doc.object();   //创建JSON对象,不是字符串m_lastVerison = root_Obj.value("LatestVerison").toString();  //V1.0m_strUrl = root_Obj.value("Url").toString();QString UpdateTime = root_Obj.value("UpdateTime").toString();QString ReleaseNote = root_Obj.value("ReleaseNote").toString();if (m_lastVerison > m_CurVerison){this->show();QString warningStr = QString::fromLocal8Bit("检测到新版本!\n版本号:") + m_lastVerison + "\n" + QString::fromLocal8Bit("更新时间:") + UpdateTime + "\n" + QString::fromLocal8Bit("更新说明:") + ReleaseNote;ui.textBrowser->append(warningStr);}else{QString strCurrentDir = QString(QString::fromLocal8Bit(Configer::GetInstance()->FilePath().c_str()));//当前程序运行路径ExitApp(strCurrentDir + QString::fromStdString(Configer::GetInstance()->GetLocation()) + Configer::GetInstance()->GetRunExe().c_str());}}return 0;
}
void AutoUpdate::replyFinished(QNetworkReply *reply)
{QVariant statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);if (statusCode != 200){//QMessageBox::critical(this, QString::fromLocal8Bit("Json文件下载失败 状态码:%1").arg(statusCode.toString()), reply->errorString());return;}ParseJson(reply->readAll());reply->deleteLater();
}
void AutoUpdate::slotUpdateNow()
{//杀掉正在运行的进程QProcess taskkill;taskkill.execute("taskkill", QStringList() << "-im" << Configer::GetInstance()->GetRunExe().c_str() << "-f");ui.stackedWidget->setCurrentWidget(ui.pageInfo);ui.butNow->hide();DownLoadXML();
}
void AutoUpdate::slotClose()
{reject();qApp->exit(0);m_httpXML->DestroyData();/*this->close();*/}
void AutoUpdate::onDownloadProgress(QString fileName,qint64 bytesReceived, qint64 bytesTotal)
{ui.lblFileInfo->setText(fileName);// 总时间int nTime = m_downloadTime.elapsed();// 本次下载所用时间nTime -= m_nTime;// 下载速度double dBytesSpeed = (bytesReceived * 1000.0) / nTime;double dSpeed = dBytesSpeed;//剩余时间qint64 leftBytes = (bytesTotal - bytesReceived);double dLeftTime = (leftBytes * 1.0) / dBytesSpeed;ui.lblSpeedInfo->setText(speed(dSpeed));ui.lblLeftTime->setText(timeFormat(dLeftTime));ui.lblFileInfoSize->setText(formatSize(bytesTotal));ui.lblDownLoadInfo->setText(formatSize(bytesReceived));ui.progressBar->setMaximum(bytesTotal);ui.progressBar->setValue(bytesReceived);// 获取上一次的时间m_nTime = nTime;
}void AutoUpdate::DownLoadXML()
{/**从网页下载XML版本控制文件,里面记录了最新的文件版本**/string str = Configer::GetInstance()->FilePath();QString strDownLoad = QString::fromLocal8Bit(Configer::GetInstance()->FilePath().c_str()) + QString::fromStdString(Configer::GetInstance()->GetLocation());//存放下载文件的路径QDir directory(strDownLoad);//如果路径不存在,则创建if (!directory.exists()){directory.mkpath(strDownLoad);}QUrl url(QString::fromStdString(Configer::GetInstance()->GetXmlQurl()));QFileInfo fileInfo(url.path());m_strXmlName = fileInfo.fileName();m_httpXML = new CHttpDownLoadFile(QString::fromStdString(Configer::GetInstance()->GetXmlQurl()), "", strDownLoad, this);//调用下载文件的类connect(m_httpXML, SIGNAL(DownloadFinishedSignal()), this, SLOT(ReplyHttpFinished()));//发生错误时一样会发送此信号m_httpXML->DownLoadFile();
}QString AutoUpdate::GetElementVersion(const QString &xml, const QString &name)
{QString result = "";if (xml.isEmpty() || name.isEmpty()){qDebug() << "名称或者xml文件路径为空";return result;}if (!QFile::exists(xml)){qDebug() << "xml文件不存在";return result;}QFile file(xml);if (file.open(QIODevice::ReadOnly | QFile::Text))//文件打开成功{QDomDocument doc;QString errorStr;int errorLine;int errorColumn;if (doc.setContent(&file, false, &errorStr, &errorLine, &errorColumn)){QDomElement root = doc.documentElement();if (root.tagName() == "filelist"){int i = 0;QDomNodeList nodeList = root.elementsByTagName("file");for (; i<nodeList.size(); i++){QString tempName = nodeList.at(i).toElement().attribute("name");//QStringdir    =nodeList.at(i).toElement().attribute("dir");QString version = nodeList.at(i).toElement().attribute("version");if (name == tempName){qDebug() << "find!" << name;result = version;break;}}if (i == nodeList.size()){qDebug() << "can'tfind!" << name;}}else{qDebug() << "root.tagname!=filelist..";}}else{qDebug() << "setcontenterror...";}file.close();}else{qDebug() << "openforreaderror...";}return result;
}bool AutoUpdate::CheckVersion(const QString &v1, const QString &v2)
{return v1 == v2;
}int AutoUpdate::CheckUpdateFiles(const QString &xml1, const QString &xml2)
{m_listFileDir.clear();m_listFileName.clear();if (xml1.isEmpty() || xml2.isEmpty())return 0;if (QFile::exists(xml2)){if (QFile::exists(xml1)){m_strTip = QString::fromLocal8Bit("检查需要更新的文件...");QFile file(xml1);if (file.open(QIODevice::ReadOnly | QFile::Text))//文件打开成功{QString errorStr;int errorLine;int errorColumn;QDomDocument doc;if (doc.setContent(&file, false, &errorStr, &errorLine, &errorColumn)){QDomElement root = doc.documentElement();if (root.tagName() == "filelist"){QDomNodeList nodeList = root.elementsByTagName("file");for (int i = 0; i<nodeList.size(); i++){QString name = nodeList.at(i).toElement().attribute("name");QString dir = nodeList.at(i).toElement().attribute("dir");QString version = nodeList.at(i).toElement().attribute("version");QString versionDownload = GetElementVersion(xml2, name);//获取本地xml文件对应文件(name)的版本信息if (versionDownload.isEmpty())//本地XML没有此文件:下载,并放到相应的目录中{m_listFileDir.append(dir);m_listFileName.append(name);}else{/**检查版本,如果本地版本低于下载的版本,则下载**/if (!CheckVersion(version, versionDownload)){m_listFileDir.append(dir);m_listFileName.append(name);}else{qDebug() << name << QString::fromLocal8Bit("文件是最新版本,不需要更新");}}}return 1;//此时要退出,避免关闭程序}else{m_strTip = "XML内容错误!";return 0;}}else{qDebug() << "setcontenterror...";return 0;}file.close();}else{m_strTip = "不能打开更新文件!";return 0;}}else{m_strTip = "下载更新文件错误!";return 0;}}else{m_strTip = "本地的更新文件不存在!";return 0;}
}void AutoUpdate::DownLoadUpdateFiles()
{QString strServer = QString::fromStdString(Configer::GetInstance()->GetSerQurl());//需要下载的文件存储位置QString strCurrentDir = QString(QString::fromLocal8Bit(Configer::GetInstance()->FilePath().c_str()));//当前程序运行路径if (m_listFileDir.isEmpty() || m_listFileDir.isEmpty()){ExitApp(strCurrentDir + QString::fromLocal8Bit(Configer::GetInstance()->GetLocation().c_str()) + Configer::GetInstance()->GetRunExe().c_str());return;}m_strTip = QString::fromLocal8Bit("开始下载更新文件...");for (int i = 0; i<m_listFileName.size(); i++){m_strTip = QString::fromLocal8Bit("正在下载文件") + m_listFileName.at(i);ui.progressBar->setValue(100 * i / m_listFileName.size());/**放置下载的文件的路径**/QString temp = m_listFileDir.at(i);QString tempdir = m_listFileDir.at(i);if (!m_listFileDir.at(i).isEmpty()){temp = temp + "/";tempdir = tempdir + "\\";}QString strPlaceDir = strCurrentDir + QString::fromStdString(Configer::GetInstance()->GetLocation()) + tempdir;QDir directory(strPlaceDir);//如果路径不存在,则创建if (!directory.exists())directory.mkpath(strPlaceDir);QString strFileDirServer = strServer + temp + m_listFileName.at(i);//文件在服务器中的存储位置CHttpDownLoadFile *http = new CHttpDownLoadFile(strFileDirServer, "", strPlaceDir, this);//调用下载文件的类http->DownLoadFile();connect(http, SIGNAL(DownloadProcess(QString, qint64, qint64)), this, SLOT(onDownloadProgress(QString, qint64, qint64)));m_downloadTime.start();while (!http->m_bIsFinished){if (http->m_nTotal == -1){ui.progressBar->setValue(1);}else{ui.progressBar->setValue(100 * http->m_nReceived / http->m_nTotal);}QCoreApplication::processEvents();}m_strTip = QString::fromLocal8Bit("文件") + m_listFileName.at(i) + QString::fromLocal8Bit("下载完成");///**将下载好的文件复制到主目录中,先删除原先的文件**///QString strLocalFileName = strCurrentDir + "\\" + m_listFileDir.at(i) + "\\" + m_listFileName.at(i);//if (QFile::exists(strLocalFileName))QFile::remove(strLocalFileName);//QDir directory1(strCurrentDir + "\\" + m_listFileDir.at(i));//如果路径不存在,则创建//if (!directory1.exists())directory1.mkpath(strCurrentDir + "\\" + m_listFileDir.at(i));//QFile::copy(strPlaceDir + "\\" + m_listFileName.at(i), strLocalFileName);}m_strTip = QString::fromLocal8Bit("更新完成!");/**替换旧的xml文件**/QString strNewXML = strCurrentDir + QString::fromStdString(Configer::GetInstance()->GetLocation()) + m_strXmlName;//最新的XML文件QString strOldXML = strCurrentDir + "\\" + m_strXmlName;//旧的XML文件QFile::remove(strOldXML);QFile::copy(strNewXML, strOldXML);ExitApp(strCurrentDir + QString::fromStdString(Configer::GetInstance()->GetLocation()) + Configer::GetInstance()->GetRunExe().c_str());
}void AutoUpdate::ExitApp(const QString &name)
{if (!name.isEmpty()){/**运行主程序,并且退出当前更新程序(说明:主程序在上上一级目录中)**///更新配置文件版本号Configer::GetInstance()->SaveVersion(m_lastVerison.toStdString().c_str());if (!QProcess::startDetached(name, QStringList()))//启动主程序,主程序在其上一级目录{QMessageBox::warning(this, QString::fromLocal8Bit("警告信息"), QString::fromLocal8Bit("启动主程序错误!\n可能主程序不存在或者被破坏!\n解决办法:重新安装程序!"));}}this->close();
}void AutoUpdate::ReplyHttpFinished()
{CheckUpdateFiles(QString::fromLocal8Bit(Configer::GetInstance()->FilePath().c_str()) + QString::fromStdString(Configer::GetInstance()->GetLocation()) + m_strXmlName, QString::fromLocal8Bit(Configer::GetInstance()->FilePath().c_str()) + "\\" + m_strXmlName);DownLoadUpdateFiles();
}

三、遇到的问题

  • 程序支持中文路径,不需要中间进行转换,设置编码。
  • 如果程序安装到C盘,需要以管理员的方式启动程序。
  • 如果要更新的程序已经在运行中,那么就需要杀掉正在运行的进程,再进行更新,不然会更新失败。
  • 相对上https://blog.csdn.net/weixin_38739598/article/details/106571074,避免了下载整个压缩包,没必要更新的文件也下载了。

四、实现效果



参考文章:https://blog.csdn.net/hulinhulin/article/details/46839107
由于博主只写了部分,所以自己实现了部分。

源码下载地址:Qt 实现软件自动升级

Qt 实现软件自动更新相关推荐

  1. 软件自动更新解决方案及QT实现

    from:https://blog.csdn.net/hulinhulin/article/details/46839107 软件自动更新解决放案及QT实现...1 1 文件的版本控制-XML.2 2 ...

  2. 软件自动更新解决方案及QT实现(源码已上传)

    软件自动更新解决放案及QT实现...1 1 文件的版本控制-XML.2 2 更新程序的实现...2 2.1 界面设置...2 2.2 程序功能...3 2.2.1 下载网络数据...3 2.2.2 X ...

  3. C#软件自动更新程序

    2019独角兽企业重金招聘Python工程师标准>>> 基于C#实现的软件自动更新程序,之前在网上搜集了两款软件自动更新程序,在实际应用中,对部分BUG进行修复,添加+完善一些功能. ...

  4. Android - 软件自动更新的实现

    Android - 软件自动更新的实现 2012年11月18日 天气慢慢变凉了,给位亲,注意保暖啊. 接触到一个很实用的技术,那就是软件自动更新.一般开发者是通过自行在应用平台添加更新版本的apk.这 ...

  5. 《『若水新闻』客户端开发教程》——17.软件自动更新

    本节课的主要内容: 1.增加软件自动更新功能 课程下载: http://115.com/file/e77ow25d

  6. 软件自动更新功能的实现

    今天一朋友在群里面问,软件自动更新功能怎么做,大家都不知道怎么搞,我下午刚好没事情,就研究了下. 附上我的源代码 考虑下基本的思路 1 .客户端(主程序)调用升级程序,升级程序连接到最新的服务器上. ...

  7. 如何屏蔽 iOS 16 软件自动更新,去除更新通知和标记

    如何禁用 iPhone.iPad 软件自动更新.适用于 iOS.iPadOS 和 watchOS,即 iPhone.iPad 和 Apple Watch 通用. 请访问原文链接:https://sys ...

  8. MAC 关闭office软件自动更新提示 (Microsoft AutoUpdate)

    MAC 关闭office软件自动更新提示 (Microsoft AutoUpdate) cd /Library/Application\ Support/Microsoft/MAU2.0 sudo c ...

  9. 【Electron】酷家乐客户端开发实践分享 — 软件自动更新

    作者:钟离,酷家乐PC客户端负责人 原文地址:webfe.kujiale.com/electron-au- 酷家乐客户端:下载地址 www.kujiale.com/activity/13- 文章背景: ...

最新文章

  1. Go 语言编程 — panic 和 recover
  2. Python:zip()函数
  3. 订阅基础:RSS、ATOM、FEED、聚合、供稿、合烧与订阅
  4. 【网址收藏】spark on k8s operator github地址
  5. 计算机上网英语词汇,计算机网络专用英语词汇1500词
  6. php树菜单转化为一维菜单,php树型菜单 - iturtle的个人空间 - OSCHINA - 中文开源技术交流社区...
  7. Toad9.7与Oracle11g在X86的Win7下的情况
  8. Problem J: 蛇形填阵
  9. 2020中国数字化转型优秀案例征集
  10. vue-cli+webpack打包配置
  11. Linux中RAID机制的实现
  12. 计算机基础ps变换蝴蝶,PS利用自由变换制作飞舞的蝴蝶
  13. 2021-09-21用pyecharts做全球各个国家的gdp色彩深度图
  14. 论文笔记《ST-GRAT: A Novel Spatio-temporal Graph Attention Network for Accurately Forecasting》
  15. 自动化测试平台化[v1.0.0][模块化设计方法]
  16. ios android 系统占用空间,iOS 系统占用了 20G 储存空间?别担心,教你快速解决!...
  17. 康得新董事长是谁?_您人生董事会中的谁?
  18. [Android][sensor]物理sensor bring up流程
  19. python窗口设置背景图片_PyQt5 实现给窗口设置背景图片的方法
  20. php 神经网络,神经网络算法基础入门

热门文章

  1. Druid连接池 一个设置 removeAbandonedTimeout
  2. CMD查找域名对应的IP地址
  3. android的银联支付,android 银联支付Demo
  4. kafka磁盘写满处理
  5. Java版spring cloud 企业工程项目管理系统平台源码(三控:进度组织、质量安全、预算资金成本、二平台:招采、设计管理)
  6. 【KAWAKO】基于frp和腾讯云服务器的内网穿透
  7. 《痞子衡嵌入式半月刊》 第 16 期
  8. 【数据结构】栈(实现+原码)
  9. 用 Unity 和 HTC Vive 实现高级 VR 机制(1)
  10. 苹果官网上关于iOS/iPadOS设备尺寸