写点什么

【实践案例】软件差异化升级——吃包篇

作者:golf
  • 2023-09-29
    陕西
  • 本文字数:6539 字

    阅读完需:约 21 分钟

【实践案例】软件差异化升级——吃包篇

引言

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



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



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


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


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

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
    复制代码

    小结

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


    发布于: 刚刚阅读数: 2
    用户头像

    golf

    关注

    还未添加个人签名 2018-09-21 加入

    还未添加个人简介

    评论

    发布
    暂无评论
    【实践案例】软件差异化升级——吃包篇_golang_golf_InfoQ写作社区