# TCP, UDP & Unix sockets

> Drop below HTTP to handle raw TCP streams, UDP datagrams, and Unix-domain-socket connections with a per-connection async handler.

Not every service speaks HTTP. Tako exposes three raw listeners in
`tako-rs-server` that skip the HTTP layer entirely and hand each accepted
connection (or datagram) straight to your async closure: raw **TCP**, raw **UDP**,
and **Unix domain sockets**. Use them for custom line protocols, relays, gateways,
game servers, telemetry collectors, or local IPC.

Raw TCP and Unix sockets are part of the default build; raw UDP needs the `udp`
feature. Every listener exists on both runtimes under the same function name: with
the `compio` feature the handler receives compio stream types and reads and writes
through compio's owned-buffer I/O. Unix domain sockets are **unix-only** and
compile out on other platforms.

| Listener | Function                        | Handler receives                                                            |
| -------- | ------------------------------- | --------------------------------------------------------------------------- |
| TCP      | `tako::server_tcp::serve_tcp`   | `(TcpStream, SocketAddr)`                                                   |
| UDP      | `tako::server_udp::serve_udp`   | `(Vec<u8>, SocketAddr, Arc<UdpSocket>)`                                     |
| Unix     | `tako::server_unix::serve_unix` | `(UnixStream, SocketAddr)` on Tokio, `(UnixStream, UnixPeerAddr)` on Compio |

Each handler returns a `Pin<Box<dyn Future>>` — wrap the body in `Box::pin(async move { … })`.
The address argument is the peer; for UDP you also get a clone of the bound socket
so you can reply.

## Raw TCP

`serve_tcp` binds the address, sets `TCP_NODELAY` on each connection, and spawns
your handler per accepted stream. The handler owns the `TcpStream` and reads/writes
it directly with `tokio::io` traits. A common shape is a read loop that echoes:

```rust
use tako::server_tcp::serve_tcp;
use tokio::io::{AsyncReadExt, AsyncWriteExt};

#[tokio::main]
async fn main() -> std::io::Result<()> {
  serve_tcp("127.0.0.1:9001", |mut stream, addr| {
    Box::pin(async move {
      let mut buf = vec![0u8; 4096];
      loop {
        let n = stream.read(&mut buf).await?;
        if n == 0 { break; }
        stream.write_all(&buf[..n]).await?;
        eprintln!("echoed {n} bytes for {addr}");
      }
      Ok(())
    })
  })
  .await?;
  Ok(())
}
```

## Raw UDP

UDP is datagram-oriented, so there is no per-connection stream. `serve_udp` binds
one socket, receives each packet (buffer sized for the 64 KiB max datagram), and
calls your handler with the bytes, the sender's address, and an `Arc` of the socket
for sending replies:

```rust
use tako::server_udp::serve_udp;

#[tokio::main]
async fn main() -> std::io::Result<()> {
  serve_udp("127.0.0.1:9000", |data, addr, socket| {
    Box::pin(async move {
      let _ = socket.send_to(&data, addr).await;
    })
  })
  .await?;
  Ok(())
}
```

The UDP handler returns `()`, not a `Result` — there is no connection to tear down,
so send errors are handled inside the closure (here ignored with `let _ =`).

## Unix domain sockets

Unix sockets give you local, file-backed IPC without binding a TCP port — ideal for
sidecars, supervisors, and admin endpoints. `serve_unix` is the raw equivalent of
`serve_tcp`, handing you a `UnixStream` per connection:

