Когда пишешь сетевой протокол, очень легко прийти к такой цепочке:
прочитали байты -> скопировали в новую область памяти -> распарсили -> получили структуру
На небольших объёмах это вообще не проблема. Но когда через приложение постоянно проходят тысячи сообщений, лишние аллокации и копирование памяти начинают складываться.
В Rust мне для таких задач нравится tokio_util::codec.
В частности - связка Decoder + BytesMut.
Когда реализуем Decoder, мы получаем &mut BytesMut - тот самый буфер, в который Tokio уже складывает данные из сокета.
И если мы нашли в нём полный frame, нам не обязательно копировать его в новый Vec<u8>.
Можно просто сделать:
src.split_to(len)
В результате начало буфера отделяется в новый BytesMut, а исходный буфер остаётся с непрочитанными данными. Само содержимое при этом не приходится перекладывать из одного массива в другой.
Если нужен immutable-буфер, результат можно затем превратить в Bytes через freeze().
Что мне здесь особенно нравится:
Меньше копирований. Не нужно каждый раз создавать новый буфер и переносить туда payload.
Меньше нагрузки на allocator. Один read buffer можно переиспользовать между вызовами декодера вместо постоянного создания временных объектов.
Парсер становится довольно простым. Проверили, хватает ли данных на frame -> отделили нужный кусок -> распарсили.
В итоге вместо постоянного перемещения данных по памяти большая часть работы сводится к управлению границами уже существующего буфера.
Мне вообще нравится этот момент в Rust: с одной стороны пишешь довольно высокоуровневый код через Decoder и traits, а с другой всё ещё хорошо понимаешь, что происходит с памятью под капотом.