DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones AWS Cloud
by AWS Developer Relations
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones
AWS Cloud
by AWS Developer Relations
The Latest "Software Integration: The Intersection of APIs, Microservices, and Cloud-Based Systems" Trend Report
Get the report
  1. DZone
  2. Software Design and Architecture
  3. Performance
  4. Rust-Based Load Balancing Proxy Server With Async I/O

Rust-Based Load Balancing Proxy Server With Async I/O

My problem is that every single change that I made in the code had repercussions down the line that were hard for me to predict.

Oren Eini user avatar by
Oren Eini
·
Jan. 16, 17 · Opinion
Like (0)
Save
Tweet
Share
6.56K Views

Join the DZone community and get the full member experience.

Join For Free

In my previous Rust post, I built a simple echo server that spun a whole new thread for each connection. In this one, I want to do this in an async manner. Rust doesn’t have the notion of async/await, or something similar to go green threads (it seems that it used to, and it was removed as a costly abstraction for low-level system languages).

I’m going to use Tokio.rs to do that, but sadly enough, the example on the front page is about doing an async echo server. That kinda killed the mood for me there since I wanted to deal with actually implementing it from scratch. Because of that, I decided to do something different and build an async Rust based TCP level proxy server.

Expected usage: cargo run live-test.ravendb.net:80 localhost:8080.

This should print the port that this proxy runs on and then route the connection to one of those endpoints.

This led to something pretty strange. Check out the following code:

image

Can you figure out what the type of addr is? It is inferred, but from what? The addr definition line does not have enough detail to figure it out. Therefore, the compiler actually goes down and sees that we are passing it to the bind() method, which takes std::net::SocketAddra value. So, it figures out that the value must be a std::net::SocketAddr.

This seems to be utterly backward and fragile to me. For example, I added this:

image

The compiler was very upset with me:

image

I’m not used to the variable type being impacted by its usage. It seems very odd and awkward. It also seems to be pretty hard to actually figure out what the type of a variable is from just looking at the code. Also, there isn’t an easy way to get it short of causing an intentional compiler error that would reveal those details.

The final code looks like this:

#![feature(lookup_host)]
​
extern crate futures;
extern crate tokio_core;
extern crate rand;
​
use rand::Rng;
use std::env;
use futures::{Future, Stream};
use tokio_core::io::{copy, Io};
use tokio_core::net::TcpListener;
use tokio_core::reactor::Core;
use tokio_core::net::TcpStream;
​
fn main() {
let mut core = Core::new().unwrap();
let handle = core.handle();
​
let addr = "127.0.0.1:0".parse().unwrap();
let sock = TcpListener::bind(&addr, &handle).unwrap();
​
println!("Listening on 127.0.0.1:{}",
sock.local_addr().unwrap().port());
​
let mut urls: Vec<std::net::SocketAddr> = Vec::new();
​
for url in env::args().skip(1) {
let parts: Vec<&str> = url.split(":").collect();
print!("{} was resolved to: ", parts[0]);
for mut host in std::net::lookup_host(parts[0]).unwrap() {
host.set_port(parts[1].parse().unwrap());
print!(" {},", host);
urls.push(host);
}
println!();
}
​
let server = sock.incoming().for_each(|(client_stream, remote_addr)| {
​
let index = rand::thread_rng().gen_range(0, urls.len());
​
let (client_read, client_write) = client_stream.split();
​
println!("{} connected and will be forwarded to {}", &remote_addr, &urls[index]);
​
let send_data = TcpStream::connect(&urls[index], &handle).and_then(|server_stream| {
let (server_read, server_write) = server_stream.split();
​
let client_to_server = copy(client_read, server_write);
let server_to_client = copy(server_read, client_write);
​
client_to_server.join(server_to_client)
})
// erase the types
.map(|(_client_to_server,_server_to_client)| {} ).map_err(|_err| {} );
​
handle.spawn(send_data);
​
Ok(())
});
​
core.run(server).unwrap();
}

At the same time, there is a lot going on here and this is very simple.

Lines 1-15 are really not interesting. Lines 17-29 are about parsing the user’s input, but the fun stuff begins from line 30 and onward.

I use fun cautiously; it wasn’t very fun to work with, to be honest. On lines 30 and 31, I setup the event loop handlers. And then bind them to a TCP listener.

On lines 40-62, I’m building the server (more on that later) and on line 64, I’m actually running the event loop.

The crazy stuff is all in the server handling. The incoming().for_each() call will call the method for each connected client, passing the stream and the remote IP. I then split the TCP stream into a read half and a write half, and select a node to load the balance to.

Following that, I’m doing an async connect to that node, and if it is successful, I’m splitting the server and then reverse them using the copy methods. Basically, I'm attaching the input and output of each to the other side. Finally, I’m joining the two together, so we’ll have a future that will only be done when both sending and receiving is done, and then I’m sending it back to the event loop.

Note that when I’m accepting a new TCP connection, I’m not actually pausing to connect to the remote server. Instead, I’m going to setup the call and then pass the next stage to the event loop (the spawn) method.

This was crazy hard to do and generated a lot of compilation errors along the way. Why? See line 57, where we erase the types?

The type of  send_data without this line is something like Future<Result<(u64,u64), Error>>. However, the map and map_err turn it into just a Future. If you don’t do that? Well, the compiler errors are generally very good, but it seems that inference can take you into la-la land. See this compiler error. That reminds me of trying to make sense of C++ template errors in 1999.

image

Now, here is the definition of the spawn method:

image

I didn’t understand this syntax at all. Future is a trait, and it has associated types, but I’m thinking about generics as only the stuff inside the <>, so that was pretty confusing.

Basically, the problem was that I was passing a future that was returning values, while the spawn method expected one that was expecting none.

I also tried to change the and_then to just then, but at that point I got:

image

At which point I just quit.

However, just looking at the code on its own, it is quite nicely done, and it expresses exactly what I want it to. My problem is that every single change that I made there had repercussions down the line, which is hard for me to predict.

Load balancing (computing)

Published at DZone with permission of Oren Eini, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Popular on DZone

  • What Is JavaScript Slice? Practical Examples and Guide
  • Rust vs Go: Which Is Better?
  • mTLS Everywere
  • Specification by Example Is Not a Test Framework

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: