购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

1.2 二进制世界

易有太极,是生两仪,两仪生四象,四象生八卦。

——《周易·系辞上》

本节我们介绍计算机世界里最基础的二进制知识,并设计一个二进制处理类。

从二进制数的角度理解计算机世界的一切技术,可以让我们的知识更加清晰,理解更加深刻。

1.2.1 一切皆比特

比特

在计算机世界里,一切复杂的数据背后,都是二进制数的0和1。在中文里,我们称这样最小的单元为1位。在英文中,对应的术语是binary digit,简称 比特 (bit)。

1940年,图基(John Tukey,美国,1915—2000)首次发明了缩写bit。

1948年,香农(Claude Shannon,美国,1916—2001)在著作 A Mathematical Theory of Communication 中使用了术语bit。

从比特的视角来理解计算机技术,可以直接洞悉问题的本质,不被编程语言的语法所困扰。

字节

将8 bit分为一组,我们定义了 字节 (Byte)。

1956年6月,IBM的工程师维尔纳·布赫霍尔兹(Werner Buchholz,德裔美籍,1922—2019),在IBM 7030 Stretch的早期设计阶段,首次使用了Byte这个术语,用来表示数字信息的基本单元。

最早的字节并非8 bit。《计算机程序设计的艺术》一书中的MIX机器采用6 bit作为1 Byte。8 bit的Byte约定,和IBM System/360大型机的研发有关,由其领导者布鲁克斯(Frederick P.Brooks,美国,1931—2022)推行开来。

对于单字节中的位,从左到右,我们依次编号为bit7~bit0,如图1-3所示。

图1-3 Byte的构成

字节是计算机世界中计量大小的基本单位,例如:1KB=1024 Byte,1MB=1024 KB,1GB=1024 MB。这里1024=2 10

为了方便表示二进制,人们将其4位一组,使用0~F这16个字符进行十六进制编码,如表1-2所示。

表1-2 数的进制表示

所以,一字节的表示范围是0~255,即十六进制的0x00~0xFF。

字节序

字节用来表示整数时,存在存储顺序的问题。

例如,0x12345678,在内存中的存储可能是0x12、0x34、0x56、0x78,也可能是0x78、0x56、0x34、0x12。前者被称为 大端法 (Big-Endian),后者被称为 小端法 (Little-Endian)。小端法表示法如图1-4所示。

图1-4 小端法表示法

比如,在计算机系统的本地内存中,Intel 与ARM架构常使用小端法表示法,也叫作 本机序 (Host Order)。而在网络传输上,人们习惯于使用大端法,即 网络序 (Net Order)。

又如,BMP文件使用的是小端法,而MP4文件使用的是大端法。

位序

图1-3所定义的位存储在字节内的顺序是固定的。但是我们在读取位的时候,会存在两个方向:从bit0读到bit7,或从bit7读到bit0。

例如,对于0x39=0b 00111001,从不同的方向读取,就会得到00111001或者10011100两种结果。

后面我们会看到, 位序 对于理解图像格式和压缩算法都是非常重要的。例如,对于PNG、GIF图像格式,位序是从bit0开始的。但是对于JPEG图像格式,位序则是从bit7开始的。

比特流与比特率

对于计算机中连续存储的字节单元,我们可以将其看作一个 比特流

为了衡量信息的传递速度,我们定义 比特率 为每秒钟传送的比特,单位为bps(bit per second)。比特率也称为 码率

表1-3反映了国际单位制SI(International System of Units)下的各种比特率单位

表1-3 国际单位制SI下的各种比特率单位

表1-4是一些常见传输设备的比特率。

表1-4 一些常见传输设备的比特率

bps中的b是小写的。对于字节,用大写的B表示Byte,1 Bps=8 bps。

在音视频通话时,若带宽小于300 kbps,则我们称之为 低带宽

低带宽下RTC优化标准为:纯音频最低支持60 kbps,音视频最低支持100 kbps。

Base64表示法

十六进制通过将4 bit分为一组,得到了0x00~0x0F这16种不同的值。

在早期,由于历史原因,电子邮件只允许传输英文字符数据 。当传输非字符数据时,网关(Gateway)会将字节中8位的最高位置为0。

为了能将二进制值用英文字符表示出来,人们设计了Base64 编码

