文章

Git为了保护我们的屎山,悄悄做了什么努力?

在大家愉快地敲完代码,帅气地将代码commit到git仓库时,它会出现类似于这样的提示

QQ截图20230805213705

在中括号[]中,很显然第一个词指的是你在哪个分支(在这个例子中是main)提交的,但是第二个词是什么鬼?看上去完全没有意义的样子。

IMG_20230222_100915_8494

这就是本文的主角了,也就是——校验和。

什么是校验和?

检验和(checksum),在数据处理和数据通信领域中,用于校验目的地一组数据项的和。在计算机科学和通信领域中,校验和常用于确保数据在传输或存储过程中没有被损坏或篡改。

如果将数据的传输比作快递的过程,那显然是有“货物损坏或丢失”的风险。而与真正的快递过程不同的是,数据的接收者“验货”的成本可比快递的接收者要高,毕竟你没法用肉眼验证依托二进制数字的真确性与否。

IMG_20230805_222235_9619

为了降低“验货”的成本,机智的前辈们搞出各种了校验的方法。

从思想上来说,是数据的发出者和接收者约定同一种算法,数据传输之前将数据输入这种算法,输出某些信息(校验和),并将此信息一起传输给接收者。接收者收到数据之后,也使用相同的算法,并对比自己算出来的信息和发出者给的信息是否一致,如果一致则证明数据完整。

再用一下快递的例子,卖小零食卖家害怕快递小哥偷偷吃小零食,于是和买家做出约定:我在发出之前给小零食称了个重量(算法),我发出的小零食的重量是3752g(校验和),你收到货之后称一下,如果重量对那说明小零食很完整。

校验算法

既然如此,我们只需要找到一种可靠的算法就能轻松“验货”了。

IMG_20230625_104428_9450

可靠的程序员

在这里笔者只介绍一下三种常见的算法

1.简单累加和校验(Simple Additive Checksum,简称SAC):

顾名思义这种算法简单的一b,将数据中每个字节的数值相加,然后取结果的低位作为校验和。通常,校验和的长度为16位或32位。

举个例子,在网上我想要和网友友好沟通,不知道平台会不会对沟通内容进行和谐修改,于是我与对方约定我们用“简单累加和校验”。

我想要发送的数据是:SB。查询ascii码表,我们得知”SB“的二进制表示是01010011 01000010,二者相加为10010101。于是我发送”SB 10010101“。

对面一看,也这样计算了一通,算出来结果正是10010101,说明平台没有进行和谐,空气里充满了快活的气息。

IMG_20230805_234349_9620

顺带提一嘴,如果发送的数据很长,那计算出来的数字相应的也会很长,此时我们通常会取结果的低16或32位(低n位的意思为从右往左数n位)

这种方式很简单,但或许你也感觉到了,这样子很有可能出现错误,毕竟TA(01010100 01000001)相加也是10010101,但是显然数据出现了偏差。

2.循环冗余校验(Cyclic Redundancy Check,简称CRC)

这种方法会稍微复杂一点,基本思想是将传输的数据当做一个位数很长的数。将这个数除以另一个数。得到的余数作为校验数据附加到原数据后面。

在介绍这种方法之前,我们先了解一下它的类似算法:

举个例子,我想要传输6、23、4。如果用八位二进制(因为八位组成一个字节,所以通常用八位)表示,那便是:00000110 00010111 00000010。把它们组合成一个二进制数:000001100001011100000010。之后,假设我们约定被除数是9,二进制表示是00001001。

二者相除,如图

1345351771_3252

最后算出来的余数是0001,换成十进制就是1。这个1就是算出来的校验和。于是我传输的数据就是:6、23、4、1。

CRC算法和这种算法很像,只不过略有不同。在乘法运算时,和一般的二进制乘法一致;但是在加减运算时,采用模2(异或)运算。

何为模2(异或)运算?

模2运算指的是两个数字相加后进行再计算一下和2的模。比如:4+1 = 1;4+2 = 0。

异或运算的符号是XOR,其真值表如下:

