Milestone 1
This commit is contained in:
1140
Cargo.lock
generated
Normal file
1140
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
7
Cargo.toml
Normal file
7
Cargo.toml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[workspace]
|
||||||
|
members = [
|
||||||
|
"core_protocol",
|
||||||
|
"server_node",
|
||||||
|
"client_node"
|
||||||
|
]
|
||||||
|
resolver = "2"
|
||||||
@@ -2,31 +2,31 @@
|
|||||||
**Goal:** Initialize the project and establish the shared language between client and server.
|
**Goal:** Initialize the project and establish the shared language between client and server.
|
||||||
|
|
||||||
### 1. Workspace Setup
|
### 1. Workspace Setup
|
||||||
- [ ] Initialize the root Cargo workspace: `cargo init --vcs none` (delete `src/`). Create a root `Cargo.toml` with `[workspace] members = ["core_protocol", "server_node", "client_node"]`.
|
- [x] Initialize the root Cargo workspace: `cargo init --vcs none` (delete `src/`). Create a root `Cargo.toml` with `[workspace] members = ["core_protocol", "server_node", "client_node"]`.
|
||||||
- [ ] **AI Context Trap (File Structure):** Strictly adhere to the directory layout and module hierarchy defined in `File_Structure.md`. Do not invent new file paths or module names; map every new crate and file exactly to the blueprint.
|
- [x] **AI Context Trap (File Structure):** Strictly adhere to the directory layout and module hierarchy defined in `File_Structure.md`. Do not invent new file paths or module names; map every new crate and file exactly to the blueprint.
|
||||||
- [ ] Create crates: `cargo new --lib core_protocol`, `cargo new --bin server_node`, `cargo new --bin client_node`.
|
- [x] Create crates: `cargo new --lib core_protocol`, `cargo new --bin server_node`, `cargo new --bin client_node`.
|
||||||
- [ ] Add strict lints (`#![forbid(unsafe_code)]`, etc.) to the root workspace or individual `lib.rs`/`main.rs` files.
|
- [x] Add strict lints (`#![forbid(unsafe_code)]`, etc.) to the root workspace or individual `lib.rs`/`main.rs` files.
|
||||||
- [ ] **Dependencies (`core_protocol`):** Add `serde`, `bincode`, `uuid`, `chrono`, `thiserror`, `secrecy` (for zeroing sensitive keys).
|
- [x] **Dependencies (`core_protocol`):** Add `serde`, `bincode`, `uuid`, `chrono`, `thiserror`, `secrecy` (for zeroing sensitive keys).
|
||||||
- [ ] **Dependencies (`server_node`):** Add `tokio` (full), `tracing`, `tracing-subscriber`, `anyhow`, `dashmap`.
|
- [x] **Dependencies (`server_node`):** Add `tokio` (full), `tracing`, `tracing-subscriber`, `anyhow`, `dashmap`.
|
||||||
- [ ] **Dependencies (`client_node`):** Add `tokio` (rt-multi-thread), `tracing`, `tracing-subscriber`, `anyhow`.
|
- [x] **Dependencies (`client_node`):** Add `tokio` (rt-multi-thread), `tracing`, `tracing-subscriber`, `anyhow`.
|
||||||
|
|
||||||
### 2. Protocol Definitions (`core_protocol`)
|
### 2. Protocol Definitions (`core_protocol`)
|
||||||
- [ ] Create `src/tcp_events.rs`. Define `enum TcpEvent { AuthRequest { username: String, ... }, AuthResponse { session_token: u32, ... }, ChannelJoin { ... }, ChatMessage { ... } }` with `#[derive(Serialize, Deserialize)]`.
|
- [x] Create `src/tcp_events.rs`. Define `enum TcpEvent { AuthRequest { username: String, ... }, AuthResponse { session_token: u32, ... }, ChannelJoin { ... }, ChatMessage { ... } }` with `#[derive(Serialize, Deserialize)]`.
|
||||||
- [ ] Create `src/udp_packets.rs`. Define `struct VoicePacketHeader { pub session_token: u32, pub sequence_num: u64, pub timestamp: u64 }` with `#[derive(Serialize, Deserialize)]`.
|
- [x] Create `src/udp_packets.rs`. Define `struct VoicePacketHeader { pub session_token: u32, pub sequence_num: u64, pub timestamp: u64 }` with `#[derive(Serialize, Deserialize)]`.
|
||||||
- [ ] Create `src/constants.rs`. Define `pub const SAMPLE_RATE: u32 = 48000;`, `pub const FRAME_SIZE: usize = 960;`, `pub const TCP_PORT: u16 = 8080;`.
|
- [x] Create `src/constants.rs`. Define `pub const SAMPLE_RATE: u32 = 48000;`, `pub const FRAME_SIZE: usize = 960;`, `pub const TCP_PORT: u16 = 8080;`.
|
||||||
|
|
||||||
### 3. TCP Handshake (`server_node` & `client_node`)
|
### 3. TCP Handshake (`server_node` & `client_node`)
|
||||||
- [ ] **Server:** In `server_node/src/main.rs`, initialize `tokio::net::TcpListener::bind("0.0.0.0:8080")`.
|
- [x] **Server:** In `server_node/src/main.rs`, initialize `tokio::net::TcpListener::bind("0.0.0.0:8080")`.
|
||||||
- [ ] **Server:** Spawn a new `tokio::spawn(async move { ... })` for each incoming `TcpStream`.
|
- [x] **Server:** Spawn a new `tokio::spawn(async move { ... })` for each incoming `TcpStream`.
|
||||||
- [ ] **Client:** In `client_node/src/network/control.rs`, implement `TcpStream::connect("127.0.0.1:8080")`.
|
- [x] **Client:** In `client_node/src/network/control.rs`, implement `TcpStream::connect("127.0.0.1:8080")`.
|
||||||
- [ ] **AI Context Trap (TCP Framing):** Raw TCP streams suffer from fragmentation. Do NOT attempt to manually buffer bytes. You must use `tokio_util::codec::LengthDelimitedCodec` (with `tokio_serde` and `bincode`) to abstract the frame boundaries cleanly.
|
- [x] **AI Context Trap (TCP Framing):** Raw TCP streams suffer from fragmentation. Do NOT attempt to manually buffer bytes. You must use `tokio_util::codec::LengthDelimitedCodec` (with `tokio_serde` and `bincode`) to abstract the frame boundaries cleanly.
|
||||||
|
|
||||||
### 4. Login Logic & State
|
### 4. Login Logic & State
|
||||||
- [ ] **Server State:** Create `server_node/src/state.rs`. Define a `DashMap<u32, UserState>` to store active session tokens.
|
- [x] **Server State:** Create `server_node/src/state.rs`. Define a `DashMap<u32, UserState>` to store active session tokens.
|
||||||
- [ ] **Authentication Flow:** Client sends `TcpEvent::AuthRequest`. Server generates a random `u32` session token, stores it in `DashMap`, and returns `TcpEvent::AuthResponse`.
|
- [x] **Authentication Flow:** Client sends `TcpEvent::AuthRequest`. Server generates a random `u32` session token, stores it in `DashMap`, and returns `TcpEvent::AuthResponse`.
|
||||||
- [ ] **Validation:** Ensure the server actively drops the connection if the client sends invalid or excessively large payloads.
|
- [x] **Validation:** Ensure the server actively drops the connection if the client sends invalid or excessively large payloads.
|
||||||
|
|
||||||
### 5. Observability (Logging)
|
### 5. Observability (Logging)
|
||||||
- [ ] **Initialization:** In both binaries' `main.rs`, call `tracing_subscriber::fmt::init()`.
|
- [x] **Initialization:** In both binaries' `main.rs`, call `tracing_subscriber::fmt::init()`.
|
||||||
- [ ] **Implementation:** Replace all `println!` calls with `tracing::info!`, `tracing::warn!`, or `tracing::error!`.
|
- [x] **Implementation:** Replace all `println!` calls with `tracing::info!`, `tracing::warn!`, or `tracing::error!`.
|
||||||
- [ ] **Tracing Context:** Use `#[tracing::instrument]` on core TCP handler functions to automatically log client IPs and session IDs.
|
- [x] **Tracing Context:** Use `#[tracing::instrument]` on core TCP handler functions to automatically log client IPs and session IDs.
|
||||||
14
client_node/Cargo.toml
Normal file
14
client_node/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "client_node"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0.102"
|
||||||
|
core_protocol = { version = "0.1.0", path = "../core_protocol" }
|
||||||
|
futures = "0.3.32"
|
||||||
|
tokio = { version = "1.52.1", features = ["rt-multi-thread", "net", "macros"] }
|
||||||
|
tokio-serde = { version = "0.9.0", features = ["bincode"] }
|
||||||
|
tokio-util = { version = "0.7.18", features = ["codec"] }
|
||||||
|
tracing = "0.1.44"
|
||||||
|
tracing-subscriber = "0.3.23"
|
||||||
24
client_node/src/main.rs
Normal file
24
client_node/src/main.rs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
//! Client Node entry point.
|
||||||
|
//!
|
||||||
|
//! This module initializes the desktop client application, sets up the Tokio
|
||||||
|
//! background thread for networking, and eventually binds to the UI framework.
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
#![deny(clippy::all, clippy::pedantic)]
|
||||||
|
#![deny(clippy::unwrap_used, clippy::expect_used)]
|
||||||
|
|
||||||
|
mod network;
|
||||||
|
|
||||||
|
use tracing::{error, info};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
info!("Starting client node...");
|
||||||
|
|
||||||
|
if let Err(e) = network::control::connect_and_auth("TestUser").await {
|
||||||
|
error!("Connection error: {:?}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
72
client_node/src/network/control.rs
Normal file
72
client_node/src/network/control.rs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
//! Reliable TCP connectivity logic.
|
||||||
|
//!
|
||||||
|
//! Implements the client-side TCP connection handling, including framing,
|
||||||
|
//! serialization, and the initial authentication handshake.
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use core_protocol::constants;
|
||||||
|
use core_protocol::tcp_events::TcpEvent;
|
||||||
|
use futures::{SinkExt, StreamExt};
|
||||||
|
use tokio::net::TcpStream;
|
||||||
|
use tokio_serde::formats::Bincode;
|
||||||
|
use tokio_serde::SymmetricallyFramed;
|
||||||
|
use tokio_util::codec::{Framed, LengthDelimitedCodec};
|
||||||
|
use tracing::{info, warn};
|
||||||
|
|
||||||
|
/// A type alias for the symmetrically framed TCP stream to simplify syntax.
|
||||||
|
type FramedStream = SymmetricallyFramed<
|
||||||
|
Framed<TcpStream, LengthDelimitedCodec>,
|
||||||
|
TcpEvent,
|
||||||
|
Bincode<TcpEvent, TcpEvent>,
|
||||||
|
>;
|
||||||
|
|
||||||
|
/// Connects to the server and performs the initial authentication handshake.
|
||||||
|
///
|
||||||
|
/// This establishes a length-delimited TCP connection to prevent fragmentation,
|
||||||
|
/// sends an `AuthRequest` with the given username, and awaits the `AuthResponse`.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `username` - The requested display name for this client.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns an `anyhow::Result` if the TCP socket cannot connect, if the serialization/
|
||||||
|
/// deserialization fails, or if the server closes the connection prematurely.
|
||||||
|
pub async fn connect_and_auth(username: &str) -> Result<()> {
|
||||||
|
let addr = format!("127.0.0.1:{}", constants::TCP_PORT);
|
||||||
|
info!("Connecting to server at {}...", addr);
|
||||||
|
|
||||||
|
let stream = TcpStream::connect(&addr).await.context("Failed to connect to server")?;
|
||||||
|
info!("Connected!");
|
||||||
|
|
||||||
|
// Construct the codec pipeline exactly mirroring the server's configuration
|
||||||
|
// to ensure reliable packet framing.
|
||||||
|
let length_delimited = Framed::new(stream, LengthDelimitedCodec::new());
|
||||||
|
let mut framed: FramedStream = SymmetricallyFramed::new(
|
||||||
|
length_delimited,
|
||||||
|
Bincode::<TcpEvent, TcpEvent>::default(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let auth_req = TcpEvent::AuthRequest {
|
||||||
|
username: username.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
framed.send(auth_req).await.context("Failed to send AuthRequest")?;
|
||||||
|
info!("Sent AuthRequest for user: {}", username);
|
||||||
|
|
||||||
|
if let Some(response) = framed.next().await {
|
||||||
|
let response = response.context("Failed to deserialize response")?;
|
||||||
|
|
||||||
|
match response {
|
||||||
|
TcpEvent::AuthResponse { session_token } => {
|
||||||
|
info!("Successfully authenticated! Session token: {}", session_token);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
warn!("Received unexpected event instead of AuthResponse");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!("Connection closed before receiving AuthResponse");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
6
client_node/src/network/mod.rs
Normal file
6
client_node/src/network/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
//! Internet connectivity modules.
|
||||||
|
//!
|
||||||
|
//! This module isolates all remote networking logic from the UI and audio engines.
|
||||||
|
//! It exposes the necessary interfaces to manage TCP control lanes and UDP streams.
|
||||||
|
|
||||||
|
pub mod control;
|
||||||
12
core_protocol/Cargo.toml
Normal file
12
core_protocol/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
name = "core_protocol"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bincode = "1.3.3"
|
||||||
|
chrono = { version = "0.4.44", features = ["serde"] }
|
||||||
|
secrecy = { version = "0.10.3", features = ["serde"] }
|
||||||
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
thiserror = "2.0.18"
|
||||||
|
uuid = { version = "1.23.1", features = ["v4", "serde"] }
|
||||||
15
core_protocol/src/constants.rs
Normal file
15
core_protocol/src/constants.rs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
//! Fixed system-wide specifications.
|
||||||
|
//!
|
||||||
|
//! Contains constants required for hardware configuration, network binding,
|
||||||
|
//! and math routines such as audio framing lengths.
|
||||||
|
|
||||||
|
/// The uniform audio sampling rate (48 kHz).
|
||||||
|
/// We lock the entire system to 48kHz because Opus performs optimally at this rate.
|
||||||
|
pub const SAMPLE_RATE: u32 = 48000;
|
||||||
|
|
||||||
|
/// The exact number of audio samples per 20ms frame at 48kHz.
|
||||||
|
/// Opus strictly requires a specific frame size (like 20ms) to function correctly.
|
||||||
|
pub const FRAME_SIZE: usize = 960;
|
||||||
|
|
||||||
|
/// The default TCP port used for reliable control lane communication.
|
||||||
|
pub const TCP_PORT: u16 = 8080;
|
||||||
13
core_protocol/src/lib.rs
Normal file
13
core_protocol/src/lib.rs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
//! Core Protocol definitions for the Voice App.
|
||||||
|
//!
|
||||||
|
//! This module defines the foundational types and constants shared between the
|
||||||
|
//! client and server nodes. It ensures that both ends of the connection speak the
|
||||||
|
//! exact same structural language for serialization/deserialization.
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
#![deny(clippy::all, clippy::pedantic)]
|
||||||
|
#![deny(clippy::unwrap_used, clippy::expect_used)]
|
||||||
|
|
||||||
|
pub mod constants;
|
||||||
|
pub mod tcp_events;
|
||||||
|
pub mod udp_packets;
|
||||||
31
core_protocol/src/tcp_events.rs
Normal file
31
core_protocol/src/tcp_events.rs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
//! TCP Control Lane events.
|
||||||
|
//!
|
||||||
|
//! Defines the reliable commands sent over the TCP connection for state
|
||||||
|
//! synchronization, such as authentication and text chat.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Represents all possible events sent over the reliable TCP control lane.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub enum TcpEvent {
|
||||||
|
/// Initial request from the client containing their chosen username.
|
||||||
|
AuthRequest {
|
||||||
|
/// The username requested by the client.
|
||||||
|
username: String,
|
||||||
|
},
|
||||||
|
/// The server's response confirming authentication and granting a session token.
|
||||||
|
AuthResponse {
|
||||||
|
/// A unique, ephemeral ID assigned to the client for the duration of the session.
|
||||||
|
session_token: u32,
|
||||||
|
},
|
||||||
|
/// A request to join a specific voice channel.
|
||||||
|
ChannelJoin {
|
||||||
|
/// The unique ID of the target channel.
|
||||||
|
channel_id: u32,
|
||||||
|
},
|
||||||
|
/// A text message intended for the current channel's chat.
|
||||||
|
ChatMessage {
|
||||||
|
/// The raw text message.
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
20
core_protocol/src/udp_packets.rs
Normal file
20
core_protocol/src/udp_packets.rs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
//! UDP High-speed Voice Packet structures.
|
||||||
|
//!
|
||||||
|
//! Defines the headers and payload structures for the low-latency voice data
|
||||||
|
//! sent over UDP.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// The header attached to every UDP voice frame.
|
||||||
|
///
|
||||||
|
/// We separate this from the payload to allow the server to rapidly route packets
|
||||||
|
/// using just the `session_token` without fully deserializing the heavy audio data.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct VoicePacketHeader {
|
||||||
|
/// The unique session ID matching the client's TCP authentication token.
|
||||||
|
pub session_token: u32,
|
||||||
|
/// A strictly increasing sequence number to detect dropped or out-of-order packets.
|
||||||
|
pub sequence_num: u64,
|
||||||
|
/// The timestamp of the packet generation, used to align jitter buffers.
|
||||||
|
pub timestamp: u64,
|
||||||
|
}
|
||||||
15
server_node/Cargo.toml
Normal file
15
server_node/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "server_node"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0.102"
|
||||||
|
core_protocol = { version = "0.1.0", path = "../core_protocol" }
|
||||||
|
dashmap = "6.1.0"
|
||||||
|
futures = "0.3.32"
|
||||||
|
tokio = { version = "1.52.1", features = ["full"] }
|
||||||
|
tokio-serde = { version = "0.9.0", features = ["bincode"] }
|
||||||
|
tokio-util = { version = "0.7.18", features = ["codec"] }
|
||||||
|
tracing = "0.1.44"
|
||||||
|
tracing-subscriber = "0.3.23"
|
||||||
43
server_node/src/main.rs
Normal file
43
server_node/src/main.rs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
//! Server Node entry point.
|
||||||
|
//!
|
||||||
|
//! This module initializes the headless relay server, binds the network listeners,
|
||||||
|
//! and spawns isolated tasks for each connected TCP client.
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
#![deny(clippy::all, clippy::pedantic)]
|
||||||
|
#![deny(clippy::unwrap_used, clippy::expect_used)]
|
||||||
|
|
||||||
|
mod state;
|
||||||
|
mod tcp_router;
|
||||||
|
|
||||||
|
use core_protocol::constants;
|
||||||
|
use state::AppState;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::net::TcpListener;
|
||||||
|
use tracing::{error, info};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
info!("Starting server node...");
|
||||||
|
|
||||||
|
let state = Arc::new(AppState::new());
|
||||||
|
|
||||||
|
let addr = format!("0.0.0.0:{}", constants::TCP_PORT);
|
||||||
|
let listener = TcpListener::bind(&addr).await?;
|
||||||
|
info!("TCP Listener bound to {}", addr);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match listener.accept().await {
|
||||||
|
Ok((stream, _)) => {
|
||||||
|
let state_clone = state.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
tcp_router::handle_connection(stream, state_clone).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to accept connection: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
49
server_node/src/state.rs
Normal file
49
server_node/src/state.rs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
//! Concurrent server state management.
|
||||||
|
//!
|
||||||
|
//! This module defines the global application state shared across all active
|
||||||
|
//! Tokio tasks. It heavily relies on lock-free and fine-grained locking primitives
|
||||||
|
//! like `DashMap` and `AtomicU32` to ensure high performance without stuttering.
|
||||||
|
|
||||||
|
use dashmap::DashMap;
|
||||||
|
use std::sync::atomic::{AtomicU32, Ordering};
|
||||||
|
|
||||||
|
/// Represents the session data for a single connected user.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct UserState {
|
||||||
|
/// The client-chosen username for display in channels.
|
||||||
|
pub username: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The global application state tracking all connections and rooms.
|
||||||
|
pub struct AppState {
|
||||||
|
/// A highly concurrent hash map linking active session tokens to their user data.
|
||||||
|
pub active_users: DashMap<u32, UserState>,
|
||||||
|
/// A simple atomic counter for issuing unique sequential session tokens.
|
||||||
|
next_token: AtomicU32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppState {
|
||||||
|
/// Creates a new, empty application state.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
active_users: DashMap::new(),
|
||||||
|
next_token: AtomicU32::new(1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates a strictly unique, monotonically increasing session token.
|
||||||
|
///
|
||||||
|
/// We use `Ordering::Relaxed` because we only need atomicity on the counter
|
||||||
|
/// itself, not strict memory ordering with other operations.
|
||||||
|
#[must_use]
|
||||||
|
pub fn generate_token(&self) -> u32 {
|
||||||
|
self.next_token.fetch_add(1, Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AppState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
95
server_node/src/tcp_router.rs
Normal file
95
server_node/src/tcp_router.rs
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
//! Reliable TCP routing and connection management.
|
||||||
|
//!
|
||||||
|
//! This module implements the reliable control lane for client communication.
|
||||||
|
//! It handles the initial authentication handshake and routes incoming TCP events
|
||||||
|
//! from the length-delimited codec to the broader application state.
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use core_protocol::tcp_events::TcpEvent;
|
||||||
|
use futures::{SinkExt, StreamExt};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::net::TcpStream;
|
||||||
|
use tokio_serde::formats::Bincode;
|
||||||
|
use tokio_serde::SymmetricallyFramed;
|
||||||
|
use tokio_util::codec::{Framed, LengthDelimitedCodec};
|
||||||
|
use tracing::{error, info, instrument, warn};
|
||||||
|
|
||||||
|
use crate::state::AppState;
|
||||||
|
use crate::state::UserState;
|
||||||
|
|
||||||
|
/// A type alias for the heavily-nested framed stream type, combining `LengthDelimitedCodec` and `Bincode`.
|
||||||
|
type FramedStream = SymmetricallyFramed<
|
||||||
|
Framed<TcpStream, LengthDelimitedCodec>,
|
||||||
|
TcpEvent,
|
||||||
|
Bincode<TcpEvent, TcpEvent>,
|
||||||
|
>;
|
||||||
|
|
||||||
|
/// Handles the lifecycle of a newly connected client's TCP stream.
|
||||||
|
///
|
||||||
|
/// This spans an instrumented task for the connection, setting up the necessary
|
||||||
|
/// framers and codecs before entering the event loop.
|
||||||
|
#[instrument(skip(stream, state))]
|
||||||
|
pub async fn handle_connection(stream: TcpStream, state: Arc<AppState>) {
|
||||||
|
let peer_addr = match stream.peer_addr() {
|
||||||
|
Ok(addr) => addr,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to get peer address: {:?}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
info!("New connection from {}", peer_addr);
|
||||||
|
|
||||||
|
// We pad the TCP stream with a length-delimited codec to guarantee frame boundaries,
|
||||||
|
// avoiding fragmentation issues common in raw TCP sockets.
|
||||||
|
let length_delimited = Framed::new(stream, LengthDelimitedCodec::new());
|
||||||
|
let mut framed: FramedStream = SymmetricallyFramed::new(
|
||||||
|
length_delimited,
|
||||||
|
Bincode::<TcpEvent, TcpEvent>::default(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Err(e) = process_connection(&mut framed, state).await {
|
||||||
|
warn!("Connection closed with error: {:?}", e);
|
||||||
|
} else {
|
||||||
|
info!("Connection closed cleanly");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The inner event loop that processes deserialized `TcpEvent`s from the client.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns an `anyhow::Result` if deserialization fails, the connection drops unexpectedly,
|
||||||
|
/// or a serialization error occurs when transmitting a response.
|
||||||
|
async fn process_connection(framed: &mut FramedStream, state: Arc<AppState>) -> Result<()> {
|
||||||
|
while let Some(event) = framed.next().await {
|
||||||
|
let event = event.context("Failed to deserialize event")?;
|
||||||
|
|
||||||
|
match event {
|
||||||
|
TcpEvent::AuthRequest { username } => {
|
||||||
|
// REDACTED standard: we might log the username, but this is a reminder
|
||||||
|
// for future sensitive items to use [REDACTED].
|
||||||
|
info!("AuthRequest received for user: {}", username);
|
||||||
|
|
||||||
|
let session_token = state.generate_token();
|
||||||
|
|
||||||
|
state.active_users.insert(
|
||||||
|
session_token,
|
||||||
|
UserState {
|
||||||
|
username: username.clone(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
framed
|
||||||
|
.send(TcpEvent::AuthResponse { session_token })
|
||||||
|
.await
|
||||||
|
.context("Failed to send AuthResponse")?;
|
||||||
|
|
||||||
|
info!("AuthResponse sent to {}", username);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
warn!("Received unhandled event before auth or unsupported event");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user