Rust中如何回滚BytesMut读取位置重复调用get_u64方法
BytesMut实现的Buftrait默认是流语义,读指针只会向前移动,原生没有提供安全的公开回退API,你可以根据实际场景选择以下三种实现方式:
方案1:用std::io::Cursor包装(最推荐,全安全代码,零开销)
Cursor本身就是为支持随机访问读写位置设计的,Cursor<BytesMut>完整实现了Buf和BufMuttrait,所有get_u64/put_u64等方法和直接使用BytesMut完全一致,同时提供了position()获取当前读位置、set_position()设置读位置的方法,可以任意回滚,没有unsafe代码,也没有额外性能损耗。
示例代码:
use bytes::{Buf, BufMut, BytesMut}; use std::io::Cursor; fn main() { // 用Cursor包装BytesMut let mut buf = Cursor::new(BytesMut::with_capacity(1024)); buf.put_u64(100); // 写入完成后将读指针重置到起始位置(写入操作会移动Cursor指针) buf.set_position(0); // 第一次读取 let val1 = buf.get_u64(); assert_eq!(val1, 100); assert_eq!(buf.position(), 8); // 读完u64指针移动到偏移8的位置 // 回滚到起始位置 buf.set_position(0); // 第二次读取,可正常拿到相同值 let val2 = buf.get_u64(); assert_eq!(val2, 100); }
如果需要回滚到任意读取前的位置,只需要在读取前调用let checkpoint = buf.position()保存位置,需要回滚时调用buf.set_position(checkpoint)即可。
方案2:快照法(适合回滚到固定检查点的场景,全安全代码)
BytesMut的clone()是O(1)的浅拷贝,仅增加引用计数,不会立刻复制内存(只有后续修改缓冲区时才会触发写时复制)。你可以在需要设置回滚点的时候clone一份当前的BytesMut作为快照,回滚时直接把原变量替换为快照即可。
示例代码:
use bytes::{Buf, BufMut, BytesMut}; fn main() { let mut buf = BytesMut::with_capacity(1024); buf.put_u64(100); // 设置回滚快照 let snapshot = buf.clone(); // 第一次读取 let val1 = buf.get_u64(); assert_eq!(val1, 100); assert_eq!(buf.remaining(), 0); // 读完后剩余可读长度为0 // 回滚到快照位置 buf = snapshot; // 第二次读取 let val2 = buf.get_u64(); assert_eq!(val2, 100); }
这个方案非常适合协议解析类场景:先尝试解析一段数据,如果解析失败(比如数据不完整)就直接回滚到快照位置,等后续数据到齐再重新解析。
方案3:原地回退(极致性能场景,需使用unsafe)
BytesMut内部提供了set_start(usize)方法可以直接设置读指针相对于缓冲区起始的偏移,但是这个方法是unsafe的,你必须自己保证新的读指针位置合法:不能小于缓冲区起始位置,不能超过当前写指针的位置(否则会读到未初始化的内存,触发未定义行为)。
可以封装一个带边界检查的安全回滚方法:
use bytes::{Buf, BufMut, BytesMut}; /// 回滚指定长度的读指针,回滚长度不能超过已经读取的总字节数 unsafe fn rollback(buf: &mut BytesMut, rollback_len: usize) { let new_start = buf.as_ptr().offset(-(rollback_len as isize)); buf.set_start(new_start as usize); } fn main() { let mut buf = BytesMut::with_capacity(1024); buf.put_u64(100); let val1 = buf.get_u64(); assert_eq!(val1, 100); // 回滚8字节(一个u64的长度) unsafe { rollback(&mut buf, 8); } let val2 = buf.get_u64(); assert_eq!(val2, 100); }
注意:这个方案对边界检查的要求非常高,不推荐在普通业务代码中使用,优先选择前两种安全方案。
注意事项
- 禁止在没有做严格边界检查的情况下直接调用
set_start,会导致内存安全问题。 - 如果需要频繁随机调整读位置,优先选择Cursor方案,代码最简洁,安全性最高。
内容的提问来源于stack exchange,提问作者yang guangyue

