Skip to content

Commit

Permalink
revolutionary protocol
Browse files Browse the repository at this point in the history
  • Loading branch information
cospectrum committed Jan 19, 2024
1 parent c7b389a commit 65eec9d
Show file tree
Hide file tree
Showing 11 changed files with 240 additions and 297 deletions.
1 change: 1 addition & 0 deletions memcrab-protocol/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ edition = "2021"

[dependencies]
async-trait = "0.1.77"
itertools = { version = "0.12.0", default-features = false }
num_enum = "0.7.2"
thiserror.workspace = true
tokio = { workspace = true, features = ["io-util", "net"] }
Expand Down
61 changes: 26 additions & 35 deletions memcrab-protocol/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ This crate contains the implementation of the protocol that is used by server an

**Note that this is not meant to be the official Rust API for memcrab, use the official wrapper client instead.**


## Usage
```rust
TODO
Expand All @@ -17,7 +16,6 @@ async fn main() {
}
```


## Protocol description

### Encoding
Expand All @@ -27,50 +25,43 @@ This is a binary protocol. The keys should be encoded as valid UTF-8 strings tho
### Messages
TCP messages are not framed to distinct messages by themselves. Instead, we need to implement message borders ourselves.


Memcrab messages contain a header of a fixed length, and then payload of variable length.

The first byte of the header encodes the kind of message,
the remaining 8 bytes encode the payload length as u64 (also known as `PayloadLen`).

The first byte of the header encodes the kind of message. The rest of the header encodes the information about the lengths of the payload or other metainfo.


Message kinds are shared by all messages for client and server. Clients should only send request messages and understand responses messages however, vice versa.


| Message kind | first byte | rest of the header | payload
| --- | --- | --- | ---
| VersionRequest | 0 | version | none
| PingRequest | 1 | none | none
| GetRequest | 2 | klen | key
| SetRequest | 3 | klen, vlen, exp | key, value
| DeleteRequest | 4 | klen | key
| ClearRequest | 5 | none | none
The header length is `9` bytes.

| PongResponse | 128 | none | none
| OkResponse | 129 | none | none
| ValueResponse | 130 | vlen | value
| KeyNotFoundResponse | 131 | none | none
| ErrorResponse | 132 | vlen | value
Message kinds are shared by all messages for client and server.
Clients should only send request messages and understand responses messages however, vice versa.

### Some type definitions
```rs
type PayloadLen = u64; // number of bytes in payload

The lengths of fields for klen, vlen, version, etc are as follows:


| header field | size (bytes) |
| --- | --- |
| klen | 8 |
| vlen | 8 |
| version | 2 |
| exp | 4 |

The header length is 21 bytes.
type Version = u16; // protocol-version
type KeyLen = u64; // number of bytes in the encoded utf8 string key
type Expirtaion = u32; // expiration in seconds
```

### Mapping
| Message kind | first byte | remaining 8 bytes in header | payload
| --- | --- | --- | ---
| VersionRequest | 0 | PayloadLen | Version
| PingRequest | 1 | zeros | none
| GetRequest | 2 | PayloadLen | key
| SetRequest | 3 | PayloadLen | KeyLen, Expirtaion, key, value
| DeleteRequest | 4 | PayloadLen | key
| ClearRequest | 5 | zeros | none
| PongResponse | 128 | zeros | none
| OkResponse | 129 | zeros | none
| ValueResponse | 130 | PayloadLen | value
| KeyNotFoundResponse | 131 | zeros | none
| ErrorResponse | 255 | PayloadLen | value (utf8 encoded)

### Versioning
Protocol is versioned by a number and are not backwards compatible.

The current version is `0`.

The clients must send `Version` message as their first message. The server must close the connection if the version is not compatible.


3 changes: 2 additions & 1 deletion memcrab-protocol/src/alias.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub type PayloadLen = u64;

pub type Version = u16;
pub type KeyLen = u64;
pub type ValueLen = u64;
pub type Expiration = u32;
7 changes: 6 additions & 1 deletion memcrab-protocol/src/err.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::{array::TryFromSliceError, string::FromUtf8Error};

use thiserror::Error;

