Now lets implement the Messenger
trait for our Clt
& Svc
types. The Messenger
trait is responsible for serializing & deserializing messages into bytes. It also specifies what message types Clt
& Svc
will be able to send & receive.
A few things to note about the Messenger
trait implementation:
-
The two associated types
RecvT
&SendT
are used to specify what message typesClt
&Svc
will be able torecv
&send
. In our example, we chose to use the same message type for bothClt
&Svc
but in a real world scenarioClt
&Svc
would likely have different message types. Hence, bothClt
&Svc
would need to provide their own implementation of theMessenger
trait. That would mean there will be two separate structuresCltMessageProtocol
&SvcMessageProtocol
one forClt
& one forSvc
respectively. -
links
library is designed with performance in mind and is aiming to avoid runtime heap allocation. As a resultMessenger::deserialize
method signature returns an owned type instead of a smart pointer, whileMessenger::serialize
returns a fixed sizestack allocated
byte array. Note that to a void a stack frame copy on these function invocations we encourage you toinline
both of these method's implementations.Note: The
Messenger::serialize<const MAX_MSG_SIZE: usize>
has a genericconst
argument that will be propagated from instantiations that will looks something like this:Clt::<_, _, MAX_MSG_SIZE>::connect(...)
Svc::<_, _, MAX_MSG_SIZE>::bind(...)
It is also important to note that in our example we choose to deserialize a byte array into a json
String
, which requires heap allocation, before converting it into aExchangeDataModel
enum. This is done for simplicity of the example, but in a real world you would likely choose to deserialize without a jsonString
step and avoidheap allocation
by going direction from the byte array into aExchangeDataModel
. One of the ways of doing it is by leveraging byteserde crate instead of serde_json. -
ProtocolCore
&Protocol
traits provide advanced features, that will not be covered in this example but need to be implemented anyway. The default implementations are sufficient and will be optimized away by the compiler unless overridden.
# use links_nonblocking::prelude::*;
#
# #[derive(Debug, serde::Serialize, serde::Deserialize)]
# struct Ping;
# #[derive(Debug, serde::Serialize, serde::Deserialize)]
# struct Pong;
#
# #[derive(Debug, serde::Serialize, serde::Deserialize)]
# enum ExchangeDataModel {
# Ping(Ping),
# Pong(Pong),
# }
#
# #[derive(Debug, Clone)] // Note Clone required for Protocol
# struct MessageProtocol;
#
# impl Framer for MessageProtocol {
# fn get_frame_length(bytes: &bytes::BytesMut) -> Option<usize> {
# for (idx, byte) in bytes.iter().enumerate() {
# if *byte == b'\n' {
# return Some(idx + 1);
# }
# }
# None
# }
# }
#
impl Messenger for MessageProtocol {
type RecvT = ExchangeDataModel;
type SendT = ExchangeDataModel;
#[inline(always)] // DO inline to avoid a potentially expensive stack frame copy
fn deserialize(frame: &[u8]) -> Result<Self::RecvT, std::io::Error> {
let frame = std::str::from_utf8(frame).map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
let received = serde_json::from_str(frame).map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
Ok(received)
}
#[inline(always)] // DO inline to avoid a potentially expensive stack frame copy
fn serialize<const MAX_MSG_SIZE: usize>(msg: &Self::SendT) -> Result<([u8; MAX_MSG_SIZE], usize), std::io::Error> {
let msg = serde_json::to_string(msg).map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
let mut msg = msg.into_bytes();
msg.push(b'\n');
let mut buf = [0_u8; MAX_MSG_SIZE];
buf[..msg.len()].copy_from_slice(&msg);
Ok((buf, msg.len()))
}
}
// Default implementation is sufficient for this example and will be optimized away by the compiler
impl ProtocolCore for MessageProtocol {}
impl Protocol for MessageProtocol {}