<Tabs items="['Tokio','Compio']">
  <Tab value="Tokio">
    ```rust
    use tako::server_unix::serve_unix;
    use tokio::io::{AsyncReadExt, AsyncWriteExt};

    #[tokio::main]
    async fn main() -> std::io::Result<()> {
      serve_unix("/tmp/tako.sock", |mut stream, _addr| {
        Box::pin(async move {
          let mut buf = vec![0u8; 4096];
          let n = stream.read(&mut buf).await?;
          stream.write_all(&buf[..n]).await?;
          Ok(())
        })
      })
      .await?;
      Ok(())
    }
    ```
  </Tab>

  <Tab value="Compio">
    ```rust
    use compio::io::{AsyncRead, AsyncWriteExt};
    use tako::server_unix::serve_unix;

    #[compio::main]
    async fn main() -> std::io::Result<()> {
      serve_unix("/tmp/tako.sock", |mut stream, _peer| {
        Box::pin(async move {
          let compio::BufResult(result, mut buf) = stream.read(vec![0u8; 4096]).await;
          buf.truncate(result?);
          let compio::BufResult(result, _) = stream.write_all(buf).await;
          result
        })
      })
      .await
    }
    ```
  </Tab>
</Tabs>

To serve your full HTTP [`Router`](/docs/routing) over a socket file instead of TCP
— the typical "behind nginx/HAProxy on a local socket" deployment — use
`Server::spawn_unix_http` on Tokio or `CompioServer::spawn_unix_http` on Compio.
The `try_spawn_unix_http` variants bind the socket before returning, so bind errors
surface immediately. The peer path is exposed to handlers as
`tako::conn_info::UnixPeerAddr` in the request extensions, next to a `ConnInfo`
whose transport is `Unix`:

<Tabs items="['Tokio','Compio']">
  <Tab value="Tokio">
    ```rust
    use tako::router::Router;
    use tako::Server;

    #[tokio::main]
    async fn main() {
      let mut router = Router::new();
      router.route(tako::Method::GET, "/", || async { "hello from unix" });
      Server::builder().build().spawn_unix_http("/tmp/tako-http.sock", router)
        .result().await.expect("Unix HTTP server failed");
    }
    ```
  </Tab>

  <Tab value="Compio">
    ```rust
    use tako::CompioServer;
    use tako::router::Router;
    use tako::types::BoxError;

    #[compio::main]
    async fn main() -> Result<(), BoxError> {
      let mut router = Router::new();
      router.get("/", || async { "hello from unix" });

      let handle = CompioServer::builder()
        .build()
        .try_spawn_unix_http("/tmp/tako-http.sock", router)
        .await?;
      handle.result().await?;
      Ok(())
    }
    ```
  </Tab>
</Tabs>

```bash
curl --unix-socket /tmp/tako-http.sock http://localhost/
```

<Callout type="info">
  A path starting with `@` is treated as a Linux **abstract-namespace** socket
  (e.g. `@tako.sock`), which never touches the filesystem. Abstract sockets are
  Linux-only; on other unix platforms an `@`-path returns an `Unsupported` error.
</Callout>

<Callout type="warn">
  Unix sockets are **unix-only** and are absent on non-unix targets. Both runtimes
  clean up a stale filesystem socket before binding — it refuses to unlink a path
  that is not actually an `AF_UNIX` socket (symlink-escalation guard) and removes the
  file again on graceful shutdown so the next run can re-bind.
</Callout>

## Graceful shutdown

Each function has `…_with_shutdown` and `…_with_shutdown_and_drain` variants that
take a shutdown-signal future. On signal the listener stops accepting and in-flight
work is drained, with a default 30-second bound you can override on the
`_and_drain` form. For HTTP-over-unix, the builder's `spawn_unix_http` takes its
[`ServerConfig`](/docs/concepts/runtimes) for connection caps and timeouts.

## HTTP alternatives

If you want HTTP rather than a raw protocol, you usually do not need these: plain
HTTP is covered in [HTTP/1.1 & HTTP/2](/docs/transports/http), and the
builder's `spawn_unix_http` serves HTTP over a Unix socket. Reach for the raw listeners
only when you control both ends of a non-HTTP protocol.

## Examples

* `examples/tcp-echo` — line-echo over raw TCP.
* `examples/udp-echo` — datagram echo over raw UDP.
* `examples/unix-socket` — HTTP `Router` served over a Unix socket with `UnixPeerAddr`.

Source: https://tako.rust-dd.com/docs/transports/tcp-udp-unix