Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第三 字节

字节(Byte)是计算机信息技术用于计量存储容量的基本计量单位,也是计算机编程语言中常用的数据类型。字节是连接硬件存储与软件数据的桥梁——无论是内存中的变量、磁盘上的文件,还是网络中传输的数据包,其底层都是以字节为单位进行组织和处理的。

本章将从字节的计量单位出发,深入探讨字节序、Base64 编码、Hex 编码等核心概念,并结合 Rust 代码展示如何在实际开发中处理字节数据。

3.1 计量单位

3.1.1 位与字节

计算机中所有数据最终都以二进制形式存储和传输。二进制中最小的存储单位是位(bit,Binary Digit),它只能表示 01 两种状态。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$
B1B = 8b(1Byte = 8bit)亿万万$10^8$
KB1KB = 1024B$2^{10}$万亿$10^{12}$
MB1MB = 1024KB$2^{20}$万兆$10^{16}$
GB1GB = 1024MB$2^{30}$万京$10^{20}$
TB1TB = 1024GB$2^{40}$万垓$10^{24}$
PB1PB = 1024TB$2^{50}$万秭$10^{28}$
EB1EB = 1024PB$2^{60}$万穰$10^{32}$
ZB1ZB = 1024EB$2^{70}$万沟$10^{36}$
YB1YB = 1024ZB$2^{80}$万涧$10^{40}$
BB1BB = 1024YB$2^{90}$万正$10^{44}$
NB1NB = 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 的倍数时,使用 = 进行填充:

原始字节数剩余位数输出字符数填充
304
21631 个 =
1822 个 =

例如,编码字符串 "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),将 + 替换为 -,将 / 替换为 _,并通常省略填充符 =

变体字符集填充适用场景
标准 Base64A-Z a-z 0-9 + /=一般文本传输
URL 安全 Base64A-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-9A-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 编码方式对比

特性Base64Hex原始二进制
编码后体积原始数据的 4/3(约 133%)原始数据的 2 倍(200%)100%
字符集大小6416256
可读性中等差(不可直接阅读)
是否需要填充是(=
适用场景邮件附件、URL、嵌入图片调试、哈希、颜色值存储、传输
URL 安全需使用 URL 安全变体

3.6.2 关键概念汇总

概念说明
$1 \text{ Byte} = 8 \text{ bits}$字节与位的基本关系
大端序高位字节存低地址,网络标准
小端序低位字节存低地址,x86/x64 架构
网络字节序统一使用大端序
Base643 字节 $\rightarrow$ 4 字符,6 位一组
Hex1 字节 $\rightarrow$ 2 字符,4 位一组
MMAP文件映射到内存,零拷贝

3.7 练习题

  1. 基础题:编写 Rust 程序,将字符串 "Rust" 分别进行 Base64 编码和 Hex 编码,输出编码结果并验证解码后是否还原。

  2. 字节序实践:定义一个 u32 变量 0xDEADBEEF,分别输出其大端序和小端序的字节表示,并用 from_be_bytesfrom_le_bytes 还原验证。

  3. 字节序检测:编写一个函数 detect_endianness(),返回当前系统的字节序(大端或小端),并在 main 函数中打印结果。

  4. Base64 填充分析:分别对 1 字节、2 字节、3 字节的数据进行 Base64 编码,观察输出结果中 = 填充符的数量,验证填充规则。

  5. URL 安全 Base64:使用 base64 crate 的 URL_SAFE 引擎,对一个包含中文的字符串进行编码,验证输出中不含 +/ 字符。

  6. Hex 编码实现:不使用任何第三方库,手动实现一个函数 fn bytes_to_hex(input: &[u8]) -> String,将字节数组转换为 Hex 字符串。

  7. 内存映射读取:使用 memmap crate 编写程序,将一个文本文件映射到内存,读取其内容并输出前 100 个字符。

  8. 思考题:为什么网络协议统一使用大端序?如果发送方和接收方使用不同的字节序,会发生什么问题?请结合 TCP/IP 协议栈说明。