一种软件包差异化升级方法(上篇)

本文详细解释了软件升级中的差异化升级过程,涉及软件包的准备(包括新版本包和升级配置),吃包(部署软件包并存储,以及版本信息发布),并通过命令行工具封装整个过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

引言

很多软件在使用过程中,都会自动弹出新版本提示,如下图:

当我们点击立即更新后,就自动下载升级包走升级流程

这个升级过程往往比较快,不需要像第一次下载安装包时等那么久,其中最主要的区别就在于走了差异化升级,客户端只下载和安装了差量包。

差量包是一种只包含了新版本与当前版本之间差异的文件集合。相比于完整的软件包,差量包的大小通常更小,它不仅能减少网络带宽,提高升级效率,还能改善用户体验。

接下来,我们就分两篇来探究下整个差异化升级的过程,本篇为上篇,主要介绍吃包,差量包会放在下篇介绍。

2.准备工作

做差异化升级之前,我们首先需要准备一个待发布的新版本软件包和对应的升级配置:

  • 软件包:包含软件所有文件的压缩包
  • 升级配置:待发布的版本信息和文件清单

2.1 软件包

为了方便解析并让软件包格式通用,可以直接使用zip将软件所有文件连带目录打成压缩包,就得到我们需要的软件包,如下图:

2.2 升级配置

发布时需要一些配置来描述软件包的基本构成,包括但不限于:

  • release: 发布版本信息,包括应用、版本号、客户端类型、产品等信息
  • file:软件包含的文件列表信息,差异化升级以文件为单位,我们需要知道软件包含的所有文件;
  • notes: 也称为release notes, 软件更新内容的说明,即用户看到的升级内容提示;

版本信息的格式用xml示例如下:

  • application: 应用标识,一个应用(如Mac端)会发布很多版本,但这些版本的应用标识是不变的;
  • version:该软件包的发布版本号
  • site: 产品标识,一个产品可以有多个终端应用,如Windows、Mac、Android等
  • time: 出包时间
<?xml version="1.0" encoding="UTF-8"?>
<release application="10006" version="6.16.23090504" site="60000" time="2023-09-05 05:51:20">
  <file …… />  <!-- 文件,下面再展开 -->
  <notes ……/>  <!-- release notes, 下面再展开 -->
</release>

文件列表格式用xml示例如下:

  • path: 文件在包内统一定位符,即包内的相对路径
  • checksum: 文件的MD5值,用于比较文件是否有变化
  • size: 文件大小,单位:字节
  <file path="/6/60000/api-ms-win-core-console-l1-1-0.dll" checksum="3c444dc1b72aeee3e458d2874eb3aec0" size="176048"/>
  <file path="/6/60000/TangClient.exe" checksum="ed5a1240ba6aa942722c733d0cf480fb" size="3313"/>
  <file path="/resources/html/page/index.html" checksum="e89a61f155ff434773bf780832270c52" size="9778"/>
  <file path="/resources/pages/scripts/index.js" checksum="effc8503cf54c7b9da6c7c0715b8cc72" size="151513"/>

release notes 格式示例如下:

  • lang: 指明notes适用的升级语言,默认中文;
  • 可以指定多个不同语言的release notes;
<notes lang="zh-cn">
<![CDATA[
1. 解决重要bug及视觉交互优化
]]>
</notes>

这个升级配置的构造工作是可以集成到CI工具中在编译打包时就自动生成的,包括文件列表、每个文件的MD5校验和,类似Jenkins的工具中都支持添加构建步骤。

3. 吃包

吃包主要做两件事情:

  • 部署软件包
  • 发布软件版本信息

3.1 部署软件包

部署包就是把软件包里的文件解压,以文件为单位转存到磁盘的固定目录中,最好使用共享存储或云存储,以便集群中多台机器都能访问到。

首先,我们要创建一个目录用于存放此软件包的文件,为了方便识别,我们直接用application和version拼起来作为目录名。

