DEV Community

jtenner
jtenner

Posted on • Updated on

Rust Nightly Telnet Echo Server Example

Rust is a tough language to learn, but it teaches you in a trial by fire most developers would call "the grind." Learning how to use the latest async syntax with Futures has certainly been quite a challenge, but I genuinely love working in rust now.

Here in this blog post, I've documented the source code I've written with a small narrative describing each part of a simple telnet echo server. Hopefully, I can also provide some feedback on the development process too, since async function in rust are so new.

So let's break down setting up a tcp server to emit and receive telnet events. This of course is trivial task provided you have a codec, and are working with bleeding edge rust. The ink real challenge is working with minimal documentation and nightly rust-docs.

In the following example, we use a TelnetCodec (which hasn't been open sourced yet!) For now we can treat it like a black box that provides an abstraction layer for processing telnet events.

use tokio::net::TcpListener;
use tokio::prelude::*;
use tokio::codec::Decoder;

mod telnet_codec;
use telnet_codec::*;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
  let mut listener = TcpListener::bind("127.0.0.1:7000").await?;
  println!("listening on port 7000");
  loop {
    let (socket, _) = listener.accept().await?;
    tokio::spawn(async move {
      let codec = TelnetCodec::new(4096_u16);
      let (mut sink, mut input) = codec.framed(socket).split();
      while let Some(Ok(event)) = input.next().await {
        println!("Event {:?}", event);
        match event {
          TelnetEvent::Message(value) => {
            println!("Echoing message: {}", value);
            if let Err(error) = sink.send(TelnetEvent::Message(value)).await {
              println!("An error occured {}", error);
            }
          },
          _ => {
            // nop
          }
        }
      }
    });
  }
}

This example is pretty bare bones, and a similar example using an older vesion of tokio would be way more complicated, but in this case it's very easy to see what is happening.

Let's break it down line by line.

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {

Tokio defines a directive that lets us label our application entry point as async. It works like a decorator and all the details of hooking up a regular main() function are delegated to tokio itself.

let mut listener = TcpListener::bind("127.0.0.1:7000").await?;

Here, we create a tcp listener that is bound to port 7000. The interesting part about this line is the last part which uses the new await syntax. It looks like a property access instead of a first class keyword, however, it still does the same thing. It dispatches a task that will be completed, and returns a Result<(), Box<dyn std::error::Error>>. If an error occurs, the process exits, displaying the error. The ? operator at the end of the expression causes the main() function to return with the error if an error exists. This error will be displayed on stderr.

loop {
  let (socket, _) = listener.accept().await?;
  tokio::spawn(async move {

Now the main task will simply wait and poll the listener to accept tcp connections.

The socket is extracted from from the future by using the .await? syntax, then it is moveed into the async closure. Using an async move closure allows us to define a task called a Future that will be managed by the tokio reactor. After Tokio::spawn is called, it will add this newly created future to be processed whenever cpu can be used.

At this point, we need a way to process the incoming tcp connections. In particular we are going to use a Codec to process the incoming bytes. Each of the incoming bytes needs to be parsed using an Encoder and a Decoder which is provided by the codec.

let codec = TelnetCodec::new(4096_u16);

This particular codec requires an underlying buffer for each socket, so passing it 4096 notifies the TelnetCodec that it should only retain 4096 bytes before truncating messages.

As for the actual telnet messages, the codec provides an enum that encapsulates all the message types. This encapsulation makes it possible to communicate with the tcp socket using TelnetEvent enum values.

#[derive(Debug,PartialEq)]
pub enum TelnetEvent {
  Do(TelnetOption),
  Dont(TelnetOption),
  Will(TelnetOption),
  Wont(TelnetOption),
  Subnegotiation(TelnetOption, Vec<u8>),
  Message(String),
  Nop,
}

If you happen to be familiar with the way telnet is encoded, this list of events should be familiar to you. In short, Message events send virtual terminal stdin and stdout bytes to the client or server. In order to return the "frames" (or TelnetEvent enum objects,) we ask codec to take ownership of the socket, and let it handle parsing input using Streams and sending output using Sinks. Calling .split() will return both the Sink and the Stream associated with this codec, so they can be seperated.

let (mut sink, mut input) = codec.framed(socket).split();

In order to obtain the incoming events, we use the .await syntax again, this time handling errors manually because the connection doesn't always need to close when an error occurs.

while let Some(Ok(event)) = input.next().await {

Next, match the events and log them out to stdout.

println!("Event {:?}", event);
match event {
  TelnetEvent::Message(value) => {

To echo the Message events, we pass the value string to the sink in a Message event. Sending a TelnetEvent to the sink returns a future because sending will happen asynchronously. Again, we use the .await syntax check to see if an error occured.

if let Err(error) = sink.send(TelnetEvent::Message(value)).await {

Finally, log any errors that occur using println!. That's it! Now, all that's left is to process the incoming event stream.

This concludes how to setup a Tcp telnet server using the latest tokio. The next blog post of this series will go over how to create a codec, and what they are.

Thanks for reading!

Top comments (0)