告别Vec!掌握Rustbytes库,解锁零拷贝的真正威力还在为Rust网络编程中的Vec<u8>频繁拼接和拷贝而烦恼吗?在追求极致性能的道路上,这些不必要的数据操作正是我们需要清除的障碍。是时候告别这种传统但低效的方式,拥抱专为高性能I/O而生的bytes库了!byte
还在为 Rust 网络编程中的 Vec<u8>
频繁拼接和拷贝而烦恼吗?在追求极致性能的道路上,这些不必要的数据操作正是我们需要清除的障碍。是时候告别这种传统但低效的方式,拥抱专为高性能 I/O 而生的 bytes
库了!
bytes
是 Tokio 生态的基石,其设计的核心就是为了榨干每一分性能。本文将不再停留在理论,而是带你亲手实战,从零开始掌握 BytesMut
与 Bytes
的核心用法,让你彻底理解并解锁其背后“零拷贝”的真正威力。准备好,让我们一起见证代码性能的飞跃!
本文是一篇旨在帮你告别低效 Vec<u8>
操作的 Rust bytes
库实战指南。通过代码和分步解析,你将掌握 Bytes
与 BytesMut
的核心技巧,并解锁 split
、freeze
等操作背后零拷贝的威力。无论你是进行网络编程还是优化 I/O 性能,这都是一篇能让你应用性能显著提升的必读之选。
cargo add bytes --dev
rust-ecosystem-learning on main is 📦 0.1.0 via 🦀 1.88.0
➜ tree . -L 6 -I "build|out|lib|docs|target"
.
├── _typos.toml
├── Cargo.lock
├── Cargo.toml
├── CHANGELOG.md
├── cliff.toml
├── CONTRIBUTING.md
├── deny.toml
├── examples
│ ├── axum_tracing.rs
│ ├── bytes.rs
│ └── err.rs
├── LICENSE
├── README.md
├── rust-toolchain.toml
├── src
│ └── lib.rs
└── test.http
3 directories, 15 files
use anyhow::Result;
use bytes::{BufMut, Bytes, BytesMut};
fn main() -> Result<()> {
// 1. 创建并填充一个可变缓冲区 BytesMut
let mut buf = BytesMut::with_capacity(1024);
buf.extend_from_slice(b"hello world\n");
buf.put(&b"goodbye world"[..]);
buf.put_u8(b'\n');
buf.put_i64(1234567890);
println!("buf: {:?}", buf);
// 2. 分割 BytesMut 并冻结为 Bytes
let buf1 = buf.split();
println!("buf1: {:?}", buf1);
let mut buf2 = buf1.freeze();
println!("buf2: {:?}", buf2);
// 3. 分割不可变的 Bytes
let buf3 = buf2.split_to(12);
println!("buf3: {:?}", buf3);
println!("buf2: {:?}", buf2);
// 4. 其他创建方式和断言
let mut bytes = BytesMut::new();
bytes.extend_from_slice(b"hello");
println!("bytes: {:?}", bytes);
let bytes = Bytes::from(b"hello".to_vec());
assert_eq!(BytesMut::from(bytes), BytesMut::from(&b"hello"[..]));
Ok(())
}
这段代码集中展示了 bytes
包的核心功能,这个包在高性能网络编程和I/O操作中是 Rust 生态的基石。它的主要目标是提供一种高效、低开销的方式来处理连续的内存块(字节缓冲区),特别是避免不必要的数据拷贝。
代码主要围绕两种核心类型展开:
BytesMut
: 一个可变的字节缓冲区。你可以把它想象成一个更强大的 Vec<u8>
,专门为网络数据包或分块读取等场景优化。它支持高效地追加(put
)和扩展(extend
)数据。Bytes
: 一个不可变的、线程安全(通过原子引用计数)的字节缓冲区。它被设计用来在不同任务或线程之间安全、高效地共享数据。从 BytesMut
"冻结" (freeze
) 成 Bytes
是一个零成本的操作。下面我们来逐段解析代码的逻辑:
BytesMut
let mut buf = BytesMut::with_capacity(1024);
buf.extend_from_slice(b"hello world\n");
buf.put(&b"goodbye world"[..]);
buf.put_u8(b'\n');
buf.put_i64(1234567890);
BytesMut::with_capacity(1024)
: 我们初始化一个可变的缓冲区 buf
,并预先分配 1024 字节的容量。预分配容量可以有效避免在后续添加数据时因容量不足而产生的额外内存分配和数据拷贝。extend_from_slice(...)
: 将一个字节切片 b"hello world\n"
的内容追加到缓冲区末尾。put(...)
: 与 extend_from_slice
类似,put
是 BufMut
trait 提供的方法,同样用于追加数据。put_u8(...)
和 put_i64(...)
: bytes
包提供了方便的方法来按特定格式写入原生类型。这里我们写入了一个单字节 u8
和一个64位整数 i64
。put_i64
会以大端序(Big-Endian)将整数转换为8个字节写入缓冲区。执行到这里,buf
中已经包含了我们写入的所有数据。
BytesMut
并冻结为 Bytes
let buf1 = buf.split();
let buf2 = buf1.freeze();
buf.split()
: 这是一个关键操作。它会将 buf
从中“劈开”,将其中已写入的所有数据(从开头到当前位置)移动到一个新的 BytesMut
实例(buf1
)中,而原始的 buf
则变为空(但保留其分配的容量以备复用)。这是一个高效的操作,因为它只涉及指针移动,不涉及数据拷贝。buf1.freeze()
: 这是 bytes
包的核心设计理念的体现。我们将可变的 buf1
转换(冻结)成一个不可变的 Bytes
实例 buf2
。这个操作几乎是零成本的,因为它不复制任何字节。它只是将缓冲区的控制权交给了 Bytes
,使其变为只读,并通过引用计数来管理其生命周期。buf2
现在可以被安全地共享和传递。Bytes
let buf3 = buf2.split_to(12);
buf2.split_to(12)
: split_to
操作会从 buf2
的开头切下前12个字节,创建一个新的、指向这12字节的 Bytes
实例(buf3
)。原始的 buf2
会随之更新,其内部指针会向前移动12个字节,指向剩余的数据。buf2
和 buf3
现在共享同一块底层内存,只是它们各自的“视图”范围不同。这都得益于 Bytes
的引用计数机制,只有当所有指向这块内存的 Bytes
实例都被销毁时,内存才会被释放。这极大地提升了数据分片和传递的效率。let bytes = Bytes::from(b"hello".to_vec());
assert_eq!(BytesMut::from(bytes), BytesMut::from(&b"hello"[..]));
Bytes
和 BytesMut
之间灵活的转换关系。Bytes::from(b"hello".to_vec())
: 从一个 Vec<u8>
创建一个 Bytes
实例。assert_eq!(...)
: 这里的断言验证了两种创建 BytesMut
的方式是等价的:一种是从 Bytes
对象转换而来,另一种是直接从字节切片创建。这保证了 API 的一致性。rust-ecosystem-learning on main [!?] is 📦 0.1.0 via 🦀 1.88.0
➜ cargo run --example bytes
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.07s
Running `target/debug/examples/bytes`
buf: b"hello world\ngoodbye world\n\0\0\0\0I\x96\x02\xd2"
buf1: b"hello world\ngoodbye world\n\0\0\0\0I\x96\x02\xd2"
buf2: b"hello world\ngoodbye world\n\0\0\0\0I\x96\x02\xd2"
buf3: b"hello world\n"
buf2: b"goodbye world\n\0\0\0\0I\x96\x02\xd2"
bytes: b"hello"
buf: b"hello world\ngoodbye world\n\0\0\0\0I\x96\x02\xd2"
println!("buf: {:?}", buf);
BytesMut
缓冲区 buf
在被填充所有数据后的最终内容。它包含了:
b"hello world\n"
(12 字节)b"goodbye world"
(13 字节)b'\n'
(1 字节)1234567890
这个 i64
整数的大端字节表示 \0\0\0\0I\x96\x02\xd2
(8 字节)。十六进制 499602D2
正好等于十进制的 1234567890
。buf1: b"hello world\ngoodbye world\n\0\0\0\0I\x96\x02\xd2"
let buf1 = buf.split(); println!("buf1: {:?}", buf1);
buf.split()
操作将 buf
中所有的数据都“切”出来,并移动到一个新的 BytesMut
实例 buf1
中。因此,buf1
的内容和操作前的 buf
完全一样。执行此操作后,buf
变为空,但其预分配的内存容量得以保留。buf2: b"hello world\ngoodbye world\n\0\0\0\0I\x96\x02\xd2"
let buf2 = buf1.freeze(); println!("buf2: {:?}", buf2);
freeze()
将可变的 buf1
转换(冻结)成一个不可变的 Bytes
实例 buf2
。这是一个零拷贝操作,所以 buf2
的内容和 buf1
完全相同,只是类型从 BytesMut
变成了 Bytes
,现在可以被安全地共享。buf3: b"hello world\n"
let buf3 = buf2.split_to(12); println!("buf3: {:?}", buf3);
split_to(12)
从 buf2
的开头切下了前 12 个字节,创建了一个新的、指向这 12 字节的 Bytes
实例 buf3
。输出的内容 b"hello world\n"
正好是 12 个字节,完全符合预期。buf2: b"goodbye world\n\0\0\0\0I\x96\x02\xd2"
println!("buf2: {:?}", buf2);
(在 split_to
之后)split_to
的效果。当 buf3
从 buf2
分离出去后,buf2
本身也发生了变化:它现在指向了剩余的数据。所以,我们看到 buf2
的内容就是原始数据中从第 13 个字节开始的所有内容。这同样是一个零拷贝操作,buf2
和 buf3
只是指向了同一块底层内存的不同部分。bytes: b"hello"
println!("bytes: {:?}", bytes);
BytesMut
,并只向其中添加了字符串 b"hello"
,所以结果就是 b"hello"
。bytes
库是 Rust 生态中名副其实的高性能利器。通过本文的实战,我们再次印证了它的核心优势:
BytesMut
负责高效构建,Bytes
负责安全共享。freeze
, split
等操作通过共享内存而非复制,实现了极致的效率。with_capacity
和容量复用机制,从源头减少了内存分配开销。总而言之,bytes
库通过其精巧的零拷贝设计,完美解决了网络编程中的性能痛点。现在,你已经掌握了它的核心用法,并亲手解锁了其威力。将这些技巧应用到你的项目中,无论是 Tokio、Hyper 还是 Axum,都将让你编写的 Rust 应用在性能上更胜一筹,真正做到高效、健壮。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!