Unix sockets read

Hi folks,

I try to make a Unix socket demo using rust and netcat (nc).
nc to nc works fine, I run on the same Unix (Arch linux) machine two terminals:
Server:
nc -U /tmp/demo.sock -l
Client:
nc -U /tmp/demo.sock

and what I type on one terminal, I see it on the other (and vice versa).

Now I want to replace the server nc with a rust program.

use std::io::prelude::*;
use std::os::unix::net::{UnixListener, UnixStream};
use std::thread;
use std::time::Duration;

fn main() -> std::io::Result<()> {
    let sock_path = "/tmp/demo1.sock";
    let _ = std::fs::remove_file(sock_path);
    let listener = UnixListener::bind(sock_path)?;
    for stream in listener.incoming() {
        match stream {
            Ok(mut stream) => {
                stream.write_all(b"hello world\n")?;
                thread::spawn(|| handle_client(stream));
            }
            Err(e) => {
                println!("listener.incoming() err: {}", e);
                break;
            }
        }
    }
    Ok(())
}

fn handle_client(mut stream: UnixStream) {
    println!("handle_client");
    let sw1 = false;
    if sw1 {
        let mut buf_str = String::with_capacity(50);
        // stream.set_read_timeout(Some(Duration::from_secs(3))).unwrap();
        match stream.read_to_string(&mut buf_str) {
            Ok(v) => {
                println!("read {} bytes: {}", v, buf_str);
            }
            Err(e) => {
                println!("read_to_string failed: {}", e);
            }
        }
    } else {
        let mut buf_bytes: Vec<u8> = vec![0; 50];
        loop {
            match stream.read(&mut buf_bytes) {
                Ok(v) => {
                    println!("read {} bytes: {:?}", v, buf_bytes);
                }
                Err(e) => {
                    println!("read_to_string failed: {}", e);
                }
            }
        }
    }
}

When sw1 == false I can send from nc data to the server, it works as expected.
Q1: When sw1 == true, I start this program and then on a separate terminal run client nc. 'hello' appears, but when I type something, I don't see it on the rust server. Why EOF from netcat isn't detected (i. e. read_to_string() blocks)? On the nc side, enter doesn't work (as it does when I test nc - nc), ^d neither, only when I type something and exit nc (^c) I see that text on the server side.
Q2: Any better idea than rm socket file -> bind? Could it be problematic?

Many thanks!

Here a working demo based on UnixDatagram:

use std::io::prelude::*;
use std::os::unix::net::{UnixDatagram, UnixListener, UnixStream};
use std::thread;
use std::time::Duration;

fn main() -> std::io::Result<()> {
    let sock_path = "/tmp/demo1.sock";
    let _ = std::fs::remove_file(sock_path);
    let sock = UnixDatagram::bind(sock_path)?;
    let mut buf = vec![0;50];
    println!("waiting for data over the socket");
    loop {
        sock.recv(buf.as_mut_slice()).expect("sock rcv failed");
        // println!("received: {:?}", buf);
        println!("str: {}", String::from_utf8_lossy(&buf));
    }
    Ok(())
}
$ nc -U /tmp/demo1.sock -u

Still curious regarding my initial Qs, especially Q2.

I tried your program on my computer (macOS) and set sw1 to true, and it worked as I expected: when I typed some text into nc and then pressed ^D, the text appeared in the Rust program’s output.

Here is a modification of your function that might help — it reads and prints individual lines instead of reading to EOF.

fn handle_client(stream: UnixStream) {
    println!("handle_client");
    let mut stream = std::io::BufReader::new(stream);
    let mut buf_str = String::with_capacity(50);
    loop {
        buf_str.clear();
        match stream.read_line(&mut buf_str) {
            Ok(0) => break,
            Ok(v) => {
                print!("read {} bytes: {}", v, buf_str);
            }
            Err(e) => {
                println!("read_to_string failed: {}", e);
            }
        }
    }
}

Thanks for the suggestion, Kevin! Your example is nice, I also did bi-directional communication using buffered read and write.

    let mut w_stream = std::io::BufWriter::new(stream.try_clone().unwrap());
    let mut r_stream = std::io::BufReader::new(stream)

What I learned (correct me if I'm wrong) :

  1. UNIX Sockets are similar to TCP and UDP network sockets, but they use slightly different terminology. 2. "Peer" means "Client"
    3 "Listener" means "Server" (why using new words for standard concepts - or I miss something?)
  2. We have (file system) Paths and Socket Addresses. For example UnixDatagram has .connect("/path/to/sock") and .connect_addr(sock_addr)

The std::os::unix::net Rust API confuses me a bit, because:

  1. Unix Listener described as "A structure representing a Unix domain socket server." Not clear whether Stream or Datagram. Or it could be both? Also, can I only use UnixDatagram and UnixStream, without dealing directly with the listener?
  2. bind() available in UnixDatagram and UnixListener, but not UnixStream. I'd expect the UnixDatagram and UnixStream to be very close to each other.
  3. UnixDatagram and UnixStream both have .connect(path) method:
    UnixStream:
    pub fn connect<P: AsRef>(path: P) -> Result
    "Connects to the socket named by path."
    UnixDatagram:
    pub fn connect<P: AsRef>(&self, path: P) -> Result<()>
    "Connects the socket to the specified path address.
    The send method may be used to send data to the specified address. recv and recv_from will only receive data from that address."
    I'd expect those signatures to match. What 'only' means here?
    Maybe an even simpler example showing on one place how to create all 4 combinations of (Datagram, Stream) and (Server and Client) would help.

After failing to find a way to easily identify the client (the "peer") of my socket, I found this: https://serverfault.com/questions/252723/how-to-find-other-end-of-unix-socket-connection

The books I skimmed over regarding OS (System programming in Linux - S. N. Weiss, No Starch, and Betriebssysteme - E. Glatz, dpunkt.verlag - both are absolutely great!) don't say much regarding sockets, they explain and give examples with other IPCs.

One last thing regarding UnixListener::bind(path) docs:
I think the description is wrong: "Creates a new UnixListener bound to the specified socket." - they meant 'socket path'?

As I don't need performance for what I do, I plan to try for a couple more hours to make sockets work as expected - and if not, use tcp.

Thanks again, and I hope my confused questions don't confuse you any further :slight_smile:

Actually, the terminology is not different. Once a TCP or Unix-stream connection has been established, the connection is symmetrical — there is no distinguished client or server end. The “peer” is “the owner of the other end of the connection”, regardless of its initial role. (You may have seen this term before in the error message “connection reset by peer”.)

Also, in general, “client”, “server”, and “peer” refer to roles that machines/processes have, not the objects they manipulate. A server using streams uses a Listener to accept them, but a server using datagrams does does not.

Listeners are for accepting connections. Datagram sockets (whether IP or Unix) do not have connections; only streams do. (The documentation could be clearer on this point, yes.)

This is because a UnixStream inherently is connected to a single other socket in some other process, but a UnixDatagram is not — it can, by default, receive datagrams from many senders, and filtering to one sender is an optional feature. (The same relationship exists between TCP sockets and UDP sockets.)

Generally, it is better to design protocols that do not need this kind of information. For what purpose do you want to identify the peer, and what kind of identification do you want?

Thank you so much for the quick and detailed answer Kevin!

For what purpose do you want to identify the peer, and what kind of identification do you want?

I was just curious if I can identify (from OS perspective) that the nc was connected to the socket.
I used

lsof -U
ss -x

and I expected to see that those two processes, my rust program and nc, communicate.