diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..5b86d40 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,33 @@ +name: Rust + +on: + [push, pull_request] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + strategy: + matrix: + host_os: + - ubuntu-latest + - macos-latest + - windows-latest + + runs-on: ${{ matrix.host_os }} + + steps: + - uses: actions/checkout@v3 + - name: check + run: cargo check + - name: rustfmt + run: | + rustc --version + cargo fmt --all -- --check + - name: clippy + run: cargo clippy -- -D warnings + - name: Build + run: cargo build --verbose --all-features --tests --examples + - name: Run tests + run: cargo test --verbose diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..269985d --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.vscode/ +.VSCodeCounter/ +build/ +tmp/ +Cargo.lock +target/ diff --git a/Cargo.toml b/Cargo.toml index 9a096fb..207c59a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,17 +12,17 @@ readme = "README.md" [dependencies] chrono = "0.4" -clap = "~2.33.3" -core_affinity = "0.5" -ctrlc = "3.1" -env_logger = "0.8" -log = {version = "0.4", features = ["std"]} -mio = "0.6" -nix = "0.20" -serde = {version = "1.0", features = ["derive"]} +clap = { version = "4.4", features = ["derive", "wrap_help"] } +core_affinity = "0.8" +ctrlc2 = { version = "3.5", features = ["termination"] } +env_logger = "0.10" +log = { version = "0.4", features = ["std"] } +mio = { version = "0.8", features = ["log", "os-poll", "net"] } +serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -simple-error = "0.2" -uuid = {version = "0.8", features = ["v4"]} +simple-error = "0.3" +socket2 = { version = "0.5", features = ["all"] } +uuid = { version = "1.6", features = ["v4"] } #configuration for cargo-deb #install with "cargo install cargo-deb" diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..8449be0 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +max_width = 140 diff --git a/src/args.rs b/src/args.rs new file mode 100644 index 0000000..8944b1e --- /dev/null +++ b/src/args.rs @@ -0,0 +1,184 @@ +/// rperf, validates network throughput capacity and reliability, +/// https://github.com/opensource-3d-p/rperf +#[derive(clap::Parser, Debug, Clone)] +#[command(author, version, about, long_about = None)] +pub struct Args { + /// the port used for client-server interactions + #[arg(short, long, value_name = "number", default_value_t = 5199)] + pub port: u16, + + /// specify logical CPUs, delimited by commas, across which to round-robin affinity; + /// not supported on all systems + #[arg(short = 'A', long, value_name = "numbers", default_value = "")] + pub affinity: String, + + /// bind to the interface associated with the address + #[arg( + short = 'B', + long, + conflicts_with = "client", + default_value_if("version6", "true", Some("::")), + default_value = "0.0.0.0", + value_name = "host" + )] + pub bind: std::net::IpAddr, + + /// emit debug-level logging on stderr; default is info and above + #[arg(short, long)] + pub debug: bool, + + /// run in server mode + #[arg(short, long, conflicts_with = "client")] + pub server: bool, + + /// enable IPv6 on the server (on most hosts, this will allow both IPv4 and IPv6, + /// but it might limit to just IPv6 on some) + #[arg(short = '6', long, conflicts_with = "client")] + pub version6: bool, + + /// limit the number of concurrent clients that can be processed by a server; + /// any over this count will be immediately disconnected + #[arg(long, value_name = "number", default_value = "0", conflicts_with = "client")] + pub client_limit: usize, + + /// run in client mode; value is the server's address + #[arg(short, long, value_name = "host", conflicts_with = "server")] + pub client: Option, + + /// run in reverse-mode (server sends, client receives) + #[arg(short = 'R', long, conflicts_with = "server")] + pub reverse: bool, + + /// the format in which to deplay information (json, megabit/sec, megabyte/sec) + #[arg( + short, + long, + value_enum, + value_name = "format", + default_value = "megabit", + conflicts_with = "server" + )] + pub format: Format, + + /// use UDP rather than TCP + #[arg(short, long, conflicts_with = "server")] + pub udp: bool, + + /// target bandwidth in bytes/sec; this value is applied to each stream, + /// with a default target of 1 megabit/second for all protocols (note: megabit, not mebibit); + /// the suffixes kKmMgG can also be used for xbit and xbyte, respectively + #[arg(short, long, default_value = "125000", value_name = "bytes/sec", conflicts_with = "server")] + pub bandwidth: String, + + /// the time in seconds for which to transmit + #[arg(short, long, default_value = "10.0", value_name = "seconds", conflicts_with = "server")] + pub time: f64, + + /// the interval at which to send batches of data, in seconds, between [0.0 and 1.0); + /// this is used to evenly spread packets out over time + #[arg(long, default_value = "0.05", value_name = "seconds", conflicts_with = "server")] + pub send_interval: f64, + + /// length of the buffer to exchange; for TCP, this defaults to 32 kibibytes; for UDP, it's 1024 bytes + #[arg( + short, + long, + conflicts_with = "server", + default_value = "32768", + default_value_if("udp", "true", Some("1024")), + value_name = "bytes" + )] + pub length: usize, + + /// send buffer, in bytes (only supported on some platforms; + /// if set too small, a 'resource unavailable' error may occur; + /// affects UDP and TCP window-size) + #[arg(long, default_value = "0", value_name = "bytes", conflicts_with = "server")] + pub send_buffer: usize, + + /// receive buffer, in bytes (only supported on some platforms; + /// if set too small, a 'resource unavailable' error may occur; affects UDP) + #[arg(long, default_value = "0", value_name = "bytes", conflicts_with = "server")] + pub receive_buffer: usize, + + /// the number of parallel data-streams to use + #[arg(short = 'P', long, value_name = "number", default_value = "1", conflicts_with = "server")] + pub parallel: usize, + + /// omit a number of seconds from the start of calculations, + /// primarily to avoid including TCP ramp-up in averages; + /// using this option may result in disagreement between bytes sent and received, + /// since data can be in-flight across time-boundaries + #[arg(short = 'O', long, default_value = "0", value_name = "seconds", conflicts_with = "server")] + pub omit: usize, + + /// use no-delay mode for TCP tests, disabling Nagle's Algorithm + #[arg(short = 'N', long, conflicts_with = "server")] + pub no_delay: bool, + + /// an optional pool of IPv4 TCP ports over which data will be accepted; + /// if omitted, any OS-assignable port is used; format: 1-10,19,21 + #[arg(long, value_name = "ports", default_value = "")] + pub tcp_port_pool: String, + + /// an optional pool of IPv6 TCP ports over which data will be accepted; + /// if omitted, any OS-assignable port is used; format: 1-10,19,21 + #[arg(long, value_name = "ports", default_value = "")] + pub tcp6_port_pool: String, + + /// an optional pool of IPv4 UDP ports over which data will be accepted; + /// if omitted, any OS-assignable port is used; format: 1-10,19,21 + #[arg(long, value_name = "ports", default_value = "")] + pub udp_port_pool: String, + + /// an optional pool of IPv6 UDP ports over which data will be accepted; + /// if omitted, any OS-assignable port is used; format: 1-10,19,21 + #[arg(long, value_name = "ports", default_value = "")] + pub udp6_port_pool: String, + + /// Verbosity level + #[arg(short, long, value_name = "level", value_enum, default_value = "info")] + pub verbosity: ArgVerbosity, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum, Default)] +pub enum Format { + #[default] + Json, + Megabit, + Megabyte, +} + +impl std::fmt::Display for Format { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Format::Json => write!(f, "json"), + Format::Megabit => write!(f, "megabit/sec"), + Format::Megabyte => write!(f, "megabyte/sec"), + } + } +} + +#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)] +pub enum ArgVerbosity { + Off, + Error, + Warn, + #[default] + Info, + Debug, + Trace, +} + +impl std::fmt::Display for ArgVerbosity { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ArgVerbosity::Off => write!(f, "off"), + ArgVerbosity::Error => write!(f, "error"), + ArgVerbosity::Warn => write!(f, "warn"), + ArgVerbosity::Info => write!(f, "info"), + ArgVerbosity::Debug => write!(f, "debug"), + ArgVerbosity::Trace => write!(f, "trace"), + } + } +} diff --git a/src/client.rs b/src/client.rs index 797c540..72b9a85 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,91 +1,93 @@ /* * Copyright (C) 2021 Evtech Solutions, Ltd., dba 3D-P * Copyright (C) 2021 Neil Tallim - * + * * This file is part of rperf. - * + * * rperf is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * rperf is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with rperf. If not, see . */ -use std::net::{IpAddr, Shutdown, ToSocketAddrs}; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; -use std::sync::mpsc::channel; -use std::thread; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; - -use clap::ArgMatches; - -use mio::net::{TcpStream}; - -use crate::protocol::communication::{receive, send, KEEPALIVE_DURATION}; - -use crate::protocol::messaging::{ - prepare_begin, prepare_end, - prepare_upload_configuration, prepare_download_configuration, +use crate::{ + args, + protocol::{ + communication::{receive, send}, + messaging::{prepare_begin, prepare_download_configuration, prepare_end, prepare_upload_configuration}, + results::{ClientDoneResult, ClientFailedResult}, + results::{IntervalResultBox, IntervalResultKind, TcpTestResults, TestResults, UdpTestResults}, + }, + stream::{tcp, udp, TestStream}, + BoxResult, +}; +use std::{ + net::{IpAddr, Shutdown, TcpStream, ToSocketAddrs}, + sync::{ + atomic::{AtomicBool, Ordering}, + mpsc::channel, + Arc, Mutex, + }, + thread, + time::{Duration, SystemTime, UNIX_EPOCH}, }; - -use crate::protocol::results::{IntervalResult, IntervalResultKind, TestResults, TcpTestResults, UdpTestResults}; - -use crate::stream::TestStream; -use crate::stream::tcp; -use crate::stream::udp; - -use std::error::Error; -type BoxResult = Result>; /// when false, the system is shutting down -static ALIVE:AtomicBool = AtomicBool::new(true); +static ALIVE: AtomicBool = AtomicBool::new(true); /// a deferred kill-switch to handle shutdowns a bit more gracefully in the event of a probable disconnect -static mut KILL_TIMER_RELATIVE_START_TIME:f64 = 0.0; //the time at which the kill-timer was started -const KILL_TIMEOUT:f64 = 5.0; //once testing finishes, allow a few seconds for the server to respond +static mut KILL_TIMER_RELATIVE_START_TIME: f64 = 0.0; //the time at which the kill-timer was started +const KILL_TIMEOUT: f64 = 5.0; //once testing finishes, allow a few seconds for the server to respond -const CONNECT_TIMEOUT:Duration = Duration::from_secs(2); +const CONNECT_TIMEOUT: Duration = Duration::from_secs(2); -fn connect_to_server(address:&str, port:&u16) -> BoxResult { +fn connect_to_server(address: &str, port: &u16) -> BoxResult { let destination = format!("{}:{}", address, port); log::info!("connecting to server at {}...", destination); - + let server_addr = destination.to_socket_addrs()?.next(); if server_addr.is_none() { return Err(Box::new(simple_error::simple_error!("unable to resolve {}", address))); } - let raw_stream = match std::net::TcpStream::connect_timeout(&server_addr.unwrap(), CONNECT_TIMEOUT) { + let stream = match std::net::TcpStream::connect_timeout(&server_addr.unwrap(), CONNECT_TIMEOUT) { Ok(s) => s, Err(e) => return Err(Box::new(simple_error::simple_error!("unable to connect: {}", e))), }; - let stream = match TcpStream::from_stream(raw_stream) { - Ok(s) => s, - Err(e) => return Err(Box::new(simple_error::simple_error!("unable to prepare TCP control-channel: {}", e))), - }; - log::info!("connected to server"); - + + log::debug!("connected TCP control-channel to {}", destination); stream.set_nodelay(true).expect("cannot disable Nagle's algorithm"); - stream.set_keepalive(Some(KEEPALIVE_DURATION)).expect("unable to set TCP keepalive"); - + + #[cfg(unix)] + { + use crate::protocol::communication::KEEPALIVE_DURATION; + let keepalive_parameters = socket2::TcpKeepalive::new().with_time(KEEPALIVE_DURATION); + let raw_socket = socket2::SockRef::from(&stream); + raw_socket.set_tcp_keepalive(&keepalive_parameters)?; + } + + log::info!("connected to server"); + Ok(stream) } -fn prepare_test_results(is_udp:bool, stream_count:u8) -> Mutex> { - if is_udp { //UDP +fn prepare_test_results(is_udp: bool, stream_count: u8) -> Mutex> { + if is_udp { + //UDP let mut udp_test_results = UdpTestResults::new(); for i in 0..stream_count { udp_test_results.prepare_index(&i); } Mutex::new(Box::new(udp_test_results)) - } else { //TCP + } else { + //TCP let mut tcp_test_results = TcpTestResults::new(); for i in 0..stream_count { tcp_test_results.prepare_index(&i); @@ -94,120 +96,110 @@ fn prepare_test_results(is_udp:bool, stream_count:u8) -> Mutex BoxResult<()> { +pub fn execute(args: &args::Args) -> BoxResult<()> { let mut complete = false; - + //config-parsing and pre-connection setup - let mut tcp_port_pool = tcp::receiver::TcpPortPool::new( - args.value_of("tcp_port_pool").unwrap().to_string(), - args.value_of("tcp6_port_pool").unwrap().to_string(), - ); - let mut udp_port_pool = udp::receiver::UdpPortPool::new( - args.value_of("udp_port_pool").unwrap().to_string(), - args.value_of("udp6_port_pool").unwrap().to_string(), - ); - - let cpu_affinity_manager = Arc::new(Mutex::new(crate::utils::cpu_affinity::CpuAffinityManager::new(args.value_of("affinity").unwrap())?)); - - let display_json:bool; - let display_bit:bool; - match args.value_of("format").unwrap() { - "json" => { + let mut tcp_port_pool = tcp::receiver::TcpPortPool::new(&args.tcp_port_pool, &args.tcp6_port_pool); + let mut udp_port_pool = udp::receiver::UdpPortPool::new(&args.udp_port_pool, &args.udp6_port_pool); + + let cpu_affinity_manager = Arc::new(Mutex::new(crate::utils::cpu_affinity::CpuAffinityManager::new(&args.affinity)?)); + + let display_json: bool; + let display_bit: bool; + match args.format { + args::Format::Json => { display_json = true; display_bit = false; - }, - "megabit" => { + } + args::Format::Megabit => { display_json = false; display_bit = true; - }, - "megabyte" => { + } + args::Format::Megabyte => { display_json = false; display_bit = false; - }, - _ => { - log::error!("unsupported display-mode; defaulting to JSON"); - display_json = true; - display_bit = false; - }, + } } - - let is_udp = args.is_present("udp"); - + + let is_udp = args.udp; + let test_id = uuid::Uuid::new_v4(); - let mut upload_config = prepare_upload_configuration(&args, test_id.as_bytes())?; - let mut download_config = prepare_download_configuration(&args, test_id.as_bytes())?; - - + let mut upload_config = prepare_upload_configuration(args, test_id.as_bytes())?; + let mut download_config = prepare_download_configuration(args, test_id.as_bytes())?; + //connect to the server - let mut stream = connect_to_server(&args.value_of("client").unwrap(), &(args.value_of("port").unwrap().parse()?))?; + let mut stream = connect_to_server(args.client.as_ref().unwrap(), &args.port)?; let server_addr = stream.peer_addr()?; - - + //scaffolding to track and relay the streams and stream-results associated with this test let stream_count = download_config.get("streams").unwrap().as_i64().unwrap() as usize; - let mut parallel_streams:Vec>> = Vec::with_capacity(stream_count); + let mut parallel_streams: Vec>> = Vec::with_capacity(stream_count); let mut parallel_streams_joinhandles = Vec::with_capacity(stream_count); - let (results_tx, results_rx):(std::sync::mpsc::Sender>, std::sync::mpsc::Receiver>) = channel(); - - let test_results:Mutex> = prepare_test_results(is_udp, stream_count as u8); - + let (results_tx, results_rx): ( + std::sync::mpsc::Sender, + std::sync::mpsc::Receiver, + ) = channel(); + + let test_results: Mutex> = prepare_test_results(is_udp, stream_count as u8); + //a closure used to pass results from stream-handlers to the test-result structure let mut results_handler = || -> BoxResult<()> { - loop { //drain all results every time this closer is invoked - match results_rx.try_recv() { //see if there's a result to pass on - Ok(result) => { - if !display_json { //since this runs in the main thread, which isn't involved in any testing, render things immediately - println!("{}", result.to_string(display_bit)); + //drain all results every time this closer is invoked + while let Ok(result) = results_rx.try_recv() { + //see if there's a result to pass on + if !display_json { + //since this runs in the main thread, which isn't involved in any testing, render things immediately + println!("{}", result.to_string(display_bit)); + } + + //update the test-results accordingly + let mut tr = test_results.lock().unwrap(); + match result.kind() { + IntervalResultKind::ClientDone | IntervalResultKind::ClientFailed => { + if result.kind() == IntervalResultKind::ClientDone { + log::info!("stream {} is done", result.get_stream_idx()); + } else { + log::warn!("stream {} failed", result.get_stream_idx()); } - - //update the test-results accordingly - let mut tr = test_results.lock().unwrap(); - match result.kind() { - IntervalResultKind::ClientDone | IntervalResultKind::ClientFailed => { - if result.kind() == IntervalResultKind::ClientDone { - log::info!("stream {} is done", result.get_stream_idx()); - } else { - log::warn!("stream {} failed", result.get_stream_idx()); - } - tr.mark_stream_done(&result.get_stream_idx(), result.kind() == IntervalResultKind::ClientDone); - if tr.count_in_progress_streams() == 0 { - complete = true; - - if tr.count_in_progress_streams_server() > 0 { - log::info!("giving the server a few seconds to report results..."); - start_kill_timer(); - } else { //all data gathered from both sides - kill(); - } - } - }, - _ => { - tr.update_from_json(result.to_json())?; + tr.mark_stream_done(&result.get_stream_idx(), result.kind() == IntervalResultKind::ClientDone); + if tr.count_in_progress_streams() == 0 { + complete = true; + + if tr.count_in_progress_streams_server() > 0 { + log::info!("giving the server a few seconds to report results..."); + start_kill_timer(); + } else { + //all data gathered from both sides + kill(); } } - }, - Err(_) => break, //whether it's empty or disconnected, there's nothing to do + } + _ => { + tr.update_from_json(result.to_json())?; + } } } Ok(()) }; - + //depending on whether this is a forward- or reverse-test, the order of configuring test-streams will differ - if args.is_present("reverse") { + if args.reverse { log::debug!("running in reverse-mode: server will be uploading data"); - + //when we're receiving data, we're also responsible for letting the server know where to send it let mut stream_ports = Vec::with_capacity(stream_count); - - if is_udp { //UDP + + if is_udp { + //UDP log::info!("preparing for reverse-UDP test with {} streams...", stream_count); - + let test_definition = udp::UdpTestDefinition::new(&download_config)?; for stream_idx in 0..stream_count { log::debug!("preparing UDP-receiver for stream {}...", stream_idx); let test = udp::receiver::UdpReceiver::new( - test_definition.clone(), &(stream_idx as u8), + test_definition.clone(), + &(stream_idx as u8), &mut udp_port_pool, &server_addr.ip(), &(download_config["receive_buffer"].as_i64().unwrap() as usize), @@ -215,67 +207,85 @@ pub fn execute(args:ArgMatches) -> BoxResult<()> { stream_ports.push(test.get_port()?); parallel_streams.push(Arc::new(Mutex::new(test))); } - } else { //TCP + } else { + //TCP log::info!("preparing for reverse-TCP test with {} streams...", stream_count); - + let test_definition = tcp::TcpTestDefinition::new(&download_config)?; for stream_idx in 0..stream_count { log::debug!("preparing TCP-receiver for stream {}...", stream_idx); - let test = tcp::receiver::TcpReceiver::new( - test_definition.clone(), &(stream_idx as u8), - &mut tcp_port_pool, - &server_addr.ip(), - &(download_config["receive_buffer"].as_i64().unwrap() as usize), - )?; + let test = + tcp::receiver::TcpReceiver::new(test_definition.clone(), &(stream_idx as u8), &mut tcp_port_pool, &server_addr.ip())?; stream_ports.push(test.get_port()?); parallel_streams.push(Arc::new(Mutex::new(test))); - } + } } - + //add the port-list to the upload-config that the server will receive; this is in stream-index order upload_config["stream_ports"] = serde_json::json!(stream_ports); - + //let the server know what we're expecting send(&mut stream, &upload_config)?; } else { log::debug!("running in forward-mode: server will be receiving data"); - + //let the server know to prepare for us to connect send(&mut stream, &download_config)?; //NOTE: we don't prepare to send data at this point; that happens in the loop below, after the server signals that it's ready } - - + //now that the server knows what we need to do, we have to wait for its response let connection_payload = receive(&mut stream, is_alive, &mut results_handler)?; match connection_payload.get("kind") { Some(kind) => { match kind.as_str().unwrap_or_default() { - "connect" => { //we need to connect to the server - if is_udp { //UDP + "connect" => { + //we need to connect to the server + if is_udp { + //UDP log::info!("preparing for UDP test with {} streams...", stream_count); - + let test_definition = udp::UdpTestDefinition::new(&upload_config)?; - for (stream_idx, port) in connection_payload.get("stream_ports").unwrap().as_array().unwrap().iter().enumerate() { + for (stream_idx, port) in connection_payload + .get("stream_ports") + .unwrap() + .as_array() + .unwrap() + .iter() + .enumerate() + { log::debug!("preparing UDP-sender for stream {}...", stream_idx); let test = udp::sender::UdpSender::new( - test_definition.clone(), &(stream_idx as u8), - &0, &server_addr.ip(), &(port.as_i64().unwrap() as u16), + test_definition.clone(), + &(stream_idx as u8), + &0, + &server_addr.ip(), + &(port.as_i64().unwrap() as u16), &(upload_config["duration"].as_f64().unwrap() as f32), &(upload_config["send_interval"].as_f64().unwrap() as f32), &(upload_config["send_buffer"].as_i64().unwrap() as usize), )?; parallel_streams.push(Arc::new(Mutex::new(test))); } - } else { //TCP + } else { + //TCP log::info!("preparing for TCP test with {} streams...", stream_count); - + let test_definition = tcp::TcpTestDefinition::new(&upload_config)?; - for (stream_idx, port) in connection_payload.get("stream_ports").unwrap().as_array().unwrap().iter().enumerate() { + for (stream_idx, port) in connection_payload + .get("stream_ports") + .unwrap() + .as_array() + .unwrap() + .iter() + .enumerate() + { log::debug!("preparing TCP-sender for stream {}...", stream_idx); let test = tcp::sender::TcpSender::new( - test_definition.clone(), &(stream_idx as u8), - &server_addr.ip(), &(port.as_i64().unwrap() as u16), + test_definition.clone(), + &(stream_idx as u8), + &server_addr.ip(), + &(port.as_i64().unwrap() as u16), &(upload_config["duration"].as_f64().unwrap() as f32), &(upload_config["send_interval"].as_f64().unwrap() as f32), &(upload_config["send_buffer"].as_i64().unwrap() as usize), @@ -284,139 +294,174 @@ pub fn execute(args:ArgMatches) -> BoxResult<()> { parallel_streams.push(Arc::new(Mutex::new(test))); } } - }, + } "connect-ready" => { //server is ready to connect to us - //nothing more to do in this flow - }, + //nothing more to do in this flow + } _ => { - log::error!("invalid data from {}: {}", stream.peer_addr()?, serde_json::to_string(&connection_payload)?); + log::error!( + "invalid data from {}: {}", + stream.peer_addr()?, + serde_json::to_string(&connection_payload)? + ); kill(); - }, + } } - }, + } None => { - log::error!("invalid data from {}: {}", stream.peer_addr()?, serde_json::to_string(&connection_payload)?); + log::error!( + "invalid data from {}: {}", + stream.peer_addr()?, + serde_json::to_string(&connection_payload)? + ); kill(); - }, + } } - - - if is_alive() { //if interrupted while waiting for the server to respond, there's no reason to continue + + if is_alive() { + //if interrupted while waiting for the server to respond, there's no reason to continue log::info!("informing server that testing can begin..."); //tell the server to start send(&mut stream, &prepare_begin())?; - + log::debug!("spawning stream-threads"); //begin the test-streams for (stream_idx, parallel_stream) in parallel_streams.iter_mut().enumerate() { log::info!("beginning execution of stream {}...", stream_idx); - let c_ps = Arc::clone(¶llel_stream); + let c_ps = Arc::clone(parallel_stream); let c_results_tx = results_tx.clone(); let c_cam = cpu_affinity_manager.clone(); let handle = thread::spawn(move || { - { //set CPU affinity, if enabled + { + //set CPU affinity, if enabled c_cam.lock().unwrap().set_affinity(); } loop { let mut test = c_ps.lock().unwrap(); log::debug!("beginning test-interval for stream {}", test.get_idx()); - match test.run_interval() { - Some(interval_result) => match interval_result { - Ok(ir) => match c_results_tx.send(ir) { + + let interval_result = match test.run_interval() { + Some(interval_result) => interval_result, + None => { + match c_results_tx.send(Box::new(ClientDoneResult { + stream_idx: test.get_idx(), + })) { Ok(_) => (), Err(e) => { - log::error!("unable to report interval-result: {}", e); - break - }, - }, - Err(e) => { - log::error!("unable to process stream: {}", e); - match c_results_tx.send(Box::new(crate::protocol::results::ClientFailedResult{stream_idx: test.get_idx()})) { - Ok(_) => (), - Err(e) => log::error!("unable to report interval-failed-result: {}", e), + log::error!("unable to report interval-done-result: {}", e) } + } + break; + } + }; + + match interval_result { + Ok(ir) => match c_results_tx.send(ir) { + Ok(_) => (), + Err(e) => { + log::error!("unable to report interval-result: {}", e); break; - }, + } }, - None => { - match c_results_tx.send(Box::new(crate::protocol::results::ClientDoneResult{stream_idx: test.get_idx()})) { + Err(e) => { + log::error!("unable to process stream: {}", e); + match c_results_tx.send(Box::new(ClientFailedResult { + stream_idx: test.get_idx(), + })) { Ok(_) => (), - Err(e) => log::error!("unable to report interval-done-result: {}", e), + Err(e) => { + log::error!("unable to report interval-failed-result: {}", e) + } } break; - }, + } } } }); parallel_streams_joinhandles.push(handle); } - + //watch for events from the server while is_alive() { - match receive(&mut stream, is_alive, &mut results_handler) { - Ok(payload) => { - match payload.get("kind") { - Some(kind) => { - match kind.as_str().unwrap_or_default() { - "receive" | "send" => { //receive/send-results from the server - if !display_json { - let result = crate::protocol::results::interval_result_from_json(payload.clone())?; - println!("{}", result.to_string(display_bit)); - } - let mut tr = test_results.lock().unwrap(); - tr.update_from_json(payload)?; - }, - "done" | "failed" => match payload.get("stream_idx") { //completion-result from the server - Some(stream_idx) => match stream_idx.as_i64() { - Some(idx64) => { - let mut tr = test_results.lock().unwrap(); - match kind.as_str().unwrap() { - "done" => { - log::info!("server reported completion of stream {}", idx64); - }, - "failed" => { - log::warn!("server reported failure with stream {}", idx64); - tr.mark_stream_done(&(idx64 as u8), false); - }, - _ => (), //not possible - } - tr.mark_stream_done_server(&(idx64 as u8)); - - if tr.count_in_progress_streams() == 0 && tr.count_in_progress_streams_server() == 0 { //all data gathered from both sides - kill(); - } - }, - None => log::error!("completion from server did not include a valid stream_idx"), - }, - None => log::error!("completion from server did not include stream_idx"), - }, - _ => { - log::error!("invalid data from {}: {}", stream.peer_addr()?, serde_json::to_string(&connection_payload)?); - break; - }, - } - }, - None => { - log::error!("invalid data from {}: {}", stream.peer_addr()?, serde_json::to_string(&connection_payload)?); - break; - }, - } - }, + let payload = match receive(&mut stream, is_alive, &mut results_handler) { + Ok(payload) => payload, Err(e) => { - if !complete { //when complete, this also occurs + if !complete { + // when complete, this also occurs return Err(e); } break; + } + }; + + let kind = match payload.get("kind") { + Some(kind) => kind, + None => { + log::error!( + "invalid data from {}: {}", + stream.peer_addr()?, + serde_json::to_string(&connection_payload)? + ); + break; + } + }; + + match kind.as_str().unwrap_or_default() { + "receive" | "send" => { + //receive/send-results from the server + if !display_json { + let result = crate::protocol::results::interval_result_from_json(payload.clone())?; + println!("{}", result.to_string(display_bit)); + } + let mut tr = test_results.lock().unwrap(); + tr.update_from_json(payload)?; + } + "done" | "failed" => match payload.get("stream_idx") { + //completion-result from the server + Some(stream_idx) => match stream_idx.as_i64() { + Some(idx64) => { + let mut tr = test_results.lock().unwrap(); + match kind.as_str().unwrap() { + "done" => { + log::info!("server reported completion of stream {}", idx64); + } + "failed" => { + log::warn!("server reported failure with stream {}", idx64); + tr.mark_stream_done(&(idx64 as u8), false); + } + _ => (), //not possible + } + tr.mark_stream_done_server(&(idx64 as u8)); + if tr.count_in_progress_streams() == 0 && tr.count_in_progress_streams_server() == 0 { + //all data gathered from both sides + kill(); + } + } + None => { + log::error!("completion from server did not include a valid stream_idx") + } + }, + None => { + log::error!("completion from server did not include stream_idx") + } }, + _ => { + log::error!( + "invalid data from {}: {}", + stream.peer_addr()?, + serde_json::to_string(&connection_payload)? + ); + break; + } } } } - + //assume this is a controlled shutdown; if it isn't, this is just a very slight waste of time send(&mut stream, &prepare_end()).unwrap_or_default(); thread::sleep(Duration::from_millis(250)); //wait a moment for the "end" message to be queued for delivery to the server stream.shutdown(Shutdown::Both).unwrap_or_default(); - + log::debug!("stopping any still-in-progress streams"); for ps in parallel_streams.iter_mut() { let mut stream = match (*ps).lock() { @@ -424,7 +469,7 @@ pub fn execute(args:ArgMatches) -> BoxResult<()> { Err(poisoned) => { log::error!("a stream-handler was poisoned; this indicates some sort of logic error"); poisoned.into_inner() - }, + } }; stream.stop(); } @@ -435,8 +480,8 @@ pub fn execute(args:ArgMatches) -> BoxResult<()> { Err(e) => log::error!("error in parallel stream: {:?}", e), } } - - let common_config:serde_json::Value; + + let common_config: serde_json::Value; //sanitise the config structures for export { let upload_config_map = upload_config.as_object_mut().unwrap(); @@ -450,7 +495,7 @@ pub fn execute(args:ArgMatches) -> BoxResult<()> { if upload_config_map["send_buffer"].as_i64().unwrap() == 0 { upload_config_map.remove("send_buffer"); } - + let download_config_map = download_config.as_object_mut().unwrap(); download_config_map.remove("family"); download_config_map.remove("kind"); @@ -461,32 +506,41 @@ pub fn execute(args:ArgMatches) -> BoxResult<()> { if download_config_map["receive_buffer"].as_i64().unwrap() == 0 { download_config_map.remove("receive_buffer"); } - + common_config = serde_json::json!({ "family": cc_family, "length": cc_length, "streams": cc_streams, }); } - + log::debug!("displaying test results"); - let omit_seconds:usize = args.value_of("omit").unwrap().parse()?; + let omit_seconds: usize = args.omit; { let tr = test_results.lock().unwrap(); if display_json { - println!("{}", tr.to_json_string(omit_seconds, upload_config, download_config, common_config, serde_json::json!({ - "omit_seconds": omit_seconds, - "ip_version": match server_addr.ip() { - IpAddr::V4(_) => 4, - IpAddr::V6(_) => 6, - }, - "reverse": args.is_present("reverse"), - }))); + println!( + "{}", + tr.to_json_string( + omit_seconds, + upload_config, + download_config, + common_config, + serde_json::json!({ + "omit_seconds": omit_seconds, + "ip_version": match server_addr.ip() { + IpAddr::V4(_) => 4, + IpAddr::V6(_) => 6, + }, + "reverse": args.reverse, + }) + ) + ); } else { println!("{}", tr.to_string(display_bit, omit_seconds)); } } - + Ok(()) } @@ -500,7 +554,8 @@ fn start_kill_timer() { } fn is_alive() -> bool { unsafe { - if KILL_TIMER_RELATIVE_START_TIME != 0.0 { //initialised + if KILL_TIMER_RELATIVE_START_TIME != 0.0 { + //initialised if SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs_f64() - KILL_TIMER_RELATIVE_START_TIME >= KILL_TIMEOUT { return false; } diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..60440dc --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,14 @@ +pub mod args; +pub mod client; +pub(crate) mod protocol; +pub mod server; +pub(crate) mod stream; +pub(crate) mod utils; + +pub type BoxResult = Result>; + +/// a global token generator +pub(crate) fn get_global_token() -> mio::Token { + mio::Token(TOKEN_SEED.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1) +} +static TOKEN_SEED: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); diff --git a/src/main.rs b/src/main.rs index 6ec2614..07dc205 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,283 +1,76 @@ /* * Copyright (C) 2021 Evtech Solutions, Ltd., dba 3D-P * Copyright (C) 2021 Neil Tallim - * + * * This file is part of rperf. - * + * * rperf is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * rperf is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with rperf. If not, see . */ - -extern crate log; -extern crate env_logger; -use clap::{App, Arg}; +use rperf::{args, client, server, BoxResult}; -mod protocol; -mod stream; -mod utils; -mod client; -mod server; +fn main() -> BoxResult<()> { + use clap::Parser; + let args = args::Args::parse(); -fn main() { - let args = App::new("rperf") - .about(clap::crate_description!()) - .author("https://github.com/opensource-3d-p/rperf") - .name(clap::crate_name!()) - .version(clap::crate_version!()) - .arg( - Arg::with_name("port") - .help("the port used for client-server interactions") - .takes_value(true) - .long("port") - .short("p") - .required(false) - .default_value("5199") - ) - - .arg( - Arg::with_name("affinity") - .help("specify logical CPUs, delimited by commas, across which to round-robin affinity; not supported on all systems") - .takes_value(true) - .long("affinity") - .short("A") - .required(false) - .multiple(true) - .default_value("") - ) - .arg( - Arg::with_name("debug") - .help("emit debug-level logging on stderr; default is info and above") - .takes_value(false) - .long("debug") - .short("d") - .required(false) - ) - - - .arg( - Arg::with_name("server") - .help("run in server mode") - .takes_value(false) - .long("server") - .short("s") - .required(false) - ) - .arg( - Arg::with_name("version6") - .help("enable IPv6 on the server (on most hosts, this will allow both IPv4 and IPv6, but it might limit to just IPv6 on some)") - .takes_value(false) - .long("version6") - .short("6") - .required(false) - ) - .arg( - Arg::with_name("client_limit") - .help("limit the number of concurrent clients that can be processed by a server; any over this count will be immediately disconnected") - .takes_value(true) - .long("client-limit") - .required(false) - .default_value("0") - ) - - .arg( - Arg::with_name("client") - .help("run in client mode; value is the server's address") - .takes_value(true) - .long("client") - .short("c") - .required(false) - ) - .arg( - Arg::with_name("reverse") - .help("run in reverse-mode (server sends, client receives)") - .takes_value(false) - .long("reverse") - .short("R") - .required(false) - ) - .arg( - Arg::with_name("format") - .help("the format in which to deplay information (json, megabit/sec, megabyte/sec)") - .takes_value(true) - .long("format") - .short("f") - .required(false) - .default_value("megabit") - .possible_values(&["json", "megabit", "megabyte"]) - ) - .arg( - Arg::with_name("udp") - .help("use UDP rather than TCP") - .takes_value(false) - .long("udp") - .short("u") - .required(false) - ) - .arg( - Arg::with_name("bandwidth") - .help("target bandwidth in bytes/sec; this value is applied to each stream, with a default target of 1 megabit/second for all protocols (note: megabit, not mebibit); the suffixes kKmMgG can also be used for xbit and xbyte, respectively") - .takes_value(true) - .long("bandwidth") - .short("b") - .required(false) - .default_value("125000") - ) - .arg( - Arg::with_name("time") - .help("the time in seconds for which to transmit") - .takes_value(true) - .long("time") - .short("t") - .required(false) - .default_value("10.0") - ) - .arg( - Arg::with_name("send_interval") - .help("the interval at which to send batches of data, in seconds, between [0.0 and 1.0); this is used to evenly spread packets out over time") - .takes_value(true) - .long("send-interval") - .required(false) - .default_value("0.05") - ) - .arg( - Arg::with_name("length") - .help("length of the buffer to exchange; for TCP, this defaults to 32 kibibytes; for UDP, it's 1024 bytes") - .takes_value(true) - .long("length") - .short("l") - .required(false) - .default_value("0") - ) - .arg( - Arg::with_name("send_buffer") - .help("send_buffer, in bytes (only supported on some platforms; if set too small, a 'resource unavailable' error may occur; affects TCP window-size)") - .takes_value(true) - .long("send-buffer") - .required(false) - .default_value("0") - ) - .arg( - Arg::with_name("receive_buffer") - .help("receive_buffer, in bytes (only supported on some platforms; if set too small, a 'resource unavailable' error may occur; affects TCP window-size)") - .takes_value(true) - .long("receive-buffer") - .required(false) - .default_value("0") - ) - .arg( - Arg::with_name("parallel") - .help("the number of parallel data-streams to use") - .takes_value(true) - .long("parallel") - .short("P") - .required(false) - .default_value("1") - ) - .arg( - Arg::with_name("omit") - .help("omit a number of seconds from the start of calculations, primarily to avoid including TCP ramp-up in averages; using this option may result in disagreement between bytes sent and received, since data can be in-flight across time-boundaries") - .takes_value(true) - .long("omit") - .short("O") - .default_value("0") - .required(false) - ) - .arg( - Arg::with_name("no_delay") - .help("use no-delay mode for TCP tests, disabling Nagle's Algorithm") - .takes_value(false) - .long("no-delay") - .short("N") - .required(false) - ) - .arg( - Arg::with_name("tcp_port_pool") - .help("an optional pool of IPv4 TCP ports over which data will be accepted; if omitted, any OS-assignable port is used; format: 1-10,19,21") - .takes_value(true) - .long("tcp-port-pool") - .required(false) - .default_value("") - ) - .arg( - Arg::with_name("tcp6_port_pool") - .help("an optional pool of IPv6 TCP ports over which data will be accepted; if omitted, any OS-assignable port is used; format: 1-10,19,21") - .takes_value(true) - .long("tcp6-port-pool") - .required(false) - .default_value("") - ) - .arg( - Arg::with_name("udp_port_pool") - .help("an optional pool of IPv4 UDP ports over which data will be accepted; if omitted, any OS-assignable port is used; format: 1-10,19,21") - .takes_value(true) - .long("udp-port-pool") - .required(false) - .default_value("") - ) - .arg( - Arg::with_name("udp6_port_pool") - .help("an optional pool of IPv6 UDP ports over which data will be accepted; if omitted, any OS-assignable port is used; format: 1-10,19,21") - .takes_value(true) - .long("udp6-port-pool") - .required(false) - .default_value("") - ) - .get_matches(); - - let mut env = env_logger::Env::default() - .filter_or("RUST_LOG", "info"); - if args.is_present("debug") { + let default = args.verbosity.to_string(); + let mut env = env_logger::Env::default().filter_or("RUST_LOG", &default); + if args.debug { env = env.filter_or("RUST_LOG", "debug"); } env_logger::init_from_env(env); - - if args.is_present("server") { + + if args.server { log::debug!("registering SIGINT handler..."); - ctrlc::set_handler(move || { + let exiting = ctrlc2::set_handler(move || { if server::kill() { log::warn!("shutdown requested; please allow a moment for any in-progress tests to stop"); } else { log::warn!("forcing shutdown immediately"); std::process::exit(3); } - }).expect("unable to set SIGINT handler"); - + true + })?; + log::debug!("beginning normal operation..."); - let service = server::serve(args); - if service.is_err() { - log::error!("unable to run server: {}", service.unwrap_err()); + if let Err(err) = server::serve(&args) { + log::error!("unable to run server: {}", err); std::process::exit(4); } - } else if args.is_present("client") { + exiting.join().expect("unable to join SIGINT handler thread"); + } else if args.client.is_some() { log::debug!("registering SIGINT handler..."); - ctrlc::set_handler(move || { + ctrlc2::set_handler(move || { if client::kill() { log::warn!("shutdown requested; please allow a moment for any in-progress tests to stop"); } else { log::warn!("forcing shutdown immediately"); std::process::exit(3); } - }).expect("unable to set SIGINT handler"); - + true + })?; + log::debug!("connecting to server..."); - let execution = client::execute(args); - if execution.is_err() { - log::error!("unable to run client: {}", execution.unwrap_err()); + if let Err(err) = client::execute(&args) { + log::error!("unable to run client: {}", err); std::process::exit(4); } } else { - std::println!("{}", args.usage()); + use clap::CommandFactory; + let mut cmd = args::Args::command(); + cmd.print_help().unwrap(); std::process::exit(2); } + Ok(()) } diff --git a/src/protocol/communication.rs b/src/protocol/communication.rs index 5eaae27..8316b79 100644 --- a/src/protocol/communication.rs +++ b/src/protocol/communication.rs @@ -1,168 +1,181 @@ /* * Copyright (C) 2021 Evtech Solutions, Ltd., dba 3D-P * Copyright (C) 2021 Neil Tallim - * + * * This file is part of rperf. - * + * * rperf is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * rperf is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with rperf. If not, see . */ use std::io::{self, Read, Write}; -use std::time::Duration; +use std::net::TcpStream; +use std::time::{Duration, Instant}; -use mio::{Events, Ready, Poll, PollOpt, Token}; -use mio::net::{TcpStream}; - -use std::error::Error; -type BoxResult = Result>; +use crate::BoxResult; /// how long to wait for keepalive events -// the communications channels typically exchange data every second, so 2s is reasonable to avoid excess noise -pub const KEEPALIVE_DURATION:Duration = Duration::from_secs(2); +/// the communications channels typically exchange data every second, so 2s is reasonable to avoid excess noise +#[cfg(unix)] +pub const KEEPALIVE_DURATION: Duration = Duration::from_secs(3); /// how long to block on polling operations -const POLL_TIMEOUT:Duration = Duration::from_millis(50); +const POLL_TIMEOUT: Duration = Duration::from_millis(50); + +/// how long to allow for send-operations to complete +const SEND_TIMEOUT: Duration = Duration::from_secs(5); /// sends JSON data over a client-server communications stream -pub fn send(stream:&mut TcpStream, message:&serde_json::Value) -> BoxResult<()> { +pub fn send(stream: &mut TcpStream, message: &serde_json::Value) -> BoxResult<()> { + stream.set_write_timeout(Some(POLL_TIMEOUT))?; + let serialised_message = serde_json::to_vec(message)?; - - log::debug!("sending message of length {}, {:?}, to {}...", serialised_message.len(), message, stream.peer_addr()?); - let mut output_buffer = vec![0_u8; (serialised_message.len() + 2).into()]; + + log::debug!( + "sending message to {}, length {}, {:?}...", + stream.peer_addr()?, + serialised_message.len(), + message, + ); + let mut output_buffer = vec![0_u8; serialised_message.len() + 2]; output_buffer[..2].copy_from_slice(&(serialised_message.len() as u16).to_be_bytes()); output_buffer[2..].copy_from_slice(serialised_message.as_slice()); - Ok(stream.write_all(&output_buffer)?) + + let start = Instant::now(); + let mut total_bytes_written: usize = 0; + + while start.elapsed() < SEND_TIMEOUT { + match stream.write(&output_buffer[total_bytes_written..]) { + Ok(bytes_written) => { + total_bytes_written += bytes_written; + if total_bytes_written == output_buffer.len() { + return Ok(()); + } + } + Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock || e.kind() == std::io::ErrorKind::TimedOut => { + // unable to write at the moment; keep trying until the full timeout is reached + continue; + } + Err(e) => { + return Err(Box::new(e)); + } + } + } + let err = simple_error::simple_error!("timed out while attempting to send status-message to {}", stream.peer_addr()?); + Err(Box::new(err)) } /// receives the length-count of a pending message over a client-server communications stream -fn receive_length(stream:&mut TcpStream, alive_check:fn() -> bool, results_handler:&mut dyn FnMut() -> BoxResult<()>) -> BoxResult { - let mut cloned_stream = stream.try_clone()?; - - let mio_token = Token(0); - let poll = Poll::new()?; - poll.register( - &cloned_stream, - mio_token, - Ready::readable(), - PollOpt::edge(), - )?; - let mut events = Events::with_capacity(1); //only interacting with one stream - +fn receive_length(stream: &mut TcpStream, alive_check: fn() -> bool, handler: &mut dyn FnMut() -> BoxResult<()>) -> BoxResult { + stream.set_read_timeout(Some(POLL_TIMEOUT)).expect("unable to set TCP read-timeout"); + let mut length_bytes_read = 0; - let mut length_spec:[u8; 2] = [0; 2]; - while alive_check() { //waiting to find out how long the next message is - results_handler()?; //send any outstanding results between cycles - poll.poll(&mut events, Some(POLL_TIMEOUT))?; - for event in events.iter() { - match event.token() { - _ => loop { - match cloned_stream.read(&mut length_spec[length_bytes_read..]) { - Ok(size) => { - if size == 0 { - if alive_check() { - return Err(Box::new(simple_error::simple_error!("connection lost"))); - } else { //shutting down; a disconnect is expected - return Err(Box::new(simple_error::simple_error!("local shutdown requested"))); - } - } - - length_bytes_read += size; - if length_bytes_read == 2 { - let length = u16::from_be_bytes(length_spec); - log::debug!("received length-spec of {} from {}", length, stream.peer_addr()?); - return Ok(length); - } else { - log::debug!("received partial length-spec from {}", stream.peer_addr()?); - } - }, - Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => { //nothing left to process - break; - }, - Err(e) => { - return Err(Box::new(e)); - }, - } - }, + let mut length_spec: [u8; 2] = [0; 2]; + while alive_check() { + //waiting to find out how long the next message is + handler()?; //send any outstanding results between cycles + + let size = match stream.read(&mut length_spec[length_bytes_read..]) { + Ok(size) => size, + Err(ref e) if e.kind() == io::ErrorKind::WouldBlock || e.kind() == std::io::ErrorKind::TimedOut => { + // nothing available to process + continue; } + Err(e) => { + return Err(Box::new(e)); + } + }; + if size == 0 { + if alive_check() { + return Err(Box::new(simple_error::simple_error!("connection lost"))); + } else { + // shutting down; a disconnect is expected + return Err(Box::new(simple_error::simple_error!("local shutdown requested"))); + } + } + + length_bytes_read += size; + if length_bytes_read == 2 { + let length = u16::from_be_bytes(length_spec); + log::debug!("received length-spec of {} from {}", length, stream.peer_addr()?); + return Ok(length); + } else { + log::debug!("received partial length-spec from {}", stream.peer_addr()?); } } Err(Box::new(simple_error::simple_error!("system shutting down"))) } + /// receives the data-value of a pending message over a client-server communications stream -fn receive_payload(stream:&mut TcpStream, alive_check:fn() -> bool, results_handler:&mut dyn FnMut() -> BoxResult<()>, length:u16) -> BoxResult { - let mut cloned_stream = stream.try_clone()?; - - let mio_token = Token(0); - let poll = Poll::new()?; - poll.register( - &cloned_stream, - mio_token, - Ready::readable(), - PollOpt::edge(), - )?; - let mut events = Events::with_capacity(1); //only interacting with one stream - +fn receive_payload( + stream: &mut TcpStream, + alive_check: fn() -> bool, + results_handler: &mut dyn FnMut() -> BoxResult<()>, + length: u16, +) -> BoxResult { + stream.set_read_timeout(Some(POLL_TIMEOUT)).expect("unable to set TCP read-timeout"); + let mut bytes_read = 0; let mut buffer = vec![0_u8; length.into()]; - while alive_check() { //waiting to receive the payload + while alive_check() { + //waiting to receive the payload results_handler()?; //send any outstanding results between cycles - poll.poll(&mut events, Some(POLL_TIMEOUT))?; - for event in events.iter() { - match event.token() { - _ => loop { - match cloned_stream.read(&mut buffer[bytes_read..]) { - Ok(size) => { - if size == 0 { - if alive_check() { - return Err(Box::new(simple_error::simple_error!("connection lost"))); - } else { //shutting down; a disconnect is expected - return Err(Box::new(simple_error::simple_error!("local shutdown requested"))); - } - } - - bytes_read += size; - if bytes_read == length as usize { - match serde_json::from_slice(&buffer) { - Ok(v) => { - log::debug!("received {:?} from {}", v, stream.peer_addr()?); - return Ok(v); - }, - Err(e) => { - return Err(Box::new(e)); - }, - } - } else { - log::debug!("received partial payload from {}", stream.peer_addr()?); - } - }, - Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => { //nothing left to process - break; - }, - Err(e) => { - return Err(Box::new(e)); - }, - } - }, + + let size = match stream.read(&mut buffer[bytes_read..]) { + Ok(size) => size, + Err(ref e) if e.kind() == io::ErrorKind::WouldBlock || e.kind() == std::io::ErrorKind::TimedOut => { + // nothing available to process + continue; } + Err(e) => { + return Err(Box::new(e)); + } + }; + if size == 0 { + if alive_check() { + return Err(Box::new(simple_error::simple_error!("connection lost"))); + } else { + //shutting down; a disconnect is expected + return Err(Box::new(simple_error::simple_error!("local shutdown requested"))); + } + } + + bytes_read += size; + if bytes_read == length as usize { + match serde_json::from_slice(&buffer) { + Ok(v) => { + log::debug!("received message from {}: {:?}", stream.peer_addr()?, v); + return Ok(v); + } + Err(e) => { + return Err(Box::new(e)); + } + } + } else { + log::debug!("received partial payload from {}", stream.peer_addr()?); } } Err(Box::new(simple_error::simple_error!("system shutting down"))) } + /// handles the full process of retrieving a message from a client-server communications stream -pub fn receive(mut stream:&mut TcpStream, alive_check:fn() -> bool, results_handler:&mut dyn FnMut() -> BoxResult<()>) -> BoxResult { +pub fn receive( + stream: &mut TcpStream, + alive_check: fn() -> bool, + results_handler: &mut dyn FnMut() -> BoxResult<()>, +) -> BoxResult { log::debug!("awaiting length-value from {}...", stream.peer_addr()?); - let length = receive_length(&mut stream, alive_check, results_handler)?; + let length = receive_length(stream, alive_check, results_handler)?; log::debug!("awaiting payload from {}...", stream.peer_addr()?); - receive_payload(&mut stream, alive_check, results_handler, length) + receive_payload(stream, alive_check, results_handler, length) } diff --git a/src/protocol/messaging.rs b/src/protocol/messaging.rs index a4ec012..90a4c71 100644 --- a/src/protocol/messaging.rs +++ b/src/protocol/messaging.rs @@ -1,25 +1,24 @@ /* * Copyright (C) 2021 Evtech Solutions, Ltd., dba 3D-P * Copyright (C) 2021 Neil Tallim - * + * * This file is part of rperf. - * + * * rperf is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * rperf is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with rperf. If not, see . */ -use std::error::Error; -type BoxResult = Result>; +use crate::BoxResult; /// prepares a message used to tell the server to begin operations pub fn prepare_begin() -> serde_json::Value { @@ -29,10 +28,10 @@ pub fn prepare_begin() -> serde_json::Value { } /// prepares a message used to tell the client to connect its test-streams -pub fn prepare_connect(stream_ports:&[u16]) -> serde_json::Value { +pub fn prepare_connect(stream_ports: &[u16]) -> serde_json::Value { serde_json::json!({ "kind": "connect", - + "stream_ports": stream_ports, }) } @@ -51,86 +50,98 @@ pub fn prepare_end() -> serde_json::Value { }) } - - - /// prepares a message used to describe the upload role of a TCP test -fn prepare_configuration_tcp_upload(test_id:&[u8; 16], streams:u8, bandwidth:u64, seconds:f32, length:usize, send_interval:f32, send_buffer:u32, no_delay:bool) -> serde_json::Value { +#[allow(clippy::too_many_arguments)] +fn prepare_configuration_tcp_upload( + test_id: &[u8; 16], + streams: u8, + bandwidth: u64, + seconds: f32, + length: usize, + send_interval: f32, + send_buffer: u32, + no_delay: bool, +) -> serde_json::Value { serde_json::json!({ "kind": "configuration", - + "family": "tcp", "role": "upload", - + "test_id": test_id, "streams": validate_streams(streams), - + "bandwidth": validate_bandwidth(bandwidth), "duration": seconds, "length": calculate_length_tcp(length), "send_interval": validate_send_interval(send_interval), - + "send_buffer": send_buffer, "no_delay": no_delay, }) } /// prepares a message used to describe the download role of a TCP test -fn prepare_configuration_tcp_download(test_id:&[u8; 16], streams:u8, length:usize, receive_buffer:u32) -> serde_json::Value { +fn prepare_configuration_tcp_download(test_id: &[u8; 16], streams: u8, length: usize, receive_buffer: u32) -> serde_json::Value { serde_json::json!({ "kind": "configuration", - + "family": "tcp", "role": "download", - + "test_id": test_id, "streams": validate_streams(streams), - + "length": calculate_length_tcp(length), "receive_buffer": receive_buffer, }) } /// prepares a message used to describe the upload role of a UDP test -fn prepare_configuration_udp_upload(test_id:&[u8; 16], streams:u8, bandwidth:u64, seconds:f32, length:u16, send_interval:f32, send_buffer:u32) -> serde_json::Value { +fn prepare_configuration_udp_upload( + test_id: &[u8; 16], + streams: u8, + bandwidth: u64, + seconds: f32, + length: u16, + send_interval: f32, + send_buffer: u32, +) -> serde_json::Value { serde_json::json!({ "kind": "configuration", - + "family": "udp", "role": "upload", - + "test_id": test_id, "streams": validate_streams(streams), - + "bandwidth": validate_bandwidth(bandwidth), "duration": seconds, "length": calculate_length_udp(length), "send_interval": validate_send_interval(send_interval), - + "send_buffer": send_buffer, }) } /// prepares a message used to describe the download role of a UDP test -fn prepare_configuration_udp_download(test_id:&[u8; 16], streams:u8, length:u16, receive_buffer:u32) -> serde_json::Value { +fn prepare_configuration_udp_download(test_id: &[u8; 16], streams: u8, length: u16, receive_buffer: u32) -> serde_json::Value { serde_json::json!({ "kind": "configuration", - + "family": "udp", "role": "download", - + "test_id": test_id, "streams": validate_streams(streams), - + "length": calculate_length_udp(length), "receive_buffer": receive_buffer, }) } - - - -fn validate_streams(streams:u8) -> u8 { +fn validate_streams(streams: u8) -> u8 { if streams > 0 { streams } else { @@ -139,7 +150,7 @@ fn validate_streams(streams:u8) -> u8 { } } -fn validate_bandwidth(bandwidth:u64) -> u64 { +fn validate_bandwidth(bandwidth: u64) -> u64 { if bandwidth > 0 { bandwidth } else { @@ -148,7 +159,7 @@ fn validate_bandwidth(bandwidth:u64) -> u64 { } } -fn validate_send_interval(send_interval:f32) -> f32 { +fn validate_send_interval(send_interval: f32) -> f32 { if send_interval > 0.0 && send_interval <= 1.0 { send_interval } else { @@ -157,140 +168,193 @@ fn validate_send_interval(send_interval:f32) -> f32 { } } -fn calculate_length_tcp(length:usize) -> usize { - if length < crate::stream::tcp::TEST_HEADER_SIZE { //length must be at least enough to hold the test data +fn calculate_length_tcp(length: usize) -> usize { + if length < crate::stream::tcp::TEST_HEADER_SIZE { + //length must be at least enough to hold the test data crate::stream::tcp::TEST_HEADER_SIZE } else { length } } -fn calculate_length_udp(length:u16) -> u16 { - if length < crate::stream::udp::TEST_HEADER_SIZE { //length must be at least enough to hold the test data +fn calculate_length_udp(length: u16) -> u16 { + if length < crate::stream::udp::TEST_HEADER_SIZE { + //length must be at least enough to hold the test data crate::stream::udp::TEST_HEADER_SIZE } else { length } } - /// prepares a message used to describe the upload role in a test -pub fn prepare_upload_configuration(args:&clap::ArgMatches, test_id:&[u8; 16]) -> BoxResult { - let parallel_streams:u8 = args.value_of("parallel").unwrap().parse()?; - let mut seconds:f32 = args.value_of("time").unwrap().parse()?; - let mut send_interval:f32 = args.value_of("send_interval").unwrap().parse()?; - let mut length:u32 = args.value_of("length").unwrap().parse()?; - - let mut send_buffer:u32 = args.value_of("send_buffer").unwrap().parse()?; - - let mut bandwidth_string = args.value_of("bandwidth").unwrap(); - let bandwidth:u64; - let bandwidth_multiplier:f64; +pub fn prepare_upload_configuration(args: &crate::args::Args, test_id: &[u8; 16]) -> BoxResult { + let parallel_streams: u8 = args.parallel as u8; + let mut seconds: f32 = args.time as f32; + let mut send_interval: f32 = args.send_interval as f32; + let mut length: u32 = args.length as u32; + + let mut send_buffer: u32 = args.send_buffer as u32; + + let mut bandwidth_string = args.bandwidth.as_str(); + let bandwidth: u64; + let bandwidth_multiplier: f64; match bandwidth_string.chars().last() { Some(v) => { match v { - 'k' => { //kilobits + 'k' => { + //kilobits bandwidth_multiplier = 1000.0 / 8.0; - }, - 'K' => { //kilobytes + } + 'K' => { + //kilobytes bandwidth_multiplier = 1000.0; - }, - 'm' => { //megabits + } + 'm' => { + //megabits bandwidth_multiplier = 1000.0 * 1000.0 / 8.0; - }, - 'M' => { //megabytes + } + 'M' => { + //megabytes bandwidth_multiplier = 1000.0 * 1000.0; - }, - 'g' => { //gigabits + } + 'g' => { + //gigabits bandwidth_multiplier = 1000.0 * 1000.0 * 1000.0 / 8.0; - }, - 'G' => { //gigabytes + } + 'G' => { + //gigabytes bandwidth_multiplier = 1000.0 * 1000.0 * 1000.0; - }, + } _ => { bandwidth_multiplier = 1.0; - }, + } } - - if bandwidth_multiplier != 1.0 { //the value uses a suffix - bandwidth_string = &bandwidth_string[0..(bandwidth_string.len() - 1)]; + + if bandwidth_multiplier != 1.0 { + //the value uses a suffix + bandwidth_string = &bandwidth_string[0..(bandwidth_string.len() - 1)]; } - + match bandwidth_string.parse::() { Ok(v2) => { bandwidth = (v2 * bandwidth_multiplier) as u64; - }, - Err(_) => { //invalid input; fall back to 1mbps - log::warn!("invalid bandwidth: {}; setting value to 1mbps", args.value_of("bandwidth").unwrap()); + } + Err(_) => { + //invalid input; fall back to 1mbps + log::warn!("invalid bandwidth: {}; setting value to 1mbps", args.bandwidth); bandwidth = 125000; - }, + } } - }, - None => { //invalid input; fall back to 1mbps - log::warn!("invalid bandwidth: {}; setting value to 1mbps", args.value_of("bandwidth").unwrap()); + } + None => { + //invalid input; fall back to 1mbps + log::warn!("invalid bandwidth: {}; setting value to 1mbps", args.bandwidth); bandwidth = 125000; - }, + } } - + if seconds <= 0.0 { log::warn!("time was not in an acceptable range and has been set to 0.0"); seconds = 0.0 } - + if send_interval > 1.0 || send_interval <= 0.0 { log::warn!("send-interval was not in an acceptable range and has been set to 0.05"); send_interval = 0.05 } - - if args.is_present("udp") { + + if args.udp { log::debug!("preparing UDP upload config..."); if length == 0 { length = 1024; } if send_buffer != 0 && send_buffer < length { - log::warn!("requested send-buffer, {}, is too small to hold the data to be exchanged; it will be increased to {}", send_buffer, length * 2); + log::warn!( + "requested send-buffer, {}, is too small to hold the data to be exchanged; it will be increased to {}", + send_buffer, + length * 2 + ); send_buffer = length * 2; } - Ok(prepare_configuration_udp_upload(test_id, parallel_streams, bandwidth, seconds, length as u16, send_interval, send_buffer)) + Ok(prepare_configuration_udp_upload( + test_id, + parallel_streams, + bandwidth, + seconds, + length as u16, + send_interval, + send_buffer, + )) } else { log::debug!("preparing TCP upload config..."); if length == 0 { length = 32 * 1024; } if send_buffer != 0 && send_buffer < length { - log::warn!("requested send-buffer, {}, is too small to hold the data to be exchanged; it will be increased to {}", send_buffer, length * 2); + log::warn!( + "requested send-buffer, {}, is too small to hold the data to be exchanged; it will be increased to {}", + send_buffer, + length * 2 + ); send_buffer = length * 2; } - - let no_delay:bool = args.is_present("no_delay"); - - Ok(prepare_configuration_tcp_upload(test_id, parallel_streams, bandwidth, seconds, length as usize, send_interval, send_buffer, no_delay)) + + let no_delay: bool = args.no_delay; + + Ok(prepare_configuration_tcp_upload( + test_id, + parallel_streams, + bandwidth, + seconds, + length as usize, + send_interval, + send_buffer, + no_delay, + )) } } /// prepares a message used to describe the download role in a test -pub fn prepare_download_configuration(args:&clap::ArgMatches, test_id:&[u8; 16]) -> BoxResult { - let parallel_streams:u8 = args.value_of("parallel").unwrap().parse()?; - let mut length:u32 = args.value_of("length").unwrap().parse()?; - let mut receive_buffer:u32 = args.value_of("receive_buffer").unwrap().parse()?; - - if args.is_present("udp") { +pub fn prepare_download_configuration(args: &crate::args::Args, test_id: &[u8; 16]) -> BoxResult { + let parallel_streams: u8 = args.parallel as u8; + let mut length: u32 = args.length as u32; + let mut receive_buffer: u32 = args.receive_buffer as u32; + + if args.udp { log::debug!("preparing UDP download config..."); if length == 0 { length = 1024; } if receive_buffer != 0 && receive_buffer < length { - log::warn!("requested receive-buffer, {}, is too small to hold the data to be exchanged; it will be increased to {}", receive_buffer, length * 2); + log::warn!( + "requested receive-buffer, {}, is too small to hold the data to be exchanged; it will be increased to {}", + receive_buffer, + length * 2 + ); receive_buffer = length * 2; } - Ok(prepare_configuration_udp_download(test_id, parallel_streams, length as u16, receive_buffer)) + Ok(prepare_configuration_udp_download( + test_id, + parallel_streams, + length as u16, + receive_buffer, + )) } else { log::debug!("preparing TCP download config..."); if length == 0 { length = 32 * 1024; } if receive_buffer != 0 && receive_buffer < length { - log::warn!("requested receive-buffer, {}, is too small to hold the data to be exchanged; it will be increased to {}", receive_buffer, length * 2); + log::warn!( + "requested receive-buffer, {}, is too small to hold the data to be exchanged; it will be increased to {}", + receive_buffer, + length * 2 + ); receive_buffer = length * 2; } - Ok(prepare_configuration_tcp_download(test_id, parallel_streams, length as usize, receive_buffer)) + Ok(prepare_configuration_tcp_download( + test_id, + parallel_streams, + length as usize, + receive_buffer, + )) } } diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index 8e4a093..afedd6d 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -1,19 +1,19 @@ /* * Copyright (C) 2021 Evtech Solutions, Ltd., dba 3D-P * Copyright (C) 2021 Neil Tallim - * + * * This file is part of rperf. - * + * * rperf is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * rperf is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with rperf. If not, see . */ diff --git a/src/protocol/results.rs b/src/protocol/results.rs index 852361f..88db0cd 100644 --- a/src/protocol/results.rs +++ b/src/protocol/results.rs @@ -1,37 +1,33 @@ /* * Copyright (C) 2021 Evtech Solutions, Ltd., dba 3D-P * Copyright (C) 2021 Neil Tallim - * + * * This file is part of rperf. - * + * * rperf is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * rperf is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with rperf. If not, see . */ +use crate::BoxResult; +use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use std::time::{SystemTime, UNIX_EPOCH}; -use serde::{Serialize, Deserialize}; - -use std::error::Error; -type BoxResult = Result>; - /* This module contains structures used to represent and collect the results of tests. * Since everything is basically just a data-container with representation methods, * it isn't extensively documented. */ - #[derive(PartialEq)] pub enum IntervalResultKind { ClientDone, @@ -53,27 +49,29 @@ pub fn get_unix_timestamp() -> f64 { pub trait IntervalResult { fn kind(&self) -> IntervalResultKind; - + fn get_stream_idx(&self) -> u8; - + fn to_json(&self) -> serde_json::Value; - + //produces test-results in tabular form - fn to_string(&self, bit:bool) -> String; + fn to_string(&self, bit: bool) -> String; } +pub type IntervalResultBox = Box; + pub struct ClientDoneResult { pub stream_idx: u8, } impl IntervalResult for ClientDoneResult { - fn kind(&self) -> IntervalResultKind{ + fn kind(&self) -> IntervalResultKind { IntervalResultKind::ClientDone } - + fn get_stream_idx(&self) -> u8 { self.stream_idx } - + fn to_json(&self) -> serde_json::Value { serde_json::json!({ "kind": "done", @@ -81,11 +79,12 @@ impl IntervalResult for ClientDoneResult { "stream_idx": self.stream_idx, }) } - - fn to_string(&self, _bit:bool) -> String { - format!("----------\n\ - End of stream from client | stream: {}", - self.stream_idx, + + fn to_string(&self, _bit: bool) -> String { + format!( + "----------\n\ + End of stream from client | stream: {}", + self.stream_idx, ) } } @@ -93,14 +92,14 @@ pub struct ServerDoneResult { pub stream_idx: u8, } impl IntervalResult for ServerDoneResult { - fn kind(&self) -> IntervalResultKind{ + fn kind(&self) -> IntervalResultKind { IntervalResultKind::ServerDone } - + fn get_stream_idx(&self) -> u8 { self.stream_idx } - + fn to_json(&self) -> serde_json::Value { serde_json::json!({ "kind": "done", @@ -108,11 +107,12 @@ impl IntervalResult for ServerDoneResult { "stream_idx": self.stream_idx, }) } - - fn to_string(&self, _bit:bool) -> String { - format!("----------\n\ - End of stream from server | stream: {}", - self.stream_idx, + + fn to_string(&self, _bit: bool) -> String { + format!( + "----------\n\ + End of stream from server | stream: {}", + self.stream_idx, ) } } @@ -121,14 +121,14 @@ pub struct ClientFailedResult { pub stream_idx: u8, } impl IntervalResult for ClientFailedResult { - fn kind(&self) -> IntervalResultKind{ + fn kind(&self) -> IntervalResultKind { IntervalResultKind::ClientFailed } - + fn get_stream_idx(&self) -> u8 { self.stream_idx } - + fn to_json(&self) -> serde_json::Value { serde_json::json!({ "kind": "failed", @@ -136,11 +136,12 @@ impl IntervalResult for ClientFailedResult { "stream_idx": self.stream_idx, }) } - - fn to_string(&self, _bit:bool) -> String { - format!("----------\n\ - Failure in client stream | stream: {}", - self.stream_idx, + + fn to_string(&self, _bit: bool) -> String { + format!( + "----------\n\ + Failure in client stream | stream: {}", + self.stream_idx, ) } } @@ -148,14 +149,14 @@ pub struct ServerFailedResult { pub stream_idx: u8, } impl IntervalResult for ServerFailedResult { - fn kind(&self) -> IntervalResultKind{ + fn kind(&self) -> IntervalResultKind { IntervalResultKind::ServerFailed } - + fn get_stream_idx(&self) -> u8 { self.stream_idx } - + fn to_json(&self) -> serde_json::Value { serde_json::json!({ "kind": "failed", @@ -163,32 +164,32 @@ impl IntervalResult for ServerFailedResult { "stream_idx": self.stream_idx, }) } - - fn to_string(&self, _bit:bool) -> String { - format!("----------\n\ - Failure in server stream | stream: {}", - self.stream_idx, + + fn to_string(&self, _bit: bool) -> String { + format!( + "----------\n\ + Failure in server stream | stream: {}", + self.stream_idx, ) } } - #[derive(Serialize, Deserialize)] pub struct TcpReceiveResult { pub timestamp: f64, - + pub stream_idx: u8, - + pub duration: f32, - + pub bytes_received: u64, } impl TcpReceiveResult { - fn from_json(value:serde_json::Value) -> BoxResult { - let receive_result:TcpReceiveResult = serde_json::from_value(value)?; + fn from_json(value: serde_json::Value) -> BoxResult { + let receive_result: TcpReceiveResult = serde_json::from_value(value)?; Ok(receive_result) } - + fn to_result_json(&self) -> serde_json::Value { let mut serialised = serde_json::to_value(self).unwrap(); serialised.as_object_mut().unwrap().remove("stream_idx"); @@ -196,41 +197,41 @@ impl TcpReceiveResult { } } impl IntervalResult for TcpReceiveResult { - fn kind(&self) -> IntervalResultKind{ + fn kind(&self) -> IntervalResultKind { IntervalResultKind::TcpReceive } - + fn get_stream_idx(&self) -> u8 { self.stream_idx } - + fn to_json(&self) -> serde_json::Value { let mut serialised = serde_json::to_value(self).unwrap(); serialised["family"] = serde_json::json!("tcp"); serialised["kind"] = serde_json::json!("receive"); serialised } - - fn to_string(&self, bit:bool) -> String { - let duration_divisor; - if self.duration == 0.0 { //avoid zerodiv, which can happen if the stream fails - duration_divisor = 1.0; + + fn to_string(&self, bit: bool) -> String { + let duration_divisor = if self.duration == 0.0 { + //avoid zerodiv, which can happen if the stream fails + 1.0 } else { - duration_divisor = self.duration; - } - + self.duration + }; + let bytes_per_second = self.bytes_received as f32 / duration_divisor; - + let throughput = match bit { true => format!("megabits/second: {:.3}", bytes_per_second / (1_000_000.00 / 8.0)), false => format!("megabytes/second: {:.3}", bytes_per_second / 1_000_000.00), }; - - format!("----------\n\ - TCP receive result over {:.2}s | stream: {}\n\ - bytes: {} | per second: {:.3} | {}", - self.duration, self.stream_idx, - self.bytes_received, bytes_per_second, throughput, + + format!( + "----------\n\ + TCP receive result over {:.2}s | stream: {}\n\ + bytes: {} | per second: {:.3} | {}", + self.duration, self.stream_idx, self.bytes_received, bytes_per_second, throughput, ) } } @@ -238,25 +239,26 @@ impl IntervalResult for TcpReceiveResult { #[derive(Serialize, Deserialize)] pub struct TcpSendResult { pub timestamp: f64, - + pub stream_idx: u8, - + pub duration: f32, - + pub bytes_sent: u64, pub sends_blocked: u64, } impl TcpSendResult { - fn from_json(value:serde_json::Value) -> BoxResult { + fn from_json(value: serde_json::Value) -> BoxResult { let mut local_value = value.clone(); - if local_value.get("sends_blocked").is_none() { //pre-0.1.8 peer - local_value["sends_blocked"] = serde_json::json!(0 as u64); //report pre-0.1.8 status + if local_value.get("sends_blocked").is_none() { + //pre-0.1.8 peer + local_value["sends_blocked"] = serde_json::json!(0_u64); //report pre-0.1.8 status } - - let send_result:TcpSendResult = serde_json::from_value(local_value)?; + + let send_result: TcpSendResult = serde_json::from_value(local_value)?; Ok(send_result) } - + fn to_result_json(&self) -> serde_json::Value { let mut serialised = serde_json::to_value(self).unwrap(); serialised.as_object_mut().unwrap().remove("stream_idx"); @@ -264,41 +266,41 @@ impl TcpSendResult { } } impl IntervalResult for TcpSendResult { - fn kind(&self) -> IntervalResultKind{ + fn kind(&self) -> IntervalResultKind { IntervalResultKind::TcpSend } - + fn get_stream_idx(&self) -> u8 { self.stream_idx } - + fn to_json(&self) -> serde_json::Value { let mut serialised = serde_json::to_value(self).unwrap(); serialised["family"] = serde_json::json!("tcp"); serialised["kind"] = serde_json::json!("send"); serialised } - - fn to_string(&self, bit:bool) -> String { - let duration_divisor; - if self.duration == 0.0 { //avoid zerodiv, which can happen if the stream fails - duration_divisor = 1.0; + + fn to_string(&self, bit: bool) -> String { + let duration_divisor = if self.duration == 0.0 { + //avoid zerodiv, which can happen if the stream fails + 1.0 } else { - duration_divisor = self.duration; - } - + self.duration + }; + let bytes_per_second = self.bytes_sent as f32 / duration_divisor; - + let throughput = match bit { true => format!("megabits/second: {:.3}", bytes_per_second / (1_000_000.00 / 8.0)), false => format!("megabytes/second: {:.3}", bytes_per_second / 1_000_000.00), }; - - let mut output = format!("----------\n\ - TCP send result over {:.2}s | stream: {}\n\ - bytes: {} | per second: {:.3} | {}", - self.duration, self.stream_idx, - self.bytes_sent, bytes_per_second, throughput, + + let mut output = format!( + "----------\n\ + TCP send result over {:.2}s | stream: {}\n\ + bytes: {} | per second: {:.3} | {}", + self.duration, self.stream_idx, self.bytes_sent, bytes_per_second, throughput, ); if self.sends_blocked > 0 { output.push_str(&format!("\nstalls due to full send-buffer: {}", self.sends_blocked)); @@ -307,30 +309,29 @@ impl IntervalResult for TcpSendResult { } } - #[derive(Serialize, Deserialize)] pub struct UdpReceiveResult { pub timestamp: f64, - + pub stream_idx: u8, - + pub duration: f32, - + pub bytes_received: u64, pub packets_received: u64, pub packets_lost: i64, pub packets_out_of_order: u64, pub packets_duplicated: u64, - + pub unbroken_sequence: u64, pub jitter_seconds: Option, } impl UdpReceiveResult { - fn from_json(value:serde_json::Value) -> BoxResult { - let receive_result:UdpReceiveResult = serde_json::from_value(value)?; + fn from_json(value: serde_json::Value) -> BoxResult { + let receive_result: UdpReceiveResult = serde_json::from_value(value)?; Ok(receive_result) } - + fn to_result_json(&self) -> serde_json::Value { let mut serialised = serde_json::to_value(self).unwrap(); serialised.as_object_mut().unwrap().remove("stream_idx"); @@ -338,46 +339,58 @@ impl UdpReceiveResult { } } impl IntervalResult for UdpReceiveResult { - fn kind(&self) -> IntervalResultKind{ + fn kind(&self) -> IntervalResultKind { IntervalResultKind::UdpReceive } - + fn get_stream_idx(&self) -> u8 { self.stream_idx } - + fn to_json(&self) -> serde_json::Value { let mut serialised = serde_json::to_value(self).unwrap(); serialised["family"] = serde_json::json!("udp"); serialised["kind"] = serde_json::json!("receive"); serialised } - - fn to_string(&self, bit:bool) -> String { - let duration_divisor; - if self.duration == 0.0 { //avoid zerodiv, which can happen if the stream fails - duration_divisor = 1.0; + + fn to_string(&self, bit: bool) -> String { + let duration_divisor = if self.duration == 0.0 { + //avoid zerodiv, which can happen if the stream fails + 1.0 } else { - duration_divisor = self.duration; - } - + self.duration + }; + let bytes_per_second = self.bytes_received as f32 / duration_divisor; - + let throughput = match bit { true => format!("megabits/second: {:.3}", bytes_per_second / (1_000_000.00 / 8.0)), false => format!("megabytes/second: {:.3}", bytes_per_second / 1_000_000.00), }; - - let mut output = format!("----------\n\ - UDP receive result over {:.2}s | stream: {}\n\ - bytes: {} | per second: {:.3} | {}\n\ - packets: {} | lost: {} | out-of-order: {} | duplicate: {} | per second: {:.3}", - self.duration, self.stream_idx, - self.bytes_received, bytes_per_second, throughput, - self.packets_received, self.packets_lost, self.packets_out_of_order, self.packets_duplicated, self.packets_received as f32 / duration_divisor, + + let mut output = format!( + "----------\n\ + UDP receive result over {:.2}s | stream: {}\n\ + bytes: {} | per second: {:.3} | {}\n\ + packets: {} | lost: {} | out-of-order: {} | duplicate: {} | per second: {:.3}", + self.duration, + self.stream_idx, + self.bytes_received, + bytes_per_second, + throughput, + self.packets_received, + self.packets_lost, + self.packets_out_of_order, + self.packets_duplicated, + self.packets_received as f32 / duration_divisor, ); if self.jitter_seconds.is_some() { - output.push_str(&format!("\njitter: {:.6}s over {} consecutive packets", self.jitter_seconds.unwrap(), self.unbroken_sequence)); + output.push_str(&format!( + "\njitter: {:.6}s over {} consecutive packets", + self.jitter_seconds.unwrap(), + self.unbroken_sequence + )); } output } @@ -386,26 +399,27 @@ impl IntervalResult for UdpReceiveResult { #[derive(Serialize, Deserialize)] pub struct UdpSendResult { pub timestamp: f64, - + pub stream_idx: u8, - + pub duration: f32, - + pub bytes_sent: u64, pub packets_sent: u64, pub sends_blocked: u64, } impl UdpSendResult { - fn from_json(value:serde_json::Value) -> BoxResult { + fn from_json(value: serde_json::Value) -> BoxResult { let mut local_value = value.clone(); - if local_value.get("sends_blocked").is_none() { //pre-0.1.8 peer - local_value["sends_blocked"] = serde_json::json!(0 as u64); //report pre-0.1.8 status + if local_value.get("sends_blocked").is_none() { + //pre-0.1.8 peer + local_value["sends_blocked"] = serde_json::json!(0_u64); //report pre-0.1.8 status } - - let send_result:UdpSendResult = serde_json::from_value(local_value)?; + + let send_result: UdpSendResult = serde_json::from_value(local_value)?; Ok(send_result) } - + fn to_result_json(&self) -> serde_json::Value { let mut serialised = serde_json::to_value(self).unwrap(); serialised.as_object_mut().unwrap().remove("stream_idx"); @@ -413,43 +427,48 @@ impl UdpSendResult { } } impl IntervalResult for UdpSendResult { - fn kind(&self) -> IntervalResultKind{ + fn kind(&self) -> IntervalResultKind { IntervalResultKind::UdpSend } - + fn get_stream_idx(&self) -> u8 { self.stream_idx } - + fn to_json(&self) -> serde_json::Value { let mut serialised = serde_json::to_value(self).unwrap(); serialised["family"] = serde_json::json!("udp"); serialised["kind"] = serde_json::json!("send"); serialised } - - fn to_string(&self, bit:bool) -> String { - let duration_divisor; - if self.duration == 0.0 { //avoid zerodiv, which can happen if the stream fails - duration_divisor = 1.0; + + fn to_string(&self, bit: bool) -> String { + let duration_divisor = if self.duration == 0.0 { + //avoid zerodiv, which can happen if the stream fails + 1.0 } else { - duration_divisor = self.duration; - } - + self.duration + }; + let bytes_per_second = self.bytes_sent as f32 / duration_divisor; - + let throughput = match bit { true => format!("megabits/second: {:.3}", bytes_per_second / (1_000_000.00 / 8.0)), false => format!("megabytes/second: {:.3}", bytes_per_second / 1_000_000.00), }; - - let mut output = format!("----------\n\ - UDP send result over {:.2}s | stream: {}\n\ - bytes: {} | per second: {:.3} | {}\n\ - packets: {} per second: {:.3}", - self.duration, self.stream_idx, - self.bytes_sent, bytes_per_second, throughput, - self.packets_sent, self.packets_sent as f32 / duration_divisor, + + let mut output = format!( + "----------\n\ + UDP send result over {:.2}s | stream: {}\n\ + bytes: {} | per second: {:.3} | {}\n\ + packets: {} per second: {:.3}", + self.duration, + self.stream_idx, + self.bytes_sent, + bytes_per_second, + throughput, + self.packets_sent, + self.packets_sent as f32 / duration_divisor, ); if self.sends_blocked > 0 { output.push_str(&format!("\nstalls due to full send-buffer: {}", self.sends_blocked)); @@ -458,8 +477,7 @@ impl IntervalResult for UdpSendResult { } } - -pub fn interval_result_from_json(value:serde_json::Value) -> BoxResult> { +pub fn interval_result_from_json(value: serde_json::Value) -> BoxResult { match value.get("family") { Some(f) => match f.as_str() { Some(family) => match family { @@ -485,7 +503,10 @@ pub fn interval_result_from_json(value:serde_json::Value) -> BoxResult Err(Box::new(simple_error::simple_error!("interval-result has no kind"))), }, - _ => Err(Box::new(simple_error::simple_error!("unsupported interval-result family: {}", family))), + _ => Err(Box::new(simple_error::simple_error!( + "unsupported interval-result family: {}", + family + ))), }, None => Err(Box::new(simple_error::simple_error!("interval-result's family is not a string"))), }, @@ -493,13 +514,10 @@ pub fn interval_result_from_json(value:serde_json::Value) -> BoxResult BoxResult<()>; - - fn to_json(&self, omit_seconds:usize) -> serde_json::Value; + fn update_from_json(&mut self, value: serde_json::Value) -> BoxResult<()>; + + fn to_json(&self, omit_seconds: usize) -> serde_json::Value; } struct TcpStreamResults { @@ -507,53 +525,62 @@ struct TcpStreamResults { send_results: Vec, } impl StreamResults for TcpStreamResults { - fn update_from_json(&mut self, value:serde_json::Value) -> BoxResult<()> { + fn update_from_json(&mut self, value: serde_json::Value) -> BoxResult<()> { match value.get("kind") { Some(k) => match k.as_str() { Some(kind) => match kind { - "send" => Ok(self.send_results.push(TcpSendResult::from_json(value)?)), - "receive" => Ok(self.receive_results.push(TcpReceiveResult::from_json(value)?)), - _ => Err(Box::new(simple_error::simple_error!("unsupported kind for TCP stream-result: {}", kind))), + "send" => { + self.send_results.push(TcpSendResult::from_json(value)?); + Ok(()) + } + "receive" => { + self.receive_results.push(TcpReceiveResult::from_json(value)?); + Ok(()) + } + _ => Err(Box::new(simple_error::simple_error!( + "unsupported kind for TCP stream-result: {}", + kind + ))), }, None => Err(Box::new(simple_error::simple_error!("kind must be a string for TCP stream-result"))), }, None => Err(Box::new(simple_error::simple_error!("no kind specified for TCP stream-result"))), } } - - fn to_json(&self, omit_seconds:usize) -> serde_json::Value { - let mut duration_send:f64 = 0.0; - let mut bytes_sent:u64 = 0; - - let mut duration_receive:f64 = 0.0; - let mut bytes_received:u64 = 0; - + + fn to_json(&self, omit_seconds: usize) -> serde_json::Value { + let mut duration_send: f64 = 0.0; + let mut bytes_sent: u64 = 0; + + let mut duration_receive: f64 = 0.0; + let mut bytes_received: u64 = 0; + for (i, sr) in self.send_results.iter().enumerate() { if i < omit_seconds { continue; } - + duration_send += sr.duration as f64; bytes_sent += sr.bytes_sent; } - + for (i, rr) in self.receive_results.iter().enumerate() { if i < omit_seconds { continue; } - + duration_receive += rr.duration as f64; bytes_received += rr.bytes_received; } - + let summary = serde_json::json!({ "duration_send": duration_send, "bytes_sent": bytes_sent, - + "duration_receive": duration_receive, "bytes_received": bytes_received, }); - + serde_json::json!({ "receive": self.receive_results.iter().map(|rr| rr.to_result_json()).collect::>(), "send": self.send_results.iter().map(|sr| sr.to_result_json()).collect::>(), @@ -567,78 +594,85 @@ struct UdpStreamResults { send_results: Vec, } impl StreamResults for UdpStreamResults { - fn update_from_json(&mut self, value:serde_json::Value) -> BoxResult<()> { + fn update_from_json(&mut self, value: serde_json::Value) -> BoxResult<()> { match value.get("kind") { Some(k) => match k.as_str() { Some(kind) => match kind { - "send" => Ok(self.send_results.push(UdpSendResult::from_json(value)?)), - "receive" => Ok(self.receive_results.push(UdpReceiveResult::from_json(value)?)), - _ => Err(Box::new(simple_error::simple_error!("unsupported kind for UDP stream-result: {}", kind))), + "send" => { + self.send_results.push(UdpSendResult::from_json(value)?); + Ok(()) + } + "receive" => { + self.receive_results.push(UdpReceiveResult::from_json(value)?); + Ok(()) + } + _ => Err(Box::new(simple_error::simple_error!( + "unsupported kind for UDP stream-result: {}", + kind + ))), }, None => Err(Box::new(simple_error::simple_error!("kind must be a string for UDP stream-result"))), }, None => Err(Box::new(simple_error::simple_error!("no kind specified for UDP stream-result"))), } } - - fn to_json(&self, omit_seconds:usize) -> serde_json::Value { - let mut duration_send:f64 = 0.0; - - let mut bytes_sent:u64 = 0; - let mut packets_sent:u64 = 0; - - - let mut duration_receive:f64 = 0.0; - - let mut bytes_received:u64 = 0; - let mut packets_received:u64 = 0; - let mut packets_out_of_order:u64 = 0; - let mut packets_duplicated:u64 = 0; - + + fn to_json(&self, omit_seconds: usize) -> serde_json::Value { + let mut duration_send: f64 = 0.0; + + let mut bytes_sent: u64 = 0; + let mut packets_sent: u64 = 0; + + let mut duration_receive: f64 = 0.0; + + let mut bytes_received: u64 = 0; + let mut packets_received: u64 = 0; + let mut packets_out_of_order: u64 = 0; + let mut packets_duplicated: u64 = 0; + let mut jitter_calculated = false; - let mut unbroken_sequence_count:u64 = 0; - let mut jitter_weight:f64 = 0.0; - + let mut unbroken_sequence_count: u64 = 0; + let mut jitter_weight: f64 = 0.0; + for (i, sr) in self.send_results.iter().enumerate() { if i < omit_seconds { continue; } - + duration_send += sr.duration as f64; - + bytes_sent += sr.bytes_sent; packets_sent += sr.packets_sent; } - + for (i, rr) in self.receive_results.iter().enumerate() { if i < omit_seconds { continue; } - + duration_receive += rr.duration as f64; - + bytes_received += rr.bytes_received; packets_received += rr.packets_received; packets_out_of_order += rr.packets_out_of_order; packets_duplicated += rr.packets_duplicated; - + if rr.jitter_seconds.is_some() { jitter_weight += (rr.unbroken_sequence as f64) * (rr.jitter_seconds.unwrap() as f64); unbroken_sequence_count += rr.unbroken_sequence; - + jitter_calculated = true; } } - + let mut summary = serde_json::json!({ "duration_send": duration_send, - + "bytes_sent": bytes_sent, "packets_sent": packets_sent, - - + "duration_receive": duration_receive, - + "bytes_received": bytes_received, "packets_received": packets_received, "packets_lost": packets_sent - packets_received, @@ -652,7 +686,7 @@ impl StreamResults for UdpStreamResults { summary["jitter_average"] = serde_json::json!(jitter_weight / (unbroken_sequence_count as f64)); summary["jitter_packets_consecutive"] = serde_json::json!(unbroken_sequence_count); } - + serde_json::json!({ "receive": self.receive_results.iter().map(|rr| rr.to_result_json()).collect::>(), "send": self.send_results.iter().map(|sr| sr.to_result_json()).collect::>(), @@ -661,27 +695,40 @@ impl StreamResults for UdpStreamResults { } } - pub trait TestResults { fn count_in_progress_streams(&self) -> u8; - fn mark_stream_done(&mut self, idx:&u8, success:bool); - + fn mark_stream_done(&mut self, idx: &u8, success: bool); + fn count_in_progress_streams_server(&self) -> u8; - fn mark_stream_done_server(&mut self, idx:&u8); - + fn mark_stream_done_server(&mut self, idx: &u8); + fn is_success(&self) -> bool; - - fn update_from_json(&mut self, value:serde_json::Value) -> BoxResult<()>; - - fn to_json(&self, omit_seconds:usize, upload_config:serde_json::Value, download_config:serde_json::Value, common_config:serde_json::Value, additional_config:serde_json::Value) -> serde_json::Value; - + + fn update_from_json(&mut self, value: serde_json::Value) -> BoxResult<()>; + + fn to_json( + &self, + omit_seconds: usize, + upload_config: serde_json::Value, + download_config: serde_json::Value, + common_config: serde_json::Value, + additional_config: serde_json::Value, + ) -> serde_json::Value; + //produces a pretty-printed JSON string with the test results - fn to_json_string(&self, omit_seconds:usize, upload_config:serde_json::Value, download_config:serde_json::Value, common_config:serde_json::Value, additional_config:serde_json::Value) -> String { + fn to_json_string( + &self, + omit_seconds: usize, + upload_config: serde_json::Value, + download_config: serde_json::Value, + common_config: serde_json::Value, + additional_config: serde_json::Value, + ) -> String { serde_json::to_string_pretty(&self.to_json(omit_seconds, upload_config, download_config, common_config, additional_config)).unwrap() } - + //produces test-results in tabular form - fn to_string(&self, bit:bool, omit_seconds:usize) -> String; + fn to_string(&self, bit: bool, omit_seconds: usize) -> String; } pub struct TcpTestResults { @@ -692,18 +739,21 @@ pub struct TcpTestResults { } impl TcpTestResults { pub fn new() -> TcpTestResults { - TcpTestResults{ + TcpTestResults { stream_results: HashMap::new(), pending_tests: HashSet::new(), failed_tests: HashSet::new(), server_tests_finished: HashSet::new(), } } - pub fn prepare_index(&mut self, idx:&u8) { - self.stream_results.insert(*idx, TcpStreamResults{ - receive_results: Vec::new(), - send_results: Vec::new(), - }); + pub fn prepare_index(&mut self, idx: &u8) { + self.stream_results.insert( + *idx, + TcpStreamResults { + receive_results: Vec::new(), + send_results: Vec::new(), + }, + ); self.pending_tests.insert(*idx); } } @@ -711,15 +761,15 @@ impl TestResults for TcpTestResults { fn count_in_progress_streams(&self) -> u8 { self.pending_tests.len() as u8 } - fn mark_stream_done(&mut self, idx:&u8, success:bool) { + fn mark_stream_done(&mut self, idx: &u8, success: bool) { self.pending_tests.remove(idx); if !success { self.failed_tests.insert(*idx); } } - + fn count_in_progress_streams_server(&self) -> u8 { - let mut count:u8 = 0; + let mut count: u8 = 0; for idx in self.stream_results.keys() { if !self.server_tests_finished.contains(idx) { count += 1; @@ -727,15 +777,15 @@ impl TestResults for TcpTestResults { } count } - fn mark_stream_done_server(&mut self, idx:&u8) { + fn mark_stream_done_server(&mut self, idx: &u8) { self.server_tests_finished.insert(*idx); } - + fn is_success(&self) -> bool { - self.pending_tests.len() == 0 && self.failed_tests.len() == 0 + self.pending_tests.is_empty() && self.failed_tests.is_empty() } - - fn update_from_json(&mut self, value:serde_json::Value) -> BoxResult<()> { + + fn update_from_json(&mut self, value: serde_json::Value) -> BoxResult<()> { match value.get("family") { Some(f) => match f.as_str() { Some(family) => match family { @@ -743,28 +793,40 @@ impl TestResults for TcpTestResults { Some(idx) => match idx.as_i64() { Some(idx64) => match self.stream_results.get_mut(&(idx64 as u8)) { Some(stream_results) => stream_results.update_from_json(value), - None => Err(Box::new(simple_error::simple_error!("stream-index {} is not a valid identifier", idx64))), + None => Err(Box::new(simple_error::simple_error!( + "stream-index {} is not a valid identifier", + idx64 + ))), }, None => Err(Box::new(simple_error::simple_error!("stream-index is not an integer"))), }, None => Err(Box::new(simple_error::simple_error!("no stream-index specified"))), }, - _ => Err(Box::new(simple_error::simple_error!("unsupported family for TCP stream-result: {}", family))), + _ => Err(Box::new(simple_error::simple_error!( + "unsupported family for TCP stream-result: {}", + family + ))), }, None => Err(Box::new(simple_error::simple_error!("kind must be a string for TCP stream-result"))), }, None => Err(Box::new(simple_error::simple_error!("no kind specified for TCP stream-result"))), } } - - fn to_json(&self, omit_seconds:usize, upload_config:serde_json::Value, download_config:serde_json::Value, common_config:serde_json::Value, additional_config:serde_json::Value) -> serde_json::Value { - let mut duration_send:f64 = 0.0; - let mut bytes_sent:u64 = 0; - - let mut duration_receive:f64 = 0.0; - let mut bytes_received:u64 = 0; - - + + fn to_json( + &self, + omit_seconds: usize, + upload_config: serde_json::Value, + download_config: serde_json::Value, + common_config: serde_json::Value, + additional_config: serde_json::Value, + ) -> serde_json::Value { + let mut duration_send: f64 = 0.0; + let mut bytes_sent: u64 = 0; + + let mut duration_receive: f64 = 0.0; + let mut bytes_received: u64 = 0; + let mut streams = Vec::with_capacity(self.stream_results.len()); for (idx, stream) in self.stream_results.iter() { streams.push(serde_json::json!({ @@ -772,34 +834,34 @@ impl TestResults for TcpTestResults { "abandoned": self.pending_tests.contains(idx), "failed": self.failed_tests.contains(idx), })); - + for (i, sr) in stream.send_results.iter().enumerate() { if i < omit_seconds { continue; } - + duration_send += sr.duration as f64; bytes_sent += sr.bytes_sent; } - + for (i, rr) in stream.receive_results.iter().enumerate() { if i < omit_seconds { continue; } - + duration_receive += rr.duration as f64; bytes_received += rr.bytes_received; } } - + let summary = serde_json::json!({ "duration_send": duration_send, "bytes_sent": bytes_sent, - + "duration_receive": duration_receive, "bytes_received": bytes_received, }); - + serde_json::json!({ "config": { "upload": upload_config, @@ -812,103 +874,123 @@ impl TestResults for TcpTestResults { "success": self.is_success(), }) } - - fn to_string(&self, bit:bool, omit_seconds:usize) -> String { + + fn to_string(&self, bit: bool, omit_seconds: usize) -> String { let stream_count = self.stream_results.len(); let mut stream_send_durations = vec![0.0; stream_count]; let mut stream_receive_durations = vec![0.0; stream_count]; - - let mut duration_send:f64 = 0.0; - let mut bytes_sent:u64 = 0; - - let mut duration_receive:f64 = 0.0; - let mut bytes_received:u64 = 0; - + + let mut duration_send: f64 = 0.0; + let mut bytes_sent: u64 = 0; + + let mut duration_receive: f64 = 0.0; + let mut bytes_received: u64 = 0; + let mut sends_blocked = false; - + for (stream_idx, stream) in self.stream_results.values().enumerate() { for (i, sr) in stream.send_results.iter().enumerate() { if i < omit_seconds { continue; } - + duration_send += sr.duration as f64; stream_send_durations[stream_idx] += sr.duration as f64; - + bytes_sent += sr.bytes_sent; - + sends_blocked |= sr.sends_blocked > 0; } - + for (i, rr) in stream.receive_results.iter().enumerate() { if i < omit_seconds { continue; } - + duration_receive += rr.duration as f64; stream_receive_durations[stream_idx] += rr.duration as f64; - + bytes_received += rr.bytes_received; } } stream_send_durations.sort_by(|a, b| a.partial_cmp(b).unwrap()); stream_receive_durations.sort_by(|a, b| a.partial_cmp(b).unwrap()); - - let send_duration_divisor; - if duration_send == 0.0 { //avoid zerodiv, which can happen if all streams fail - send_duration_divisor = 1.0; + + let send_duration_divisor = if duration_send == 0.0 { + //avoid zerodiv, which can happen if all streams fail + 1.0 } else { - send_duration_divisor = duration_send; - } + duration_send + }; let send_bytes_per_second = bytes_sent as f64 / send_duration_divisor; let send_throughput = match bit { true => format!("megabits/second: {:.3}", send_bytes_per_second / (1_000_000.00 / 8.0)), false => format!("megabytes/second: {:.3}", send_bytes_per_second / 1_000_000.00), }; let total_send_throughput = match bit { - true => format!("megabits/second: {:.3}", (send_bytes_per_second / (1_000_000.00 / 8.0)) * stream_count as f64), - false => format!("megabytes/second: {:.3}", (send_bytes_per_second / 1_000_000.00) * stream_count as f64), + true => format!( + "megabits/second: {:.3}", + (send_bytes_per_second / (1_000_000.00 / 8.0)) * stream_count as f64 + ), + false => format!( + "megabytes/second: {:.3}", + (send_bytes_per_second / 1_000_000.00) * stream_count as f64 + ), }; - - let receive_duration_divisor; - if duration_receive == 0.0 { //avoid zerodiv, which can happen if all streams fail - receive_duration_divisor = 1.0; + + let receive_duration_divisor = if duration_receive == 0.0 { + //avoid zerodiv, which can happen if all streams fail + 1.0 } else { - receive_duration_divisor = duration_receive; - } + duration_receive + }; let receive_bytes_per_second = bytes_received as f64 / receive_duration_divisor; let receive_throughput = match bit { true => format!("megabits/second: {:.3}", receive_bytes_per_second / (1_000_000.00 / 8.0)), false => format!("megabytes/second: {:.3}", receive_bytes_per_second / 1_000_000.00), }; let total_receive_throughput = match bit { - true => format!("megabits/second: {:.3}", (receive_bytes_per_second / (1_000_000.00 / 8.0)) * stream_count as f64), - false => format!("megabytes/second: {:.3}", (receive_bytes_per_second / 1_000_000.00) * stream_count as f64), + true => format!( + "megabits/second: {:.3}", + (receive_bytes_per_second / (1_000_000.00 / 8.0)) * stream_count as f64 + ), + false => format!( + "megabytes/second: {:.3}", + (receive_bytes_per_second / 1_000_000.00) * stream_count as f64 + ), }; - - let mut output = format!("==========\n\ - TCP send result over {:.2}s | streams: {}\n\ - stream-average bytes per second: {:.3} | {}\n\ - total bytes: {} | per second: {:.3} | {}\n\ - ==========\n\ - TCP receive result over {:.2}s | streams: {}\n\ - stream-average bytes per second: {:.3} | {}\n\ - total bytes: {} | per second: {:.3} | {}", - stream_send_durations[stream_send_durations.len() - 1], stream_count, - send_bytes_per_second, send_throughput, - bytes_sent, send_bytes_per_second * stream_count as f64, total_send_throughput, - - stream_receive_durations[stream_receive_durations.len() - 1], stream_count, - receive_bytes_per_second, receive_throughput, - bytes_received, receive_bytes_per_second * stream_count as f64, total_receive_throughput, + + let mut output = format!( + "==========\n\ + TCP send result over {:.2}s | streams: {}\n\ + stream-average bytes per second: {:.3} | {}\n\ + total bytes: {} | per second: {:.3} | {}\n\ + ==========\n\ + TCP receive result over {:.2}s | streams: {}\n\ + stream-average bytes per second: {:.3} | {}\n\ + total bytes: {} | per second: {:.3} | {}", + stream_send_durations[stream_send_durations.len() - 1], + stream_count, + send_bytes_per_second, + send_throughput, + bytes_sent, + send_bytes_per_second * stream_count as f64, + total_send_throughput, + stream_receive_durations[stream_receive_durations.len() - 1], + stream_count, + receive_bytes_per_second, + receive_throughput, + bytes_received, + receive_bytes_per_second * stream_count as f64, + total_receive_throughput, ); if sends_blocked { - output.push_str(&format!("\nthroughput throttled by buffer limitations")); + output.push_str("\nthroughput throttled by buffer limitations"); } if !self.is_success() { - output.push_str(&format!("\nTESTING DID NOT COMPLETE SUCCESSFULLY")); + output.push_str("\nTESTING DID NOT COMPLETE SUCCESSFULLY"); } - + output } } @@ -921,18 +1003,21 @@ pub struct UdpTestResults { } impl UdpTestResults { pub fn new() -> UdpTestResults { - UdpTestResults{ + UdpTestResults { stream_results: HashMap::new(), pending_tests: HashSet::new(), failed_tests: HashSet::new(), server_tests_finished: HashSet::new(), } } - pub fn prepare_index(&mut self, idx:&u8) { - self.stream_results.insert(*idx, UdpStreamResults{ - receive_results: Vec::new(), - send_results: Vec::new(), - }); + pub fn prepare_index(&mut self, idx: &u8) { + self.stream_results.insert( + *idx, + UdpStreamResults { + receive_results: Vec::new(), + send_results: Vec::new(), + }, + ); self.pending_tests.insert(*idx); } } @@ -940,15 +1025,15 @@ impl TestResults for UdpTestResults { fn count_in_progress_streams(&self) -> u8 { self.pending_tests.len() as u8 } - fn mark_stream_done(&mut self, idx:&u8, success:bool) { + fn mark_stream_done(&mut self, idx: &u8, success: bool) { self.pending_tests.remove(idx); if !success { self.failed_tests.insert(*idx); } } - + fn count_in_progress_streams_server(&self) -> u8 { - let mut count:u8 = 0; + let mut count: u8 = 0; for idx in self.stream_results.keys() { if !self.server_tests_finished.contains(idx) { count += 1; @@ -956,15 +1041,15 @@ impl TestResults for UdpTestResults { } count } - fn mark_stream_done_server(&mut self, idx:&u8) { + fn mark_stream_done_server(&mut self, idx: &u8) { self.server_tests_finished.insert(*idx); } - + fn is_success(&self) -> bool { - self.pending_tests.len() == 0 && self.failed_tests.len() == 0 + self.pending_tests.is_empty() && self.failed_tests.is_empty() } - - fn update_from_json(&mut self, value:serde_json::Value) -> BoxResult<()> { + + fn update_from_json(&mut self, value: serde_json::Value) -> BoxResult<()> { match value.get("family") { Some(f) => match f.as_str() { Some(family) => match family { @@ -972,39 +1057,50 @@ impl TestResults for UdpTestResults { Some(idx) => match idx.as_i64() { Some(idx64) => match self.stream_results.get_mut(&(idx64 as u8)) { Some(stream_results) => stream_results.update_from_json(value), - None => Err(Box::new(simple_error::simple_error!("stream-index {} is not a valid identifier", idx64))), + None => Err(Box::new(simple_error::simple_error!( + "stream-index {} is not a valid identifier", + idx64 + ))), }, None => Err(Box::new(simple_error::simple_error!("stream-index is not an integer"))), }, None => Err(Box::new(simple_error::simple_error!("no stream-index specified"))), }, - _ => Err(Box::new(simple_error::simple_error!("unsupported family for UDP stream-result: {}", family))), + _ => Err(Box::new(simple_error::simple_error!( + "unsupported family for UDP stream-result: {}", + family + ))), }, None => Err(Box::new(simple_error::simple_error!("kind must be a string for UDP stream-result"))), }, None => Err(Box::new(simple_error::simple_error!("no kind specified for UDP stream-result"))), } } - - fn to_json(&self, omit_seconds:usize, upload_config:serde_json::Value, download_config:serde_json::Value, common_config:serde_json::Value, additional_config:serde_json::Value) -> serde_json::Value { - let mut duration_send:f64 = 0.0; - - let mut bytes_sent:u64 = 0; - let mut packets_sent:u64 = 0; - - - let mut duration_receive:f64 = 0.0; - - let mut bytes_received:u64 = 0; - let mut packets_received:u64 = 0; - let mut packets_out_of_order:u64 = 0; - let mut packets_duplicated:u64 = 0; - + + fn to_json( + &self, + omit_seconds: usize, + upload_config: serde_json::Value, + download_config: serde_json::Value, + common_config: serde_json::Value, + additional_config: serde_json::Value, + ) -> serde_json::Value { + let mut duration_send: f64 = 0.0; + + let mut bytes_sent: u64 = 0; + let mut packets_sent: u64 = 0; + + let mut duration_receive: f64 = 0.0; + + let mut bytes_received: u64 = 0; + let mut packets_received: u64 = 0; + let mut packets_out_of_order: u64 = 0; + let mut packets_duplicated: u64 = 0; + let mut jitter_calculated = false; - let mut unbroken_sequence_count:u64 = 0; - let mut jitter_weight:f64 = 0.0; - - + let mut unbroken_sequence_count: u64 = 0; + let mut jitter_weight: f64 = 0.0; + let mut streams = Vec::with_capacity(self.stream_results.len()); for (idx, stream) in self.stream_results.iter() { streams.push(serde_json::json!({ @@ -1012,48 +1108,47 @@ impl TestResults for UdpTestResults { "abandoned": self.pending_tests.contains(idx), "failed": self.failed_tests.contains(idx), })); - + for (i, sr) in stream.send_results.iter().enumerate() { if i < omit_seconds { continue; } - + duration_send += sr.duration as f64; - + bytes_sent += sr.bytes_sent; packets_sent += sr.packets_sent; } - + for (i, rr) in stream.receive_results.iter().enumerate() { if i < omit_seconds { continue; } - + duration_receive += rr.duration as f64; - + bytes_received += rr.bytes_received; packets_received += rr.packets_received; packets_out_of_order += rr.packets_out_of_order; packets_duplicated += rr.packets_duplicated; - + if rr.jitter_seconds.is_some() { jitter_weight += (rr.unbroken_sequence as f64) * (rr.jitter_seconds.unwrap() as f64); unbroken_sequence_count += rr.unbroken_sequence; - + jitter_calculated = true; } } } - + let mut summary = serde_json::json!({ "duration_send": duration_send, - + "bytes_sent": bytes_sent, "packets_sent": packets_sent, - - + "duration_receive": duration_receive, - + "bytes_received": bytes_received, "packets_received": packets_received, "packets_lost": packets_sent - packets_received, @@ -1067,7 +1162,7 @@ impl TestResults for UdpTestResults { summary["jitter_average"] = serde_json::json!(jitter_weight / (unbroken_sequence_count as f64)); summary["jitter_packets_consecutive"] = serde_json::json!(unbroken_sequence_count); } - + serde_json::json!({ "config": { "upload": upload_config, @@ -1080,139 +1175,168 @@ impl TestResults for UdpTestResults { "success": self.is_success(), }) } - - fn to_string(&self, bit:bool, omit_seconds:usize) -> String { + + fn to_string(&self, bit: bool, omit_seconds: usize) -> String { let stream_count = self.stream_results.len(); let mut stream_send_durations = vec![0.0; stream_count]; let mut stream_receive_durations = vec![0.0; stream_count]; - - let mut duration_send:f64 = 0.0; - - let mut bytes_sent:u64 = 0; - let mut packets_sent:u64 = 0; - - - let mut duration_receive:f64 = 0.0; - - let mut bytes_received:u64 = 0; - let mut packets_received:u64 = 0; - let mut packets_out_of_order:u64 = 0; - let mut packets_duplicated:u64 = 0; - + + let mut duration_send: f64 = 0.0; + + let mut bytes_sent: u64 = 0; + let mut packets_sent: u64 = 0; + + let mut duration_receive: f64 = 0.0; + + let mut bytes_received: u64 = 0; + let mut packets_received: u64 = 0; + let mut packets_out_of_order: u64 = 0; + let mut packets_duplicated: u64 = 0; + let mut jitter_calculated = false; - let mut unbroken_sequence_count:u64 = 0; - let mut jitter_weight:f64 = 0.0; - + let mut unbroken_sequence_count: u64 = 0; + let mut jitter_weight: f64 = 0.0; + let mut sends_blocked = false; - + for (stream_idx, stream) in self.stream_results.values().enumerate() { for (i, sr) in stream.send_results.iter().enumerate() { if i < omit_seconds { continue; } - + duration_send += sr.duration as f64; stream_send_durations[stream_idx] += sr.duration as f64; - + bytes_sent += sr.bytes_sent; packets_sent += sr.packets_sent; - + sends_blocked |= sr.sends_blocked > 0; } - + for (i, rr) in stream.receive_results.iter().enumerate() { if i < omit_seconds { continue; } - + duration_receive += rr.duration as f64; stream_receive_durations[stream_idx] += rr.duration as f64; - + bytes_received += rr.bytes_received; packets_received += rr.packets_received; packets_out_of_order += rr.packets_out_of_order; packets_duplicated += rr.packets_duplicated; - + if rr.jitter_seconds.is_some() { jitter_weight += (rr.unbroken_sequence as f64) * (rr.jitter_seconds.unwrap() as f64); unbroken_sequence_count += rr.unbroken_sequence; - + jitter_calculated = true; } } } stream_send_durations.sort_by(|a, b| a.partial_cmp(b).unwrap()); stream_receive_durations.sort_by(|a, b| a.partial_cmp(b).unwrap()); - - let send_duration_divisor; - if duration_send == 0.0 { //avoid zerodiv, which can happen if all streams fail - send_duration_divisor = 1.0; + + let send_duration_divisor = if duration_send == 0.0 { + //avoid zerodiv, which can happen if all streams fail + 1.0 } else { - send_duration_divisor = duration_send; - } + duration_send + }; let send_bytes_per_second = bytes_sent as f64 / send_duration_divisor; let send_throughput = match bit { true => format!("megabits/second: {:.3}", send_bytes_per_second / (1_000_000.00 / 8.0)), false => format!("megabytes/second: {:.3}", send_bytes_per_second / 1_000_000.00), }; let total_send_throughput = match bit { - true => format!("megabits/second: {:.3}", (send_bytes_per_second / (1_000_000.00 / 8.0)) * stream_count as f64), - false => format!("megabytes/second: {:.3}", (send_bytes_per_second / 1_000_000.00) * stream_count as f64), + true => format!( + "megabits/second: {:.3}", + (send_bytes_per_second / (1_000_000.00 / 8.0)) * stream_count as f64 + ), + false => format!( + "megabytes/second: {:.3}", + (send_bytes_per_second / 1_000_000.00) * stream_count as f64 + ), }; - - let receive_duration_divisor; - if duration_receive == 0.0 { //avoid zerodiv, which can happen if all streams fail - receive_duration_divisor = 1.0; + + let receive_duration_divisor = if duration_receive == 0.0 { + //avoid zerodiv, which can happen if all streams fail + 1.0 } else { - receive_duration_divisor = duration_receive; - } + duration_receive + }; let receive_bytes_per_second = bytes_received as f64 / receive_duration_divisor; let receive_throughput = match bit { true => format!("megabits/second: {:.3}", receive_bytes_per_second / (1_000_000.00 / 8.0)), false => format!("megabytes/second: {:.3}", receive_bytes_per_second / 1_000_000.00), }; let total_receive_throughput = match bit { - true => format!("megabits/second: {:.3}", (receive_bytes_per_second / (1_000_000.00 / 8.0)) * stream_count as f64), - false => format!("megabytes/second: {:.3}", (receive_bytes_per_second / 1_000_000.00) * stream_count as f64), + true => format!( + "megabits/second: {:.3}", + (receive_bytes_per_second / (1_000_000.00 / 8.0)) * stream_count as f64 + ), + false => format!( + "megabytes/second: {:.3}", + (receive_bytes_per_second / 1_000_000.00) * stream_count as f64 + ), }; - + let packets_lost = packets_sent - (packets_received - packets_duplicated); - let packets_sent_divisor; - if packets_sent == 0 { //avoid zerodiv, which can happen if all streams fail - packets_sent_divisor = 1.0 + let packets_sent_divisor = if packets_sent == 0 { + //avoid zerodiv, which can happen if all streams fail + 1.0 } else { - packets_sent_divisor = packets_sent as f64; - } - let mut output = format!("==========\n\ - UDP send result over {:.2}s | streams: {}\n\ - stream-average bytes per second: {:.3} | {}\n\ - total bytes: {} | per second: {:.3} | {}\n\ - packets: {} per second: {:.3}\n\ - ==========\n\ - UDP receive result over {:.2}s | streams: {}\n\ - stream-average bytes per second: {:.3} | {}\n\ - total bytes: {} | per second: {:.3} | {}\n\ - packets: {} | lost: {} ({:.1}%) | out-of-order: {} | duplicate: {} | per second: {:.3}", - stream_send_durations[stream_send_durations.len() - 1], stream_count, - send_bytes_per_second, send_throughput, - bytes_sent, send_bytes_per_second * stream_count as f64, total_send_throughput, - packets_sent, (packets_sent as f64 / send_duration_divisor) * stream_count as f64, - - stream_receive_durations[stream_receive_durations.len() - 1], stream_count, - receive_bytes_per_second, receive_throughput, - bytes_received, receive_bytes_per_second * stream_count as f64, total_receive_throughput, - packets_received, packets_lost, (packets_lost as f64 / packets_sent_divisor) * 100.0, packets_out_of_order, packets_duplicated, (packets_received as f64 / receive_duration_divisor) * stream_count as f64, + packets_sent as f64 + }; + let mut output = format!( + "==========\n\ + UDP send result over {:.2}s | streams: {}\n\ + stream-average bytes per second: {:.3} | {}\n\ + total bytes: {} | per second: {:.3} | {}\n\ + packets: {} per second: {:.3}\n\ + ==========\n\ + UDP receive result over {:.2}s | streams: {}\n\ + stream-average bytes per second: {:.3} | {}\n\ + total bytes: {} | per second: {:.3} | {}\n\ + packets: {} | lost: {} ({:.1}%) | out-of-order: {} | duplicate: {} | per second: {:.3}", + stream_send_durations[stream_send_durations.len() - 1], + stream_count, + send_bytes_per_second, + send_throughput, + bytes_sent, + send_bytes_per_second * stream_count as f64, + total_send_throughput, + packets_sent, + (packets_sent as f64 / send_duration_divisor) * stream_count as f64, + stream_receive_durations[stream_receive_durations.len() - 1], + stream_count, + receive_bytes_per_second, + receive_throughput, + bytes_received, + receive_bytes_per_second * stream_count as f64, + total_receive_throughput, + packets_received, + packets_lost, + (packets_lost as f64 / packets_sent_divisor) * 100.0, + packets_out_of_order, + packets_duplicated, + (packets_received as f64 / receive_duration_divisor) * stream_count as f64, ); if jitter_calculated { - output.push_str(&format!("\njitter: {:.6}s over {} consecutive packets", jitter_weight / (unbroken_sequence_count as f64), unbroken_sequence_count)); + output.push_str(&format!( + "\njitter: {:.6}s over {} consecutive packets", + jitter_weight / (unbroken_sequence_count as f64), + unbroken_sequence_count + )); } if sends_blocked { - output.push_str(&format!("\nthroughput throttled by buffer limitations")); + output.push_str("\nthroughput throttled by buffer limitations"); } if !self.is_success() { - output.push_str(&format!("\nTESTING DID NOT COMPLETE SUCCESSFULLY")); + output.push_str("\nTESTING DID NOT COMPLETE SUCCESSFULLY"); } - + output } } diff --git a/src/server.rs b/src/server.rs index 7256226..e686c31 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,259 +1,266 @@ /* * Copyright (C) 2021 Evtech Solutions, Ltd., dba 3D-P * Copyright (C) 2021 Neil Tallim - * + * * This file is part of rperf. - * + * * rperf is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * rperf is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with rperf. If not, see . */ -use std::error::Error; use std::io; -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, Shutdown, SocketAddr}; +use std::net::{Shutdown, SocketAddr}; use std::sync::atomic::{AtomicBool, AtomicU16, Ordering}; -use std::sync::{Arc, Mutex}; -use std::sync::mpsc::channel; +use std::sync::{mpsc, Arc, Mutex}; use std::thread; -use std::time::{Duration}; +use std::time::Duration; -use clap::ArgMatches; +use std::net::{TcpListener, TcpStream}; -use mio::net::{TcpListener, TcpStream}; -use mio::{Events, Ready, Poll, PollOpt, Token}; +use crate::args::Args; +use crate::protocol::communication::{receive, send}; +use crate::protocol::messaging::{prepare_connect, prepare_connect_ready}; +use crate::protocol::results::{IntervalResultBox, ServerDoneResult}; +use crate::stream::{tcp, udp, TestStream}; +use crate::BoxResult; -use crate::protocol::communication::{receive, send, KEEPALIVE_DURATION}; - -use crate::protocol::messaging::{ - prepare_connect, prepare_connect_ready, -}; - -use crate::stream::TestStream; -use crate::stream::tcp; -use crate::stream::udp; - -type BoxResult = Result>; - -const POLL_TIMEOUT:Duration = Duration::from_millis(500); +const POLL_TIMEOUT: Duration = Duration::from_millis(500); /// when false, the system is shutting down -static ALIVE:AtomicBool = AtomicBool::new(true); +static ALIVE: AtomicBool = AtomicBool::new(true); /// a count of connected clients -static CLIENTS:AtomicU16 = AtomicU16::new(0); - +static CLIENTS: AtomicU16 = AtomicU16::new(0); fn handle_client( - stream:&mut TcpStream, - cpu_affinity_manager:Arc>, - tcp_port_pool:Arc>, - udp_port_pool:Arc>, + stream: &mut TcpStream, + cpu_affinity_manager: Arc>, + tcp_port_pool: Arc>, + udp_port_pool: Arc>, ) -> BoxResult<()> { let mut started = false; let peer_addr = stream.peer_addr()?; - - + //scaffolding to track and relay the streams and stream-results associated with this client - let mut parallel_streams:Vec>> = Vec::new(); + let mut parallel_streams: Vec>> = Vec::new(); let mut parallel_streams_joinhandles = Vec::new(); - let (results_tx, results_rx):(std::sync::mpsc::Sender>, std::sync::mpsc::Receiver>) = channel(); - + + let (results_tx, results_rx) = mpsc::channel::(); + //a closure used to pass results from stream-handlers to the client-communication stream let mut forwarding_send_stream = stream.try_clone()?; let mut results_handler = || -> BoxResult<()> { - loop { //drain all results every time this closer is invoked - match results_rx.try_recv() { //if there's something to forward, write it to the client - Ok(result) => { - send(&mut forwarding_send_stream, &result.to_json())?; - }, - Err(_) => break, //whether it's empty or disconnected, there's nothing to do - } + // drain all results every time this closer is invoked + while let Ok(result) = results_rx.try_recv() { + // if there's something to forward, write it to the client + send(&mut forwarding_send_stream, &result.to_json())?; } Ok(()) }; - - + //server operations are entirely driven by client-signalling, making this a (simple) state-machine while is_alive() { let payload = receive(stream, is_alive, &mut results_handler)?; - match payload.get("kind") { - Some(kind) => { - match kind.as_str().unwrap() { - "configuration" => { //we either need to connect streams to the client or prepare to receive connections - if payload.get("role").unwrap_or(&serde_json::json!("download")).as_str().unwrap() == "download" { - log::info!("[{}] running in forward-mode: server will be receiving data", &peer_addr); - - let stream_count = payload.get("streams").unwrap_or(&serde_json::json!(1)).as_i64().unwrap(); - //since we're receiving data, we're also responsible for letting the client know where to send it - let mut stream_ports = Vec::with_capacity(stream_count as usize); - - if payload.get("family").unwrap_or(&serde_json::json!("tcp")).as_str().unwrap() == "udp" { - log::info!("[{}] preparing for UDP test with {} streams...", &peer_addr, stream_count); - - let mut c_udp_port_pool = udp_port_pool.lock().unwrap(); - - let test_definition = udp::UdpTestDefinition::new(&payload)?; - for stream_idx in 0..stream_count { - log::debug!("[{}] preparing UDP-receiver for stream {}...", &peer_addr, stream_idx); - let test = udp::receiver::UdpReceiver::new( - test_definition.clone(), &(stream_idx as u8), - &mut c_udp_port_pool, - &peer_addr.ip(), - &(payload["receive_buffer"].as_i64().unwrap() as usize), - )?; - stream_ports.push(test.get_port()?); - parallel_streams.push(Arc::new(Mutex::new(test))); - } - } else { //TCP - log::info!("[{}] preparing for TCP test with {} streams...", &peer_addr, stream_count); - - let mut c_tcp_port_pool = tcp_port_pool.lock().unwrap(); - - let test_definition = tcp::TcpTestDefinition::new(&payload)?; - for stream_idx in 0..stream_count { - log::debug!("[{}] preparing TCP-receiver for stream {}...", &peer_addr, stream_idx); - let test = tcp::receiver::TcpReceiver::new( - test_definition.clone(), &(stream_idx as u8), - &mut c_tcp_port_pool, - &peer_addr.ip(), - &(payload["receive_buffer"].as_i64().unwrap() as usize), - )?; - stream_ports.push(test.get_port()?); - parallel_streams.push(Arc::new(Mutex::new(test))); - } - } - - //let the client know we're ready to receive the connection; stream-ports are in stream-index order - send(stream, &prepare_connect(&stream_ports))?; - } else { //upload - log::info!("[{}] running in reverse-mode: server will be uploading data", &peer_addr); - - let stream_ports = payload.get("stream_ports").unwrap().as_array().unwrap(); - - if payload.get("family").unwrap_or(&serde_json::json!("tcp")).as_str().unwrap() == "udp" { - log::info!("[{}] preparing for UDP test with {} streams...", &peer_addr, stream_ports.len()); - - let test_definition = udp::UdpTestDefinition::new(&payload)?; - for (stream_idx, port) in stream_ports.iter().enumerate() { - log::debug!("[{}] preparing UDP-sender for stream {}...", &peer_addr, stream_idx); - let test = udp::sender::UdpSender::new( - test_definition.clone(), &(stream_idx as u8), - &0, &peer_addr.ip(), &(port.as_i64().unwrap_or(0) as u16), - &(payload.get("duration").unwrap_or(&serde_json::json!(0.0)).as_f64().unwrap() as f32), - &(payload.get("send_interval").unwrap_or(&serde_json::json!(1.0)).as_f64().unwrap() as f32), - &(payload["send_buffer"].as_i64().unwrap() as usize), - )?; - parallel_streams.push(Arc::new(Mutex::new(test))); - } - } else { //TCP - log::info!("[{}] preparing for TCP test with {} streams...", &peer_addr, stream_ports.len()); - - let test_definition = tcp::TcpTestDefinition::new(&payload)?; - for (stream_idx, port) in stream_ports.iter().enumerate() { - log::debug!("[{}] preparing TCP-sender for stream {}...", &peer_addr, stream_idx); - let test = tcp::sender::TcpSender::new( - test_definition.clone(), &(stream_idx as u8), - &peer_addr.ip(), &(port.as_i64().unwrap() as u16), - &(payload["duration"].as_f64().unwrap() as f32), - &(payload["send_interval"].as_f64().unwrap() as f32), - &(payload["send_buffer"].as_i64().unwrap() as usize), - &(payload["no_delay"].as_bool().unwrap()), - )?; - parallel_streams.push(Arc::new(Mutex::new(test))); - } - } - - //let the client know we're ready to begin - send(stream, &prepare_connect_ready())?; + let kind = if let Some(kind) = payload.get("kind") { + kind + } else { + log::error!("[{}] invalid data", &peer_addr); + break; + }; + match kind.as_str().unwrap() { + "configuration" => { + //we either need to connect streams to the client or prepare to receive connections + if payload.get("role").unwrap_or(&serde_json::json!("download")).as_str().unwrap() == "download" { + log::info!("[{}] running in forward-mode: server will be receiving data", &peer_addr); + + let stream_count = payload.get("streams").unwrap_or(&serde_json::json!(1)).as_i64().unwrap(); + //since we're receiving data, we're also responsible for letting the client know where to send it + let mut stream_ports = Vec::with_capacity(stream_count as usize); + + if payload.get("family").unwrap_or(&serde_json::json!("tcp")).as_str().unwrap() == "udp" { + log::info!("[{}] preparing for UDP test with {} streams...", &peer_addr, stream_count); + + let mut c_udp_port_pool = udp_port_pool.lock().unwrap(); + + let test_definition = udp::UdpTestDefinition::new(&payload)?; + for stream_idx in 0..stream_count { + log::debug!("[{}] preparing UDP-receiver for stream {}...", &peer_addr, stream_idx); + let test = udp::receiver::UdpReceiver::new( + test_definition.clone(), + &(stream_idx as u8), + &mut c_udp_port_pool, + &peer_addr.ip(), + &(payload["receive_buffer"].as_i64().unwrap() as usize), + )?; + stream_ports.push(test.get_port()?); + parallel_streams.push(Arc::new(Mutex::new(test))); + } + } else { + //TCP + log::info!("[{}] preparing for TCP test with {} streams...", &peer_addr, stream_count); + + let mut c_tcp_port_pool = tcp_port_pool.lock().unwrap(); + + let test_definition = tcp::TcpTestDefinition::new(&payload)?; + for stream_idx in 0..stream_count { + log::debug!("[{}] preparing TCP-receiver for stream {}...", &peer_addr, stream_idx); + let test = tcp::receiver::TcpReceiver::new( + test_definition.clone(), + &(stream_idx as u8), + &mut c_tcp_port_pool, + &peer_addr.ip(), + )?; + stream_ports.push(test.get_port()?); + parallel_streams.push(Arc::new(Mutex::new(test))); + } + } + + //let the client know we're ready to receive the connection; stream-ports are in stream-index order + send(stream, &prepare_connect(&stream_ports))?; + } else { + //upload + log::info!("[{}] running in reverse-mode: server will be uploading data", &peer_addr); + + let stream_ports = payload.get("stream_ports").unwrap().as_array().unwrap(); + + if payload.get("family").unwrap_or(&serde_json::json!("tcp")).as_str().unwrap() == "udp" { + log::info!("[{}] preparing for UDP test with {} streams...", &peer_addr, stream_ports.len()); + + let test_definition = udp::UdpTestDefinition::new(&payload)?; + for (stream_idx, port) in stream_ports.iter().enumerate() { + log::debug!("[{}] preparing UDP-sender for stream {}...", &peer_addr, stream_idx); + let test = udp::sender::UdpSender::new( + test_definition.clone(), + &(stream_idx as u8), + &0, + &peer_addr.ip(), + &(port.as_i64().unwrap_or(0) as u16), + &(payload.get("duration").unwrap_or(&serde_json::json!(0.0)).as_f64().unwrap() as f32), + &(payload.get("send_interval").unwrap_or(&serde_json::json!(1.0)).as_f64().unwrap() as f32), + &(payload["send_buffer"].as_i64().unwrap() as usize), + )?; + parallel_streams.push(Arc::new(Mutex::new(test))); } - }, - "begin" => { //the client has indicated that testing can begin - if !started { //a simple guard to protect against reinitialisaion - for (stream_idx, parallel_stream) in parallel_streams.iter_mut().enumerate() { - log::info!("[{}] beginning execution of stream {}...", &peer_addr, stream_idx); - let c_ps = Arc::clone(¶llel_stream); - let c_results_tx = results_tx.clone(); - let c_cam = cpu_affinity_manager.clone(); - let handle = thread::spawn(move || { - { //set CPU affinity, if enabled - c_cam.lock().unwrap().set_affinity(); + } else { + //TCP + log::info!("[{}] preparing for TCP test with {} streams...", &peer_addr, stream_ports.len()); + + let test_definition = tcp::TcpTestDefinition::new(&payload)?; + for (stream_idx, port) in stream_ports.iter().enumerate() { + log::debug!("[{}] preparing TCP-sender for stream {}...", &peer_addr, stream_idx); + let test = tcp::sender::TcpSender::new( + test_definition.clone(), + &(stream_idx as u8), + &peer_addr.ip(), + &(port.as_i64().unwrap() as u16), + &(payload["duration"].as_f64().unwrap() as f32), + &(payload["send_interval"].as_f64().unwrap() as f32), + &(payload["send_buffer"].as_i64().unwrap() as usize), + &(payload["no_delay"].as_bool().unwrap()), + )?; + parallel_streams.push(Arc::new(Mutex::new(test))); + } + } + + //let the client know we're ready to begin + send(stream, &prepare_connect_ready())?; + } + } + "begin" => { + //the client has indicated that testing can begin + if !started { + //a simple guard to protect against reinitialisaion + for (stream_idx, parallel_stream) in parallel_streams.iter_mut().enumerate() { + log::info!("[{}] beginning execution of stream {}...", &peer_addr, stream_idx); + let c_ps = Arc::clone(parallel_stream); + let c_results_tx = results_tx.clone(); + let c_cam = cpu_affinity_manager.clone(); + let handle = thread::spawn(move || { + { + //set CPU affinity, if enabled + c_cam.lock().unwrap().set_affinity(); + } + loop { + let mut test = c_ps.lock().unwrap(); + log::debug!("[{}] beginning test-interval for stream {}", &peer_addr, test.get_idx()); + let interval_result = match test.run_interval() { + Some(interval_result) => interval_result, + None => { + match c_results_tx.send(Box::new(ServerDoneResult { + stream_idx: test.get_idx(), + })) { + Ok(_) => (), + Err(e) => log::error!("[{}] unable to report interval-done-result: {}", &peer_addr, e), + } + break; } - loop { - let mut test = c_ps.lock().unwrap(); - log::debug!("[{}] beginning test-interval for stream {}", &peer_addr, test.get_idx()); - match test.run_interval() { - Some(interval_result) => match interval_result { - Ok(ir) => match c_results_tx.send(ir) { - Ok(_) => (), - Err(e) => { - log::error!("[{}] unable to process interval-result: {}", &peer_addr, e); - break - }, - }, - Err(e) => { - log::error!("[{}] unable to process stream: {}", peer_addr, e); - match c_results_tx.send(Box::new(crate::protocol::results::ServerFailedResult{stream_idx: test.get_idx()})) { - Ok(_) => (), - Err(e) => log::error!("[{}] unable to report interval-failed-result: {}", &peer_addr, e), - } - break; - }, - }, - None => { - match c_results_tx.send(Box::new(crate::protocol::results::ServerDoneResult{stream_idx: test.get_idx()})) { - Ok(_) => (), - Err(e) => log::error!("[{}] unable to report interval-done-result: {}", &peer_addr, e), - } - break; - }, + }; + + match interval_result { + Ok(ir) => match c_results_tx.send(ir) { + Ok(_) => (), + Err(e) => { + log::error!("[{}] unable to process interval-result: {}", &peer_addr, e); + break; } + }, + Err(e) => { + log::error!("[{}] unable to process stream: {}", peer_addr, e); + match c_results_tx.send(Box::new(crate::protocol::results::ServerFailedResult { + stream_idx: test.get_idx(), + })) { + Ok(_) => (), + Err(e) => log::error!("[{}] unable to report interval-failed-result: {}", &peer_addr, e), + } + break; } - }); - parallel_streams_joinhandles.push(handle); + } } - started = true; - } else { //this can only happen in case of malicious action - log::error!("[{}] duplicate begin-signal", &peer_addr); - break; - } - }, - "end" => { //the client has indicated that testing is done; stop cleanly - log::info!("[{}] end of testing signaled", &peer_addr); - break; - }, - _ => { - log::error!("[{}] invalid data", &peer_addr); - break; - }, + }); + parallel_streams_joinhandles.push(handle); + } + started = true; + } else { + //this can only happen in case of malicious action + log::error!("[{}] duplicate begin-signal", &peer_addr); + break; } - }, - None => { + } + "end" => { + //the client has indicated that testing is done; stop cleanly + log::info!("[{}] end of testing signaled", &peer_addr); + break; + } + _ => { log::error!("[{}] invalid data", &peer_addr); break; - }, + } } } - + log::debug!("[{}] stopping any still-in-progress streams", &peer_addr); for ps in parallel_streams.iter_mut() { - let mut stream = match (*ps).lock() { + let mut test_stream = match (*ps).lock() { Ok(guard) => guard, Err(poisoned) => { - log::error!("[{}] a stream-handler was poisoned; this indicates some sort of logic error", &peer_addr); + log::error!( + "[{}] a stream-handler was poisoned; this indicates some sort of logic error", + &peer_addr + ); poisoned.into_inner() - }, + } }; - stream.stop(); + test_stream.stop(); } log::debug!("[{}] waiting for all streams to end", &peer_addr); for jh in parallel_streams_joinhandles { @@ -262,7 +269,7 @@ fn handle_client( Err(e) => log::error!("[{}] error in parallel stream: {:?}", &peer_addr, e), } } - + Ok(()) } @@ -273,7 +280,7 @@ struct ClientThreadMonitor { impl Drop for ClientThreadMonitor { fn drop(&mut self) { CLIENTS.fetch_sub(1, Ordering::Relaxed); - if thread::panicking(){ + if thread::panicking() { log::warn!("{} disconnecting due to panic", self.client_address); } else { log::info!("{} disconnected", self.client_address); @@ -281,95 +288,82 @@ impl Drop for ClientThreadMonitor { } } -pub fn serve(args:ArgMatches) -> BoxResult<()> { +pub fn serve(args: &Args) -> BoxResult<()> { //config-parsing and pre-connection setup let tcp_port_pool = Arc::new(Mutex::new(tcp::receiver::TcpPortPool::new( - args.value_of("tcp_port_pool").unwrap().to_string(), - args.value_of("tcp6_port_pool").unwrap().to_string(), + &args.tcp_port_pool, + &args.tcp6_port_pool, ))); let udp_port_pool = Arc::new(Mutex::new(udp::receiver::UdpPortPool::new( - args.value_of("udp_port_pool").unwrap().to_string(), - args.value_of("udp6_port_pool").unwrap().to_string(), + &args.udp_port_pool, + &args.udp6_port_pool, ))); - - let cpu_affinity_manager = Arc::new(Mutex::new(crate::utils::cpu_affinity::CpuAffinityManager::new(args.value_of("affinity").unwrap())?)); - - let client_limit:u16 = args.value_of("client_limit").unwrap().parse()?; + + let cpu_affinity_manager = Arc::new(Mutex::new(crate::utils::cpu_affinity::CpuAffinityManager::new(&args.affinity)?)); + + let client_limit: u16 = args.client_limit as u16; if client_limit > 0 { log::debug!("limiting service to {} concurrent clients", client_limit); } - + //start listening for connections - let port:u16 = args.value_of("port").unwrap().parse()?; - let mut listener:TcpListener; - if args.is_present("version6") { - listener = TcpListener::bind(&SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), port)).expect(format!("failed to bind TCP socket, port {}", port).as_str()); - } else { - listener = TcpListener::bind(&SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port)).expect(format!("failed to bind TCP socket, port {}", port).as_str()); - } + let port: u16 = args.port; + let listener = TcpListener::bind(SocketAddr::new(args.bind, port))?; + listener.set_nonblocking(true)?; log::info!("server listening on {}", listener.local_addr()?); - - let mio_token = Token(0); - let poll = Poll::new()?; - poll.register( - &mut listener, - mio_token, - Ready::readable(), - PollOpt::edge(), - )?; - let mut events = Events::with_capacity(32); - + while is_alive() { - poll.poll(&mut events, Some(POLL_TIMEOUT))?; - for event in events.iter() { - match event.token() { - _ => loop { - match listener.accept() { - Ok((mut stream, address)) => { - log::info!("connection from {}", address); - - stream.set_nodelay(true).expect("cannot disable Nagle's algorithm"); - stream.set_keepalive(Some(KEEPALIVE_DURATION)).expect("unable to set TCP keepalive"); - - let client_count = CLIENTS.fetch_add(1, Ordering::Relaxed) + 1; - if client_limit > 0 && client_count > client_limit { - log::warn!("client-limit ({}) reached; disconnecting {}...", client_limit, address.to_string()); - stream.shutdown(Shutdown::Both).unwrap_or_default(); - CLIENTS.fetch_sub(1, Ordering::Relaxed); - } else { - let c_cam = cpu_affinity_manager.clone(); - let c_tcp_port_pool = tcp_port_pool.clone(); - let c_udp_port_pool = udp_port_pool.clone(); - let thread_builder = thread::Builder::new() - .name(address.to_string().into()); - thread_builder.spawn(move || { - //ensure the client is accounted-for even if the handler panics - let _client_thread_monitor = ClientThreadMonitor{ - client_address: address.to_string(), - }; - - match handle_client(&mut stream, c_cam, c_tcp_port_pool, c_udp_port_pool) { - Ok(_) => (), - Err(e) => log::error!("error in client-handler: {}", e), - } - - //in the event of panic, this will happen when the stream is dropped - stream.shutdown(Shutdown::Both).unwrap_or_default(); - })?; - } - }, - Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => { //nothing to do - break; - }, - Err(e) => { - return Err(Box::new(e)); - }, - } - }, + let (mut stream, address) = match listener.accept() { + Ok((stream, address)) => (stream, address), + Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => { + // no pending clients + thread::sleep(POLL_TIMEOUT); + continue; + } + Err(e) => { + return Err(Box::new(e)); } + }; + + log::info!("connection from {}", address); + + stream.set_nodelay(true).expect("cannot disable Nagle's algorithm"); + + #[cfg(unix)] + { + use crate::protocol::communication::KEEPALIVE_DURATION; + let keepalive_parameters = socket2::TcpKeepalive::new().with_time(KEEPALIVE_DURATION); + let raw_socket = socket2::SockRef::from(&stream); + raw_socket.set_tcp_keepalive(&keepalive_parameters)?; + } + + let client_count = CLIENTS.fetch_add(1, Ordering::Relaxed) + 1; + if client_limit > 0 && client_count > client_limit { + log::warn!("client-limit ({}) reached; disconnecting {}...", client_limit, address.to_string()); + stream.shutdown(Shutdown::Both).unwrap_or_default(); + CLIENTS.fetch_sub(1, Ordering::Relaxed); + } else { + let c_cam = cpu_affinity_manager.clone(); + let c_tcp_port_pool = tcp_port_pool.clone(); + let c_udp_port_pool = udp_port_pool.clone(); + let thread_builder = thread::Builder::new().name(address.to_string()); + thread_builder.spawn(move || { + // ensure the client is accounted-for even if the handler panics + let _client_thread_monitor = ClientThreadMonitor { + client_address: address.to_string(), + }; + + match handle_client(&mut stream, c_cam, c_tcp_port_pool, c_udp_port_pool) { + Ok(_) => (), + Err(e) => log::error!("error in client-handler: {}", e), + } + + //in the event of panic, this will happen when the stream is dropped + stream.shutdown(Shutdown::Both).unwrap_or_default(); + })?; } } - + //wait until all clients have been disconnected loop { let clients_count = CLIENTS.load(Ordering::Relaxed); diff --git a/src/stream/mod.rs b/src/stream/mod.rs index 62e768a..1cd0455 100644 --- a/src/stream/mod.rs +++ b/src/stream/mod.rs @@ -1,37 +1,36 @@ /* * Copyright (C) 2021 Evtech Solutions, Ltd., dba 3D-P * Copyright (C) 2021 Neil Tallim - * + * * This file is part of rperf. - * + * * rperf is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * rperf is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with rperf. If not, see . */ - + pub mod tcp; pub mod udp; -use std::error::Error; -type BoxResult = Result>; +use crate::{protocol::results::IntervalResultBox, BoxResult}; -pub const INTERVAL:std::time::Duration = std::time::Duration::from_secs(1); +pub const INTERVAL: std::time::Duration = std::time::Duration::from_secs(1); /// tests in this system are self-contained iterator-likes, where run_interval() may be invoked multiple times /// until it returns None, indicating that the test has run its course; each invocation blocks for up to approximately /// INTERVAL while gathering data. pub trait TestStream { /// gather data; returns None when the test is over - fn run_interval(&mut self) -> Option>>; + fn run_interval(&mut self) -> Option>; /// return the port associated with the test-stream; this may vary over the test's lifetime fn get_port(&self) -> BoxResult; /// returns the index of the test, used to match client and server data @@ -40,7 +39,7 @@ pub trait TestStream { fn stop(&mut self); } -fn parse_port_spec(port_spec:String) -> Vec { +fn parse_port_spec(port_spec: &str) -> Vec { let mut ports = Vec::::new(); if !port_spec.is_empty() { for range in port_spec.split(',') { @@ -48,7 +47,7 @@ fn parse_port_spec(port_spec:String) -> Vec { let mut range_spec = range.split('-'); let range_first = range_spec.next().unwrap().parse::().unwrap(); let range_last = range_spec.last().unwrap().parse::().unwrap(); - + for port in range_first..=range_last { ports.push(port); } @@ -56,9 +55,9 @@ fn parse_port_spec(port_spec:String) -> Vec { ports.push(range.parse::().unwrap()); } } - + ports.sort(); } - - return ports; + + ports } diff --git a/src/stream/tcp.rs b/src/stream/tcp.rs index 721ed4a..fdae5b0 100644 --- a/src/stream/tcp.rs +++ b/src/stream/tcp.rs @@ -1,35 +1,31 @@ /* * Copyright (C) 2021 Evtech Solutions, Ltd., dba 3D-P * Copyright (C) 2021 Neil Tallim - * + * * This file is part of rperf. - * + * * rperf is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * rperf is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with rperf. If not, see . */ -extern crate nix; +use crate::protocol::results::{get_unix_timestamp, TcpReceiveResult, TcpSendResult}; +use crate::stream::{parse_port_spec, TestStream, INTERVAL}; +use crate::BoxResult; -use nix::sys::socket::{setsockopt, sockopt::RcvBuf, sockopt::SndBuf}; +pub const TEST_HEADER_SIZE: usize = 16; -use crate::protocol::results::{IntervalResult, TcpReceiveResult, TcpSendResult, get_unix_timestamp}; - -use super::{INTERVAL, TestStream, parse_port_spec}; - -use std::error::Error; -type BoxResult = Result>; - -pub const TEST_HEADER_SIZE:usize = 16; +#[cfg(unix)] +const KEEPALIVE_DURATION: std::time::Duration = std::time::Duration::from_secs(5); #[derive(Clone)] pub struct TcpTestDefinition { @@ -41,42 +37,57 @@ pub struct TcpTestDefinition { pub length: usize, } impl TcpTestDefinition { - pub fn new(details:&serde_json::Value) -> super::BoxResult { + pub fn new(details: &serde_json::Value) -> BoxResult { let mut test_id_bytes = [0_u8; 16]; - for (i, v) in details.get("test_id").unwrap_or(&serde_json::json!([])).as_array().unwrap().iter().enumerate() { - if i >= 16 { //avoid out-of-bounds if given malicious data + for (i, v) in details + .get("test_id") + .unwrap_or(&serde_json::json!([])) + .as_array() + .unwrap() + .iter() + .enumerate() + { + if i >= 16 { + //avoid out-of-bounds if given malicious data break; } test_id_bytes[i] = v.as_i64().unwrap_or(0) as u8; } - - let length = details.get("length").unwrap_or(&serde_json::json!(TEST_HEADER_SIZE)).as_i64().unwrap() as usize; + + let length = details + .get("length") + .unwrap_or(&serde_json::json!(TEST_HEADER_SIZE)) + .as_i64() + .unwrap() as usize; if length < TEST_HEADER_SIZE { - return Err(Box::new(simple_error::simple_error!(std::format!("{} is too short of a length to satisfy testing requirements", length)))); + return Err(Box::new(simple_error::simple_error!(std::format!( + "{} is too short of a length to satisfy testing requirements", + length + )))); } - - Ok(TcpTestDefinition{ + + Ok(TcpTestDefinition { test_id: test_id_bytes, bandwidth: details.get("bandwidth").unwrap_or(&serde_json::json!(0.0)).as_f64().unwrap() as u64, - length: length, + length, }) } } - pub mod receiver { + use mio::net::{TcpListener, TcpStream}; use std::io::Read; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; - use std::os::unix::io::AsRawFd; - use std::sync::{Mutex}; + use std::sync::atomic::{AtomicBool, Ordering::Relaxed}; + use std::sync::Mutex; use std::time::{Duration, Instant}; - - use mio::net::{TcpListener, TcpStream}; - use mio::{Events, Ready, Poll, PollOpt, Token}; - - const POLL_TIMEOUT:Duration = Duration::from_millis(250); - const RECEIVE_TIMEOUT:Duration = Duration::from_secs(3); - + + use crate::{protocol::results::IntervalResultBox, BoxResult}; + + const POLL_TIMEOUT: Duration = Duration::from_millis(250); + const CONNECTION_TIMEOUT: Duration = Duration::from_secs(1); + const RECEIVE_TIMEOUT: Duration = Duration::from_secs(3); + pub struct TcpPortPool { pub ports_ip4: Vec, pub ports_ip6: Vec, @@ -86,287 +97,326 @@ pub mod receiver { lock_ip6: Mutex, } impl TcpPortPool { - pub fn new(port_spec:String, port_spec6:String) -> TcpPortPool { + pub fn new(port_spec: &str, port_spec6: &str) -> TcpPortPool { let ports = super::parse_port_spec(port_spec); if !ports.is_empty() { log::debug!("configured IPv4 TCP port pool: {:?}", ports); } else { log::debug!("using OS assignment for IPv4 TCP ports"); } - + let ports6 = super::parse_port_spec(port_spec6); if !ports.is_empty() { log::debug!("configured IPv6 TCP port pool: {:?}", ports6); } else { log::debug!("using OS assignment for IPv6 TCP ports"); } - + TcpPortPool { ports_ip4: ports, pos_ip4: 0, lock_ip4: Mutex::new(0), - + ports_ip6: ports6, pos_ip6: 0, lock_ip6: Mutex::new(0), } } - - pub fn bind(&mut self, peer_ip:&IpAddr) -> super::BoxResult { + + pub fn bind(&mut self, peer_ip: &IpAddr) -> BoxResult { match peer_ip { IpAddr::V6(_) => { if self.ports_ip6.is_empty() { - return Ok(TcpListener::bind(&SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0)).expect(format!("failed to bind OS-assigned IPv6 TCP socket").as_str())); + return Ok(TcpListener::bind(SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0)) + .expect("failed to bind OS-assigned IPv6 TCP socket")); } else { let _guard = self.lock_ip6.lock().unwrap(); - - for port_idx in (self.pos_ip6 + 1)..self.ports_ip6.len() { //iterate to the end of the pool; this will skip the first element in the pool initially, but that's fine - let listener_result = TcpListener::bind(&SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), self.ports_ip6[port_idx])); - if listener_result.is_ok() { + + for port_idx in (self.pos_ip6 + 1)..self.ports_ip6.len() { + //iterate to the end of the pool; this will skip the first element in the pool initially, but that's fine + let listener_result = + TcpListener::bind(SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), self.ports_ip6[port_idx])); + if let Ok(listener_result) = listener_result { self.pos_ip6 = port_idx; - return Ok(listener_result.unwrap()); + return Ok(listener_result); } else { log::warn!("unable to bind IPv6 TCP port {}", self.ports_ip6[port_idx]); } } - for port_idx in 0..=self.pos_ip6 { //circle back to where the search started - let listener_result = TcpListener::bind(&SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), self.ports_ip6[port_idx])); - if listener_result.is_ok() { + for port_idx in 0..=self.pos_ip6 { + //circle back to where the search started + let listener_result = + TcpListener::bind(SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), self.ports_ip6[port_idx])); + if let Ok(listener_result) = listener_result { self.pos_ip6 = port_idx; - return Ok(listener_result.unwrap()); + return Ok(listener_result); } else { log::warn!("unable to bind IPv6 TCP port {}", self.ports_ip6[port_idx]); } } } - return Err(Box::new(simple_error::simple_error!("unable to allocate IPv6 TCP port"))); - }, + Err(Box::new(simple_error::simple_error!("unable to allocate IPv6 TCP port"))) + } IpAddr::V4(_) => { if self.ports_ip4.is_empty() { - return Ok(TcpListener::bind(&SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0)).expect(format!("failed to bind OS-assigned IPv4 TCP socket").as_str())); + return Ok(TcpListener::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0)) + .expect("failed to bind OS-assigned IPv4 TCP socket")); } else { let _guard = self.lock_ip4.lock().unwrap(); - - for port_idx in (self.pos_ip4 + 1)..self.ports_ip4.len() { //iterate to the end of the pool; this will skip the first element in the pool initially, but that's fine - let listener_result = TcpListener::bind(&SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), self.ports_ip4[port_idx])); - if listener_result.is_ok() { + + for port_idx in (self.pos_ip4 + 1)..self.ports_ip4.len() { + //iterate to the end of the pool; this will skip the first element in the pool initially, but that's fine + let listener_result = + TcpListener::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), self.ports_ip4[port_idx])); + if let Ok(listener_result) = listener_result { self.pos_ip4 = port_idx; - return Ok(listener_result.unwrap()); + return Ok(listener_result); } else { log::warn!("unable to bind IPv4 TCP port {}", self.ports_ip4[port_idx]); } } - for port_idx in 0..=self.pos_ip4 { //circle back to where the search started - let listener_result = TcpListener::bind(&SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), self.ports_ip4[port_idx])); - if listener_result.is_ok() { + for port_idx in 0..=self.pos_ip4 { + //circle back to where the search started + let listener_result = + TcpListener::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), self.ports_ip4[port_idx])); + if let Ok(listener_result) = listener_result { self.pos_ip4 = port_idx; - return Ok(listener_result.unwrap()); + return Ok(listener_result); } else { log::warn!("unable to bind IPv4 TCP port {}", self.ports_ip4[port_idx]); } } } - return Err(Box::new(simple_error::simple_error!("unable to allocate IPv4 TCP port"))); - }, - }; + Err(Box::new(simple_error::simple_error!("unable to allocate IPv4 TCP port"))) + } + } } } - + pub struct TcpReceiver { - active: bool, + active: AtomicBool, test_definition: super::TcpTestDefinition, stream_idx: u8, - + listener: Option, stream: Option, - mio_poll_token: Token, - mio_poll: Poll, - - receive_buffer: usize, + + mio_events: mio::Events, + mio_poll: mio::Poll, + mio_token: mio::Token, } + impl TcpReceiver { - pub fn new(test_definition:super::TcpTestDefinition, stream_idx:&u8, port_pool:&mut TcpPortPool, peer_ip:&IpAddr, receive_buffer:&usize) -> super::BoxResult { + pub fn new( + test_definition: super::TcpTestDefinition, + stream_idx: &u8, + port_pool: &mut TcpPortPool, + peer_ip: &IpAddr, + ) -> BoxResult { log::debug!("binding TCP listener for stream {}...", stream_idx); - let listener:TcpListener = port_pool.bind(peer_ip).expect(format!("failed to bind TCP socket").as_str()); + let mut listener: TcpListener = port_pool.bind(peer_ip).expect("failed to bind TCP socket"); log::debug!("bound TCP listener for stream {}: {}", stream_idx, listener.local_addr()?); - - let mio_poll_token = Token(0); - let mio_poll = Poll::new()?; - - Ok(TcpReceiver{ - active: true, - test_definition: test_definition, + + let _port = listener.local_addr()?.port(); + + let mio_events = mio::Events::with_capacity(1); + let mio_poll = mio::Poll::new()?; + let mio_token = crate::get_global_token(); + mio_poll.registry().register(&mut listener, mio_token, mio::Interest::READABLE)?; + + Ok(TcpReceiver { + active: AtomicBool::new(true), + test_definition, stream_idx: stream_idx.to_owned(), - + listener: Some(listener), stream: None, - mio_poll_token: mio_poll_token, - mio_poll: mio_poll, - - receive_buffer: receive_buffer.to_owned(), + mio_events, + mio_token, + mio_poll, }) } - - fn process_connection(&mut self) -> super::BoxResult { + + fn process_connection(&mut self) -> BoxResult<(TcpStream, u64, f32)> { log::debug!("preparing to receive TCP stream {} connection...", self.stream_idx); - + let listener = self.listener.as_mut().unwrap(); - let mio_token = Token(0); - let poll = Poll::new()?; - poll.register( - listener, - mio_token, - Ready::readable(), - PollOpt::edge(), - )?; - let mut events = Events::with_capacity(1); - + let mio_token = self.mio_token; + let start = Instant::now(); - - while self.active { + + loop { + if !self.active.load(Relaxed) { + return Err(Box::new(simple_error::simple_error!("local shutdown requested"))); + } if start.elapsed() >= RECEIVE_TIMEOUT { - return Err(Box::new(simple_error::simple_error!("TCP listening for stream {} timed out", self.stream_idx))); + let err = simple_error::simple_error!("TCP listening for stream {} timed out", self.stream_idx); + return Err(Box::new(err)); } - - poll.poll(&mut events, Some(POLL_TIMEOUT))?; - for event in events.iter() { - match event.token() { - _ => loop { - match listener.accept() { - Ok((stream, address)) => { - log::debug!("received TCP stream {} connection from {}", self.stream_idx, address); - - let mut verification_stream = stream.try_clone()?; - let mio_token2 = Token(0); - let poll2 = Poll::new()?; - poll2.register( - &verification_stream, - mio_token2, - Ready::readable(), - PollOpt::edge(), - )?; - - let mut buffer = [0_u8; 16]; - let mut events2 = Events::with_capacity(1); - poll2.poll(&mut events2, Some(RECEIVE_TIMEOUT))?; - for event2 in events2.iter() { - match event2.token() { - _ => match verification_stream.read(&mut buffer) { - Ok(_) => { - if buffer == self.test_definition.test_id { - log::debug!("validated TCP stream {} connection from {}", self.stream_idx, address); - if !cfg!(windows) { //NOTE: features unsupported on Windows - if self.receive_buffer != 0 { - log::debug!("setting receive-buffer to {}...", self.receive_buffer); - super::setsockopt(stream.as_raw_fd(), super::RcvBuf, &self.receive_buffer)?; - } - } - - self.mio_poll.register( - &stream, - self.mio_poll_token, - Ready::readable(), - PollOpt::edge(), - )?; - return Ok(stream); - } - }, - Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => { //client didn't provide anything - break; - }, - Err(e) => { - return Err(Box::new(e)); - }, - } - } - } - log::warn!("could not validate TCP stream {} connection from {}", self.stream_idx, address); - }, - Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => { //nothing to do + + // assigned upon establishing a connection + let mut stream: Option = None; + + self.mio_poll.poll(&mut self.mio_events, Some(POLL_TIMEOUT))?; + for event in self.mio_events.iter() { + if event.token() != mio_token { + log::warn!("got event for unbound token: {:?}", event); + continue; + } + + let (mut new_stream, address) = match listener.accept() { + Ok((new_stream, address)) => (new_stream, address), + Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => { + // no pending connections available + break; + } + Err(e) => { + return Err(Box::new(e)); + } + }; + + log::debug!("received TCP stream {} connection from {}", self.stream_idx, address); + + // hand over flow to the new connection + self.mio_poll.registry().deregister(listener)?; + self.mio_poll + .registry() + .register(&mut new_stream, mio_token, mio::Interest::READABLE)?; + stream = Some(new_stream); + break; + } + + if stream.is_none() { + // no pending connections available + continue; + } + let mut unwrapped_stream = stream.unwrap(); + + // process the stream + let mut buf = vec![0_u8; self.test_definition.length]; + let mut validated: bool = false; + let mut bytes_received: u64 = 0; + + let start_validation = Instant::now(); + + self.mio_poll.poll(&mut self.mio_events, Some(CONNECTION_TIMEOUT))?; + for event in self.mio_events.iter() { + if event.token() == mio_token { + loop { + let packet_size = match unwrapped_stream.read(&mut buf) { + Ok(packet_size) => packet_size, + Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock || e.kind() == std::io::ErrorKind::TimedOut => { + // end of input reached break; - }, + } Err(e) => { + let _ = unwrapped_stream.shutdown(std::net::Shutdown::Both); + self.mio_poll.registry().deregister(&mut unwrapped_stream)?; return Err(Box::new(e)); - }, + } + }; + + if !validated { + if buf[..16] == self.test_definition.test_id { + log::debug!( + "validated TCP stream {} connection from {}", + self.stream_idx, + unwrapped_stream.peer_addr()? + ); + validated = true; + } else { + log::warn!( + "unexpected ID in stream {} connection from {}", + self.stream_idx, + unwrapped_stream.peer_addr()? + ); + break; + } + } + + if validated { + bytes_received += packet_size as u64; } - }, + } } } + if validated { + return Ok((unwrapped_stream, bytes_received, start_validation.elapsed().as_secs_f32())); + } + + let _ = unwrapped_stream.shutdown(std::net::Shutdown::Both); + self.mio_poll.registry().deregister(&mut unwrapped_stream)?; + log::warn!( + "could not validate TCP stream {} connection from {}", + self.stream_idx, + unwrapped_stream.peer_addr()? + ); } - Err(Box::new(simple_error::simple_error!("did not receive a connection"))) } } + impl super::TestStream for TcpReceiver { - fn run_interval(&mut self) -> Option>> { - let mut bytes_received:u64 = 0; - - if self.stream.is_none() { //if still in the setup phase, receive the sender + fn run_interval(&mut self) -> Option> { + let mut bytes_received: u64 = 0; + + let mut additional_time_elapsed: f32 = 0.0; + if self.stream.is_none() { + //if still in the setup phase, receive the sender match self.process_connection() { - Ok(stream) => { + Ok((stream, bytes_received_in_validation, time_spent_in_validation)) => { self.stream = Some(stream); - //NOTE: the connection process consumes the test-header; account for those bytes - bytes_received += super::TEST_HEADER_SIZE as u64; - }, + // NOTE: the connection process consumes packets; account for those bytes + bytes_received += bytes_received_in_validation; + additional_time_elapsed += time_spent_in_validation; + } Err(e) => { return Some(Err(e)); - }, + } } self.listener = None; //drop it, closing the socket } let stream = self.stream.as_mut().unwrap(); - - let mut events = Events::with_capacity(1); //only watching one socket + + let mio_token = self.mio_token; let mut buf = vec![0_u8; self.test_definition.length]; - + let peer_addr = match stream.peer_addr() { Ok(pa) => pa, Err(e) => return Some(Err(Box::new(e))), }; let start = Instant::now(); - - while self.active { - if start.elapsed() >= RECEIVE_TIMEOUT { - return Some(Err(Box::new(simple_error::simple_error!("TCP reception for stream {} from {} timed out", self.stream_idx, peer_addr)))); - } - + + while self.active.load(Relaxed) && start.elapsed() < super::INTERVAL { log::trace!("awaiting TCP stream {} from {}...", self.stream_idx, peer_addr); - let poll_result = self.mio_poll.poll(&mut events, Some(POLL_TIMEOUT)); - if poll_result.is_err() { - return Some(Err(Box::new(poll_result.unwrap_err()))); - } - for event in events.iter() { - if event.token() == self.mio_poll_token { + self.mio_poll.poll(&mut self.mio_events, Some(POLL_TIMEOUT)).ok()?; + for event in self.mio_events.iter() { + if event.token() == mio_token { loop { - match stream.read(&mut buf) { - Ok(packet_size) => { - log::trace!("received {} bytes in TCP stream {} from {}", packet_size, self.stream_idx, peer_addr); - if packet_size == 0 { //test's over - self.active = false; //HACK: can't call self.stop() because it's a double-borrow due to the unwrapped stream - break; - } - - bytes_received += packet_size as u64; - - let elapsed_time = start.elapsed(); - if elapsed_time >= super::INTERVAL { - return Some(Ok(Box::new(super::TcpReceiveResult{ - timestamp: super::get_unix_timestamp(), - - stream_idx: self.stream_idx, - - duration: elapsed_time.as_secs_f32(), - - bytes_received: bytes_received, - }))) - } - }, - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { //receive timeout + let packet_size = match stream.read(&mut buf) { + Ok(packet_size) => packet_size, + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock || e.kind() == std::io::ErrorKind::TimedOut => { + // receive timeout break; - }, + } Err(e) => { return Some(Err(Box::new(e))); - }, + } + }; + + log::trace!( + "received {} bytes in TCP stream {} from {}", + packet_size, + self.stream_idx, + peer_addr + ); + if packet_size == 0 { + // test's over + // HACK: can't call self.stop() because it's a double-borrow due to the unwrapped stream + self.active.store(false, Relaxed); + break; } + + bytes_received += packet_size as u64; } } else { log::warn!("got event for unbound token: {:?}", event); @@ -374,203 +424,248 @@ pub mod receiver { } } if bytes_received > 0 { - Some(Ok(Box::new(super::TcpReceiveResult{ + log::debug!( + "{} bytes received via TCP stream {} from {} in this interval; reporting...", + bytes_received, + self.stream_idx, + peer_addr + ); + Some(Ok(Box::new(super::TcpReceiveResult { timestamp: super::get_unix_timestamp(), - + stream_idx: self.stream_idx, - - duration: start.elapsed().as_secs_f32(), - - bytes_received: bytes_received, + + duration: start.elapsed().as_secs_f32() + additional_time_elapsed, + + bytes_received, }))) } else { + log::debug!( + "no bytes received via TCP stream {} from {} in this interval", + self.stream_idx, + peer_addr + ); None } } - - fn get_port(&self) -> super::BoxResult { + + fn get_port(&self) -> BoxResult { match &self.listener { Some(listener) => Ok(listener.local_addr()?.port()), None => match &self.stream { Some(stream) => Ok(stream.local_addr()?.port()), None => Err(Box::new(simple_error::simple_error!("no port currently bound"))), - } + }, } } - + fn get_idx(&self) -> u8 { self.stream_idx.to_owned() } - + fn stop(&mut self) { - self.active = false; + self.active.store(false, Relaxed); } } } - - pub mod sender { use std::io::Write; - use std::net::{IpAddr, SocketAddr}; - use std::os::unix::io::AsRawFd; + use std::net::{IpAddr, SocketAddr, TcpStream}; + use std::thread::sleep; use std::time::{Duration, Instant}; - - use mio::net::TcpStream; - - use std::thread::{sleep}; - - const CONNECT_TIMEOUT:Duration = Duration::from_secs(2); - const WRITE_TIMEOUT:Duration = Duration::from_millis(50); - const BUFFER_FULL_TIMEOUT:Duration = Duration::from_millis(1); - + + use crate::{protocol::results::IntervalResultBox, BoxResult}; + + const CONNECT_TIMEOUT: Duration = Duration::from_secs(2); + const WRITE_TIMEOUT: Duration = Duration::from_millis(50); + const BUFFER_FULL_TIMEOUT: Duration = Duration::from_millis(1); + + #[allow(dead_code)] pub struct TcpSender { active: bool, test_definition: super::TcpTestDefinition, stream_idx: u8, - + socket_addr: SocketAddr, stream: Option, - + //the interval, in seconds, at which to send data send_interval: f32, - + remaining_duration: f32, staged_buffer: Vec, - - send_buffer:usize, - no_delay:bool, + + send_buffer: usize, + no_delay: bool, } impl TcpSender { - pub fn new(test_definition:super::TcpTestDefinition, stream_idx:&u8, receiver_ip:&IpAddr, receiver_port:&u16, send_duration:&f32, send_interval:&f32, send_buffer:&usize, no_delay:&bool) -> super::BoxResult { + #[allow(clippy::too_many_arguments)] + pub fn new( + test_definition: super::TcpTestDefinition, + stream_idx: &u8, + receiver_ip: &IpAddr, + receiver_port: &u16, + send_duration: &f32, + send_interval: &f32, + send_buffer: &usize, + no_delay: &bool, + ) -> BoxResult { let mut staged_buffer = vec![0_u8; test_definition.length]; - for i in super::TEST_HEADER_SIZE..(staged_buffer.len()) { //fill the packet with a fixed sequence - staged_buffer[i] = (i % 256) as u8; + for (i, staged_buffer_i) in staged_buffer.iter_mut().enumerate().skip(super::TEST_HEADER_SIZE) { + //fill the packet with a fixed sequence + *staged_buffer_i = (i % 256) as u8; } //embed the test ID staged_buffer[0..16].copy_from_slice(&test_definition.test_id); - - Ok(TcpSender{ + + Ok(TcpSender { active: true, - test_definition: test_definition, + test_definition, stream_idx: stream_idx.to_owned(), - + socket_addr: SocketAddr::new(*receiver_ip, *receiver_port), stream: None, - + send_interval: send_interval.to_owned(), - + remaining_duration: send_duration.to_owned(), - staged_buffer: staged_buffer, - + staged_buffer, + send_buffer: send_buffer.to_owned(), no_delay: no_delay.to_owned(), }) } - - fn process_connection(&mut self) -> super::BoxResult { - log::debug!("preparing to connect TCP stream {}...", self.stream_idx); - - let raw_stream = match std::net::TcpStream::connect_timeout(&self.socket_addr, CONNECT_TIMEOUT) { - Ok(s) => s, - Err(e) => return Err(Box::new(simple_error::simple_error!("unable to connect stream {}: {}", self.stream_idx, e))), - }; - raw_stream.set_write_timeout(Some(WRITE_TIMEOUT))?; - let stream = match TcpStream::from_stream(raw_stream) { + + fn process_connection(&mut self) -> BoxResult { + log::debug!("preparing to connect TCP stream {} to {} ...", self.stream_idx, self.socket_addr); + + let stream = match TcpStream::connect_timeout(&self.socket_addr, CONNECT_TIMEOUT) { Ok(s) => s, - Err(e) => return Err(Box::new(simple_error::simple_error!("unable to prepare TCP stream {}: {}", self.stream_idx, e))), + Err(e) => { + let err = simple_error::simple_error!("unable to connect stream {}: {}", self.stream_idx, e); + return Err(Box::new(err)); + } }; + + stream.set_nonblocking(true)?; + stream.set_write_timeout(Some(WRITE_TIMEOUT))?; + + // NOTE: features unsupported on Windows + #[cfg(unix)] + { + use crate::stream::tcp::KEEPALIVE_DURATION; + let keepalive_parameters = socket2::TcpKeepalive::new().with_time(KEEPALIVE_DURATION); + let raw_socket = socket2::SockRef::from(&stream); + raw_socket.set_tcp_keepalive(&keepalive_parameters)?; + } + log::debug!("connected TCP stream {} to {}", self.stream_idx, stream.peer_addr()?); - + if self.no_delay { log::debug!("setting no-delay..."); stream.set_nodelay(true)?; } - if !cfg!(windows) { //NOTE: features unsupported on Windows - if self.send_buffer != 0 { - log::debug!("setting send-buffer to {}...", self.send_buffer); - super::setsockopt(stream.as_raw_fd(), super::SndBuf, &self.send_buffer)?; - } + #[cfg(unix)] + if self.send_buffer != 0 { + log::debug!("setting send-buffer to {}...", self.send_buffer); + let raw_socket = socket2::SockRef::from(&stream); + raw_socket.set_send_buffer_size(self.send_buffer)?; } Ok(stream) } } impl super::TestStream for TcpSender { - fn run_interval(&mut self) -> Option>> { - if self.stream.is_none() { //if still in the setup phase, connect to the receiver + fn run_interval(&mut self) -> Option> { + if self.stream.is_none() { + //if still in the setup phase, connect to the receiver match self.process_connection() { Ok(stream) => { self.stream = Some(stream); - }, + } Err(e) => { return Some(Err(e)); - }, + } } } let stream = self.stream.as_mut().unwrap(); - - + let interval_duration = Duration::from_secs_f32(self.send_interval); let mut interval_iteration = 0; let bytes_to_send = ((self.test_definition.bandwidth as f32) * super::INTERVAL.as_secs_f32()) as i64; let mut bytes_to_send_remaining = bytes_to_send; let bytes_to_send_per_interval_slice = ((bytes_to_send as f32) * self.send_interval) as i64; let mut bytes_to_send_per_interval_slice_remaining = bytes_to_send_per_interval_slice; - - let mut sends_blocked:u64 = 0; - let mut bytes_sent:u64 = 0; - + + let mut sends_blocked: u64 = 0; + let mut bytes_sent: u64 = 0; + let peer_addr = match stream.peer_addr() { Ok(pa) => pa, Err(e) => return Some(Err(Box::new(e))), }; let cycle_start = Instant::now(); - + while self.active && self.remaining_duration > 0.0 && bytes_to_send_remaining > 0 { - log::trace!("writing {} bytes in TCP stream {} to {}...", self.staged_buffer.len(), self.stream_idx, peer_addr); + log::trace!( + "writing {} bytes in TCP stream {} to {}...", + self.staged_buffer.len(), + self.stream_idx, + peer_addr + ); let packet_start = Instant::now(); - - match stream.write(&self.staged_buffer) { //it doesn't matter if the whole thing couldn't be written, since it's just garbage data - Ok(packet_size) => { - log::trace!("wrote {} bytes in TCP stream {} to {}", packet_size, self.stream_idx, peer_addr); - - let bytes_written = packet_size as i64; - bytes_sent += bytes_written as u64; - bytes_to_send_remaining -= bytes_written; - bytes_to_send_per_interval_slice_remaining -= bytes_written; - - let elapsed_time = cycle_start.elapsed(); - if elapsed_time >= super::INTERVAL { - self.remaining_duration -= packet_start.elapsed().as_secs_f32(); - - return Some(Ok(Box::new(super::TcpSendResult{ - timestamp: super::get_unix_timestamp(), - - stream_idx: self.stream_idx, - - duration: elapsed_time.as_secs_f32(), - - bytes_sent: bytes_sent, - sends_blocked: sends_blocked, - }))) - } - }, - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { //send-buffer is full - //nothing to do, but avoid burning CPU cycles + + let packet_size = match stream.write(&self.staged_buffer) { + // it doesn't matter if the whole thing couldn't be written, since it's just garbage data + Ok(packet_size) => packet_size, + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + // send-buffer is full + // nothing to do, but avoid burning CPU cycles sleep(BUFFER_FULL_TIMEOUT); sends_blocked += 1; - }, + continue; + } Err(e) => { return Some(Err(Box::new(e))); - }, + } + }; + log::trace!("wrote {} bytes in TCP stream {} to {}", packet_size, self.stream_idx, peer_addr); + + let bytes_written = packet_size as i64; + bytes_sent += bytes_written as u64; + bytes_to_send_remaining -= bytes_written; + bytes_to_send_per_interval_slice_remaining -= bytes_written; + + let elapsed_time = cycle_start.elapsed(); + if elapsed_time >= super::INTERVAL { + self.remaining_duration -= packet_start.elapsed().as_secs_f32(); + + log::debug!( + "{} bytes sent via TCP stream {} to {} in this interval; reporting...", + bytes_sent, + self.stream_idx, + peer_addr + ); + return Some(Ok(Box::new(super::TcpSendResult { + timestamp: super::get_unix_timestamp(), + + stream_idx: self.stream_idx, + + duration: elapsed_time.as_secs_f32(), + + bytes_sent, + sends_blocked, + }))); } - - if bytes_to_send_remaining <= 0 { //interval's target is exhausted, so sleep until the end + + if bytes_to_send_remaining <= 0 { + //interval's target is exhausted, so sleep until the end let elapsed_time = cycle_start.elapsed(); if super::INTERVAL > elapsed_time { sleep(super::INTERVAL - elapsed_time); } - } else if bytes_to_send_per_interval_slice_remaining <= 0 { // interval subsection exhausted + } else if bytes_to_send_per_interval_slice_remaining <= 0 { + // interval subsection exhausted interval_iteration += 1; bytes_to_send_per_interval_slice_remaining = bytes_to_send_per_interval_slice; let elapsed_time = cycle_start.elapsed(); @@ -582,34 +677,45 @@ pub mod sender { self.remaining_duration -= packet_start.elapsed().as_secs_f32(); } if bytes_sent > 0 { - Some(Ok(Box::new(super::TcpSendResult{ + log::debug!( + "{} bytes sent via TCP stream {} to {} in this interval; reporting...", + bytes_sent, + self.stream_idx, + peer_addr + ); + Some(Ok(Box::new(super::TcpSendResult { timestamp: super::get_unix_timestamp(), - + stream_idx: self.stream_idx, - + duration: cycle_start.elapsed().as_secs_f32(), - - bytes_sent: bytes_sent, - sends_blocked: sends_blocked, + + bytes_sent, + sends_blocked, }))) } else { + log::debug!( + "no bytes sent via TCP stream {} to {} in this interval; shutting down...", + self.stream_idx, + peer_addr + ); //indicate that the test is over by dropping the stream self.stream = None; None } } - - fn get_port(&self) -> super::BoxResult { + + fn get_port(&self) -> BoxResult { match &self.stream { Some(stream) => Ok(stream.local_addr()?.port()), None => Err(Box::new(simple_error::simple_error!("no stream currently exists"))), } } - + fn get_idx(&self) -> u8 { self.stream_idx.to_owned() } - + fn stop(&mut self) { self.active = false; } diff --git a/src/stream/udp.rs b/src/stream/udp.rs index 80404bd..4342b09 100644 --- a/src/stream/udp.rs +++ b/src/stream/udp.rs @@ -1,38 +1,32 @@ /* * Copyright (C) 2021 Evtech Solutions, Ltd., dba 3D-P * Copyright (C) 2021 Neil Tallim - * + * * This file is part of rperf. - * + * * rperf is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * rperf is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with rperf. If not, see . */ - -extern crate log; -extern crate nix; -use std::error::Error; +use crate::{ + protocol::results::{get_unix_timestamp, UdpReceiveResult, UdpSendResult}, + BoxResult, +}; -use nix::sys::socket::{setsockopt, sockopt::RcvBuf, sockopt::SndBuf}; +use super::{parse_port_spec, TestStream, INTERVAL}; -use crate::protocol::results::{IntervalResult, UdpReceiveResult, UdpSendResult, get_unix_timestamp}; - -use super::{INTERVAL, TestStream, parse_port_spec}; - -type BoxResult = Result>; - -pub const TEST_HEADER_SIZE:u16 = 36; -const UDP_HEADER_SIZE:u16 = 8; +pub const TEST_HEADER_SIZE: u16 = 36; +const UDP_HEADER_SIZE: u16 = 8; #[derive(Clone)] pub struct UdpTestDefinition { @@ -44,266 +38,312 @@ pub struct UdpTestDefinition { pub length: u16, } impl UdpTestDefinition { - pub fn new(details:&serde_json::Value) -> super::BoxResult { + pub fn new(details: &serde_json::Value) -> BoxResult { let mut test_id_bytes = [0_u8; 16]; - for (i, v) in details.get("test_id").unwrap_or(&serde_json::json!([])).as_array().unwrap().iter().enumerate() { - if i >= 16 { //avoid out-of-bounds if given malicious data + for (i, v) in details + .get("test_id") + .unwrap_or(&serde_json::json!([])) + .as_array() + .unwrap() + .iter() + .enumerate() + { + if i >= 16 { + //avoid out-of-bounds if given malicious data break; } test_id_bytes[i] = v.as_i64().unwrap_or(0) as u8; } - - let length = details.get("length").unwrap_or(&serde_json::json!(TEST_HEADER_SIZE)).as_i64().unwrap() as u16; + + let length = details + .get("length") + .unwrap_or(&serde_json::json!(TEST_HEADER_SIZE)) + .as_i64() + .unwrap() as u16; if length < TEST_HEADER_SIZE { - return Err(Box::new(simple_error::simple_error!(std::format!("{} is too short of a length to satisfy testing requirements", length)))); + return Err(Box::new(simple_error::simple_error!(std::format!( + "{} is too short of a length to satisfy testing requirements", + length + )))); } - - Ok(UdpTestDefinition{ + + Ok(UdpTestDefinition { test_id: test_id_bytes, bandwidth: details.get("bandwidth").unwrap_or(&serde_json::json!(0.0)).as_f64().unwrap() as u64, - length: length, + length, }) } } pub mod receiver { + use crate::protocol::results::IntervalResultBox; + use crate::BoxResult; + use chrono::NaiveDateTime; use std::convert::TryInto; - use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; - use std::os::unix::io::AsRawFd; - use std::sync::{Mutex}; + use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, UdpSocket}; + use std::sync::Mutex; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; - - use chrono::{NaiveDateTime}; - - use std::net::UdpSocket; - - const READ_TIMEOUT:Duration = Duration::from_millis(50); - const RECEIVE_TIMEOUT:Duration = Duration::from_secs(3); - + + const READ_TIMEOUT: Duration = Duration::from_millis(50); + pub struct UdpPortPool { pub ports_ip4: Vec, pos_ip4: usize, lock_ip4: Mutex, - + pub ports_ip6: Vec, pos_ip6: usize, lock_ip6: Mutex, } impl UdpPortPool { - pub fn new(port_spec:String, port_spec6:String) -> UdpPortPool { + pub fn new(port_spec: &str, port_spec6: &str) -> UdpPortPool { let ports = super::parse_port_spec(port_spec); if !ports.is_empty() { log::debug!("configured IPv4 UDP port pool: {:?}", ports); } else { log::debug!("using OS assignment for IPv4 UDP ports"); } - + let ports6 = super::parse_port_spec(port_spec6); if !ports.is_empty() { log::debug!("configured IPv6 UDP port pool: {:?}", ports6); } else { log::debug!("using OS assignment for IPv6 UDP ports"); } - + UdpPortPool { ports_ip4: ports, pos_ip4: 0, lock_ip4: Mutex::new(0), - + ports_ip6: ports6, pos_ip6: 0, lock_ip6: Mutex::new(0), } } - - pub fn bind(&mut self, peer_ip:&IpAddr) -> super::BoxResult { + + pub fn bind(&mut self, peer_ip: &IpAddr) -> BoxResult { match peer_ip { IpAddr::V6(_) => { if self.ports_ip6.is_empty() { - return Ok(UdpSocket::bind(&SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0)).expect(format!("failed to bind OS-assigned IPv6 UDP socket").as_str())); + return Ok(UdpSocket::bind(SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0)) + .expect("failed to bind OS-assigned IPv6 UDP socket")); } else { let _guard = self.lock_ip6.lock().unwrap(); - - for port_idx in (self.pos_ip6 + 1)..self.ports_ip6.len() { //iterate to the end of the pool; this will skip the first element in the pool initially, but that's fine - let listener_result = UdpSocket::bind(&SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), self.ports_ip6[port_idx])); - if listener_result.is_ok() { + + for port_idx in (self.pos_ip6 + 1)..self.ports_ip6.len() { + //iterate to the end of the pool; this will skip the first element in the pool initially, but that's fine + let listener_result = + UdpSocket::bind(SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), self.ports_ip6[port_idx])); + if let Ok(listener_result) = listener_result { self.pos_ip6 = port_idx; - return Ok(listener_result.unwrap()); + return Ok(listener_result); } else { log::warn!("unable to bind IPv6 UDP port {}", self.ports_ip6[port_idx]); } } - for port_idx in 0..=self.pos_ip6 { //circle back to where the search started - let listener_result = UdpSocket::bind(&SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), self.ports_ip6[port_idx])); - if listener_result.is_ok() { + for port_idx in 0..=self.pos_ip6 { + //circle back to where the search started + let listener_result = + UdpSocket::bind(SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), self.ports_ip6[port_idx])); + if let Ok(listener_result) = listener_result { self.pos_ip6 = port_idx; - return Ok(listener_result.unwrap()); + return Ok(listener_result); } else { log::warn!("unable to bind IPv6 UDP port {}", self.ports_ip6[port_idx]); } } } - return Err(Box::new(simple_error::simple_error!("unable to allocate IPv6 UDP port"))); - }, + Err(Box::new(simple_error::simple_error!("unable to allocate IPv6 UDP port"))) + } IpAddr::V4(_) => { if self.ports_ip4.is_empty() { - return Ok(UdpSocket::bind(&SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0)).expect(format!("failed to bind OS-assigned IPv4 UDP socket").as_str())); + return Ok(UdpSocket::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0)) + .expect("failed to bind OS-assigned IPv4 UDP socket")); } else { let _guard = self.lock_ip4.lock().unwrap(); - - for port_idx in (self.pos_ip4 + 1)..self.ports_ip4.len() { //iterate to the end of the pool; this will skip the first element in the pool initially, but that's fine - let listener_result = UdpSocket::bind(&SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), self.ports_ip4[port_idx])); - if listener_result.is_ok() { + + for port_idx in (self.pos_ip4 + 1)..self.ports_ip4.len() { + //iterate to the end of the pool; this will skip the first element in the pool initially, but that's fine + let listener_result = + UdpSocket::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), self.ports_ip4[port_idx])); + if let Ok(listener_result) = listener_result { self.pos_ip4 = port_idx; - return Ok(listener_result.unwrap()); + return Ok(listener_result); } else { log::warn!("unable to bind IPv4 UDP port {}", self.ports_ip4[port_idx]); } } - for port_idx in 0..=self.pos_ip4 { //circle back to where the search started - let listener_result = UdpSocket::bind(&SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), self.ports_ip4[port_idx])); - if listener_result.is_ok() { + for port_idx in 0..=self.pos_ip4 { + //circle back to where the search started + let listener_result = + UdpSocket::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), self.ports_ip4[port_idx])); + if let Ok(listener_result) = listener_result { self.pos_ip4 = port_idx; - return Ok(listener_result.unwrap()); + return Ok(listener_result); } else { log::warn!("unable to bind IPv4 UDP port {}", self.ports_ip4[port_idx]); } } } - return Err(Box::new(simple_error::simple_error!("unable to allocate IPv4 UDP port"))); - }, - }; + Err(Box::new(simple_error::simple_error!("unable to allocate IPv4 UDP port"))) + } + } } } - + struct UdpReceiverIntervalHistory { packets_received: u64, - + packets_lost: i64, packets_out_of_order: u64, packets_duplicated: u64, - + unbroken_sequence: u64, jitter_seconds: Option, longest_unbroken_sequence: u64, longest_jitter_seconds: Option, previous_time_delta_nanoseconds: i64, } - + pub struct UdpReceiver { active: bool, test_definition: super::UdpTestDefinition, stream_idx: u8, next_packet_id: u64, - + socket: UdpSocket, } impl UdpReceiver { - pub fn new(test_definition:super::UdpTestDefinition, stream_idx:&u8, port_pool:&mut UdpPortPool, peer_ip:&IpAddr, receive_buffer:&usize) -> super::BoxResult { + #[allow(unused_variables)] + pub fn new( + test_definition: super::UdpTestDefinition, + stream_idx: &u8, + port_pool: &mut UdpPortPool, + peer_ip: &IpAddr, + receive_buffer: &usize, + ) -> BoxResult { log::debug!("binding UDP receive socket for stream {}...", stream_idx); - let socket:UdpSocket = port_pool.bind(peer_ip).expect(format!("failed to bind UDP socket").as_str()); + let socket: UdpSocket = port_pool.bind(peer_ip).expect("failed to bind UDP socket"); socket.set_read_timeout(Some(READ_TIMEOUT))?; - if !cfg!(windows) { //NOTE: features unsupported on Windows - if *receive_buffer != 0 { - log::debug!("setting receive-buffer to {}...", receive_buffer); - super::setsockopt(socket.as_raw_fd(), super::RcvBuf, receive_buffer)?; - } + // NOTE: features unsupported on Windows + #[cfg(unix)] + if *receive_buffer != 0 { + log::debug!("setting receive-buffer to {}...", receive_buffer); + let raw_socket = socket2::SockRef::from(&socket); + raw_socket.set_recv_buffer_size(*receive_buffer)?; } log::debug!("bound UDP receive socket for stream {}: {}", stream_idx, socket.local_addr()?); - - Ok(UdpReceiver{ + + Ok(UdpReceiver { active: true, - test_definition: test_definition, + test_definition, stream_idx: stream_idx.to_owned(), - + next_packet_id: 0, - - socket: socket, + + socket, }) } - - fn process_packets_ordering(&mut self, packet_id:u64, mut history:&mut UdpReceiverIntervalHistory) -> bool { + + fn process_packets_ordering(&mut self, packet_id: u64, history: &mut UdpReceiverIntervalHistory) -> bool { /* the algorithm from iperf3 provides a pretty decent approximation * for tracking lost and out-of-order packets efficiently, so it's * been minimally reimplemented here, with corrections. - * + * * returns true if packet-ordering is as-expected */ - if packet_id == self.next_packet_id { //expected sequential-ordering case - self.next_packet_id += 1; - return true; - } else if packet_id > self.next_packet_id { //something was either lost or there's an ordering problem - let lost_packet_count = (packet_id - self.next_packet_id) as i64; - log::debug!("UDP reception for stream {} observed a gap of {} packets", self.stream_idx, lost_packet_count); - history.packets_lost += lost_packet_count; //assume everything in-between has been lost - self.next_packet_id = packet_id + 1; //anticipate that ordered receipt will resume - } else { //a packet with a previous ID was received; this is either a duplicate or an ordering issue - //CAUTION: this is where the approximation part of the algorithm comes into play - if history.packets_lost > 0 { //assume it's an ordering issue in the common case - history.packets_lost -= 1; - history.packets_out_of_order += 1; - } else { //the only other thing it could be is a duplicate; in practice, duplicates don't tend to show up alongside losses; non-zero is always bad, though - history.packets_duplicated += 1; + match packet_id.cmp(&self.next_packet_id) { + std::cmp::Ordering::Equal => { + //expected sequential-ordering case + self.next_packet_id += 1; + return true; + } + std::cmp::Ordering::Greater => { + //something was either lost or there's an ordering problem + let lost_packet_count = (packet_id - self.next_packet_id) as i64; + log::debug!( + "UDP reception for stream {} observed a gap of {} packets", + self.stream_idx, + lost_packet_count + ); + history.packets_lost += lost_packet_count; //assume everything in-between has been lost + self.next_packet_id = packet_id + 1; //anticipate that ordered receipt will resume + } + std::cmp::Ordering::Less => { + //a packet with a previous ID was received; this is either a duplicate or an ordering issue + //CAUTION: this is where the approximation part of the algorithm comes into play + if history.packets_lost > 0 { + //assume it's an ordering issue in the common case + history.packets_lost -= 1; + history.packets_out_of_order += 1; + } else { + //the only other thing it could be is a duplicate; in practice, duplicates don't tend to show up alongside losses; non-zero is always bad, though + history.packets_duplicated += 1; + } } } - return false; + false } - - fn process_jitter(&mut self, timestamp:&NaiveDateTime, history:&mut UdpReceiverIntervalHistory) { + + fn process_jitter(&mut self, timestamp: &NaiveDateTime, history: &mut UdpReceiverIntervalHistory) { /* this is a pretty straightforward implementation of RFC 1889, Appendix 8 * it works on an assumption that the timestamp delta between sender and receiver * will remain effectively constant during the testing window */ let now = SystemTime::now().duration_since(UNIX_EPOCH).expect("system time before UNIX epoch"); - let current_timestamp = NaiveDateTime::from_timestamp(now.as_secs() as i64, now.subsec_nanos()); - + let current_timestamp = NaiveDateTime::from_timestamp_opt(now.as_secs() as i64, now.subsec_nanos()).unwrap(); + let time_delta = current_timestamp - *timestamp; - + let time_delta_nanoseconds = match time_delta.num_nanoseconds() { Some(ns) => ns, None => { log::warn!("sender and receiver clocks are too out-of-sync to calculate jitter"); return; - }, + } }; - - if history.unbroken_sequence > 1 { //do jitter calculation + + if history.unbroken_sequence > 1 { + //do jitter calculation let delta_seconds = (time_delta_nanoseconds - history.previous_time_delta_nanoseconds).abs() as f32 / 1_000_000_000.00; - - if history.unbroken_sequence > 2 { //normal jitter-calculation, per the RFC + + if history.unbroken_sequence > 2 { + //normal jitter-calculation, per the RFC let mut jitter_seconds = history.jitter_seconds.unwrap(); //since we have a chain, this won't be None jitter_seconds += (delta_seconds - jitter_seconds) / 16.0; history.jitter_seconds = Some(jitter_seconds); - } else { //first observed transition; use this as the calibration baseline + } else { + //first observed transition; use this as the calibration baseline history.jitter_seconds = Some(delta_seconds); } } //update time-delta history.previous_time_delta_nanoseconds = time_delta_nanoseconds; } - - fn process_packet(&mut self, packet:&[u8], mut history:&mut UdpReceiverIntervalHistory) -> bool { + + fn process_packet(&mut self, packet: &[u8], history: &mut UdpReceiverIntervalHistory) -> bool { //the first sixteen bytes are the test's ID if packet[0..16] != self.test_definition.test_id { - return false + return false; } - + //the next eight bytes are the packet's ID, in big-endian order let packet_id = u64::from_be_bytes(packet[16..24].try_into().unwrap()); - + //except for the timestamp, nothing else in the packet actually matters - + history.packets_received += 1; - if self.process_packets_ordering(packet_id, &mut history) { + if self.process_packets_ordering(packet_id, history) { //the second eight are the number of seconds since the UNIX epoch, in big-endian order let origin_seconds = i64::from_be_bytes(packet[24..32].try_into().unwrap()); //and the following four are the number of nanoseconds since the UNIX epoch let origin_nanoseconds = u32::from_be_bytes(packet[32..36].try_into().unwrap()); - let source_timestamp = NaiveDateTime::from_timestamp(origin_seconds, origin_nanoseconds); - + let source_timestamp = NaiveDateTime::from_timestamp_opt(origin_seconds, origin_nanoseconds).unwrap(); + history.unbroken_sequence += 1; - self.process_jitter(&source_timestamp, &mut history); - + self.process_jitter(&source_timestamp, history); + if history.unbroken_sequence > history.longest_unbroken_sequence { history.longest_unbroken_sequence = history.unbroken_sequence; history.longest_jitter_seconds = history.jitter_seconds; @@ -312,271 +352,283 @@ pub mod receiver { history.unbroken_sequence = 0; history.jitter_seconds = None; } - return true; + true } } impl super::TestStream for UdpReceiver { - fn run_interval(&mut self) -> Option>> { + fn run_interval(&mut self) -> Option> { let mut buf = vec![0_u8; self.test_definition.length.into()]; - - let mut bytes_received:u64 = 0; - - let mut history = UdpReceiverIntervalHistory{ + + let mut bytes_received: u64 = 0; + + let mut history = UdpReceiverIntervalHistory { packets_received: 0, - + packets_lost: 0, packets_out_of_order: 0, packets_duplicated: 0, - + unbroken_sequence: 0, jitter_seconds: None, longest_unbroken_sequence: 0, longest_jitter_seconds: None, previous_time_delta_nanoseconds: 0, }; - + let start = Instant::now(); - - while self.active { - if start.elapsed() >= RECEIVE_TIMEOUT { - return Some(Err(Box::new(simple_error::simple_error!("UDP reception for stream {} timed out, likely because the end-signal was lost", self.stream_idx)))); - } - + + while self.active && start.elapsed() < super::INTERVAL { log::trace!("awaiting UDP packets on stream {}...", self.stream_idx); - loop { - match self.socket.recv_from(&mut buf) { - Ok((packet_size, peer_addr)) => { - log::trace!("received {} bytes in UDP packet {} from {}", packet_size, self.stream_idx, peer_addr); - if packet_size == 16 { //possible end-of-test message - if &buf[0..16] == self.test_definition.test_id { //test's over - self.stop(); - break; - } - } - if packet_size < super::TEST_HEADER_SIZE as usize { - log::warn!("received malformed packet with size {} for UDP stream {} from {}", packet_size, self.stream_idx, peer_addr); - continue; - } - - if self.process_packet(&buf, &mut history) { - //NOTE: duplicate packets increase this count; this is intentional because the stack still processed data - bytes_received += packet_size as u64 + super::UDP_HEADER_SIZE as u64; - - let elapsed_time = start.elapsed(); - if elapsed_time >= super::INTERVAL { - return Some(Ok(Box::new(super::UdpReceiveResult{ - timestamp: super::get_unix_timestamp(), - - stream_idx: self.stream_idx, - - duration: elapsed_time.as_secs_f32(), - - bytes_received: bytes_received, - packets_received: history.packets_received, - packets_lost: history.packets_lost, - packets_out_of_order: history.packets_out_of_order, - packets_duplicated: history.packets_duplicated, - - unbroken_sequence: history.longest_unbroken_sequence, - jitter_seconds: history.longest_jitter_seconds, - }))) - } - } else { - log::warn!("received packet unrelated to UDP stream {} from {}", self.stream_idx, peer_addr); - continue; - } - }, - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { //receive timeout - break; - }, - Err(e) => { - return Some(Err(Box::new(e))); - }, + let (packet_size, peer_addr) = match self.socket.recv_from(&mut buf) { + Ok((packet_size, peer_addr)) => (packet_size, peer_addr), + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock || e.kind() == std::io::ErrorKind::TimedOut => { + // receive timeout + continue; + } + Err(e) => { + return Some(Err(Box::new(e))); + } + }; + + log::trace!( + "received {} bytes in UDP packet {} from {}", + packet_size, + self.stream_idx, + peer_addr + ); + if packet_size == 16 { + // possible end-of-test message + if buf[0..16] == self.test_definition.test_id { + // test's over + self.stop(); + break; } } + if packet_size < super::TEST_HEADER_SIZE as usize { + log::warn!( + "received malformed packet with size {} for UDP stream {} from {}", + packet_size, + self.stream_idx, + peer_addr + ); + continue; + } + + if self.process_packet(&buf, &mut history) { + // NOTE: duplicate packets increase this count; this is intentional because the stack still processed data + bytes_received += packet_size as u64 + super::UDP_HEADER_SIZE as u64; + } else { + log::warn!("received packet unrelated to UDP stream {} from {}", self.stream_idx, peer_addr); + continue; + } } + if bytes_received > 0 { - Some(Ok(Box::new(super::UdpReceiveResult{ + log::debug!( + "{} bytes received via UDP stream {} in this interval; reporting...", + bytes_received, + self.stream_idx + ); + Some(Ok(Box::new(super::UdpReceiveResult { timestamp: super::get_unix_timestamp(), - + stream_idx: self.stream_idx, - + duration: start.elapsed().as_secs_f32(), - - bytes_received: bytes_received, + + bytes_received, packets_received: history.packets_received, packets_lost: history.packets_lost, packets_out_of_order: history.packets_out_of_order, packets_duplicated: history.packets_duplicated, - + unbroken_sequence: history.longest_unbroken_sequence, jitter_seconds: history.longest_jitter_seconds, }))) } else { + log::debug!("no bytes received via UDP stream {} in this interval", self.stream_idx); None } } - - fn get_port(&self) -> super::BoxResult { + + fn get_port(&self) -> BoxResult { let socket_addr = self.socket.local_addr()?; Ok(socket_addr.port()) } - + fn get_idx(&self) -> u8 { self.stream_idx.to_owned() } - + fn stop(&mut self) { self.active = false; } } } - - pub mod sender { + use crate::protocol::results::IntervalResultBox; + use crate::BoxResult; + use std::net::UdpSocket; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; - use std::os::unix::io::AsRawFd; + use std::thread::sleep; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; - - use std::net::UdpSocket; - - use std::thread::{sleep}; - - const WRITE_TIMEOUT:Duration = Duration::from_millis(50); - const BUFFER_FULL_TIMEOUT:Duration = Duration::from_millis(1); - + + const WRITE_TIMEOUT: Duration = Duration::from_millis(50); + const BUFFER_FULL_TIMEOUT: Duration = Duration::from_millis(1); + pub struct UdpSender { active: bool, test_definition: super::UdpTestDefinition, stream_idx: u8, - + socket: UdpSocket, - + //the interval, in seconds, at which to send data send_interval: f32, - + remaining_duration: f32, next_packet_id: u64, staged_packet: Vec, } impl UdpSender { - pub fn new(test_definition:super::UdpTestDefinition, stream_idx:&u8, port:&u16, receiver_ip:&IpAddr, receiver_port:&u16, send_duration:&f32, send_interval:&f32, send_buffer:&usize) -> super::BoxResult { + #[allow(clippy::too_many_arguments, unused_variables)] + pub fn new( + test_definition: super::UdpTestDefinition, + stream_idx: &u8, + port: &u16, + receiver_ip: &IpAddr, + receiver_port: &u16, + send_duration: &f32, + send_interval: &f32, + send_buffer: &usize, + ) -> BoxResult { log::debug!("preparing to connect UDP stream {}...", stream_idx); let socket_addr_receiver = SocketAddr::new(*receiver_ip, *receiver_port); let socket = match receiver_ip { - IpAddr::V6(_) => UdpSocket::bind(&SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), *port)).expect(format!("failed to bind UDP socket, port {}", port).as_str()), - IpAddr::V4(_) => UdpSocket::bind(&SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), *port)).expect(format!("failed to bind UDP socket, port {}", port).as_str()), + IpAddr::V6(_) => UdpSocket::bind(SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), *port)) + .unwrap_or_else(|_| panic!("failed to bind UDP socket, port {}", port)), + IpAddr::V4(_) => UdpSocket::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), *port)) + .unwrap_or_else(|_| panic!("failed to bind UDP socket, port {}", port)), }; socket.set_write_timeout(Some(WRITE_TIMEOUT))?; - if !cfg!(windows) { //NOTE: features unsupported on Windows - if *send_buffer != 0 { - log::debug!("setting send-buffer to {}...", send_buffer); - super::setsockopt(socket.as_raw_fd(), super::SndBuf, send_buffer)?; - } + // NOTE: features unsupported on Windows + #[cfg(unix)] + if *send_buffer != 0 { + log::debug!("setting send-buffer to {}...", send_buffer); + let raw_socket = socket2::SockRef::from(&socket); + raw_socket.set_send_buffer_size(*send_buffer)?; } socket.connect(socket_addr_receiver)?; log::debug!("connected UDP stream {} to {}", stream_idx, socket_addr_receiver); - + let mut staged_packet = vec![0_u8; test_definition.length.into()]; - for i in super::TEST_HEADER_SIZE..(staged_packet.len() as u16) { //fill the packet with a fixed sequence + for i in super::TEST_HEADER_SIZE..(staged_packet.len() as u16) { + //fill the packet with a fixed sequence staged_packet[i as usize] = (i % 256) as u8; } //embed the test ID staged_packet[0..16].copy_from_slice(&test_definition.test_id); - - Ok(UdpSender{ + + Ok(UdpSender { active: true, - test_definition: test_definition, + test_definition, stream_idx: stream_idx.to_owned(), - - socket: socket, - + + socket, + send_interval: send_interval.to_owned(), - + remaining_duration: send_duration.to_owned(), next_packet_id: 0, - staged_packet: staged_packet, + staged_packet, }) } - + fn prepare_packet(&mut self) { let now = SystemTime::now().duration_since(UNIX_EPOCH).expect("system time before UNIX epoch"); - + //eight bytes after the test ID are the packet's ID, in big-endian order self.staged_packet[16..24].copy_from_slice(&self.next_packet_id.to_be_bytes()); - + //the next eight are the seconds part of the UNIX timestamp and the following four are the nanoseconds self.staged_packet[24..32].copy_from_slice(&now.as_secs().to_be_bytes()); self.staged_packet[32..36].copy_from_slice(&now.subsec_nanos().to_be_bytes()); } } impl super::TestStream for UdpSender { - fn run_interval(&mut self) -> Option>> { + fn run_interval(&mut self) -> Option> { let interval_duration = Duration::from_secs_f32(self.send_interval); let mut interval_iteration = 0; let bytes_to_send = ((self.test_definition.bandwidth as f32) * super::INTERVAL.as_secs_f32()) as i64; let mut bytes_to_send_remaining = bytes_to_send; let bytes_to_send_per_interval_slice = ((bytes_to_send as f32) * self.send_interval) as i64; let mut bytes_to_send_per_interval_slice_remaining = bytes_to_send_per_interval_slice; - - let mut packets_sent:u64 = 0; - let mut sends_blocked:u64 = 0; - let mut bytes_sent:u64 = 0; - + + let mut packets_sent: u64 = 0; + let mut sends_blocked: u64 = 0; + let mut bytes_sent: u64 = 0; + let cycle_start = Instant::now(); - + while self.active && self.remaining_duration > 0.0 && bytes_to_send_remaining > 0 { log::trace!("writing {} bytes in UDP stream {}...", self.staged_packet.len(), self.stream_idx); let packet_start = Instant::now(); - + self.prepare_packet(); match self.socket.send(&self.staged_packet) { Ok(packet_size) => { log::trace!("wrote {} bytes in UDP stream {}", packet_size, self.stream_idx); - + packets_sent += 1; //reflect that a packet is in-flight self.next_packet_id += 1; - + let bytes_written = packet_size as i64 + super::UDP_HEADER_SIZE as i64; bytes_sent += bytes_written as u64; bytes_to_send_remaining -= bytes_written; bytes_to_send_per_interval_slice_remaining -= bytes_written; - + let elapsed_time = cycle_start.elapsed(); if elapsed_time >= super::INTERVAL { self.remaining_duration -= packet_start.elapsed().as_secs_f32(); - - return Some(Ok(Box::new(super::UdpSendResult{ + log::debug!( + "{} bytes sent via UDP stream {} in this interval; reporting...", + bytes_sent, + self.stream_idx + ); + return Some(Ok(Box::new(super::UdpSendResult { timestamp: super::get_unix_timestamp(), - + stream_idx: self.stream_idx, - + duration: elapsed_time.as_secs_f32(), - - bytes_sent: bytes_sent, - packets_sent: packets_sent, - sends_blocked: sends_blocked, - }))) + + bytes_sent, + packets_sent, + sends_blocked, + }))); } - }, - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { //send-buffer is full + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + //send-buffer is full //nothing to do, but avoid burning CPU cycles sleep(BUFFER_FULL_TIMEOUT); sends_blocked += 1; - }, + } Err(e) => { return Some(Err(Box::new(e))); - }, + } } - - if bytes_to_send_remaining <= 0 { //interval's target is exhausted, so sleep until the end + + if bytes_to_send_remaining <= 0 { + //interval's target is exhausted, so sleep until the end let elapsed_time = cycle_start.elapsed(); if super::INTERVAL > elapsed_time { sleep(super::INTERVAL - elapsed_time); } - } else if bytes_to_send_per_interval_slice_remaining <= 0 { // interval subsection exhausted + } else if bytes_to_send_per_interval_slice_remaining <= 0 { + // interval subsection exhausted interval_iteration += 1; bytes_to_send_per_interval_slice_remaining = bytes_to_send_per_interval_slice; let elapsed_time = cycle_start.elapsed(); @@ -588,48 +640,59 @@ pub mod sender { self.remaining_duration -= packet_start.elapsed().as_secs_f32(); } if bytes_sent > 0 { - Some(Ok(Box::new(super::UdpSendResult{ + log::debug!( + "{} bytes sent via UDP stream {} in this interval; reporting...", + bytes_sent, + self.stream_idx + ); + Some(Ok(Box::new(super::UdpSendResult { timestamp: super::get_unix_timestamp(), - + stream_idx: self.stream_idx, - + duration: cycle_start.elapsed().as_secs_f32(), - - bytes_sent: bytes_sent, - packets_sent: packets_sent, - sends_blocked: sends_blocked, + + bytes_sent, + packets_sent, + sends_blocked, }))) } else { + log::debug!( + "no bytes sent via UDP stream {} in this interval; shutting down...", + self.stream_idx + ); //indicate that the test is over by sending the test ID by itself let mut remaining_announcements = 5; - while remaining_announcements > 0 { //do it a few times in case of loss + while remaining_announcements > 0 { + //do it a few times in case of loss match self.socket.send(&self.staged_packet[0..16]) { Ok(packet_size) => { log::trace!("wrote {} bytes of test-end signal in UDP stream {}", packet_size, self.stream_idx); remaining_announcements -= 1; - }, - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { //send-buffer is full + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + //send-buffer is full //wait to try again and avoid burning CPU cycles sleep(BUFFER_FULL_TIMEOUT); - }, + } Err(e) => { return Some(Err(Box::new(e))); - }, + } } } None } } - - fn get_port(&self) -> super::BoxResult { + + fn get_port(&self) -> BoxResult { let socket_addr = self.socket.local_addr()?; Ok(socket_addr.port()) } - + fn get_idx(&self) -> u8 { self.stream_idx.to_owned() } - + fn stop(&mut self) { self.active = false; } diff --git a/src/utils/cpu_affinity.rs b/src/utils/cpu_affinity.rs index 063d488..729099f 100644 --- a/src/utils/cpu_affinity.rs +++ b/src/utils/cpu_affinity.rs @@ -1,47 +1,44 @@ /* * Copyright (C) 2021 Evtech Solutions, Ltd., dba 3D-P * Copyright (C) 2021 Neil Tallim - * + * * This file is part of rperf. - * + * * rperf is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * rperf is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with rperf. If not, see . */ - -extern crate core_affinity; -use std::error::Error; -type BoxResult = Result>; +use crate::BoxResult; pub struct CpuAffinityManager { enabled_cores: Vec, last_core_pointer: usize, } impl CpuAffinityManager { - pub fn new(cores:&str) -> BoxResult { + pub fn new(cores: &str) -> BoxResult { let core_ids = core_affinity::get_core_ids().unwrap_or_default(); log::debug!("enumerated CPU cores: {:?}", core_ids.iter().map(|c| c.id).collect::>()); - + let mut enabled_cores = Vec::new(); for cid in cores.split(',') { if cid.is_empty() { continue; } - let cid_usize:usize = cid.parse()?; + let cid_usize: usize = cid.parse()?; let mut cid_valid = false; for core_id in &core_ids { if core_id.id == cid_usize { - enabled_cores.push(core_id.clone()); + enabled_cores.push(*core_id); cid_valid = true; } } @@ -49,20 +46,20 @@ impl CpuAffinityManager { log::warn!("unrecognised CPU core: {}", cid_usize); } } - if enabled_cores.len() > 0 { + if !enabled_cores.is_empty() { log::debug!("selecting from CPU cores {:?}", enabled_cores); } else { log::debug!("not applying CPU core affinity"); } - - Ok(CpuAffinityManager{ - enabled_cores: enabled_cores, + + Ok(CpuAffinityManager { + enabled_cores, last_core_pointer: 0, }) } - + pub fn set_affinity(&mut self) { - if self.enabled_cores.len() > 0 { + if !self.enabled_cores.is_empty() { let core_id = self.enabled_cores[self.last_core_pointer]; log::debug!("setting CPU affinity to {}", core_id.id); core_affinity::set_for_current(core_id); diff --git a/src/utils/mod.rs b/src/utils/mod.rs index c81d644..d4505fd 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,21 +1,21 @@ /* * Copyright (C) 2021 Evtech Solutions, Ltd., dba 3D-P * Copyright (C) 2021 Neil Tallim - * + * * This file is part of rperf. - * + * * rperf is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * rperf is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with rperf. If not, see . */ - + pub mod cpu_affinity;