#[derive(Error, Debug)]
Expand All @@ -15,8 +17,11 @@ pub enum ParseError {
UnknownMsgKind,

#[error("malformed string")]
InvalidString,
InvalidString(#[from] FromUtf8Error),

#[error("message is too big")]
TooBig,

#[error("conversion from a slice to an array fails")]
TryFromSlice(#[from] TryFromSliceError),
}
2 changes: 1 addition & 1 deletion memcrab-protocol/src/kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use num_enum::{IntoPrimitive, TryFromPrimitive};

#[repr(u8)]
#[derive(Debug, Clone, Copy, TryFromPrimitive, IntoPrimitive)]
pub enum MessageKind {
pub enum MsgKind {
VersionRequest = 0,
PingRequest = 1,
GetRequest = 2,
Expand Down
26 changes: 21 additions & 5 deletions memcrab-protocol/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,29 @@ mod alias;
mod err;
mod io;
mod kind;
mod message;
mod sizes;
mod msg;
mod parser;
mod socket;
mod version;

use std::mem::size_of;

use alias::Version;
use parser::Parser;

pub use err::{Error, ParseError};
pub use io::{AsyncReader, AsyncWriter};
pub use message::{Message, Request, Response};
pub use msg::{Msg, Request, Response};
pub use socket::Socket;
pub use version::VERSION;

const HEADER_SIZE: usize = size_of::<u8>() + size_of::<u64>();
pub const VERSION: Version = 0;

#[cfg(test)]
mod tests {
use crate::HEADER_SIZE;

#[test]
fn test_size() {
assert_eq!(HEADER_SIZE, 9);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::alias::{Expiration, Version};

#[derive(Debug, Clone, PartialEq)]
pub enum Message {
pub enum Msg {
Request(Request),
Response(Response),
}
Expand Down
109 changes: 109 additions & 0 deletions memcrab-protocol/src/parser.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
use std::{mem::size_of, string::FromUtf8Error};

use itertools::chain;

use crate::{
alias::{Expiration, KeyLen, PayloadLen, Version},
kind::MsgKind,
Msg, ParseError, Request, Response, HEADER_SIZE,
};

#[derive(Clone, Debug)]
pub struct Parser;

impl Parser {
pub fn decode_header(
&self,
chunk: &[u8; HEADER_SIZE],
) -> Result<(MsgKind, PayloadLen), ParseError> {
let kind = MsgKind::try_from(chunk[0]).map_err(|_| ParseError::UnknownMsgKind)?;
let payload_len = PayloadLen::from_be_bytes(chunk[1..].try_into()?);
Ok((kind, payload_len))
}
}

type Payload = Vec<u8>;

impl Parser {
pub fn decode(&self, kind: MsgKind, payload: Payload) -> Result<Msg, ParseError> {
use MsgKind as Kind;
Ok(match kind {
Kind::VersionRequest => {
let version = Version::from_be_bytes(payload.as_slice().try_into()?);
Msg::Request(Request::Version(version))
}
Kind::PingRequest => Msg::Request(Request::Ping),
Kind::GetRequest => Msg::Request(Request::Get(utf8(payload)?)),
Kind::SetRequest => {
let (klen_bytes, tail) = payload.split_at(size_of::<KeyLen>());
let (exp_bytes, tail) = tail.split_at(size_of::<Expiration>());

let klen = KeyLen::from_be_bytes(klen_bytes.try_into()?);
let expiration = Expiration::from_be_bytes(exp_bytes.try_into()?);

let (key, value) = tail.split_at(klen as usize);
let key = utf8(key)?;
Msg::Request(Request::Set {
key,
value: value.to_vec(),
expiration,
})
}
Kind::ClearRequest => Msg::Request(Request::Clear),
Kind::DeleteRequest => Msg::Request(Request::Delete(utf8(payload)?)),

Kind::OkResponse => Msg::Response(Response::Ok),
Kind::PongResponse => Msg::Response(Response::Pong),
Kind::KeyNotFoundResponse => Msg::Response(Response::KeyNotFound),
Kind::ValueResponse => Msg::Response(Response::Value(payload)),
Kind::ErrorResponse => Msg::Response(Response::Error(utf8(payload)?)),
})
}
}

impl Parser {
// (kind + payload_len) + payload
pub fn encode(&self, msg: Msg) -> Vec<u8> {
let (kind, payload) = match msg {
Msg::Request(req) => self.encode_request(req),
Msg::Response(resp) => self.encode_response(resp),
};
let payload_len = (payload.len() as u64).to_be_bytes();
chain!([kind.into()], payload_len, payload).collect()
}

fn encode_request(&self, req: Request) -> (MsgKind, Payload) {
match req {
Request::Version(version) => (MsgKind::VersionRequest, version.to_be_bytes().to_vec()),
Request::Ping => (MsgKind::PingRequest, vec![]),
Request::Clear => (MsgKind::ClearRequest, vec![]),
Request::Get(key) => (MsgKind::GetRequest, key.into()),
Request::Delete(key) => (MsgKind::DeleteRequest, key.into()),
Request::Set {
key,
value,
expiration,
} => {
let key = Vec::from(key);
let klen = (key.len() as KeyLen).to_be_bytes();
let exp = expiration.to_be_bytes();

let payload = chain!(klen, exp, key, value).collect();
(MsgKind::SetRequest, payload)
}
}
}
fn encode_response(&self, resp: Response) -> (MsgKind, Payload) {
match resp {
Response::Ok => (MsgKind::OkResponse, vec![]),
Response::Pong => (MsgKind::PongResponse, vec![]),
Response::KeyNotFound => (MsgKind::KeyNotFoundResponse, vec![]),
Response::Value(val) => (MsgKind::ValueResponse, val),
Response::Error(emsg) => (MsgKind::ErrorResponse, emsg.into()),
}
}
}

fn utf8(bytes: impl Into<Vec<u8>>) -> Result<String, FromUtf8Error> {
String::from_utf8(bytes.into())
}
11 changes: 0 additions & 11 deletions memcrab-protocol/src/sizes.rs

This file was deleted.

Loading

0 comments on commit 65eec9d

Please sign in to comment.