Async read/writes with native-tls

I'm currently working on an application that has a persistent TLS-wrapped (using native-tls) socket to a server for the duration of the client's lifetime. I'm able to connect and read/write data just fine in the normal fashion as per the native-tls documentation.

Commns with the server is effectively async. There are not immediate responses from the server. So it's not a "write some stuff, read some stuff back immediately" kind of scenario. I need to make the writing and reading async, separate from the user's experience.

My intention was to spawn two threads:

  • A "writer" thread, which receives messages on a channel that need to be written to the socket. This serialises the sending of messages without blocking the UI, leaving the UI to handle dealing with the jerk user (me).
  • A "reader" thread, which listens on the socket to read data that comes in, when it comes in!

I've read through the TcpStream documentation and seen a few other examples of how one might do this, however, I can't seem to find any examples of how to do this with native-tls. eg. I can't seem to clone the stream so that I can write on one thread while reading on another.

Does anyone know how I might go about doing this? The owernship/lifetime stuff is throwing me a bit too in this case. I'd also appreciate some critique on the thought process here. I'm still very new to Rust, so the learnings are appreciated.

Many thanks for your time.

I’m assuming you’re using the synchronous TcpStream from the standard library, and not the tokio-tls crate (which you should look into).

So with std::net::TcpStream you should use TcpStream in std::net - Rust to create handles to the same underlying socket. You can then send these handles to the reader and writer threads and work with them there.

Hi Vitalyd,

Yes I'm using the synchronous TcpStream from the standard library. I did look into the tokio stuff but wanted to at least do as much I can myself as part of the learning exercise. I may make use of that down the track.

As far as the try_clone method goes, if that is on TcpStream rather than the abstraction that native-tls provides, am I right in thinking that I'm somehow bypassing the TLS functionality if I do this? Would I not need to close the TlsStream that native-tls provides and use that instead? If so, that's where I'm stuck because the library doesn't appear to support that.

With tokio-tls, are you able to provide a link or sample that shows the reader/writer thread style functionality? The examples I've dug up only show a socket being used as part of a single thread.

Thanks for responding!

I think you are over-complicating things. Why not use non-blocking sockets and a single thread?

Hi Sergej,

Thanks for the response. So the thread main areas of action are:

  • User interface
  • Reading data from the socket (which includes events pushed from the server, not just responses)
  • Writing data to the socket (including events for which there are no responses)

To me those three things require a level of separation.

You may be right in that I might be overthinking. Does your suggestion take into account the features of native-tls? Do you happen to have an example you can point me which shows non-blocking independent reads and writes?

Thanks!

I don't think they require thread-level separation. native-tls works just fine with non-blocking sockets. You just have to check errors returned by read/write if they are io::ErrorKind::WouldBlock or io::ErrorKind::NotConnected. Everything works the same, you just have to periodically retry them if you get wouldblock.

The tokio-tls crate wraps native-tls with tokio support: tokio_tls - Rust

Sergej: I will look into this further. Thanks.

sfackler: Thank you for the reference. I had noticed that earlier, but my lack of familiarity with tokio (and Rust in general) steered me away while I tried to gain a greater understanding of $THINGSIDONTKNOW.

I appreciate both of you responding, thank you.

Yeah, I was only talking about getting multiple handles to the TcpStream. You’re right in that this isn’t what you actually want for tls purposes.

So I would (re)suggest looking at the tokio_tls crate. Since you have a UI, you’d probably want to run the reactor (Core) on a background thread.

On the reactor, you’d run the tokio_tls stack. In this stack, you can set up a Stream over the incoming data (ie server responses). For sending, you can give your UI thread a Remote handle to the reactor and you can use that to spawn send request futures for messages to the server (or use a channel to send a message. Alternatively, you can set up a futures::sync::mpsc::channel where the UI thread retains the Sender portion and the reactor thread keeps the Receiver portion and wires it up as a Stream for outgoing requests to the server.

Just some ideas.

Thanks again Vitalyd for responding. That all makes sense. I dived into this a fair bit more last night and I started to get things working. I admit to being confused a lot along the way. I'm at the point where I have tokio-tls operational and I've got two futures setup to handle reading/writing that I'm binding together with join(). The one thing that's left is for me to figure out how to receive messages on the main thread that have come from the socket via the channel (or in my case an unbounded instance).

I appreciate your time and thoughts. Thanks.