// 构造当前软件包的文件存储目录,并创建该目录
//  rootPath: 可以理解为共享存储为升级业务分配的根存储目录,由外面指定
func (self *USDeploy) buildStorageDir() (string, error) {
	storageDir := fmt.Sprintf("%s/files/%d_%s", self.rootPath, self.release.Application, self.release.Version)
	if ok := createDir(storageDir); !ok {
		return "", fmt.Errorf("create storage dir error")
	}
	return storageDir, nil
}

其次,我们打开并遍历压缩包,循环转存每个文件到存储目录下,转存时遵循以下规则:

  • 只转存文件内容,文件在包内的目录信息交由DB记录(发布时会描述);
  • 转存时需要给文件重命名,压缩包多层目录中的文件可能会重名,简单起见直接用数组下标来构造;
  • 存储完后需要将文件的相对路径和实际存储路径作个映射,就是下面代码中的filePathMap,返回给主调程序;

遍历压缩包的代码示例如下:

func (self *USDeploy) unzip(zfile string) (filePathMap map[string]string, err error) {
	rc, err := zip.OpenReader(zfile) // 打开压缩包的读取句柄
    ……  // 错误处理省略
	storageDir, err := self.buildStorageDir() // 构造当前软件包的文件存储目录
    …… // 错误处理省略
	filePathMap = make(map[string]string) // 相对目录与存储目录的映射,发布配置时需要使用
	for i, f := range rc.File {
		filePath := fmt.Sprintf("%s/f_%d", storageDir, i) // 文件存储路径,文件名直接用f_[index]来命名
		if err = self.saveFile(f, filePath); err != nil { // 保存文件
			return
		}
		// 维护相对目录与存储目录的映射, 为便于存储目录迁移,这里只存rootPath以外的部分
		filePathMap[f.Name] = strings.TrimPrefix(filePath, self.rootPath)
	}
	return filePathMap, nil
}

最后,具体到一个文件的转存,就是将数据从源文件拷贝到目标文件,3步即可完成:

  1. 打开源文件句柄
  2. 打开目标文件句柄
  3. 将数据从源文件读出来,写入目标文件

代码示例如下:

func (self *USDeploy) saveFile(file *zip.File, storagePath string) (err error) {
	src, err := file.Open()    // 源文件句柄
	if err != nil {
		return fmt.Errorf("open file[%s] error: %s", file.Name, err.Error())
	}
	defer src.Close()

	dst, err := os.Create(storagePath)   // 目标文件句柄
	if err != nil {
		return err
	}
	defer dst.Close()
	
	_, err := io.CopyN(dst, src, int64(file.UncompressedSize64))   // 从源文件读取数据,并拷贝到目标文件
	if err != nil {
		return err
	}
	return nil
}

软件包里的文件解压完后,保存到磁盘上的文件如下图所示:
在这里插入图片描述
到这里,软件包中文件的部署过程就结束了,剩下的是版本发布。

3.2 发布版本

发布一个软件版本,也简单分为两步:

  • 读取升级配置;
  • 保存并发布配置;

3.2.1 读取升级配置

还是以前面的xml配置示例来说明,首先我们要将版本信息解析出来,用一个函数来完成解析工作:

  • 入参xmlConfig为软件包的升级配置文件路径
  • 使用xml.Unmarshal方法直接将xml文件解析到一个结构体
func (self *USDeploy) readXmlContent(xmlConfig string) (*models.TRelease, error) {
	file, err := os.Open(xmlConfig)        // 打开xml文件句柄
	if err != nil {
		return nil, err
	}
	defer file.Close()

	xmlFileBytes, err := ioutil.ReadAll(file)   // 读取xml文件内容
	if err != nil {
		return nil, err
	}
	releaseInfo := &models.TRelease{}
	if err := xml.Unmarshal(xmlFileBytes, releaseInfo); err != nil {  // 利用xml在struct上的 tag直接完成反序列化
		return nil, err
	}

	return releaseInfo, nil
}

