Rust protocol implementation
General
Create simple empty rust project
. ├── Cargo.toml ├── prot │ └── protocol.prot <== protocol scheme, see below └── src └── main.rs
Let's use for a demonstration next simple protocol scheme. Here is more about protocol syntax.
struct StructExampleA { str field_str; u32 field_u32; u64 field_u64; i8 field_i8; i16 field_i16; f64 field_f64; bool field_bool; } struct StructExampleB { str[] field_str; u8[] field_u8; i8[] field_i8; bool[] field_bool; StructExampleA field_struct; StructExampleA[] field_structs; } struct StructExampleC { str field_str?; u8 field_u8?; i16 field_i16?; i32 field_i32?; i64 field_i64?; bool field_bool?; }
Dependencies
Take into account, protocol implementation requeired some crates. So add next dependencies into curgo.toml:
- bytes
Generate
As soon as we have protocol scheme, let's generate code-base.
clibri --src ./prot/protocol.prot -rs ./src/protocol.rs -o --em # --src path_to_protocol_scheme # -o overwrite protocol.rs file if it already exists # --em include all code to avoid using CLIBRI library # -rs path_to_rust_implementation_file
Now our solution looks like it:
. ├── Cargo.toml ├── prot │ └── protocol.prot └── src ├── main.rs └── protocol.rs <== protocol implementation
All our protocol's messages now are available in protocol.rs module.
pub mod protocol;
fn main() {
let struct_example_a = protocol::StructExampleA {
field_str: String::from("test"),
field_u32: 3,
field_u64: 4,
field_i8: -1,
field_i16: -2,
field_f64: 0.2,
field_bool: true,
};
}
So, every structure or enum would be a struct and enum; group - mod (module).
Take into account, sometimes we need to do tests with some kind of dummy data, but if we have complex structures it might be quite annoying. You always can use static method defaults to get an instance of your structure with dummy data.
pub mod protocol;
fn main() {
let mut struct_example_a = protocol::StructExampleA::defaults();
let mut struct_example_b = protocol::StructExampleB::defaults();
let mut struct_example_c = protocol::StructExampleC::defaults();
}
There are a couple of important methods related to decode/encode and packing:
Signature | Trait | Description |
---|---|---|
abduct(&mut self) -> Result<Vec<u8>, String> | StructEncode | Encode instance of struct/enum and returns bytes |
extract(buf: Vec<u8>) -> Result<Self, String> | StructDecode | Makes attempt to create an instance of structure from the given buffer |
defaults() -> Self | StructDecode | Returs self instance with default values |
pack(&mut self,sequence: u32,uuid: Option<String>) -> Result<Vec<u8>, String> | PackingStruct | Packs struct/enum and retures bytes |
As you can see methods abduct and pack look pretty the same. Both return bytes. The important difference is: the method pack prepares structure to send and if you are going to use CLIBRI reader, you should use a method pack. Because this method adds a header to the structure to correctly decode it after.
Let's see how CLIBRI reader works
pub mod protocol;
use protocol::{PackingStruct, StructDecode, StructEncode};
fn reading() -> Result<(), String> {
// Create a couple of examples
let mut struct_example_a = protocol::StructExampleA::defaults();
let mut struct_example_b = protocol::StructExampleB::defaults();
let mut struct_example_c = protocol::StructExampleC::defaults();
// Create reader
let mut reader = protocol::Buffer::new();
// Create a buffer and fill it with example data
let buffer: Vec<u8> = [
struct_example_a.pack(1, None).map_err(|e| e.to_string())?,
struct_example_b.pack(2, None).map_err(|e| e.to_string())?,
struct_example_c.pack(3, None).map_err(|e| e.to_string())?,
]
.concat();
// Put data into reader
reader
.chunk(&buffer, None)
.map_err(|e| format!("Fail to add data: {:?}", e))?;
// Reading messages
while let Some(msg) = reader.next() {
match msg.msg {
protocol::AvailableMessages::StructExampleA(struct_a) => {
println!("{:?}", struct_a);
}
protocol::AvailableMessages::StructExampleB(struct_b) => {
println!("{:?}", struct_b);
}
protocol::AvailableMessages::StructExampleC(struct_c) => {
println!("{:?}", struct_c);
}
_ => {}
}
}
Ok(())
}
fn main() {
reading().expect("Oops!");
}
Buffer has two major methods
Signature | Description |
---|---|
chunk(&mut self,buf: &Vec<u8>, uuid: Option<String>) -> Result<(), ReadError> | Add data into reader buffer |
next(&mut self) -> Option<IncomeMessage<T>> | Returns decoded message or None if buffer doesn't have any messages |
In some cases, it might be you want to do some additional actions on a buffer before it would be sent. For example, compress and decompress data. For such kinds of purposes, you can use struct PackingMiddleware with your own implementation.
pub mod protocol;
use protocol::PackingMiddleware;
impl PackingMiddleware {
fn decode(
buffer: Vec<u8>,
_id: u32,
_sequence: u32,
_uuid: Option<String>,
) -> Result<Vec<u8>, String> {
// Do something... Decompress for example
Ok(buffer)
}
fn encode(
buffer: Vec<u8>,
_id: u32,
_sequence: u32,
_uuid: Option<String>,
) -> Result<Vec<u8>, String> {
// Do something... Compress for example
Ok(buffer)
}
}
More examples you can find on github
Packing vs encoding
As you already noticed there is a difference between packing and encoding, even both give bytes.
Packing adds a header to each message, which includes
- id unique id of the message
- sequence numeric sequence to bind messages between consumer and producer. Most relevant example: request - response. Both should have the same sequence to be bound.
- timestamp time of packing message
At the same time encoding just gives you a byte representation of your message without any data, which would need to decode the message.