noop

noop 查看完整档案

成都编辑  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

noop 关注了用户 · 10月24日

CrazyCodes @crazycodes

https://github.com/CrazyCodes... 我的博客
_
| |__ __ _
| '_ | | | |/ _` |
| |_) | |_| | (_| |
|_.__/ \__,_|\__, |

         |___/   感谢生命可以让我成为一名程序员

                         CrazyCodes To Author

关注 4673

noop 关注了专栏 · 10月12日

寒青

前端工程师

关注 574

noop 赞了文章 · 8月12日

数据库有哪些设计技巧

本文出自:blog.csdn.net/sirfei/article/details/434994,如有侵权,请告知我删除。

1. 原始单据与实体之间的关系

可以是一对一、一对多、多对多的关系。在一般情况下,它们是一对一的关系:即一张原始单据对应且只对应一个实体。在特殊情况下,它们可能是一对多或多对一的关系,即一张原始单证对应多个实体,或多张原始单证对应一个实体。

这里的实体可以理解为基本表。明确这种对应关系后,对我们设计录入界面大有好处。

〖例1〗:一份员工履历资料,在人力资源信息系统中,就对应三个基本表:员工基本情况表、社会关系表、工作简历表。这就是“一张原始单证对应多个实体”的典型例子。

2. 主键与外键

一般而言,一个实体不能既无主键又无外键。在E—R 图中, 处于叶子部位的实体, 可以定义主键,也可以不定义主键(因为它无子孙), 但必须要有外键(因为它有父亲)。

主键与外键的设计,在全局数据库的设计中,占有重要地位。当全局数据库的设计完成以后,有个美国数据库设计专家说:“键,到处都是键,除了键之外,什么也没有”,这就是他的数据库设计经验之谈,也反映了他对信息系统核心(数据模型)的高度抽象思想。

因为:主键是实体的高度抽象,主键与外键的配对,表示实体之间的连接。

3. 基本表的性质

基本表与中间表、临时表不同,因为它具有如下四个特性:

  • 原子性。基本表中的字段是不可再分解的。
  • 原始性。基本表中的记录是原始数据(基础数据)的记录。
  • 演绎性。由基本表与代码表中的数据,可以派生出所有的输出数据。
  • 稳定性。基本表的结构是相对稳定的,表中的记录是要长期保存的。

理解基本表的性质后,在设计数据库时,就能将基本表与中间表、临时表区分开来。

4. 范式标准

基本表及其字段之间的关系, 应尽量满足第三范式。但是,满足第三范式的数据库设计,往往不是最好的设计。为了提高数据库的运行效率,常常需要降低范式标准:适当增加冗余,达到以空间换时间的目的。

〖例2〗:有一张存放商品的基本表,如表1所示。“金额”这个字段的存在,表明该表的设计不满足第三范式,因为“金额”可以由“单价”乘以“数量”得到,说明“金额”是冗余字段。但是,增加“金额”这个冗余字段,可以提高查询统计的速度,这就是以空间换时间的作法。

在Rose 2002中,规定列有两种类型:数据列和计算列。“金额”这样的列被称为“计算列”,而“单价”和“数量”这样的列被称为“数据列”。

表1 商品表的表结构

5. 通俗地理解三个范式

通俗地理解三个范式,对于数据库设计大有好处。在数据库设计中,为了更好地应用三个范式,就必须通俗地理解三个范式(通俗地理解是够用的理解,并不是最科学最准确的理解):

  • 第一范式:1NF是对属性的原子性约束,要求属性具有原子性,不可再分解;
  • 第二范式:2NF是对记录的惟一性约束,要求记录有惟一标识,即实体的惟一性;
  • 第三范式:3NF是对字段冗余性的约束,即任何字段不能由其他字段派生出来,它要求字段没有冗余。

没有冗余的数据库设计可以做到。但是,没有冗余的数据库未必是最好的数据库,有时为了提高运行效率,就必须降低范式标准,适当保留冗余数据。

具体做法是:在概念数据模型设计时遵守第三范式,降低范式标准的工作放到物理数据模型设计时考虑。降低范式就是增加字段,允许冗余。

逆锋起笔

6. 要善于识别与正确处理多对多的关系

若两个实体之间存在多对多的关系,则应消除这种关系。消除的办法是,在两者之间增加第三个实体。这样,原来一个多对多的关系,现在变为两个一对多的关系。要将原来两个实体的属性合理地分配到三个实体中去。

这里的第三个实体,实质上是一个较复杂的关系,它对应一张基本表。一般来讲,数据库设计工具不能识别多对多的关系,但能处理多对多的关系。

〖例3〗:在“图书馆信息系统”中,“图书”是一个实体,“读者”也是一个实体。这两个实体之间的关系,是一个典型的多对多关系:一本图书在不同时间可以被多个读者借阅,一个读者又可以借多本图书。

为此,要在二者之间增加第三个实体,该实体取名为“借还书”,它的属性为:借还时间、借还标志(0表示借书,1表示还书),另外,它还应该有两个外键(“图书”的主键,“读者”的主键),使它能与“图书”和“读者”连接。

7. 主键PK的取值方法

PK是供程序员使用的表间连接工具,可以是一无物理意义的数字串, 由程序自动加1来实现。也可以是有物理意义的字段名或字段名的组合。不过前者比后者好。当PK是字段名的组合时,建议字段的个数不要太多,多了不但索引占用空间大,而且速度也慢

8. 正确认识数据冗余

主键与外键在多表中的重复出现, 不属于数据冗余,这个概念必须清楚,事实上有许多人还不清楚。非键字段的重复出现, 才是数据冗余!而且是一种低级冗余,即重复性的冗余。高级冗余不是字段的重复出现,而是字段的派生出现。

〖例4〗:商品中的“单价、数量、金额”三个字段,“金额”就是由“单价”乘以“数量”派生出来的,它就是冗余,而且是一种高级冗余。冗余的目的是为了提高处理速度。

只有低级冗余才会增加数据的不一致性,因为同一数据,可能从不同时间、地点、角色上多次录入。因此,我们提倡高级冗余(派生性冗余),反对低级冗余(重复性冗余)。

9. E--R图没有标准答案

信息系统的E--R图没有标准答案,因为它的设计与画法不是惟一的,只要它覆盖了系统需求的业务范围和功能内容,就是可行的。反之要修改E--R图。

尽管它没有惟一的标准答案,并不意味着可以随意设计。好的E—R图的标准是:结构清晰、关联简洁、实体个数适中、属性分配合理、没有低级冗余。

10. 视图技术在数据库设计中很有用

与基本表、代码表、中间表不同,视图是一种虚表,它依赖数据源的实表而存在。视图是供程序员使用数据库的一个窗口,是基表数据综合的一种形式, 是数据处理的一种方法,是用户数据保密的一种手段

为了进行复杂处理、提高运算速度和节省存储空间, 视图的定义深度一般不得超过三层。若三层视图仍不够用, 则应在视图上定义临时表, 在临时表上再定义视图。这样反复交迭定义, 视图的深度就不受限制了。

对于某些与国家政治、经济、技术、军事和安全利益有关的信息系统,视图的作用更加重要。这些系统的基本表完成物理设计之后,立即在基本表上建立第一层视图,这层视图的个数和结构,与基本表的个数和结构是完全相同。并且规定,所有的程序员,一律只准在视图上操作。

只有数据库管理员,带着多个人员共同掌握的“安全钥匙”,才能直接在基本表上操作。请读者想想:这是为什么?

11. 中间表、报表和临时表

中间表是存放统计数据的表,它是为数据仓库、输出报表或查询结果而设计的,有时它没有主键与外键(数据仓库除外)。临时表是程序员个人设计的,存放临时记录,为个人所用。基表和中间表由DBA维护,临时表由程序员自己用程序自动维护

12. 完整性约束表现在三个方面

域的完整性:用Check来实现约束,在数据库设计工具中,对字段的取值范围进行定义时,有一个Check按钮,通过它定义字段的值城。

参照完整性:用PK、FK、表级触发器来实现。用户定义完整性:它是一些业务规则,用存储过程和触发器来实现。

13. 防止数据库设计打补丁的方法是“三少原则”

1、一个数据库中表的个数越少越好。只有表的个数少了,才能说明系统的E--R图少而精,去掉了重复的多余的实体,形成了对客观世界的高度抽象,进行了系统的数据集成,防止了打补丁式的设计;

2、一个表中组合主键的字段个数越少越好。因为主键的作用,一是建主键索引,二是做为子表的外键,所以组合主键的字段个数少了,不仅节省了运行时间,而且节省了索引存储空间;

3、一个表中的字段个数越少越好。只有字段的个数少了,才能说明在系统中不存在数据重复,且很少有数据冗余,更重要的是督促读者学会“列变行”,这样就防止了将子表中的字段拉入到主表中去,在主表中留下许多空余的字段。所谓“列变行”,就是将主表中的一部分内容拉出去,另外单独建一个子表。这个方法很简单,有的人就是不习惯、不采纳、不执行。

数据库设计的实用原则是:在数据冗余和处理速度之间找到合适的平衡点。“三少”是一个整体概念,综合观点,不能孤立某一个原则。

该原则是相对的,不是绝对的。“三多”原则肯定是错误的。试想:若覆盖系统同样的功能,一百个实体(共一千个属性) 的E--R图,肯定比二百个实体(共二千个属性)的E--R图,要好得多。

提倡“三少”原则,是叫读者学会利用数据库设计技术进行系统的数据集成。数据集成的步骤是将文件系统集成为应用数据库,将应用数据库集成为主题数据库,将主题数据库集成为全局综合数据库。

集成的程度越高,数据共享性就越强,信息孤岛现象就越少,整个企业信息系统的全局E—R图中实体的个数、主键的个数、属性的个数就会越少

提倡“三少”原则的目的,是防止读者利用打补丁技术,不断地对数据库进行增删改,使企业数据库变成了随意设计数据库表的“垃圾堆”,或数据库表的“大杂院”,最后造成数据库中的基本表、代码表、中间表、临时表杂乱无章,不计其数,导致企事业单位的信息系统无法维护而瘫痪。

“三多”原则任何人都可以做到,该原则是“打补丁方法”设计数据库的歪理学说。“三少”原则是少而精的原则,它要求有较高的数据库设计技巧与艺术,不是任何人都能做到的,因为该原则是杜绝用“打补丁方法”设计数据库的理论依据。

14. 提高数据库运行效率的办法

在给定的系统硬件和系统软件条件下,提高数据库系统的运行效率的办法是:

  • 在数据库物理设计时,降低范式,增加冗余, 少用触发器, 多用存储过程。
  • 当计算非常复杂、而且记录条数非常巨大时(例如一千万条),复杂计算要先在数据库外面,以文件系统方式用C++语言计算处理完成之后,最后才入库追加到表中去。这是电信计费系统设计的经验。
  • 发现某个表的记录太多,例如超过一千万条,则要对该表进行水平分割。水平分割的做法是,以该表主键PK的某个值为界线,将该表的记录水平分割为两个表。若发现某个表的字段太多,例如超过八十个,则垂直分割该表,将原来的一个表分解为两个表。
  • 对数据库管理系统DBMS进行系统优化,即优化各种系统参数,如缓冲区个数。
  • 在使用面向数据的SQL语言进行程序设计时,尽量采取优化算法。

总之,要提高数据库的运行效率,必须从数据库系统级优化、数据库设计级优化、程序实现级优化,这三个层次上同时下功夫。

上述十四个技巧,是许多人在大量的数据库分析与设计实践中,逐步总结出来的。对于这些经验的运用,读者不能生帮硬套,死记硬背,而要消化理解,实事求是,灵活掌握。并逐步做到:在应用中发展,在发展中应用

逆锋起笔

查看原文

赞 3 收藏 3 评论 0

noop 关注了专栏 · 7月8日

前端小而全的知识归纳

vue,react,小程序,php,node乱炖

关注 1244

noop 赞了文章 · 2019-12-03

一文说清「VirtualDOM」的含义与实现

专注前端与算法的系列干货分享,欢迎关注(¬‿¬):
「微信公众号:心谭博客」| xin-tan.com | GitHub

摘要

随着 React 的兴起,Virtual DOM 的原理和实现也开始出现在各大厂面试和社区的文章中。其实这种做法早在 d3.js 中就有实现,是 react 生态的快速建立让它正式进入了广大开发者的视角。

在正式开始前,抛出几个问题来引导思路,这些问题也会在不同的小节中,逐步解决:

  • 🤔️ 怎么理解 VDom?
  • 🤔️ 如何表示 VDom?
  • 🤔️ 如何比较 VDom 树,并且进行高效更新?

⚠️ 整理后的代码和效果图均存放在github.com/dongyuanxin

如何理解 VDom?

曾经,前端常做的事情就是根据数据状态的更新,来更新界面视图。大家逐渐意识到,对于复杂视图的界面,频繁地更新 DOM,会造成回流或者重绘,引发性能下降,页面卡顿。

因此,我们需要方法避免频繁地更新 DOM 树。思路也很简单,即:对比 DOM 的差距,只更新需要部分节点,而不是更新一棵树。而实现这个算法的基础,就需要遍历 DOM 树的节点,来进行比较更新。

为了处理更快,不使用 DOM 对象,而是用 JS 对象来表示,它就像是 JS 和 DOM 之间的一层缓存

如何表示 VDom?

借助 ES6 的 class,表示 VDom 语义化更强。一个基础的 VDom 需要有标签名、标签属性以及子节点,如下所示:

class Element {
  constructor(tagName, props, children) {
    this.tagName = tagName;
    this.props = props;
    this.children = children;
  }
}

为了更方便调用(不用每次都写new),将其封装返回实例的函数:

function el(tagName, props, children) {
  return new Element(tagName, props, children);
}

此时,如果想表达下面的 DOM 结构:

<div class="test">
  <span>span1</span>
</div>

用 VDom 就是:

// 子节点数组的元素可以是文本,也可以是VDom实例
const span = el("span", {}, ["span1"]);
const div = el("div", { class: "test" }, [span]);

之后在对比和更新两棵 VDom 树的时候,会涉及到将 VDom 渲染成真正的 Dom 节点。因此,为class Element增加render方法:

class Element {
  constructor(tagName, props, children) {
    this.tagName = tagName;
    this.props = props;
    this.children = children;
  }

  render() {
    const dom = document.createElement(this.tagName);
    // 设置标签属性值
    Reflect.ownKeys(this.props).forEach(name =>
      dom.setAttribute(name, this.props[name])
    );

    // 递归更新子节点
    this.children.forEach(child => {
      const childDom =
        child instanceof Element
          ? child.render()
          : document.createTextNode(child);
      dom.appendChild(childDom);
    });

    return dom;
  }
}

如何比较 VDom 树,并且进行高效更新?

前面已经说明了 VDom 的用法与含义,多个 VDom 就会组成一棵虚拟的 DOM 树。剩下需要做的就是:根据不同的情况,来进行树上节点的增删改的操作。这个过程是分为diffpatch

  • diff:递归对比两棵 VDom 树的、对应位置的节点差异
  • patch:根据不同的差异,进行节点的更新

目前有两种思路,一种是先 diff 一遍,记录所有的差异,再统一进行 patch;另外一种是 diff 的同时,进行 patch。相较而言,第二种方法少了一次递归查询,以及不需要构造过多的对象,下面采取的是第二种思路。

变量的含义

将 diff 和 patch 的过程,放入updateEl方法中,这个方法的定义如下:

/**
 *
 * @param {HTMLElement} $parent
 * @param {Element} newNode
 * @param {Element} oldNode
 * @param {Number} index
 */
function updateEl($parent, newNode, oldNode, index = 0) {
  // ...
}

所有以$开头的变量,代表着真实的 DOM

参数index表示oldNode$parent的所有子节点构成的数组的下标位置。

情况 1:新增节点

如果 oldNode 为 undefined,说明 newNode 是一个新增的 DOM 节点。直接将其追加到 DOM 节点中即可:

function updateEl($parent, newNode, oldNode, index = 0) {
  if (!oldNode) {
    $parent.appendChild(newNode.render());
  }
}

情况 2:删除节点

如果 newNode 为 undefined,说明新的 VDom 树中,当前位置没有节点,因此需要将其从实际的 DOM 中删除。删除就调用$parent.removeChild(),通过index参数,可以拿到被删除元素的引用:

function updateEl($parent, newNode, oldNode, index = 0) {
  if (!oldNode) {
    $parent.appendChild(newNode.render());
  } else if (!newNode) {
    $parent.removeChild($parent.childNodes[index]);
  }
}

情况 3:变化节点

对比 oldNode 和 newNode,有 3 种情况,均可视为改变:

  1. 节点类型发生变化:文本变成 vdom;vdom 变成文本
  2. 新旧节点都是文本,内容发生改变
  3. 节点的属性值发生变化

首先,借助Symbol更好地语义化声明这三种变化:

const CHANGE_TYPE_TEXT = Symbol("text");
const CHANGE_TYPE_PROP = Symbol("props");
const CHANGE_TYPE_REPLACE = Symbol("replace");

针对节点属性发生改变,没有现成的 api 供我们批量更新。因此封装replaceAttribute,将新 vdom 的属性直接映射到 dom 结构上:

function replaceAttribute($node, removedAttrs, newAttrs) {
  if (!$node) {
    return;
  }

  Reflect.ownKeys(removedAttrs).forEach(attr => $node.removeAttribute(attr));
  Reflect.ownKeys(newAttrs).forEach(attr =>
    $node.setAttribute(attr, newAttrs[attr])
  );
}

编写checkChangeType函数判断变化的类型;如果没有变化,则返回空:

function checkChangeType(newNode, oldNode) {
  if (
    typeof newNode !== typeof oldNode ||
    newNode.tagName !== oldNode.tagName
  ) {
    return CHANGE_TYPE_REPLACE;
  }

  if (typeof newNode === "string") {
    if (newNode !== oldNode) {
      return CHANGE_TYPE_TEXT;
    }
    return;
  }

  const propsChanged = Reflect.ownKeys(newNode.props).reduce(
    (prev, name) => prev || oldNode.props[name] !== newNode.props[name],
    false
  );

  if (propsChanged) {
    return CHANGE_TYPE_PROP;
  }
  return;
}

updateEl中,根据checkChangeType返回的变化类型,做对应的处理。如果类型为空,则不进行处理。具体逻辑如下:

function updateEl($parent, newNode, oldNode, index = 0) {
  let changeType = null;

  if (!oldNode) {
    $parent.appendChild(newNode.render());
  } else if (!newNode) {
    $parent.removeChild($parent.childNodes[index]);
  } else if ((changeType = checkChangeType(newNode, oldNode))) {
    if (changeType === CHANGE_TYPE_TEXT) {
      $parent.replaceChild(
        document.createTextNode(newNode),
        $parent.childNodes[index]
      );
    } else if (changeType === CHANGE_TYPE_REPLACE) {
      $parent.replaceChild(newNode.render(), $parent.childNodes[index]);
    } else if (changeType === CHANGE_TYPE_PROP) {
      replaceAttribute($parent.childNodes[index], oldNode.props, newNode.props);
    }
  }
}

情况 4:递归对子节点执行 Diff

如果情况 1、2、3 都没有命中,那么说明当前新旧节点自身没有变化。此时,需要遍历它们(Virtual Dom)的children数组(Dom 子节点),递归进行处理。

代码实现非常简单:

function updateEl($parent, newNode, oldNode, index = 0) {
  let changeType = null;

  if (!oldNode) {
    $parent.appendChild(newNode.render());
  } else if (!newNode) {
    $parent.removeChild($parent.childNodes[index]);
  } else if ((changeType = checkChangeType(newNode, oldNode))) {
    if (changeType === CHANGE_TYPE_TEXT) {
      $parent.replaceChild(
        document.createTextNode(newNode),
        $parent.childNodes[index]
      );
    } else if (changeType === CHANGE_TYPE_REPLACE) {
      $parent.replaceChild(newNode.render(), $parent.childNodes[index]);
    } else if (changeType === CHANGE_TYPE_PROP) {
      replaceAttribute($parent.childNodes[index], oldNode.props, newNode.props);
    }
  } else if (newNode.tagName) {
    const newLength = newNode.children.length;
    const oldLength = oldNode.children.length;
    for (let i = 0; i < newLength || i < oldLength; ++i) {
      updateEl(
        $parent.childNodes[index],
        newNode.children[i],
        oldNode.children[i],
        i
      );
    }
  }
}

效果观察

github.com/dongyuanxin/pure-virtual-dom的代码 clone 到本地,Chrome 打开index.html

新增 dom 节点.gif:

更新文本内容.gif:

更改节点属性.gif:

⚠️ 网速较慢的同学请移步 github 仓库

参考链接

专注前端与算法的系列干货分享,欢迎关注(¬‿¬)
查看原文

赞 38 收藏 28 评论 1

noop 收藏了文章 · 2019-07-02

设计一个二进制文件格式

NOTES
本文来源:Designing File Formats
翻译由 本人(赤石俊哉) 整理,若您是原作者并认为此文涉及版权侵犯,我会配合删除。


现在有很多很多种文件,它们又有着很多很多的文件格式。从简单的 ASCII 文本文档到复杂的数据库,下面是几个文件结构中必要的几个元素,设计者们往往会忽视掉其中一部分。

一个好的文件格式应该至少拥有下面的几个元素:

  • 身份标识字符(也被称为 Magic 字符或者 ID 字符。
  • 头部验证码
  • 版本信息
  • 数据位移

这也同样适用于一些从来不会实际存储在一个文件中的数据,比如通过网络传送到移动设备的数据。

身份识别字符

这个叫做 Magic 字符的历史已经很久远了,它通常是一个 2 ~ 4 字符,可能更多或者更少,用来唯一地标识一个二进制文件格式。应该尽量避免和自然语言相近的值,如果文件可能与文本文档混合使用,那使用一个纯 ASCII 字符就是一个不好的选择。随着存储容量的变大,短 Magic 正在慢慢地被长一些的字符串所代替。

身份识别字符在一些非强类型系统中(比如 UNIX )使用,是很有用的。在 Macintosh HFS 文件系统中,是很难将文件和它的创建者类型分开的, 但是在 Windows 下,你只需要重命名它就行了。

在所有的系统中,他们都是有用的:可以确保所读取的文件是你所期望的文件内容。假如一个文件的类型在文件名中缺失,在网络传输中,可能你就要话大量的时间去猜测这个文件的内容是什么。可能你又要说了,我这作为系统“内部使用”就没有必要了吧。但是在开发中,如果作为一个资源被使用的话,当你读取错误的类型的文件时,这将会很快地让你意识到而不是发生了一系列问题之后才意识到。

最帅气(没有之一)的识别字符串当属 PNG 图片文件格式中定义的,它看起来是这样的:

   (decimal)              137  80  78  71  13  10  26  10
   (hexadecimal)           89  50  4e  47  0d  0a  1a  0a
   (ASCII C notation)    \211   P   N   G  \r  \n \032 \n

第一个字符是一个非ASCII字符以防止来自文本文件的干扰。接下来的三个字符则是让人类很显眼的就认出这是一个 PNG 文件。\r\n序列则是一个可以进行一个快速测试,系统会将CRLF转换成CR还是LF。而最后的\n则测试系统会将LF转换成CR还是CRLF。倒数第二个字符是一个CTRL + Z,在有些系统里面,这个作为一个文本文件的结束标记。他不光会检测不正确的文本处理,假如你在 MS-DOS 中打印这个文件,他也能把你从一堆垃圾乱码中解救出来。

ASCII文件格式可以从身份识别字符中获益,因为读取它们的程序可以立刻知道它们是否在读取正确的文件类型。当通过网络传入一个数据流的时候,可以从身份识别字符中识别出传入数据的性质。

头部验证码

你可以用任何字符校验来做,比如 32 位的 CRC,又或者是 128 位的 MD5 哈希。头部验证码紧跟在 Magic 字符之后,用来计算它之后,数据内容(用 数据偏移 标识的位置)之前的内容的校验值。它具有较高的可信度,让你确保你现在所读取的内容与当时写入时的内容是一致的。

很多开发者将内部校验码视为是不必要的,而且他们有信息地认为 TCP/IP 网络是相当可靠的,而且如果你都不能相信你硬盘里面的数据了,你将会遇到很多问题。但是,仍然是有必要进行文件头的校验的。

比如说,存储其实并没有你想的那么稳定。在早些天我收到了很多问题,在 CD 中记录的音乐文件,文本文件和 JPEG 图像都没问题,但是存入的 ZIP 文件却出问题了。而他们没有意识到,只有 ZIP 文件档是存在 CRC 校验的。所有的数据都被损坏了,可能是受损的 SCSI 或者 IDE 数据线所引起的。但是问题那么少,只是因为在很多类型的文件中没有体现出来。你可能不会意识到你的“文本”变成了“又本”。也许你不会意识到你的图片上有些许奇怪的斑点。但是一个 32 位的 CRC 却极少可能会被错误给欺骗过去。

还有一个常见的可能损坏文件的途径是用一个 ASCII 模式的 FTP 传输,他会做行末字符转换(比如,将 LF 转换成 CRLF)。将字节混合或者修改之后可能会引起一些有趣的问题。如果有头部的校验码,你可以立刻知道头部中是否有损坏。如果你可以信任创建这个文件的程序代码,那你可以认为这个头部是可用的,或者说你可以减少你代码执行的检查量。

有一种思想认为,校验码应该放在头部的最后位置,他总是可以在 (OffsetToData - 4) 的位置上找到,而且可以让 CRC 覆盖整个头部,包含 Magic 字符。虽然测试一遍它是冗余的。但是更重要的是这样可以让他作为网络传输的头部。你可以计算 CRC 在你输出了头部字节之后,而不用将插入位置回移到前面去填写它。通常来说,文件头很小,不需要折中类型的处理,但是一定要记住。

版本信息

这应该很明显是一个有必要的字段。应用和文件格式随时间不断迭代,而且也很需要确定一个文件的内容是否可以被读取。有两种基本的方法,序列主/次

序列方法用一个简单的值,通常用一个字节存储。数字从0或者1开始,每次递增。程序可以认清和处理它的当前版本或者更早的版本,但是拒绝任何更新的版本。

主/次方法有两个值,主版本和序列方式一样,任何旧版本都可以被处理,但是更新版本不可以。而次要版本对于每一个主要版本来说,都是从0开始。当有新字段被添加的时候,增长。旧版本中不用的字段始终保留,就算过时了不用了也要填充。这个方法比较适合保证向后兼容:较旧的应用程序可以读取较新的文件,因为就算被弃用了的字段在次版本中也是肯定存在的。如果一个文件的次版本更低,程序是知道如何转义它的。如果一个文件的次版本更高,则程序知道所有的字段都被明确地标识出来了,而新加的字段可以直接跳过不读。如果一个文件被重新设计整改了结构,更新主版本以防止旧版本的应用程序会读取新版本的文件。

文件版本号不应该跟程序版本号进行绑定,也不用画蛇添足地加额外信息,比如1.3.5d1。一个或者两个稳定增长的值就足够了。

如果你不想显式地显示出版本号,比如在 PNG 文件中,就用了一种叫做块(chunk)的东西。如果数据格式需要被修改,则块类型的名称会被修改。整体的文件结构不会改变,版本数字被有效地内嵌在块名中(或者在块本身内)。这种方法只在你确信整体文件结构不会被改变时才有用。

有些文件格式会包括一个最小程序版本的数字。这个听起来有点像把马车放在马前面:应用程序最有能力决定是否处理给定的文件格式。文件格式版本应该存储在程序中,而不是其他地方。这个调整是为了保证版本的向后兼容,因为它允许文件格式设计者告诉程序它们是否可以读取这个文件。最好还是交给上面描述的主/次方法来处理。

数据位移

这个字段的优势并不会立刻体现出来,直到哪天你在考虑向后兼容的时候。一个旧的程序可以读一个新的数据文件,因为他知道如何去寻找他需要的字段,跳过不需要的字段。这个位移值告诉程序如何跳过不需要关心的头部字段。

这个偏移值应当是基于文件最开始进行测量的。这对于真实文件(SEEK_SET)和内存缓冲区(将 char* 与位移相加)进行计算都是更简便的。

你可能会试图用这个数字作为版本号。比如,Windows 中使用 sizeof() 来确定多种类型的结构,比如位图。请不要这样做。这会让你进入一种只能不断地把你的文件变大的情况。这对于 Windows API 结构来说很合理,因为他要保证多个版本的二进制兼容性。除非你需要始终向后兼容,否则这在设计上是个错误的决定。

这个属性对于一个 ASCII 文件格式是一个不需要的,在一个边长的头部后面有点显式地在说“数据从这里开始”。

其他字段

有些格式有一些复杂的结构。它们可能有很多个数据区域,每个数据区域有一个偏移值,或者是有一个链表。这些字段是从文件头部还是放入那些区块的头部就取决于设计者你了。

有一个字段你非常值得你去考虑,就是长度字段。对于一个磁盘中的文件,数据的长度被隐式地被文件长度所表示。但是,如果内嵌长度将会让你检测到文件是否不经意之间被修改了(比如从网络上下载的文件),对于通过网络流传输的数据,这点也是很重要的。

其它考虑

结构输出

使用 C 的结构直接进行读写是非常有吸引力的。通过名字访问比较便利,而且可以使代码最小化。

Stop,从历史上来说,这是一个非常糟糕的主意。因为结构的填充和组织随着平台、编译器、甚至不同版本的相同编译器不同会有不同,统一 C 的编译器和明确使用progmas可能可以大部分地解决这个问题,但是要保持最好的兼容性,你还是单独写入它们会保险一点。

使用标准的 libc 缓冲的 I/O 方法(fopenfreadfwritegetcputc)或者使用缓存的 C++ iostream。可能会感觉每个字节调用getc或者putc会比较慢。但是请记住,这些宏在处理一个缓冲区的数据时是很小的。读取数据到一个缓冲区然后你自己再转义不会让你收获多少便利,反而会严重影响代码的清晰度。

有一个常见的错误,一定要避免,当你写 C/C++ 时,写成:

unsigned short val = getc(infp) | getc(infp) << 8;

这里遇到的麻烦是,不是所有的编译器都用相同的顺序处理参数,所以你不知道第一个getc()会被先运行还是放到第二位。(ANSI C 中对于这个有定义,但是你不能确保它的实现是遵照 ANSI C 的),把他们分开十分简单,而且编译之后也肯定都是正确的顺序。

在进行一系列的getc()putc()之后,不要忘了检查feof()ferror()(以及其他类似的函数)。

低字节序和高字节序

也就是 Little-Endian 和 Big-Endian,这里最好的建议是使用和数据使用者大体相同的格式。如果你写的文件将主要在80x86机器上使用,使用低字节序。当读取数据的时候,你需要选择假设你运行在低字节序机器或者是写可移植的代码。在前者的情况下,你可以一次抓取 2 ~ 4 字节数据,并将其填充入一个整型。在后者的情况,你需要一次读取一个字节,然后进行适当的排序。如果你读取任何东西都通过小函数(Read16LE来读取 16 位的低字节序值),你可以封装你的(非)可以执行问题。

文件数据的校验值

将一个 CRC 放入文件数据比放在文件头部更值得去做。CRC 最好是放在一个数据块的结尾。这让你可以在一个流中写入数据,而不需要回滚位置填入 CRC,对于网络传输数据来说尤其重要。

参数跟文件头部的校验值差不多,但是文件头部要比数据区小得多。

文件结尾标志

文件更改可能会发生在通过网络传输数据或者磁盘产生坏道。你的程序可以通过以下途径检测出这些修改:

  • 拥有一个完整文件的 CRC。最可靠,但是性能最差。
  • 在头部拥有一个完整的文件长度。读取的时候终止于满足长度,而不是到EOF。如果你不立刻读取整个文件,则跳转位置到(length - 1),然后尝试读取一个字节。
  • 添加一个清楚的结尾标示符。跳转到文件尾,然后读取它。这个文件长度可以从文件头部得到或者从文件系统得到(使用fseekSEEK_END)。

如果你在将一个大文件直接在内存中映射到多个线程地址空间,而且不想花大量的时间去进行整个 CRC 的校验,用一个文件位标识是一个非常值得考虑的方法。

文档说明

如果你穿件了一个二进制文件格式,文档记录每一个字节的意义。比如:

All values little-endian
 +00 2B Magic number (0xd5aa)
 +02 1B Version number (currently 1)
 +03 1B (pad byte)
 +04 4B CRC-32
 +08 4B Length of data
 +0c 2B Offset to start of data
 +xx [data]

ASCII 文件格式可以自行记录,如果你在文档中定义了注释。创建一个默认的使用大量注释的配置文件,然后在你的代码树中保存下来。


查看原文

noop 关注了专栏 · 2018-02-07

前端开发之道

前端学习笔记,知识分享

关注 121

noop 关注了用户 · 2017-12-27

zhisheng @zhisheng

个人博客网站: http://www.54tianzhisheng.cn?sf
微信公众号:zhisheng

关注 140

noop 回答了问题 · 2017-02-24

vuex actions 的实际用法?

我的理解是用了vuex就意味着state都要统一管理,异步获取的数据结果会导致store里state的变化,通常就放action里管理

关注 6 回答 4

noop 回答了问题 · 2017-02-24

解决Vue前端单页项目的用户认证思路

认证信息以后台为准,不管是登录还是退出都要发送请求,然后根据api返回的结果前端进行操作,如果不记住认证信息用sesionStorage好点

关注 23 回答 5

认证与成就

  • 获得 0 次点赞
  • 获得 3 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-08-28
个人主页被 121 人浏览