Base64编码将6 bit分为一组,并用2 6 =64个不同的字符来表示,如表1-5所示。

表1-5 Base64编码表

使用A~Z、a~z、0~9、+和/,正好64个字符,来编码6 bit的一组。

由于字节是8 bit一组的,当字节数不是3的倍数时,比特数不能被6整除。

Base64编码使用等号字符=在最后做附加(padding),最终可能是3种情况:没有=,1个=,或者2个=,表示此Base64串末尾有多少个附加字节,如图1-5所示。

图1-5 Base64编码

例如,将0x01转换为Base64编码。

0x01对应的比特为0b00000001,由于不是3的倍数,我们将其附加2字节,得到0b 00000001 00000000 00000000,再将其6 bit一组,得到0b 000000 010000 000000 000000。查表得知,000000=A,010000=Q。因此,0x01的Base64编码为AQ==。

同理,0x01 02的Base64编码为AQI=,0x01 02 03的Base64编码为AQID。

使用Base64编码,会使原数据变长约三分之一。但好处是只使用了65个字符,便于二进制数据的文本展示。

1.2.2 字节管理类

本节设计一个跨平台Buffer类,用来管理二进制数据。

简单定义

最简单的Buffer类,通常结构如下:

img

其中,pBuf指向二进制数据的开始地址,nSize为二进制数据的字节数,如图1-6所示。在开源Web服务器Nginx中,字符串就是这样定义的

然而上述定义存在很多问题,比如,如何控制内存分配与释放,如何让多个线程安全地共享,如何在函数与线程间高效地传递,减少复制次数。

Buffer作为传输通信中最常用的对象,我们需要为其实现更多的易用特性。

·极小栈消耗。

·内存自动回收。

·跨线程安全传递。

·写时复制(Copy-on-Write)。

·十六进制表示。

·Base64转换。

内存布局

本书提供的Buffer实现,采用如图1-7所示的内存布局。对比图1-6的简单定义,新的内存布局有如下特点。

(1)栈区只使用了一个指向堆区的指针,这使得占用栈空间的代价极小。

(2)栈指针指向堆区Buffer的起始地址,这使得可以随时访问Buffer的内容。

(3)在堆区Buffer前面,还有两个成员:nRefCount用来维护Buffer的 引用计数 ;nSize用来表示Buffer的大小。

图1-6 简单Buffer类的内存布局

图1-7 改进后Buffer类的内存布局

为了方便读取Buffer之前的数据,我们定义了如下结构:

img

引用计数

引用计数可以有效地记录对象自身被线程持有的情况。

(1)当Buffer初始化时,其引用计数为1。

(2)当需要读取访问或复制构造Buffer时,无须复制Buffer的内容。我们仅需新增一个指向堆区的栈指针,并且让Buffer的引用计数加1。这里的栈指针,可能来自同一个线程的栈,也可能来自不同线程的栈。

(3)当不再需要访问Buffer时,将其引用计数减1。

(4)当引用计数减到0时,对象不再被任何线程持有,可安全地销毁 堆区Buffer。

(5)当需要修改Buffer时,根据引用计数的情况,决定是否需要写时复制。

由于涉及多个线程之间的共享操作,引用计数必须用 原子变量 来实现。

在Common文件夹下,有一个DAtomic.h文件,提供了对C++11<atomic>的类型封装。

写时复制

当引用计数为1时,我们可以正常地随意读写Buffer内的所有数据。

但当引用计数大于1时,如要修改Buffer的内容,我们就需要对其数据进行复制。这样各个线程仍可以像自己独占Buffer一样,访问各自的Buffer数据,使读写互不影响。

img

使用memcpy函数将自身的Buffer内容复制到新的Buffer中,由于该函数在不同平台下原型不同,我们使用DXP类对其进行了封装。

新分配的Buffer,其引用计数为1,原有的Buffer引用计数会减1。

更多Buffer的函数,可参看示例工程的DBuffer类。

1.2.3 BufferViewer

Demo展示

本节实现了一个最简单的十六进制查看工具——BufferViewer,如图1-8所示。

BufferViewer实现了如下功能。

·打开任意文件,查看其十六进制数据。

·随机生成100字节的十六进制数据,并查看。

·将十六进制转换为Base64编码表示。

