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 Future
s 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 move
ed 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 Stream
s and sending output using Sink
s. 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)