TE
TechEcho
Home24h TopNewestBestAskShowJobs
GitHubTwitter
Home

TechEcho

A tech news platform built with Next.js, providing global tech news and discussions.

GitHubTwitter

Home

HomeNewestBestAskShowJobs

Resources

HackerNews APIOriginal HackerNewsNext.js

© 2025 TechEcho. All rights reserved.

Sans-IO: The secret to effective Rust for network services

233 pointsby wh33zle11 months ago

17 comments

ComputerGuru11 months ago
This is billed as something revolutionary and forward progress but that’s exactly how we used to do async in $lang - including Rust - before language support for async&#x2F;await landed.<p>The biggest productivity boost to my rust embedded firmware development was when I could stop manually implementing state machines and marshalling all local variables into custom state after custom state between each I&#x2F;O operation snd let rust do that for me by using async&#x2F;await syntax!<p>That’s, after all, what async desugars to in rust: an automatic state machine that saves values across I&#x2F;O (await) points for you.
评论 #40872448 未加载
评论 #40875419 未加载
评论 #40875231 未加载
评论 #40872452 未加载
评论 #40875126 未加载
zamalek11 months ago
I had been mulling over this problem space in my head, and this is a seriously great approach to the direction I have been thinking (though still needs work, footnote 3 in the article).<p>What got me thinking about this was the whole fn coloring discussion, and a happy accident on my part. I had been writing a VT100 library and was doing my head in trying to unit test it. The problem was that I was essentially `parser::new(stdin())`. During the 3rd or 4th rewrite I changed the parser to `parser::push(data)` without really thinking about what I was doing. I then realized that Rust was punishing me for using an enterprise OOPism anti-pattern I have since been calling &quot;encapsulation infatuation.&quot; I now see it <i>everywhere</i> (not just in I&#x2F;O) and the havoc it wreaks.<p>The irony is that this solution is taught pre-tertiary education (and again early tertiary). The simplest description of a computer is a machine that takes input, processes&#x2F;transforms data, and produces output. This is relevant to the fn coloring discussion because only input and output need to be concerned with it, and the meat-and-potatoes is usually data transformation.<p>Again, this is patently obvious - but if you consider the size of the fn coloring &quot;controversy;&quot; we&#x27;ve clearly all been missing&#x2F;forgetting it because many of us have become hard-wired to start solving problems by encapsulation first (the functional folks probably feel mighty smug at this point).<p>Rust has seriously been a journey of more unlearning than learning for me. Great pattern, I am going to adopt it.<p>Edit: code in question: <a href="https:&#x2F;&#x2F;codeberg.org&#x2F;jcdickinson&#x2F;termkit&#x2F;src&#x2F;branch&#x2F;main&#x2F;src&#x2F;vt&#x2F;scanner.rs" rel="nofollow">https:&#x2F;&#x2F;codeberg.org&#x2F;jcdickinson&#x2F;termkit&#x2F;src&#x2F;branch&#x2F;main&#x2F;src...</a>
评论 #40873759 未加载
评论 #40874006 未加载
ziziman11 months ago
How does this design compare to using channels to send data to a dedicated handlers. When using channels i&#x27;ve found multiple issues: (1) Web-shaped code that is often hard to follow along (2) Requires to manually implement message types that can then be converted to network-sendable messages (3) Requires to explicitly give a transmitter to interested&#x2F;allowed entities (4) You get a result if your channel message failed to transmit but NOT if your message failed to transmit over network<p>But besides that it&#x27;s pretty convenient. Let&#x27;s say you have a ws_handler channel, you just send your data through that and there is a dedicated handler somewhere that may or may not send that message if it&#x27;s able to.
评论 #40873339 未加载
评论 #40872995 未加载
hardwaresofton11 months ago
See also: monads and in particular the Free(r) monad, and effects systems[0].<p>The idea of separating logic from execution is a whole thing, well trodden by the Haskell ecosystem.<p>[EDIT] Also, they didn&#x27;t mention how they encapsulated the `tokio::select!` call that shows up when they need to do time-related things -- are they just carrying around a `tokio::Runtime` that they use to make the loop code async without requiring the outside code to be async?<p>[EDIT2] Maybe they weren&#x27;t trying to show an encapsulated library doing that, but rather to show that the outside application can use the binding in an async context...<p>I would have been more interested in seeing how they could implement an encapsulated function in the sans-IO style that had to do something like wait on an action or a timer -- or maybe the answer they&#x27;re expecting there is just busy-waiting, or carrying your own async runtime instance (that can essentially do the busy waiting for you, with something like block_in_place.<p>[0]: <a href="https:&#x2F;&#x2F;okmij.org&#x2F;ftp&#x2F;Computation&#x2F;free-monad.html" rel="nofollow">https:&#x2F;&#x2F;okmij.org&#x2F;ftp&#x2F;Computation&#x2F;free-monad.html</a>
评论 #40874048 未加载
r3trohack3r11 months ago
Oh hey thomaseizinger!<p>I got half way through this article feeling like this pattern was extremely familiar after spending time down inside rust-libp2p. Seems like that wasn&#x27;t a coincidence!<p>Firezone looks amazing, connect all the things!
评论 #40872470 未加载
amluto11 months ago
&gt; Also, sequential workflows require more code to be written. In Rust, async functions compile down to state machines, with each .await point representing a transition to a different state. This makes it easy for developers to write sequential code together with non-blocking IO. Without async, we need to write our own state machines for expressing the various steps.<p>Has anyone tried to combine async and sans-io? At least morally, I ought to be able to write an async function that awaits sans-io-aware helpers, and the whole thing should be able to be compiled down to a state machine inside a struct with a nice sans-io interface that is easily callable by non-async code.<p>I’ve never tried this, but the main issues I would forsee would be getting decent ergonomics and dealing with Pin.
评论 #40872859 未加载
评论 #40872897 未加载
评论 #40872691 未加载
评论 #40872760 未加载
评论 #40879564 未加载
评论 #40875092 未加载
评论 #40873347 未加载
评论 #40873367 未加载
ethegwo11 months ago
Good job! Exposing state could make any async function &#x27;pure&#x27;. All the user needs to do is push the state machine to the next state. I have tried to bind OpenSSL to async Rust before, its async API follows a similar design.
评论 #40872293 未加载
mgaunard11 months ago
This is just normal asynchronous I&#x2F;O with callbacks instead of coroutines.
mpweiher11 months ago
Reading the article and some of the comments, it sounds like they reinvented the hexagonal or ports&#x2F;adapters architectural style?
Uptrenda11 months ago
I don&#x27;t know what the take away is supposed to be here. Everything spoken about here is already basic network programming. It seems to focus on higher level plumbing and geeks out on state management even though this is just a matter of preference and has nothing to do with networking.<p>The most interesting thing I learned from the article is that cloudflare runs a public stun server. But even that isn&#x27;t helpful because the &#x27;good&#x27; and &#x27;useful&#x27; version of the STUN protocol is the first version of the protocol which supports &#x27;change requests&#x27; -- a feature that allows for NAT enumeration. Later versions of the STUN protocol removed that feature thanks to the &#x27;helpful suggestions&#x27; of Cisco engineers who contributed to the spec.
评论 #40873620 未加载
screcth11 months ago
It would be better if the compiler could take the async code and transform it automatically to its sans io equivalent. Doing it manually seems error prone and makes it much harder to understand what the code is doing.
tmd8311 months ago
Does the actual traffic goes through the gateway or the gateway is only used for setting up the connection?
评论 #40872551 未加载
ibotty11 months ago
That&#x27;s just an initial encoding as described e.g. here: <a href="https:&#x2F;&#x2F;peddie.github.io&#x2F;encodings&#x2F;encodings-text.html" rel="nofollow">https:&#x2F;&#x2F;peddie.github.io&#x2F;encodings&#x2F;encodings-text.html</a><p>Am I missing something?
Animats11 months ago
&quot;... be it from your Android phone, MacOS computer or Linux server. &quot;<p>Why would you want this in a client? It&#x27;s not like a client needs to manage tens of thousands of connections. Unless it&#x27;s doing a DDOS job.
评论 #40872319 未加载
评论 #40872367 未加载
Arnavion11 months ago
See also this discussion from a few months ago about sans-io in Rust: <a href="https:&#x2F;&#x2F;news.ycombinator.com&#x2F;item?id=39957617">https:&#x2F;&#x2F;news.ycombinator.com&#x2F;item?id=39957617</a>
cryptonector11 months ago
&gt; This pattern isn&#x27;t something that we invented! The Python world even has a dedicated website about it.<p>I mean, this is basically what the IO monad and monadic programming in Haskell end up pushing Haskell programmers to do.
joshka11 months ago
This article &#x2F; idea really refactors two things out of some IO code<p>- the event loop<p>- the state machine of data states that occur<p>But async rust is already a state machine, so the stun binding could be expressed as a 3 line async function that is fairly close to sans-io (if you don&#x27;t consider relying on abstractions like Stream and Sink to be IO).<p><pre><code> async fn stun( server: SocketAddr, mut socket: impl Sink&lt;(BindingRequest, SocketAddr), Error = color_eyre::Report&gt; + Stream&lt;Item = Result&lt;(BindingResponse, SocketAddr), color_eyre::Report&gt;&gt; + Unpin + Send + &#x27;static, ) -&gt; Result&lt;SocketAddr&gt; { socket.send((BindingRequest, server)).await?; let (message, _server) = socket.next().await.ok_or_eyre(&quot;No response&quot;)??; Ok(message.address) } </code></pre> If you look at how the underlying async primitives are implemented, they look pretty similar to what you;ve implemented. sink.send is just a future for Option&lt;SomeMessage&gt;, a future is just something that can be polled at some later point, which is exactly equivalent to your event loop constructing the StunBinding and then calling poll_transmit to get the next message. And the same goes with the stream.next call, it&#x27;s the same as setting up a state machine that only proceeds when there is a next item that is being fed to it. The Tokio runtime is your event loop, but just generalized.<p>Restated simply: stun function above returns a future that that combines the same methods you have with a contract about how that interacts with a standard async event loop.<p>The above is testable without hitting the network. Just construct the test Stream &#x2F; Sink yourself. It also easily composes to add timeouts etc. To make it work with the network instead pass in a UdpFramed (and implement codecs to convert the messages to &#x2F; from bytes).<p>Adding timeout can be either composed from the outside caller if it&#x27;s a timeout imposed by the application, or inside the function if it&#x27;s a timeout you want to configure on the call. This can be tested using tokio test-utils and pausing &#x2F; advancing the time in your tests.<p>---<p>The problem with the approach suggested in the article is that it splits the flow (event loop) and logic (statemachine) from places where the flow is the logic (send a stun binding request, get an answer).<p>Yes, there&#x27;s arguments to be made about not wanting to use async await, but when you effectively create your own custom copy of async await, just without the syntactic sugar, and without the various benefits (threading, composability, ...), it&#x27;s worth considering whether you could use async instead.
评论 #40902579 未加载