解析后得到一个TRelease类型的结构体变量:

// 软件包版本发布信息
type TRelease struct {
	Version        string           `xml:"version,attr"`      //客户端升级包版本
	Time           string           `xml:"time,attr"`         //客户端升级包时间
	Application    int64            `xml:"application,attr"`  //客户端升级appid(6:mac/pc 202:ios 201:andriod)
	Site           string           `xml:"site,attr"`         //升级站点,对应产品标识
	ReleaseId      int64            `xml:"-"`                 //软件包发布标识,服务器生成
	Notes          []*TReleaseNotes `xml:"notes,omitempty"`   // 升级内容,可能会有多种语言的notes
	File           []*TReleaseFile  `xml:"file"`              // 文件列表
}
// release notes信息
type TReleaseNotes struct {
	XMLName xml.Name `xml:"notes,omitempty" orm:"-"`
	Id      int64    `xml:"-" orm:"column(id);pk;auto"`            // 主键,自增
	Lang    string   `xml:"lang,attr" orm:"column(lang)"`          // 语言,如zh-cn, en-us
	Notes   string   `xml:",innerxml" orm:"column(release_notes)"` // 升级提示内容
}
// 文件信息
type TReleaseFile struct {
	XMLName     xml.Name `xml:"file" orm:"-"`
	FileId      int64    `xml:"-" orm:"column(id);pk;auto"`          // 主键,自增
	Path        string   `xml:"path,attr" orm:"column(path)"`        // 文件在包内的相对路径
	Version     string   `xml:"version,attr" orm:"column(version)"`  // 文件所属包的版本号
	Time        string   `xml:"time,attr" orm:"column(release_date)"`// 文件所属包的发布时间
	CheckSum    string   `xml:"checksum,attr" orm:"column(checksum)"`// 文件的md5
	Size        int64    `xml:"size,attr" orm:"column(size)"`        // 文件的大小
	Url         string   `xml:"url,attr,omitempty" orm:"-"`          // 文件的url,适用于http(s)协议的文件
	StoragePath string   `xml:"-" orm:"column(storage_path)"`        // 存储路径
}

上面这个结构体定义中,文件信息中的StoragePath是需要我们来补充的,还记得前面部署软件包时生成的filePathMap吗?它已经维护了相对目录与存储目录的映射,我们只需要写一个函数来查找补充字段即可:

func (r *TRelease) SetFileStoragePath(fileStorageMap map[string]string) {
	for i, file := range r.File {
		//从压缩包中读出的相对路径,不带前面的"/",需要去掉
		if v, ok := fileStorageMap[file.Path[1:]]; ok {
			r.File[i].StoragePath = v
		}
	}
}

完成解析工作后,TRelease中的信息已经基本完整,下面只需要保存配置到DB即可。

3.2.2 保存并发布配置

发布版本主要是将软件包信息入库,并设为发布状态。上面提到,xml升级配置中主要包含三部分信息:

  • 版本发布信息
  • 文件列表信息
  • release notes

相应的,我们可以用三张表来存储这三块信息,分别为 :

  • 版本发布表: 用来存储TRelease的基本信息
CREATE TABLE `us_site_release` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `release_id` bigint(20) NOT NULL COMMENT '软件包发布标识',
  `release_version` varchar(50) NOT NULL COMMENT '发布版本',
  `site_id` varchar(50) NOT NULL COMMENT '产品标识',
  `application_id` bigint(20) NOT NULL COMMENT '应用标识',
  `client_type` varchar(50) NOT NULL COMMENT '终端类型',
  `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '发布状态,0:未发布,1:已发布',
  `extend_attr` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '扩展属性',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `update_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_release_id` (`release_id`) USING BTREE,
  KEY `idx_release_version` (`release_version`) USING BTREE,
  KEY `idx_release_appid` (`application_id`) USING BTREE,
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

  • 文件信息表: 用来存储TReleaseFile数据
CREATE TABLE `us_file_element` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `update_path` varchar(255) NOT NULL COMMENT '包内相对路径',
  `version` varchar(100) NOT NULL COMMENT '所属版本号',
  `storage_path` varchar(255) NOT NULL COMMENT '磁盘存储路径',
  `release_date` datetime NOT NULL COMMENT '发布时间',
  `checksum` varchar(32) NOT NULL COMMENT 'MD5校验值',
  `size` bigint(20) NOT NULL COMMENT '文件大小字节数',
  `release_id` bigint(20) NOT NULL COMMENT '发布ID',
  PRIMARY KEY (`id`),
  KEY `idx_file_rid` (`release_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
  • release notes表:用来存储TReleaseNotes数据
