49efcde301
CI / Build NRO (push) Successful in 29s
CI / Format check (push) Successful in 28s
CI / Layering check (push) Successful in 2s
CI / Build NRO (pull_request) Successful in 29s
CI / Format check (pull_request) Successful in 27s
CI / Layering check (pull_request) Successful in 1s
96 lines
3.3 KiB
Markdown
96 lines
3.3 KiB
Markdown
# NXST Wire Protocol
|
|
|
|
## Overview
|
|
|
|
NXST uses two sockets per transfer session:
|
|
|
|
| Socket | Purpose |
|
|
|--------|---------|
|
|
| UDP multicast | Receiver advertisement (discovery) |
|
|
| TCP | File data stream |
|
|
|
|
Both sides must be on the same local network segment. The sender (Transfer mode) initiates; the receiver (Receive mode) listens.
|
|
|
|
---
|
|
|
|
## Discovery — UDP Multicast
|
|
|
|
| Parameter | Value |
|
|
|-----------|-------|
|
|
| Group | `239.0.0.1` |
|
|
| Port | `8081` |
|
|
| Direction | sender → receiver |
|
|
|
|
**Flow:**
|
|
|
|
1. Receiver joins multicast group and binds `0.0.0.0:8081`.
|
|
2. Sender sends `"DISCOVER_SERVER"` (15 bytes, no null terminator) to `239.0.0.1:8081`.
|
|
3. Receiver replies `"SERVER_HERE"` (11 bytes) to the sender's source address.
|
|
4. Sender extracts the receiver's IP from the reply source and closes the UDP socket.
|
|
|
|
Sender polls in 100 ms slices for up to 3 seconds. Cancel is checked each slice.
|
|
|
|
---
|
|
|
|
## File Transfer — TCP
|
|
|
|
| Parameter | Value |
|
|
|-----------|-------|
|
|
| Port | `8080` |
|
|
| Direction | sender connects → receiver listens |
|
|
| Buffer size | 65 536 bytes (`proto::kBufSize`) |
|
|
|
|
**Connection:**
|
|
|
|
1. Receiver listens on `0.0.0.0:8080` (started concurrently with multicast listener).
|
|
2. Sender connects after receiving `"SERVER_HERE"`.
|
|
|
|
**Wire layout — one file:**
|
|
|
|
```
|
|
┌─────────────────────────────────┐
|
|
│ filename_len │ uint32_t LE │ 4 bytes
|
|
├─────────────────────────────────┤
|
|
│ filename │ filename_len │ bytes, no null terminator
|
|
│ │ bytes │
|
|
├─────────────────────────────────┤
|
|
│ file_size │ uint64_t LE │ 8 bytes
|
|
├─────────────────────────────────┤
|
|
│ file_data │ file_size │ bytes
|
|
│ │ bytes │
|
|
└─────────────────────────────────┘
|
|
```
|
|
|
|
Files are sent sequentially. The stream ends with a sentinel frame:
|
|
|
|
```
|
|
filename_len == 0 (proto::kEofSentinel)
|
|
```
|
|
|
|
No `filename` or `file_size` field follows the sentinel.
|
|
|
|
**Constraints:**
|
|
|
|
- `filename_len > proto::kMaxFilename` (4 096) is treated as a protocol error; the receiver aborts.
|
|
- Filenames are full paths as produced by `io::backup` (e.g. `/switch/NXST/<title>/<user>/...`).
|
|
- On the receiver, the username path component is rewritten to match the local user's nickname before writing to disk.
|
|
|
|
---
|
|
|
|
## Post-Transfer
|
|
|
|
After the TCP stream closes (sentinel received), the receiver calls `io::restore`:
|
|
|
|
1. Mounts the title's save filesystem.
|
|
2. Clears existing save data.
|
|
3. Copies received files into `save:/`.
|
|
4. Commits via `fsdevCommitDevice("save")`.
|
|
|
|
The sender creates a local backup via `io::backup` before connecting, so the sender's own save is never at risk.
|
|
|
|
---
|
|
|
|
## Cancellation
|
|
|
|
Either side can cancel at any time by closing the relevant socket (`shutdown` + `close`). The other side's blocking read/write will return an error and the transfer loop exits cleanly. The receiver's accept thread sets `receiver_state.done = true` regardless of how the connection ends, so the UI poll loop always terminates.
|