Proddle: Rust and Networking

2017-04-06 15:15:52

If you're unfamiliar with my project, Proddle, please visit the site and take a gander. If you're still interested please contact me to get involved!

Using Rust for Proddle

I've chosen to write the application using Mozilla's Rust. It's a newer language focused on providing low level operation combined with extremely safe variable access. It does this through it's integration of a "lifetime" paradigm, where each variable has a duration it's active for. The lifetime is automatically identified by the compiler and analyzed for safety infractions. A deeper analysis is not in the scope of this post, but I'll forward you to the official rustbook where the language is extensively documented. It's a great starting point.

Capnproto & Tokio

Upon original inception of Proddle I chose to use capnproto for message format, as the next iteration of Google's protobuf (written by the same developer) it seemed like the logical choice. Everything worked great until the rust crate for capnproto switched internally to using a all encompassing networking crate tokio. As of right now the tokio project is the bane of my existance. This is mostly based on an unclosed file descriptor bug (which I thought impossible with Rust). I understand that the project is in it's infancy, but the documentation is terrible. I found myself scouring through the source code to "understand" certain constructs.

I'm not going to dive into internals of the project, because I've put that phase of my life behind me. Instead I'll provide a brief overview. The entire framework is built upon event loops and futures. Yes, even the client. An event loop is basically a for loop listening on a channel for events. Futures are closures which allow for the result to be passed into code before it's actually computed. Both constructs are fundamental for server programming (albiet most frameworks make them transparent). Tokio exposes the user to everything, needlessley adding complexity. The tokio-proto crate is provided to obfuscate deep internals, but in my experience it falls short as well.

I don't want my ramblings to be misinterpreted as malice. I think the tokio project provides a solid networking foundation. I think that the work, and all work of Alex Crichton is improving the foundation of Rust. Which I am a strong advocate. It's just that the complex framework is NOT an uniform solution for all network applications. Tokio provides mechanisms for pipelined vs streamed protocols, for multiplexing traffic, among others. This is functionality that is not required for simple projects. At this stage, the internals are quite under-developed and noticably under-documented. I will continue to follow the project and expect nothing but improvements.

Retreat to Tcp Sockets

Admitting defeat in the realm of all things tokio I opted to fallback to trusty tcp sockets. In the standard library Rust provides access similar to that of C/C++. The serde framework provides seamless serialization of Rust structs to many formats including json, bson, bincode, etc. I believe it's used internally in the capnproto Rust crate. I opted to use the bincode as my transport format. The sample struct definiton is provided below.

    
        extern crate serde;
        #[macro_use]
        extern crate serde_derive;

        #[derive(Deserialize, Serialize)]
        struct Message {
            foo: Option,
            bar: u64,
        }
    

This is a very simple struct with just two fields. Below is the code I'm using in Proddle to read/write through a tcp socket.

    
        extern crate bincode;

        use std::io::{Read, Write};
        use std::net::TcpStream;

        pub fn message_to_stream(message: &Message, stream: &mut TcpStream) -> Result<(), ProddleError> {
            let encoded: Vec = bincode::serialize(message, Infinite).unwrap();
            let length = encoded.len() as u32;

            try!(stream.write(&[(length as u8), ((length >> 8) as u8), ((length >> 16) as u8), ((length >> 24) as u8)]));
            try!(stream.write_all(&encoded));
            try!(stream.flush());

            Ok(())
        }

        pub fn message_from_stream(stream: &mut TcpStream) -> Result {
            let mut length_buffer = vec![0u8; 4];
            try!(stream.read_exact(&mut length_buffer));
            let length = ((length_buffer[0] as u32) | ((length_buffer[1] as u32) << 8) | ((length_buffer[2] as u32) << 16) | ((length_buffer[3] as u32) << 24)) as usize;

            let mut byte_buffer = vec![0u8; length];
            try!(stream.read_exact(&mut byte_buffer));
            let message = try!(bincode::deserialize(&byte_buffer));
            Ok(message)
        }
    

As you can see in the message to stream function we serialize the Message struct using the bincode serialize method, it should be noted that this is available because we've derived the Deserialize/Serialize traits on the Message struct above. The we write a u32 length field to the stream followed by the encoded buffer. The read function just does the opposite. I know, creating a new buffer for reading the length and actual bytes is bad practice. I'll fix this in the future.

Ending Thoughts

There you have a simple, lightweight alternative to many complex networking frameworks. With this change I was able to remove 6 crate dependencies of the project (2 capnproto, 4 tokio). I'm a strong believer that the more code you include, the more things that can break.

tag(s): proddle rust tutorial