Lesson 20 of 44
UDP and Unix Sockets
This lesson covers two more kinds of socket. UDP sends independent messages called datagrams, with no connection and no delivery guarantee. Unix domain sockets connect programs on the same machine through a path in the file system, in a stream variant and a datagram variant. Tokio's types for both follow the patterns of TcpStream from TCP Clients and Servers.
| Type | Purpose |
|---|---|
UdpSocket |
bind, send_to, recv_from, connect, send, recv |
UnixListener, UnixStream |
stream sockets addressed by a path; UnixStream::pair makes two connected streams with no path |
UnixDatagram |
datagram sockets addressed by a path; pair and unbound also exist |
unix::SocketAddr |
address of a Unix socket; as_pathname, is_unnamed |
UDP examples bind to 127.0.0.1:0 and never print ports. Unix socket examples create their paths inside a fresh directory under the system temp directory and remove it at the end. UDP does not promise delivery, so every example that needs a datagram to arrive waits for a reply that proves it did.
Example 1: Send a datagram and receive it
UdpSocket::bind creates a socket. send_to(bytes, address) sends one datagram to an address. recv_from(&mut buf) waits for one datagram and returns its length and the sender's address. There is no accept and no connection: any socket can send to any address.
use tokio::net::UdpSocket;
#[tokio::main]
async fn main() -> std::io::Result<()> {
let receiver = UdpSocket::bind("127.0.0.1:0").await?;
let sender = UdpSocket::bind("127.0.0.1:0").await?;
sender.send_to(b"ping", receiver.local_addr()?).await?;
let mut buf = [0u8; 64];
let (len, from) = receiver.recv_from(&mut buf).await?;
println!("received {:?}", std::str::from_utf8(&buf[..len]).unwrap());
println!("sender address matches: {}", from == sender.local_addr()?);
Ok(())
}received "ping"
sender address matches: trueThe address returned by recv_from is the reply address. On loopback, the operating system places the datagram in the receiver's buffer during the send, so this program does not wait on a network. Over a real network the datagram can be lost, which Example 6 handles.
Example 2: Datagram boundaries are preserved
TCP merges and splits data arbitrarily. UDP does not: each send_to produces one datagram, and each recv_from returns one datagram. Three sends of different sizes give three receives of those sizes. The client waits for an acknowledgement after each send, so the program fixes the order of the datagrams, not the network.
use tokio::net::UdpSocket;
#[tokio::main]
async fn main() -> std::io::Result<()> {
let server = UdpSocket::bind("127.0.0.1:0").await?;
let server_addr = server.local_addr()?;
let client = UdpSocket::bind("127.0.0.1:0").await?;
let server_task = tokio::spawn(async move {
let mut buf = [0u8; 1024];
for _ in 0..3 {
let (len, from) = server.recv_from(&mut buf).await.unwrap();
println!("server: datagram of {len} bytes");
server.send_to(b"ack", from).await.unwrap();
}
});
for message in [&b"a"[..], &b"medium message"[..], &[7u8; 100][..]] {
client.send_to(message, server_addr).await?;
let mut ack = [0u8; 8];
let (len, _) = client.recv_from(&mut ack).await?;
assert_eq!(&ack[..len], b"ack");
}
server_task.await.unwrap();
Ok(())
}server: datagram of 1 bytes
server: datagram of 14 bytes
server: datagram of 100 bytesThe sizes are 1, 14, and 100 bytes. A stream protocol needs a framing layer to recover message boundaries. A datagram protocol gets them from the socket. The price is that each message stands alone, with no ordering or retransmission between messages.
Example 3: A small buffer truncates the datagram
If the buffer is smaller than the datagram, recv_from fills the buffer and discards the rest of that datagram. The next recv_from starts with the next datagram, not with the leftover bytes. The method documentation says excess bytes may be discarded; the output below shows what happens on this platform.
use tokio::net::UdpSocket;
#[tokio::main]
async fn main() -> std::io::Result<()> {
let receiver = UdpSocket::bind("127.0.0.1:0").await?;
let sender = UdpSocket::bind("127.0.0.1:0").await?;
let target = receiver.local_addr()?;
sender.send_to(b"0123456789", target).await?;
sender.send_to(b"next", target).await?;
let mut small = [0u8; 4];
let (len, _) = receiver.recv_from(&mut small).await?;
println!("first: {:?}", std::str::from_utf8(&small[..len]).unwrap());
let (len, _) = receiver.recv_from(&mut small).await?;
println!("second: {:?}", std::str::from_utf8(&small[..len]).unwrap());
Ok(())
}first: "0123"
second: "next"The bytes 456789 are gone. Use a buffer at least as large as the largest datagram the protocol allows. This example sends both datagrams before receiving, which is safe only because local delivery is immediate. Over a real network two datagrams can arrive in either order, so protocols number their messages when order matters.
Example 4: An echo server loop
A UDP server has one socket and one loop. It calls recv_from, processes the datagram, and calls send_to with the returned address. There is no per-client task and no accept. UdpSocket methods take &self, so an Arc<UdpSocket> can also be shared between tasks. The server below answers three clients that run at the same time. Each client waits for its own reply, and the program prints the results in client order.
use tokio::net::UdpSocket;
#[tokio::main]
async fn main() -> std::io::Result<()> {
let server = UdpSocket::bind("127.0.0.1:0").await?;
let server_addr = server.local_addr()?;
tokio::spawn(async move {
let mut buf = [0u8; 512];
loop {
let (len, from) = server.recv_from(&mut buf).await.unwrap();
let reply: Vec<u8> = buf[..len].iter().rev().copied().collect();
server.send_to(&reply, from).await.unwrap();
}
});
let mut clients = Vec::new();
for id in 1..=3 {
clients.push(tokio::spawn(async move {
let socket = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let request = format!("request {id}");
socket.send_to(request.as_bytes(), server_addr).await.unwrap();
let mut buf = [0u8; 512];
let (len, _) = socket.recv_from(&mut buf).await.unwrap();
String::from_utf8(buf[..len].to_vec()).unwrap()
}));
}
for client in clients {
println!("{}", client.await.unwrap());
}
Ok(())
}1 tseuqer
2 tseuqer
3 tseuqerThe server reverses each datagram. Each client binds its own socket, so the server's reply reaches the right task. The server loop runs until the program exits.
Example 5: connect gives UDP a default peer
UdpSocket::connect(addr) sends nothing and opens no connection. It stores a default destination and a filter: after connect, send goes to that address, and recv returns only datagrams from that address. The operating system drops datagrams from other senders. In the program below, a stranger sends to the client first, and the connected client never sees that datagram.
use tokio::net::UdpSocket;
#[tokio::main]
async fn main() -> std::io::Result<()> {
let client = UdpSocket::bind("127.0.0.1:0").await?;
let peer = UdpSocket::bind("127.0.0.1:0").await?;
let stranger = UdpSocket::bind("127.0.0.1:0").await?;
client.connect(peer.local_addr()?).await?;
peer.connect(client.local_addr()?).await?;
println!("peer_addr is the peer: {}", client.peer_addr()? == peer.local_addr()?);
stranger.send_to(b"from a stranger", client.local_addr()?).await?;
peer.send(b"from the peer").await?;
let mut buf = [0u8; 64];
let len = client.recv(&mut buf).await?;
println!("client received: {:?}", std::str::from_utf8(&buf[..len]).unwrap());
client.send(b"reply").await?;
let len = peer.recv(&mut buf).await?;
println!("peer received: {:?}", std::str::from_utf8(&buf[..len]).unwrap());
Ok(())
}peer_addr is the peer: true
client received: "from the peer"
peer received: "reply"Connected sockets use send and recv without an address. They suit a client that talks to one server. A server that answers many clients keeps its socket unconnected and uses recv_from and send_to. Connecting filters stray traffic and is not authentication: a sender can fake the source address of a datagram, as the recv_from documentation warns.
Example 6: UDP loses datagrams, so requests need timeouts and retries
The network may drop a UDP datagram without any error at the sender. A client that waits for a reply forever hangs when the request or the reply is lost. The usual pattern is a timeout on the reply and a bounded number of retries. The server here ignores its first datagram on purpose, which stands in for a lost packet, so the retry path runs on every execution.
use std::time::Duration;
use tokio::net::UdpSocket;
use tokio::time::timeout;
async fn request(socket: &UdpSocket, payload: &[u8], attempts: u32) -> Option<Vec<u8>> {
let mut buf = [0u8; 256];
for attempt in 1..=attempts {
socket.send(payload).await.ok()?;
match timeout(Duration::from_millis(100), socket.recv(&mut buf)).await {
Ok(Ok(len)) => {
println!("attempt {attempt}: reply received");
return Some(buf[..len].to_vec());
}
Ok(Err(_)) => return None,
Err(_elapsed) => println!("attempt {attempt}: timed out"),
}
}
None
}
#[tokio::main]
async fn main() -> std::io::Result<()> {
let server = UdpSocket::bind("127.0.0.1:0").await?;
let server_addr = server.local_addr()?;
tokio::spawn(async move {
let mut buf = [0u8; 256];
let mut seen = 0;
loop {
let (len, from) = server.recv_from(&mut buf).await.unwrap();
seen += 1;
if seen == 1 {
continue; // simulate a datagram lost on the way
}
server.send_to(&buf[..len], from).await.unwrap();
}
});
let client = UdpSocket::bind("127.0.0.1:0").await?;
client.connect(server_addr).await?;
let reply = request(&client, b"are you there", 3).await;
println!("final: {:?}", reply.map(|r| String::from_utf8(r).unwrap()));
Ok(())
}attempt 1: timed out
attempt 2: reply received
final: Some("are you there")recv, recv_from, send, and send_to are documented as cancel safe: when another branch wins, no message was received or sent. Timing out a receive therefore loses no data. Retries can deliver the same request twice, for example when only the reply was lost. A protocol that retries must make requests safe to repeat, or attach an identifier so the server can detect duplicates.
Example 7: Maximum datagram size
An IPv4 UDP datagram carries at most 65,507 bytes of payload: the 16-bit length field allows 65,535 bytes in total, minus 20 bytes for the IP header and 8 for the UDP header. A larger send_to fails. Datagrams bigger than the network's MTU (the largest packet a link carries, about 1,500 bytes on Ethernet) are split into fragments in transit, and losing one fragment loses the whole datagram, so protocols that cross the internet keep datagrams small.
use tokio::net::UdpSocket;
#[tokio::main]
async fn main() -> std::io::Result<()> {
let receiver = UdpSocket::bind("127.0.0.1:0").await?;
let sender = UdpSocket::bind("127.0.0.1:0").await?;
let target = receiver.local_addr()?;
let largest = vec![1u8; 65_507];
let sent = sender.send_to(&largest, target).await?;
let mut buf = vec![0u8; 65_535];
let (received, _) = receiver.recv_from(&mut buf).await?;
println!("sent {sent}, received {received}");
let too_big = vec![1u8; 65_508];
let result = sender.send_to(&too_big, target).await;
println!("65508 bytes fails: {}", result.is_err());
Ok(())
}sent 65507, received 65507
65508 bytes fails: trueExample 8: Broadcast and multicast
Two more UDP features send one datagram to many receivers. They are named here and not run, because their behavior depends on the network the program runs on.
| Feature | Methods | Meaning |
|---|---|---|
| Broadcast | set_broadcast(true), then send_to a broadcast address |
every host on the local network segment receives the datagram |
| Multicast | join_multicast_v4, leave_multicast_v4, set_multicast_loop_v4, set_multicast_ttl_v4 (and _v6 versions) |
only sockets that joined a group address receive the datagram |
Tokio also has peek_from, peek_sender, and try_recv_from. They follow the pattern of the TCP methods of the same names.
Example 9: A Unix stream server and client
UnixListener::bind(path) creates a socket file at path and listens on it. UnixListener::accept and UnixStream::connect(path) then work like their TCP counterparts, and UnixStream implements AsyncRead and AsyncWrite. The program creates a private directory for the socket path and removes it at the end.
use std::path::{Path, PathBuf};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{UnixListener, UnixStream};
struct TempDir(PathBuf);
impl TempDir {
fn new(name: &str) -> TempDir {
let dir = std::env::temp_dir().join(format!("tokio-course-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
TempDir(dir)
}
fn path(&self) -> &Path {
&self.0
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
#[tokio::main]
async fn main() -> std::io::Result<()> {
let dir = TempDir::new("unix-stream");
let socket_path = dir.path().join("server.sock");
let listener = UnixListener::bind(&socket_path)?;
println!("socket file exists: {}", socket_path.exists());
let server = tokio::spawn(async move {
let (mut stream, _addr) = listener.accept().await.unwrap();
let mut request = String::new();
stream.read_to_string(&mut request).await.unwrap();
stream.write_all(format!("hello, {request}").as_bytes()).await.unwrap();
});
let mut client = UnixStream::connect(&socket_path).await?;
client.write_all(b"unix client").await?;
client.shutdown().await?;
let mut reply = String::new();
client.read_to_string(&mut reply).await?;
println!("{reply}");
server.await.unwrap();
let dir_path = dir.path().to_path_buf();
drop(dir);
println!("directory removed: {}", !dir_path.exists());
Ok(())
}socket file exists: true
hello, unix client
directory removed: trueThe client writes, half-closes with shutdown, then reads the reply, as in the half-close example of the TCP lesson. TempDir is a guard: its Drop removes the directory and the socket file inside it, even if the program returns early through ?. The other Unix examples repeat the same guard, because each example is a complete program.
Unix sockets skip the network stack, and the permissions of the socket file control who can connect. They exist only on Unix systems.
Example 10: The socket file stays after the listener is dropped
Dropping a UnixListener closes the socket but does not delete its file. Binding the same path again fails with AddrInUse until the file is removed. A server that restarts on a fixed path removes the old file before it binds.
use std::io::ErrorKind;
use std::path::PathBuf;
use tokio::net::UnixListener;
struct TempDir(PathBuf);
impl TempDir {
fn new(name: &str) -> TempDir {
let dir = std::env::temp_dir().join(format!("tokio-course-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
TempDir(dir)
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
#[tokio::main]
async fn main() -> std::io::Result<()> {
let dir = TempDir::new("stale-socket");
let path = dir.0.join("app.sock");
let first = UnixListener::bind(&path)?;
drop(first);
println!("file after drop: {}", path.exists());
let err = UnixListener::bind(&path).unwrap_err();
println!("second bind: {:?}", err.kind());
println!("address in use: {}", err.kind() == ErrorKind::AddrInUse);
std::fs::remove_file(&path)?;
let _second = UnixListener::bind(&path)?;
println!("bind after removing the file: ok");
Ok(())
}file after drop: true
second bind: AddrInUse
address in use: true
bind after removing the file: okRemoving the file blindly has a cost: if another server process still listens on that path, it becomes unreachable by name. One approach is for the server to try UnixStream::connect on the path first and remove the file only when that connection is refused. A guard such as TempDir, or a struct that removes the socket file in Drop, cleans up on a normal exit. A crash or SIGKILL skips Drop, which is why the removal before bind stays.
Example 11: UnixStream::pair for tests
UnixStream::pair() returns two connected streams with no path and no listener, so nothing is created in the file system. It is the operating-system counterpart of tokio::io::duplex from AsyncRead and AsyncWrite, and it can drive any function written over AsyncRead + AsyncWrite.
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::UnixStream;
#[tokio::main]
async fn main() -> std::io::Result<()> {
let (mut a, mut b) = UnixStream::pair()?;
a.write_all(b"from a").await?;
let mut buf = [0u8; 16];
let n = b.read(&mut buf).await?;
println!("b read {:?}", std::str::from_utf8(&buf[..n]).unwrap());
b.write_all(b"from b").await?;
b.shutdown().await?;
let mut from_b = String::new();
a.read_to_string(&mut from_b).await?;
println!("a read {from_b:?}");
a.shutdown().await?;
let mut remaining = Vec::new();
b.read_to_end(&mut remaining).await?;
println!("b read to EOF after a.shutdown(): {} bytes", remaining.len());
Ok(())
}b read "from a"
a read "from b"
b read to EOF after a.shutdown(): 0 bytesBoth ends are anonymous: nothing in the file system names them. The buffer between the two ends is a kernel buffer whose size the operating system sets, unlike the size argument of duplex.
Example 12: A Unix stream server with a task per connection
The accept loop is the same as for TCP: accept, spawn a task, accept again. The server below handles three clients at once. Each handler reads lines and answers each one in upper case. The socket path is inside a temporary directory.
use std::path::PathBuf;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::{UnixListener, UnixStream};
struct TempDir(PathBuf);
impl TempDir {
fn new(name: &str) -> TempDir {
let dir = std::env::temp_dir().join(format!("tokio-course-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
TempDir(dir)
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
#[tokio::main]
async fn main() -> std::io::Result<()> {
let dir = TempDir::new("unix-server");
let path = dir.0.join("upper.sock");
let listener = UnixListener::bind(&path)?;
let server = tokio::spawn(async move {
let mut handlers = Vec::new();
for _ in 0..3 {
let (stream, _) = listener.accept().await.unwrap();
handlers.push(tokio::spawn(async move {
let (reader, mut writer) = stream.into_split();
let mut lines = BufReader::new(reader).lines();
let mut answered = 0;
while let Some(line) = lines.next_line().await.unwrap() {
writer.write_all(format!("{}\n", line.to_uppercase()).as_bytes()).await.unwrap();
answered += 1;
}
answered
}));
}
let mut total = 0;
for handler in handlers {
total += handler.await.unwrap();
}
total
});
let mut clients = Vec::new();
for id in 1..=3 {
let path = path.clone();
clients.push(tokio::spawn(async move {
let stream = UnixStream::connect(&path).await.unwrap();
let (reader, mut writer) = stream.into_split();
let mut lines = BufReader::new(reader).lines();
writer.write_all(format!("client {id}\n").as_bytes()).await.unwrap();
let reply = lines.next_line().await.unwrap().unwrap();
drop(writer); // shuts down the write half so the handler sees EOF
reply
}));
}
for client in clients {
println!("{}", client.await.unwrap());
}
println!("lines answered by the server: {}", server.await.unwrap());
Ok(())
}CLIENT 1
CLIENT 2
CLIENT 3
lines answered by the server: 3Each client sends one line, reads the reply, then drops its write half. The handler sees EOF and ends, which lets the server task count its answers.
Example 13: UnixDatagram with named sockets
UnixDatagram::bind(path) creates a datagram socket at a path. It keeps message boundaries like UDP. recv_from returns a unix::SocketAddr, and as_pathname() returns the sender's path when the sender is bound to one.
use std::path::PathBuf;
use tokio::net::UnixDatagram;
struct TempDir(PathBuf);
impl TempDir {
fn new(name: &str) -> TempDir {
let dir = std::env::temp_dir().join(format!("tokio-course-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
TempDir(dir)
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
#[tokio::main]
async fn main() -> std::io::Result<()> {
let dir = TempDir::new("unix-dgram");
let server_path = dir.0.join("server.sock");
let client_path = dir.0.join("client.sock");
let server = UnixDatagram::bind(&server_path)?;
let client = UnixDatagram::bind(&client_path)?;
client.send_to(b"status?", &server_path).await?;
let mut buf = [0u8; 64];
let (len, from) = server.recv_from(&mut buf).await?;
println!("server got {:?}", std::str::from_utf8(&buf[..len]).unwrap());
println!("sender path known: {}", from.as_pathname() == Some(client_path.as_path()));
let reply_to = from.as_pathname().unwrap().to_path_buf();
server.send_to(b"all good", &reply_to).await?;
let (len, _) = client.recv_from(&mut buf).await?;
println!("client got {:?}", std::str::from_utf8(&buf[..len]).unwrap());
Ok(())
}server got "status?"
sender path known: true
client got "all good"The reply went to the path taken from the request, the same way a UDP server replies to the address from recv_from.
Example 14: An unbound sender has no reply address
A UnixDatagram::unbound() socket has no path. It can send, but the receiver sees an unnamed address and has nowhere to send a reply. A client that expects a reply binds a path first (Example 13) or uses a connected pair (Example 15).
use std::path::PathBuf;
use tokio::net::UnixDatagram;
struct TempDir(PathBuf);
impl TempDir {
fn new(name: &str) -> TempDir {
let dir = std::env::temp_dir().join(format!("tokio-course-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
TempDir(dir)
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
#[tokio::main]
async fn main() -> std::io::Result<()> {
let dir = TempDir::new("unbound");
let collector_path = dir.0.join("collector.sock");
let collector = UnixDatagram::bind(&collector_path)?;
let fire_and_forget = UnixDatagram::unbound()?;
fire_and_forget.send_to(b"metric=1", &collector_path).await?;
let mut buf = [0u8; 32];
let (len, from) = collector.recv_from(&mut buf).await?;
println!("received {:?}", std::str::from_utf8(&buf[..len]).unwrap());
println!("sender is unnamed: {}", from.is_unnamed());
println!("sender has a path: {}", from.as_pathname().is_some());
Ok(())
}received "metric=1"
sender is unnamed: true
sender has a path: falseThis is the shape of a metrics or logging socket: many senders push messages to one collector and never wait for an answer.
Example 15: UnixDatagram::pair
UnixDatagram::pair() gives two connected datagram sockets with no path. Connected sockets use send and recv without an address, as connected UDP sockets do. The program below shows the boundary rule: two sends are two receives, in the order sent.
use tokio::net::UnixDatagram;
#[tokio::main]
async fn main() -> std::io::Result<()> {
let (client, server) = UnixDatagram::pair()?;
client.send(b"first").await?;
client.send(b"second message").await?;
let mut buf = [0u8; 64];
for _ in 0..2 {
let len = server.recv(&mut buf).await?;
println!("server got {len} bytes: {:?}", std::str::from_utf8(&buf[..len]).unwrap());
}
server.send(b"done").await?;
let len = client.recv(&mut buf).await?;
println!("client got {:?}", std::str::from_utf8(&buf[..len]).unwrap());
Ok(())
}server got 5 bytes: "first"
server got 14 bytes: "second message"
client got "done"The two receives returned the messages in the order they were sent. Each recv returned one whole message, matching the boundaries of the sends.
Example 16: Choosing between the socket types
| Need | Type | Reason |
|---|---|---|
| Reliable ordered bytes between machines | TcpStream |
retransmission and ordering built in |
| Small independent messages, with loss handled by the program | UdpSocket |
no connection setup; message boundaries kept |
| Reliable bytes between processes on one machine | UnixStream |
no network stack; file permissions control access |
| Independent messages between processes on one machine | UnixDatagram |
message boundaries kept |
| In-memory test double for a stream | tokio::io::duplex or UnixStream::pair |
no path or port needed |
Unix sockets can also live in an abstract namespace on Linux, where the name is not a file and needs no cleanup. Tokio exposes this through unix::SocketAddr::as_abstract_name. The examples in this course do not use abstract sockets, because they exist only on Linux.
Interview points
- UDP is connectionless and unreliable: a datagram can be lost, duplicated, or reordered, and the sender gets no error. Programs add timeouts, retries, and sequence numbers.
- Each
send_tois one datagram and eachrecv_fromreturns one datagram. A buffer smaller than the datagram truncates it and the rest is discarded. - IPv4 UDP payload is at most 65,507 bytes. Datagrams larger than the path MTU are fragmented and lost as a whole if one fragment is lost.
UdpSocket::connectsets a default peer and filters incoming datagrams by source address. It does not create a connection or add authentication.- A UDP server is one socket and one
recv_fromloop, not accept plus a task per client.UdpSocketmethods take&self, so anArc<UdpSocket>is shared between tasks. recvandrecv_fromare cancel safe, so atimeoutaround them loses no datagram.- Retried requests can arrive twice. Make handlers idempotent or deduplicate by request id.
- Unix stream sockets use
UnixListener::bind(path)andUnixStream::connect(path). The socket file remains after the listener drops, and binding an existing path fails withAddrInUse. - Clean up socket files in
Drop, and remove a stale file beforebind. A crash skipsDrop. UnixStream::pairandUnixDatagram::paircreate connected sockets without paths and serve as test doubles. A datagram sender without a bound path gives the receiver an unnamed address, so no reply is possible.
Summary
UdpSocketsends and receives datagrams. Boundaries are preserved, delivery is not guaranteed, and the receive buffer must fit the datagram.- Reliable UDP use needs a reply, a timeout, a bounded retry, and requests that are safe to repeat.
- Unix domain sockets have stream (
UnixListener,UnixStream) and datagram (UnixDatagram) forms addressed by file system paths. - Socket files are not removed automatically. Create them in a private directory, remove stale files before binding, and delete them on shutdown.
pairconstructors give connected sockets with no path, which makes local tests independent of the file system and the network.