·将Base64编码表示转换回十六进制表示。

·判断Base64编码是否合法。

完整的代码可以在ch01/01_BufferViewer中查阅。

图1-8 BufferViewer界面

使用VS2022打开rtcbook.sln,在Solution Explorer中,右键单击01_BufferViewer工程,单击Set as Startup Project,再次右键单击Build,即可运行启动。

工程说明

作为本书的第一个示例工程,我们详细介绍一下各个代码文件的作用。

源码的组织形式如图1-9所示。

主界面使用了WTL(Windows Template Library),这是微软维护的一个轻量级UI库,只有纯头文件,无须库文件。

主窗口的实现位于MainDlg.h中,这是一个派生自WTL的对话框类。

我们可以通过消息函数处理表的宏定义,为其添加各种消息函数,从而实现从UI操作到功能的调用。

img

为了方便代码复用,本书将所有示例工程的公用代码都放置于Common文件夹下。这些公用代码具有很强的复用性,可以被多个示例同时使用。

依据公用代码的功能不同,Common下又分为多个文件夹。

·Base:缓存、文字编码、线程等。

·Video:视频相关算法。

·Audio:音频相关算法。

·File:文件格式相关。

·Data:数据处理相关。

·Net:网络相关算法。

图1-9 源码的组织结构

我们看一段从文件加载到Buffer并显示其十六进制格式的代码。

img

第3行,CFileDialog表示WTL提供打开文件对话框用来获取用户所选择的文件。

第7行,DFile类是一个文件封装处理类,用来加载文件到内存。

第8行,DXP类有两个方法:ws2s和s2ws,用作Unicode与MBCS之间字符集的转换,下一节会详细介绍。

第9~10行,我们最多读取100 KB的数据到内存中。

第11行,DBuffer类将数据从文件读取到内存中。

第12行,我们使用DBuffer的ToHexList方法,将其转换为十六进制表示。

第13行,m_hex是一个CEdit的成员对象,这也是WTL提供的一个封装类,用来实现一个多行的文本框。我们将转换好的文本内容贴上去展示。

经过这些基础类的封装,整个代码的易读性都得到了明显的提升。

以二进制/十六进制形式查看任何数据,是我们理解任何计算机技术的基础。

只有细到每一比特,才能真正理解计算机程序的运行规律。

总结

本节我们介绍了比特和字节的基本概念,了解了十六进制与Base64编码。为了能方便地管理字节,我们设计了一个基于引用计数的Buffer类。我们开发了一个十六进制查看器,用来演示Buffer类。后续,我们会经常用它来分析各种媒体文件格式。

参考阅读

1.斯坦福大学的Sean Eron Anderson搜集了大量有关bit的算法。

2. Programming Pearls (1999年), More Programming Pearls (1988年),Jon Louis Bentley著,介绍了一些关于位运算的黑科技。

3.Base64编码的细节,请参考RFC 4648。

4.WTL的atlmisc.h中有一个CString类的实现,包含了更多字符串操作的内容。

5.WinHEX是Windows平台下一款好用的十六进制编辑器。macOS平台下则有hexdump显示工具。

练习题

1.[30分钟](交换字节序)编写一个交换字节序的函数。

2.[2小时](Bitset)设计一个Bitset类,支持按bit来访问4字节长度的二进制数据。

3.[1人天](BitStream)设计一个BitStream类,支持按bit来获取一段长度的比特流。

4.[1人天](Base58编码)Base58是比特币采用的一种二进制编码方式,它使用了哪些字符编码?有什么优势?

5.[30分钟](判断2的幂)写一个函数,判断一个整数是否是2的幂。

6.[30分钟](求二进制数1的个数)写一个函数,求一个整数中有多少个二进制的1。

7.[30分钟](无分支的截取)写一个函数,给定任意无符号整数v,若v大于255,则返回255,否则返回v,不能使用条件判断或问号表达式。

8.[30分钟](无分支的绝对值)写一个函数,给定任意整数v,返回v的绝对值,不能使用条件判断或问号表达式。 5+KkySGL7UzyfDQXAi5fd4ZVBubc6jGRRJyVlXuPjUDlJJpZJpJ0QIQUKqyLmghz

点击中间区域
呼出菜单
上一章
目录
下一章
×

打开