第三 字节
字节(Byte)是计算机信息技术用于计量存储容量的基本计量单位,也是计算机编程语言中常用的数据类型。字节是连接硬件存储与软件数据的桥梁——无论是内存中的变量、磁盘上的文件,还是网络中传输的数据包,其底层都是以字节为单位进行组织和处理的。
本章将从字节的计量单位出发,深入探讨字节序、Base64 编码、Hex 编码等核心概念,并结合 Rust 代码展示如何在实际开发中处理字节数据。
3.1 计量单位
3.1.1 位与字节
计算机中所有数据最终都以二进制形式存储和传输。二进制中最小的存储单位是位(bit,Binary Digit),它只能表示 0 或 1 两种状态。8 个二进制位组成一个字节(Byte),字节是计算机处理数据的基本单位。
$$1 \text{ Byte} = 8 \text{ bits}$$
为什么一个字节是 8 位而不是其他数字?这源于历史和技术两方面的因素:
- 历史因素:早期的计算机使用 6 位编码(如 BCD 码)表示字符,但随着 ASCII 标准的普及,需要 7 位来表示 128 个字符。为了容纳扩展 ASCII(256 个字符)并方便处理,8 位成为标准。
- 技术因素:8 是 2 的幂次方($2^3$),便于计算机硬件设计和寻址。同时,8 位可以表示 $2^8 = 256$ 种状态,足以覆盖绝大多数单字节字符编码需求。
3.1.2 为什么用 1024 而不是 1000
在计算机科学中,存储容量的进位通常采用 1024($2^{10}$)而非 1000。这是因为计算机基于二进制工作,$2^{10} = 1024$ 是最接近 1000 的 2 的幂次方,便于硬件寻址和计算。
然而,这带来了一个混乱:硬盘厂商通常使用 1000 作为进位(1 GB = $10^9$ 字节),而操作系统使用 1024(1 GiB = $2^{30}$ 字节)。因此,一块标称 500 GB 的硬盘,在操作系统中显示的容量约为 465 GiB。
为了区分这两种计量方式,国际电工委员会(IEC)于 1998 年引入了二进制前缀:
| 十进制前缀 | 含义 | 二进制前缀 | 含义 |
|---|---|---|---|
| KB(Kilobyte) | $10^3 = 1000$ 字节 | KiB(Kibibyte) | $2^{10} = 1024$ 字节 |
| MB(Megabyte) | $10^6$ 字节 | MiB(Mebibyte) | $2^{20}$ 字节 |
| GB(Gigabyte) | $10^9$ 字节 | GiB(Gibibyte) | $2^{30}$ 字节 |
| TB(Terabyte) | $10^{12}$ 字节 | TiB(Tebibyte) | $2^{40}$ 字节 |
3.1.3 计量单位换算表
我们常见的十进制数字的计量单位有:个、十、百、千、万、亿,比亿还大的单位有:兆、京、垓、秭、穰、沟、涧、正、载、极。 二进制也有自己的计量单位。位(bit,Binary Digits):存放一位二进制数,即 0 或 1,为最小的存储单位,8 个二进制位为一个字节单位。更大的计量单位有 KB、MB、GB、TB、PB、EB、ZB、YB、BB 等。
| 二进制计量单位 | 单位换算 | 单位换算(科学计数法) | 十进制计量单位 | 单位换算 | 单位换算(科学计数法) |
|---|---|---|---|---|---|
| b | (bit) | 万 | $10^4$ | ||
| B | 1B = 8b(1Byte = 8bit) | 亿 | 万万 | $10^8$ | |
| KB | 1KB = 1024B | $2^{10}$ | 兆 | 万亿 | $10^{12}$ |
| MB | 1MB = 1024KB | $2^{20}$ | 京 | 万兆 | $10^{16}$ |
| GB | 1GB = 1024MB | $2^{30}$ | 垓 | 万京 | $10^{20}$ |
| TB | 1TB = 1024GB | $2^{40}$ | 秭 | 万垓 | $10^{24}$ |
| PB | 1PB = 1024TB | $2^{50}$ | 穰 | 万秭 | $10^{28}$ |
| EB | 1EB = 1024PB | $2^{60}$ | 沟 | 万穰 | $10^{32}$ |
| ZB | 1ZB = 1024EB | $2^{70}$ | 涧 | 万沟 | $10^{36}$ |
| YB | 1YB = 1024ZB | $2^{80}$ | 正 | 万涧 | $10^{40}$ |
| BB | 1BB = 1024YB | $2^{90}$ | 载 | 万正 | $10^{44}$ |
| NB | 1NB = 1024BB | $2^{100}$ | 极 | 万载 | $10^{48}$ |
3.2 字节序
3.2.1 什么是字节序
当数据需要占用多个字节时(例如一个 32 位整数占 4 个字节),就涉及到一个关键问题:这些字节在内存中应该如何排列? 这就是**字节序(Endianness)**问题。
假设有一个 16 位整数 0x1234,它由两个字节组成:0x12(高位字节)和 0x34(低位字节)。在内存中存储时,有两种方式:
大端序(Big-Endian)
高位字节存储在低地址,低位字节存储在高地址。类似于人类书写数字的习惯:先写高位。
内存地址: 0x1000 0x1001
+--------+--------+
| 0x12 | 0x34 |
+--------+--------+
高位字节 低位字节
小端序(Little-Endian)
低位字节存储在低地址,高位字节存储在高地址。
内存地址: 0x1000 0x1001
+--------+--------+
| 0x34 | 0x12 |
+--------+--------+
低位字节 高位字节
3.2.2 网络字节序
在网络通信中,不同架构的计算机可能使用不同的字节序。为了确保数据能够正确解析,网络协议统一使用大端序作为标准,称为网络字节序(Network Byte Order)。
例如,IP 地址 192.168.1.1 在传输时,会按照大端序将 32 位整数 0xC0A80101 发送出去。TCP/IP 协议栈中的 htonl()(Host to Network Long)和 htons()(Host to Network Short)函数就是用于将主机字节序转换为网络字节序。
3.2.3 Rust 中的字节序处理
Rust 标准库为整数类型提供了便捷的字节序转换方法:
| 方法 | 说明 |
|---|---|
to_be() | 转换为大端序字节数组(Big-Endian) |
to_le() | 转换为小端序字节数组(Little-Endian) |
to_ne_bytes() | 转换为本机字节序字节数组(Native Endian) |
from_be_bytes() | 从大端序字节数组解析 |
from_le_bytes() | 从小端序字节数组解析 |
fn main() {
let num: u32 = 0x12345678;
// 转换为大端序字节数组
let be_bytes = num.to_be_bytes();
println!("大端序: {:02X?}", be_bytes); // [12, 34, 56, 78]
// 转换为小端序字节数组
let le_bytes = num.to_le_bytes();
println!("小端序: {:02X?}", le_bytes); // [78, 56, 34, 12]
// 从字节数组还原
let restored = u32::from_be_bytes(be_bytes);
println!("还原: 0x{:08X}", restored); // 0x12345678
}
3.2.4 检测系统字节序
大多数现代计算机(x86、x86_64、ARM 等)使用小端序,但某些架构(如网络设备、部分嵌入式系统)使用大端序。以下代码可以检测当前系统的字节序:
fn is_little_endian() -> bool {
let num: u16 = 0x0001;
let bytes = num.to_ne_bytes();
bytes[0] == 0x01 // 小端序:低位字节在低地址
}
fn main() {
if is_little_endian() {
println!("当前系统使用小端序(Little-Endian)");
} else {
println!("当前系统使用大端序(Big-Endian)");
}
}
3.3 Base64 编码
3.3.1 编码原理
Base64 是一种将二进制数据编码为 ASCII 字符串的方法。它的核心思想是:将每 3 个字节(24 位)的数据拆分为 4 个 6 位的组,每个 6 位组对应一个 Base64 字符。
编码过程如下:
$$\text{3 字节} \rightarrow \text{24 位} \rightarrow \text{4 个 6 位组} \rightarrow \text{4 个 Base64 字符}$$
Base64 字符集包含 64 个字符($2^6 = 64$):
A-Z(26 个)a-z(26 个)0-9(10 个)+和/(2 个)
填充规则:当数据长度不是 3 的倍数时,使用 = 进行填充:
| 原始字节数 | 剩余位数 | 输出字符数 | 填充 |
|---|---|---|---|
| 3 | 0 | 4 | 无 |
| 2 | 16 | 3 | 1 个 = |
| 1 | 8 | 2 | 2 个 = |
例如,编码字符串 "Man":
M a n
01001101 01100001 01101110
| 6位 || 6位 || 6位 || 6位 |
010011 010110 000101 101110
T W F u
因此 "Man" 的 Base64 编码为 "TWFu"。
3.3.2 URL 安全变体
标准的 Base64 编码包含 + 和 / 字符,这在 URL 中需要进行转义。因此出现了 URL 安全 Base64(Base64URL),将 + 替换为 -,将 / 替换为 _,并通常省略填充符 =。
| 变体 | 字符集 | 填充 | 适用场景 |
|---|---|---|---|
| 标准 Base64 | A-Z a-z 0-9 + / | = | 一般文本传输 |
| URL 安全 Base64 | A-Z a-z 0-9 - _ | 无/可选 | URL、文件名 |
3.3.3 Shell 命令示例
任何二进制文件都可以使用 Base64 进行编码,比如 txt 文本、图片。
# 编码文件
openssl enc -base64 -in 生僻字.txt -out 生僻字.base64.txt
# 解码文件
openssl enc -base64 -d -in 生僻字.base64.txt -out 生僻字.demo.txt
# 编码字符串
echo "龙行龘龘(dá dá)前程朤朤(lǎng lǎng)生活䲜䲜(yè yè)健康𣊫𣊫(liù liù)财运𨰻𨰻(bǎo bǎo)🔥" | openssl enc -base64
# 解码字符串
echo "6b6Z6KGM6b6Y6b6Y77yIZMOhIGTDoe+8ieWJjeeoi+acpOacpO+8iGzHjm5nIGzH
jm5n77yJ55Sf5rS75LKc5LKc77yIecOoIHnDqO+8ieWBpeW6t/Cjiqvwo4qr77yI
bGnDuSBsacO577yJ6LSi6L+Q8Kiwu/CosLvvvIhix45vIGLHjm/vvIkK" | openssl enc -base64 -d
3.3.4 Rust 实现
#![allow(unused)]
fn main() {
#[test]
fn base64_demo() {
let hello = b"hello rustaceans00";
let encoded = general_purpose::STANDARD.encode(hello);
let decoded = general_purpose::STANDARD.decode(&encoded).unwrap();
println!("origin: {}", str::from_utf8(hello).unwrap());
println!("base64 encoded: {}", encoded);
println!("back to origin: {}", str::from_utf8(&decoded).unwrap());
// &[u8;T]/String <= Vec<u8> => Base64 String
let common_base64_str = general_purpose::STANDARD.encode("中文@123&");
println!("common base64 string: {}", common_base64_str);
let common_vecu8 = general_purpose::STANDARD
.decode(&common_base64_str)
.unwrap();
println!("common string: {}", str::from_utf8(&common_vecu8).unwrap());
println!("道路安全千万条,安全第一条");
use base64::{
alphabet,
engine::{self, general_purpose},
Engine as _,
};
let reform = "商鞅变法、戊戌变法、改革开放";
let b64 = general_purpose::STANDARD.encode(reform.as_bytes());
println!("{}", b64);
const CUSTOM_ENGINE: engine::GeneralPurpose =
engine::GeneralPurpose::new(&alphabet::URL_SAFE, general_purpose::NO_PAD);
let b64_url = CUSTOM_ENGINE.encode(b"hello internet~");
println!("{}", b64_url);
let url = String::from("忘记过去不但意味着背叛,意味着将来可能还要重来");
let lesson = general_purpose::URL_SAFE.encode(url.as_bytes());
println!("{}", lesson);
// 图片 Base64
let read_result = fs::read("examples/file/SeaORM_banner.png");
if let Some(picture_bytes) = read_result.ok() {
let b64 = general_purpose::STANDARD.encode(&picture_bytes);
println!(
"picture_bytes len:{} ,base64 len:{}",
picture_bytes.len(),
b64.len()
);
// 输出一行字符串
fs::write("examples/file/SeaORM_banner.base64.txt", b64.as_bytes());
// 76 个字符添加一个换行符
// shell: fold -w 76 SeaORM_banner.base64.txt > SeaORM_banner.base64.3.txt
if let Ok(file) = File::create("examples/file/SeaORM_banner.base64.3.txt") {
let mut writer = BufWriter::new(file);
let mut chars = b64.chars();
for line in &chars.chunks(76usize) {
// println!("{}",line.collect::<String>());
// fs::write("examples/file/SeaORM_banner.base64.3.txt",line.collect::<String>());
let mut bytes = line.collect::<String>();
bytes.push_str("\n");
let count = writer.write(bytes.as_bytes());
//writer.write(b"\n");
}
writer.flush();
}
}
}
}
3.4 Hex 编码
3.4.1 编码原理
Hex(Hexadecimal,十六进制)编码是将二进制数据转换为十六进制字符串表示的方法。每个字节(8 位)恰好对应两个十六进制字符:
$$\text{1 字节} \rightarrow \text{8 位} \rightarrow \text{2 个十六进制字符}$$
十六进制字符集为 0-9 和 A-F(或 a-f),共 16 个字符。由于 $16 = 2^4$,每个十六进制字符恰好表示 4 位二进制数据。
例如,字节 0x4D(二进制 01001101,即 ASCII 字符 'M')的 Hex 编码为 "4D"。
Hex 编码的特点:
- 可读性好:比纯二进制更易于人工阅读和调试
- 无歧义:每个字节固定对应 2 个字符,无需填充
- 体积膨胀:编码后数据大小变为原来的 2 倍
- 常用场景:调试输出、哈希值表示、内存转储、颜色代码
3.4.2 Rust 实现
#![allow(unused)]
fn main() {
use data_encoding::{HEXUPPER, DecodeError};
// 编码和解码十六进制
let original = b"The quick brown fox jumps over the lazy dog.";
let expected = "54686520717569636B2062726F776E20666F78206A756D7073206F76\
657220746865206C617A7920646F672E";
let encoded = HEXUPPER.encode(original);
assert_eq!(encoded, expected);
let decoded = HEXUPPER.decode(&encoded.into_bytes()).unwrap();
assert_eq!(&decoded[..], &original[..]);
println!("{}", str::from_utf8(&decoded).unwrap());
let cn = HEXUPPER.encode("功成不必在我".as_bytes());
println!("{}", cn);
}
3.5 内存映射
3.5.1 什么是内存映射
内存映射(Memory Mapping,MMAP)是一种将文件内容直接映射到进程虚拟地址空间的技术。通过内存映射,程序可以像访问内存一样访问文件内容,避免了传统 I/O 中的多次数据拷贝,实现了**零拷贝(Zero-Copy)**效果。
内存映射的优势:
- 减少数据拷贝:数据直接从磁盘加载到用户空间,无需经过内核缓冲区的额外拷贝
- 延迟加载:只有实际访问的页面才会从磁盘加载,适合处理大文件
- 进程间共享:多个进程可以映射同一个文件,实现高效的进程间通信
- 简化编程:文件操作转化为内存操作,代码更简洁
3.5.2 Rust 中的 MMAP
#![allow(unused)]
fn main() {
#[test]
fn mmap_mut() -> Result<(), Error> {
use memmap::MmapMut;
use std::fs::OpenOptions;
use std::io::Write;
use std::ops::DerefMut;
use std::path::PathBuf;
// let file = File::options().write(true).open("abcd.txt")?;
let path: PathBuf = PathBuf::from("abcd.txt");
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(&path)?;
file.set_len(30)?;
let mut mmap = unsafe { MmapMut::map_mut(&file)? };
// mmap.copy_from_slice(b"Hello, world!");
(&mut mmap[..]).write_all(b"Hello BeiJing! Hello, world!")?;
mmap.flush()?;
Ok(())
}
}
- mmap-sync Rust library for concurrent data access, using memory-mapped files, zero-copy deserialization, and wait-free synchronization.
3.6 总结
3.6.1 编码方式对比
| 特性 | Base64 | Hex | 原始二进制 |
|---|---|---|---|
| 编码后体积 | 原始数据的 4/3(约 133%) | 原始数据的 2 倍(200%) | 100% |
| 字符集大小 | 64 | 16 | 256 |
| 可读性 | 中等 | 好 | 差(不可直接阅读) |
| 是否需要填充 | 是(=) | 否 | 否 |
| 适用场景 | 邮件附件、URL、嵌入图片 | 调试、哈希、颜色值 | 存储、传输 |
| URL 安全 | 需使用 URL 安全变体 | 是 | 否 |
3.6.2 关键概念汇总
| 概念 | 说明 |
|---|---|
| $1 \text{ Byte} = 8 \text{ bits}$ | 字节与位的基本关系 |
| 大端序 | 高位字节存低地址,网络标准 |
| 小端序 | 低位字节存低地址,x86/x64 架构 |
| 网络字节序 | 统一使用大端序 |
| Base64 | 3 字节 $\rightarrow$ 4 字符,6 位一组 |
| Hex | 1 字节 $\rightarrow$ 2 字符,4 位一组 |
| MMAP | 文件映射到内存,零拷贝 |
3.7 练习题
-
基础题:编写 Rust 程序,将字符串
"Rust"分别进行 Base64 编码和 Hex 编码,输出编码结果并验证解码后是否还原。 -
字节序实践:定义一个
u32变量0xDEADBEEF,分别输出其大端序和小端序的字节表示,并用from_be_bytes和from_le_bytes还原验证。 -
字节序检测:编写一个函数
detect_endianness(),返回当前系统的字节序(大端或小端),并在main函数中打印结果。 -
Base64 填充分析:分别对 1 字节、2 字节、3 字节的数据进行 Base64 编码,观察输出结果中
=填充符的数量,验证填充规则。 -
URL 安全 Base64:使用
base64crate 的 URL_SAFE 引擎,对一个包含中文的字符串进行编码,验证输出中不含+和/字符。 -
Hex 编码实现:不使用任何第三方库,手动实现一个函数
fn bytes_to_hex(input: &[u8]) -> String,将字节数组转换为 Hex 字符串。 -
内存映射读取:使用
memmapcrate 编写程序,将一个文本文件映射到内存,读取其内容并输出前 100 个字符。 -
思考题:为什么网络协议统一使用大端序?如果发送方和接收方使用不同的字节序,会发生什么问题?请结合 TCP/IP 协议栈说明。