add network test feature (PING/PONG with latency stats)

New frame types PING (0x0B) and PONG (0x0C), each carrying an 8-byte
nonce. Server echoes PING nonce verbatim in PONG. Client sends pings
at random 10-100ms intervals, correlates nonces to measure RTT.

Stats shown live: sent/recv counts, loss %, average latency, jitter
(mean absolute delta of consecutive RTTs). Test button toggles on/off.

Updated both Rust server (echo in main.rs) and C# client (PingTest.cs,
SessionManager routing, MainForm Test button + stats label).
This commit is contained in:
2026-08-13 08:28:59 +00:00
parent a2d3643d69
commit 279af33fd8
7 changed files with 242 additions and 4 deletions
+20
View File
@@ -16,6 +16,8 @@ pub const TYPE_CLOSE: u8 = 0x07;
pub const TYPE_UDP_OPEN: u8 = 0x08;
pub const TYPE_UDP_DATA: u8 = 0x09;
pub const TYPE_UDP_CLOSE: u8 = 0x0A;
pub const TYPE_PING: u8 = 0x0B;
pub const TYPE_PONG: u8 = 0x0C;
pub const PROTO_TCP: u8 = 1;
pub const PROTO_UDP: u8 = 2;
@@ -45,6 +47,8 @@ pub enum Frame {
OpenNak { upstream_id: u8, reason: u8 },
Data { session_id: u32, payload: Vec<u8> },
Close { session_id: u32, reason: Option<u8> },
Ping { nonce: u64 },
Pong { nonce: u64 },
}
#[derive(Debug)]
@@ -120,6 +124,8 @@ impl Frame {
};
build(TYPE_CLOSE, *session_id, p)
}
Frame::Ping { nonce } => build(TYPE_PING, 0, nonce.to_be_bytes().to_vec()),
Frame::Pong { nonce } => build(TYPE_PONG, 0, nonce.to_be_bytes().to_vec()),
}
}
@@ -212,6 +218,20 @@ impl Frame {
};
Ok(Frame::Close { session_id, reason })
}
TYPE_PING => {
if payload.len() != 8 {
return Err(DecodeError::BadPayload("PING payload must be 8 bytes"));
}
let nonce = u64::from_be_bytes(payload.try_into().unwrap());
Ok(Frame::Ping { nonce })
}
TYPE_PONG => {
if payload.len() != 8 {
return Err(DecodeError::BadPayload("PONG payload must be 8 bytes"));
}
let nonce = u64::from_be_bytes(payload.try_into().unwrap());
Ok(Frame::Pong { nonce })
}
TYPE_UDP_OPEN | TYPE_UDP_DATA | TYPE_UDP_CLOSE => {
Err(DecodeError::BadPayload("UDP frame types not implemented in v1"))
}
+6 -1
View File
@@ -207,7 +207,12 @@ async fn handle_frame(
Frame::Close { session_id, reason: _ } => {
store.lock().expect("store poisoned").remove(&session_id);
}
Frame::Ping { nonce } => {
let pong = Frame::Pong { nonce };
let _ = tx.send((src, pong.encode())).await;
}
// Not expected from a client; ignore.
Frame::Manifest { .. } | Frame::OpenAck { .. } | Frame::OpenNak { .. } => {}
Frame::Manifest { .. } | Frame::OpenAck { .. } | Frame::OpenNak { .. }
| Frame::Pong { .. } => {}
}
}