CREATE TABLE `us_release_notes` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `release_id` bigint(20) NOT NULL COMMENT '软件包发布标识',
  `lang` varchar(20) NOT NULL COMMENT '语言',
  `release_notes` text COMMENT '升级内容说明',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_notes_rid` (`release_id`, `lang`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

这三张表的数据需要通过release_id字段关联起来的,为此我们需要有一个机制来生成release_id, 参考前段时间讲过的ID生成器,这里可以用两条SQL实现一个简易版(鉴于这里基本不存在并发和性能问题,就简单实现):

UPDATE us_ticket set max_id=max_id+1 WHERE `key_name`='release'
SELECT max_id FROM us_ticket WHERE `key_name`='release'

与数据库操作的代码这里就省略了,入库后的信息如下:

  • 版本发布表数据
    在这里插入图片描述

  • 文件信息表数据
    在这里插入图片描述

  • release notes表数据
    在这里插入图片描述

有一点需要留意:

  • 保存数据时,三张表的写操作要加事务保护,保证全部成功或者全部失败;

信息入库后,将us_site_release表的status置为已发布,客户端就可以检测到发布的新版本。

3.3 命令行工具封装

上面部署软件包和版本发布是分成多步操作的,为方便使用,我们可以将整个吃包过程封装成一个命令行工具。

首先,用一个函数将上面两步串起来:

type USDeploy struct {
	rootPath string          // 存储根目录
	release  *models.TRelease
}
//  入参deployFile表示软件包路径,入参deployXml 表示xml配置文件路径
func (self *USDeploy) Deploy(deployFile, deployXml string) error {
	releaseInfo, err := self.readXmlContent(deployXml)   // 读取升级配置
	if err != nil {
		return err
	}
	self.release = releaseInfo
	filePathMap, err := self.unzip(deployFile)   // 部署软件包
	if err != nil {
		return err
	}
	self.release.SetFileStoragePath(filePathMap)  // 设置存储路径
	return models.SaveRelease(self.release)   // 保存并发布新版本信息
}

然后,写一个支持命令行参数解析的main函数,从命令行参数中将deployFile和deployXml读出来,传给Deploy函数即可。

封装命令行工具可能需要做一些额外的初始化和配置工作,例如:DB连接池、命令行参数解析等,具体限于篇幅不再展开,封装后的命令行工具功能:

Package Deploy:

Usage:
                        usadmin deploy <filename> <xml> [--config=<conf>]
                        usadmin revoke <version> [--config=<conf>]
                        usadmin -h | --help

Arguments:
                        <filename>             The zip package will be upgraded. e.g: (G-Net_Upgrade_PC20_2.2.428.zip)
						<xml>                  The upgrade configuration will be deployed. e.g: (config.xml)
                        <version>              The version will be revoked. e.g: (2.2.428)

Options:
                        -c, --config=<conf>         config path. [default:/uc/etc/usserver.conf]
                        -h, --help                  show details

使用示例:

usadmin deploy ../doc/G-Net_MeetNow_Update_Test_6.16.23091301.zip ../doc/Config.xml -c ../doc/us.conf

小结

本篇主要介绍了软件包的组成、吃包以及发布版本过程,下篇会基于已经发布的版本来介绍升级检测、差异包生成和使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

沉下心来学鲁班

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值