Implementing TFTP in Rust

Hello all.
I'm working on a TFTP server implementation, for a school project.
I thought it would be relatively simple, however I am now stuck trying to create structs for all the different packet types. Not because it is so difficult, rather because working with raw packets is so frustrating. I looked, and many on this forum were recommending this crate called bytes, and while it certainly feels like it should make things easier, it isn't, for whatever reason.

use bytes::{Buf, BufMut, Bytes, BytesMut};
use super::{modes, opcode, packet_errors, tftp_options};

/// Read Request packet:
pub struct RRQ {
	filename:		String,
	mode:			&'static [u8],
	blksize:		Option<usize>,
	timeout:		Option<usize>,
	tsize:			Option<usize>,
	windowsize:		Option<u16>,
}

impl RRQ {
	/// Recieves the packet currently being constructed, and appends an option to it:
	fn put_opt(packet: &mut BytesMut, opt_type: &[u8], opt_val: impl ToString) {
		packet.put(opt_type);
		packet.put_u8(0);
		packet.put(opt_val.to_string().as_bytes());
		packet.put_u8(0);
	}
}

impl Into<Bytes> for RRQ {
	fn into(self) -> Bytes {

		// This will store the bytes while we build the packet, and will be made immutable later on:
		let mut packet_bytes: BytesMut = BytesMut::new();

		// First, insert the opcode:
		packet_bytes.put_u16(opcode::RRQ);

		// Then add the file name, followed by a null byte:
		packet_bytes.put(self.filename.as_bytes());
		packet_bytes.put_u8(0);

		// Then the mode, followed by a null byte:
		packet_bytes.put(self.mode);
		packet_bytes.put_u8(0);


		// Options:
		// This is the implementation for the Options Extension:

		// For the blocksize extension:
		if let Some(blksize) = self.blksize {
			Self::put_opt(&mut packet_bytes, tftp_options::BLKSIZE, blksize);
		}

		// For the Timeout extension:
		if let Some(timeout) = self.timeout {
			Self::put_opt(&mut packet_bytes, tftp_options::TIMEOUT, timeout);
		}

		// For the Transfer Size extension:
		if let Some(tsize) = self.tsize {
			Self::put_opt(&mut packet_bytes, tftp_options::TSIZE, tsize);
		}

		// For the Window Size extension:
		if let Some(windowsize) = self.windowsize {
			Self::put_opt(&mut packet_bytes, tftp_options::WINDOWSIZE, windowsize);
		}


		// Finally, put it into the bytearray that we'll send over UDP:
		return packet_bytes.freeze();
	}
}

impl TryFrom<Bytes> for RRQ {
	type Error = packet_errors::DeserializingError;

	fn try_from(mut value: Bytes) -> Result<Self, Self::Error> {
		
		// 4 bytes is the minimum length.
		if value.len() < 4 {
			return Err(Self::Error::MalformedPacket);
		}

		// Is it the right kind of packet?
		if value.get_u16() != opcode::RRQ {
			return Err(Self::Error::BadOpcode);
		}
		// TODO: get filename, mode, and options.
		Err(Self::Error::BadOpcode)
	}
}

Note that my objective here is to make it so that I can simply send this struct over a tokio socket, and receive directly into it. Can I have some guidance?
I'm not new to programming, just really, really new to Rust.
Thanks in advance!

The pnet library has a derive macro to define custom packets if you want to give it a try

This only helps with the parsing side, but you might look at nom, specifically the nom::bytes module.

Also rather

impl From<RRQ> for Bytes

What I really want is some information on how to actually use bytes.
That is, I'd like to be able to easily and concisely retrieve a C-style string from a Bytes struct, convert it into a String, etc. while actually knowing that the cursor will be where I think it is. The bytes documentation is atrociously vague about how all these things work, and I really wish that their documentation was more like rocket's, with examples and such.
In other words, I'm looking for a more elegant solution.