Recently, as I’m progressing in learning of Rust, I wondered how asynchronous programing is done in Rust. I decided to remake my old project ptunnel (written in Python) into Rust – ptunnel is a program that tunnels arbitrary connection/protocol through HTTPS proxy, so it can be used to connect IMAP, SMTP or SSH through proxy. In the rest of this article I”l share my experiences from this project.
Rust is relatively young language and asynchronous programing in Rust is even younger – now most popular library is Tokio, which is still in version 0.1, so basically in its infancy. But site contains good documentation and guidance so it’s was relatively easy to get started.
The Long And Winding Road
I have to admit I’m still struggling with some Rust concepts (lifetime mainly) and Tokio was quite demanding for sticking with core concepts. Tokio is using intensively futures module and all programing and data processing is done through chaining of futures – this can create very complex types ( especially when anonymous closures are used) and thus provide very cryptic errors ( with type description over half of the screen – reminding me monstrous types in OCAML Ocsigen libraries ). In order to make various complex Future types compatible with rest of the program is quite often necessary to create trait objects – e.g. to box values. There is also one hideous features of rustc, errors concerning lifetime of variables are reported as last – when all more ‘serious’ errors are resolved. So often it happened to me that I had removed one final error in code only to find that others pop up and I had found that I had screwed lifetime of some variables/parameters and that it couldn’t be easily fixed and had to redesign code.
Generally I noticed that while previously in other languages I was able to start and follow a single, relatively straight path from idea to final code and was able to progress steadily further, here in async Rust I had to backtrack several times, because the way lead nowhere. It looks like there are only few ways how to make it right and if you do not catch them initially you are lost. Hopefully those ways should lead to better code.
Tokio provides nice abstractions for higher level protocols (transports + protocols + services), which I believe can help in other cases, but here I needed something rather low level – so finally I ended up with using custom stream and future types, after learning a bit about Tokio internals ( stressing again good documentation of Tokio) and chaining futures with
map (modifying future value),
and_then ( creating new future from results of previous one) and
or_else (resolving error cases)
One of most attractive features of Rust is ‘zero-costs’ abstraction. Rust provides higher level language abstractions like algebraic types, iterators etc., which are then implemented in very effective way – basically with same efficiency as if written in lower abstraction level language like C. Tokio abstractions (on lower level, where I was dwelling) are mainly iterator like traits of Streams and Sinks and future combinators. So after finally being able to cope with these I was wondering how final code will perform and if it’ll match ‘zero-costs’ promise.
Comparison to Python Version
I have to say that old Python program was a poor contender. It’s written in very basic manner and not using any features that could improve performance ( at least thread pool, or asyncio) and it creates (and destroys) two threads for each connection.
So rather then fair benchmark it’s just a toy comparison, just to assure that Rust program is doing better. And it is doing better indeed, much better as we see below.
ab <-> ptunnel <-> local squid <-> local nginx (default page)
And result is ( average time per request in ms):
|Old Python Tool||10.51 ms||1003.75 ms|
|New Rust Tool||0.31 ms||3.28 ms|
|Without ptunnel||1.83 ms||2.55 ms|
As you can see old python tool is no match – especially when no keep alive is used and thus overhead of thread creation is preposterous ( keep-alive case Python was fine on majority of requests, but few, where connection was created, time was again around 1 sec due to thread creation overhead).
For comparison there is also scenario where ab connects directly to squid (ptunnel is left out) – there is interesting keep-alive case, where setup with ptunnel is faster.
Even if this benchmark is simplistic it still shows that Rust program keeps to its promise and seems to perform well ( direct benchmark between ab and nginx gives 0.22ms per request).
Looking at code base Rust program is about 3 times bigger then old Python program ( ~ 600 lines vs ~ 200 lines).
The code is available on the github, where kind reader can review it, README.md file contains instructions for installation. New Rust program functionality and CLI copies basically old python program, only new feature available is basic authentication with proxy.