1 XOR 1 = 0; 0 XOR 0 = 0 ; 1 XOR 0 = 1 ; 0 XOR 1 = 1。

在二进制中,不难发现模2运算和异或运算是一个意思。

现在,要传输的数据为:1101011011,除数设为:10011(也叫做生成多项式,因为二进制乘法很类似多项式乘法,在此不赘述)。但是在计算之前,传输的数据后面要加4个0,原因后面讲。计算过程变成了:

1345351821_9497

不难发现,将模2(异或)运算代替加减运算有一个好处:再也不用借位了,非常人性。同时还有一个原因,那就是对于计算机来讲,计算异或的效率比加减计算高了太多(实际上异或计算效率比大多数计算都高)

当然,聪明的你也发现了,我其实不在乎除法的结果,只在乎除法的余数(在这个例子里是1110,十进制也就是14),所以你也可以这样计算:

  • 将数据11010110110000和生成多项式10011进行异或操作(按位相加,不带进位)。
  • 右移一位(相当于将最高位丢弃),继续与生成多项式10011进行异或操作。
  • 重复上述步骤,直到数据11010110110000被除尽,没有更多的位可用。

如上的计算中,数据末尾加4个0的原因如下:如果你的除数是n位,那就要在数据的末尾加上n-1个0,因为数据的首位必然是1,后面加上n-1个0的话,那这个数的位数必然大于等于你的除数,如此才能进行除法。

必须要提一嘴,这个除数(生成多项式)的选择很有讲究,如果选的不好,出错的概率就会高很多。专家们已经研究了很多,最常用的如下:

CRC8=X8+X5+X4+X0 也就是100110001,以下的就不翻译了。这个CRX8的这个8,指的不是生成的数字有8位(实际上是8+1位),而是数据后要加8个0。

CRC-CCITT=X16+X12+X5+X0

CRC16=X16+X15+X2+X0

CRC12=X12+X11+X3+X2+X0

CRC32=X32+X26+X23+X22+X16+X12+X11+X10+X8+X7+X5+X4+X2+X1+X0

3.哈希校验和:(也就是git采用的方法)

这种方式对于使用者来说很简单,效果也非常好。它基于哈希函数,哈希函数是一种将任意长度数据映射为固定长度哈希值的算法。肯定很多人对于哈希函数不陌生了,可能自己也写过极简版的哈希函数。

使用方式如下:

  • 选择一个合适的哈希函数(例如MD5、SHA-1、SHA-256等)。(git使用SHA-1)
  • 将数据作为输入传递给哈希函数,计算出哈希值。
  • 哈希值即为校验和。

哈希函数很牛逼,很难想象多少程序员秃了头。它有如下特点:

  • 输入数据的任意小改动都会导致完全不同的哈希值。
  • 哈希值的长度是固定的,不受输入数据长度的影响。
  • 常用的哈希函数具有抗碰撞(collision-resistant)性质,即很难找到两个不同的输入数据产生相同的哈希值。

我们简单地举个使用的例子,下面这个短语的MD5校验和是一长串字符,这串字符代表了这个短语:

20200731001508748

将第一行输入哈希函数,输出的就是第二行的字符。

接下来,我们做出一点点小改变,将第一行的句号删掉。

202007310015278

没错,仅仅是文件中一点细微的改变,就会产生完全不一样的校验和,通过比对校验和,你可以清晰地认识到这是两个不同的文件。

哈希函数是怎么工作的在这里就不赘述了(因为笔者懂不了一点)。

结尾

回到开头,那串数字就是基于 Git 中文件的内容或目录结构,为了验证数据的完整性而通过计算出来的。

Git费尽心思保护我们屎山的完整性,真是幸苦它了。

IMG_20230624_225840_9446

本文是由笔者查资料和个人理解所得,由于笔者水平及其有限,故错误难以避免,欢迎批评和指正。

———————————————— 参考资料:

https://blog.csdn.net/m0_37697335/article/details/124109175

https://blog.csdn.net/CHENYAoo/article/details/107703762

本文由作者按照 CC BY 4.0 进行授权