Unix sockets, the basics in Rust
I found myself wondering about unix sockets while working on Sōzu, a reverse proxy written in Rust. A bunch of Sōzu issues led me to dig into Sōzu channels, which themselves make use of Metal I/O ’s implementation of unix sockets.
Here are the questions, summed up:
- what are unix sockets?
- how can we create them in Rust?
- how do we use them to stream data?
So here we go.
What is a unix socket?
It is not a web socket like 127.0.0.1:8080
.
You may have heard that in unix, everything is a file. Unix sockets seem to be a good example of this principle. They are empty files of sorts, only there to be written to, and read from.
Sockets are a core feature of unix. In fact, if you type
man unix
in your terminal, you should land on an ancient man page:
|
|
that explains how sockets are declared in C in the kernel,
how they are created with the AF_UNIX
system call,
and many more thing that go far beyond my limited understanding.
Creating a socket is not as easy as creating just any file, using, say, touch
.
They are tools available in the command line, but most of the time,
sockets are created and used by processes, not by users.
Looking up how to create one will land you on a tutorial in C, or in python.
So let’s see how to do it in Rust.
Make a socket server in Rust
The Rust standard library has a std::os::unix module
to interact with unix processes, unix files, and so on.
Within it, we want to look at the net
module,
named that way because unix sockets are used to do networking between processes.
The std::os::unix::net
module contains, among other things:
Both those entities are unsafe wrappers of the libc
library to perform the very same unix system calls you would write in C.
They both wrap a unix file descriptor, but they are distinct in order to separate higher-level concerns.
UnixListener
is used to create sockets, (libc::bind()
andlibc::listen()
)UnixStream
is there to connect to a socket (libc::connect()
), to read from it and write on it.
Let’s use those. Install Rust and Cargo, Learn the basics of Rust, and then do:
cargo new unix_sockets
Add this to Cargo.toml
(makes error propagation easier):
|
|
Create a socket, server side
In the src
directory, create a bin
directory, in which you will create a server.rs
file.
|
|
Then do
cargo run --bin server
Which should run smoothly, and then do ls -l
in your directory,
you should have a line like this:
srwxr-xr-x 1 emmanuel users 0 Jan 7 13:08 mysocket
The s
stands for socket. Congratulations!
Do one more cargo run --bin server
and you have a neat, self-explanatory OS error:
|
|
I guess we’ll have to destroy it and recreate it each time.
|
|
Waiting for connections, server side
The UnixListener
struct has an accept()
method that waits for other processes to connect to the socket.
Once a connections comes, accept()
returns a tuple containing a UnixStream
and a SocketAddr
.
As mentioned above, UnixStream
implements Read
and Write
.
We will handle this stream to:
- read what another process will send through the socket
- write responses on the socket
Add the loop and the handle_stream
function to the server code:
|
|
Remove the existing socket and run the code:
cargo run --bin server
it should hang. Perfect! The server is waiting for connections!
Connecting to the socket, client side
The client process wants to connect to an existing socket, read and write from it.
Next to server.rs
, create the client.rs
file.
The client will merely consist of a UnixStream
:
|
|
Writing on the socket, client side
We need to import the Read
and Write
traits.
|
|
And now we can write onto the stream.
Below the unix_stream
declaration, add the write logic:
|
|
Reading from the socket, server side
Be sure to import Read
and Write
in server.rs
:
|
|
Now let’s fill the handle_stream
function with ordinary read logic:
|
|
Launch the whole thing!
Make sure you have the server running in a terminal:
cargo run --bin server
And in a separate terminal, run the client:
cargo run --bin client
If all is well, the hello message should display on the server side.
Respond to a message, server side
Let’s answer something every time the server receives anything.
|
|
Listen to responses, client side
Introducing the same reading logic we used on the server will not work. Why? After writing on a stream, we need to shut down the writing, if we want to read from it.
Let’s segregate the write and read logic into distinct functions.
Oh, and we pass mutable references (&mut
) of the unix stream to the function, because… Rust.
Don’t worry about it.
|
|
The shutdown()
method takes a Shutdown
enum we would otherwise use on TCP streams.
Write below the main function:
|
|
The stream is now clean to be read from.
|
|
Launch the whole thing, again!
Have the server running in a terminal:
cargo run --bin server
And in a separate terminal, run the client:
cargo run --bin client
If all is well,
- the hello message should display on the server side
- the “I hear you” response should display on the client side
You can run the client as many times as you want, since the server runs in a loop.
Browse the code
This tutorial comes with a github repository that contains the above code.
Feel free to write an issue for any comment, criticism, or complaint you may have. Fork and do pull requests as you please.
This blog post is a sum-up of what I learned trying to understand unix sockets while working on Sōzu. A more elaborate version of the code is available in this other repo, with additional features:
- a
UnixListener
-wrapping library with a gloriousSocketBuilder
helper (permissions! blocking/nonblocking!) - a
Message
module with serializableRequest
andResponse
structs. The Response has a status that is eitherOk
,Error
orProcessing
- a client loop that continues reading the stream as long as responses come with a
Processing
status, to stops only atOk
orError
All this happened thanks to my employer, Clever Cloud, who allows me to learn my job in the best possible conditions. Much gratitude.