Crates.io | middleman |
lib.rs | middleman |
version | 0.3.1 |
source | src |
created_at | 2018-04-19 15:43:47.040684 |
updated_at | 2018-05-05 18:01:08.191347 |
description | A wrapper for a mio::TcpStream to send and receive serializable Structs (blocking or non blocking) of arbitrary size |
homepage | |
repository | https://github.com/sirkibsirkib/middleman |
max_upload_size | |
id | 61432 |
size | 47,768 |
Middleman
is a library for sending and receiving serializable data structures over a TCP connection, abstracting away from the raw bytes. This project draws inspiration from an older library, wire
, but is intended to play nicely with the mio
polling system.
struct M: Message
↑ ╷
┆ ┆
Middleman (⌐■_■)
TCP
~~~~~~~~~
▲ ▼
▲ bytes ▼
▲ ▼
~~~~~~~~~
TCP
Middleman (⌐■_■)
┆ ┆
╵ ↓
struct M: Message
If you want to build some kind of Tcp-based network program, you'll need to do a few things. Many of these are in common with mio
, but let's start somewhere. For our example, I will consider the case of setting up a single client-server connection for a baseline.
Before we begin, this is at the core of what you conceptually want on either end of a communication link:
Middleman
that exposes non-blocking functions send(&T)
and recv() -> Option<T>
, where T
is the type of the message structure(s) you wish to send over the network. Easy.Old versions of middleman
stopped there. This always presented the problem: When should you call recv
? The naive solution is just to keep calling it all the time. Thanksfully, mio
exists to help with exactly that. It relies on polling to lazily do work and unblock when something can potentially progress. So here we see how to get this all working together smoothly:
serde
. I would suggest relying on the macros in serde_derive
.middleman::Message
for your messages.
All in all it may leave things looking like this:#[derive(Serialize, Deserialize)]
enum MyMsg {
// some examples of variants. All irrelevant to `Middleman`
SayHello,
SendGameState(Box<GameState>),
Coordinate(f32, f32),
...
}
impl middleman::Message for MyMsg {}
mio::net::TcpStream
object connected to the relevant peer(s). This is stuff not unique to middleman
.Middleman
.Poll
objects (as you would with the mio::TcpStream
itself).Middleman::recv
at the appropriate time. Your job is to ensure that you always drain all the waiting messages. recv
will never block, so feel free to spuriously try and recv something.send
as necessary.That's it. The flow isnt' very different from that of the typical Tcp setting. The bulk of the work involves getting your Poll, Middleman and TcpStream objects to all work nicely together. For more detailed examples, see the tests.
When implementing high level algorithms, one likes to think not of bytes and packets, but rather of discrete messages. Enums and Structs are more neat mappings to these theoretical constructs than byte sequences are. Middleman aims to hide all the byte-level stuff, but hide nothing more.
Someone familiar with the use of mio
for using the select-loop-like construct to poll the progress of one or more Evented
structures will see the use of middleman
doesn't change much.
At a high level, your code may look something like this:
let poll = ...
... // setup other mio stuff
let mut mm = Middleman::new(tcp_stream);
poll.register(&mm, MIDDLEMAN_TOK, ...).unwrap();
loop {
poll.poll(&mut events, ... ).unwrap();
for event in events.iter() {
match event.token() {
MIDDLEMAN_TOK => {
if mm.recv_all_map<_, MyType>(|mm_ref, msg| {
// do something with `msg`
}).1.is_err() {
// handle errors
}
},
...
_ => unreachable!(),
}
}
}
There are ways of approaching how to precisely get at the messages, when to deserialize them and what to do next,
but this is the crux of it: When you get a notification from poll, you try to read all waiting messages and handle them. That's it. At any point you can send a message the other way using mm.send::<MyType>(& msg)
. No extra threads are needed. No busy-waiting spinning is required (thanks to mio::Poll
).
recv_blocking
mio
is asynchronous and non-blocking by nature. However, sometimes a blocking receive is a more ergonomic fit, for cases where exactly one message is expected. Functions recv_blocking
and recv_blocking_solo
exist as a compact means of hijacking the polling loop flow temporarily until a message is ready. See the documentation for more details and see the tests for some examples.
This library concentrates on flexibility. Messages of the same type can be represented with different sizes at runtime (eg: an empty hashmap takes fewer bytes than a full one). You don't have much to fear as far as the byte-size of your messages are concerned, but still watch out for the effect some pathological cases may have on the size in memory.
#[derive(Serialize, Deserialize)]
enum Large {
A([u64; 32]),
B(bool),
}
impl Message for Large {}
fn test() {
let packed = PackedMessage::new(& Large::B(true)).unwrap();
println!("packed bytes {:?}", packed.byte_len());
println!("memory bytes {:?}", ::std::mem::size_of::<Large>());
}
Calling test
may print:
packed bytes 9
memory bytes 264