From f381437d180f0cba6530ad4719d11e24fdd3542d Mon Sep 17 00:00:00 2001 From: An Long Date: Fri, 10 Jul 2026 23:28:15 +0900 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20Tokio=20async=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 8 ++- README.md | 27 +++++++ src/ping.rs | 8 ++- src/ping/async_ping.rs | 160 +++++++++++++++++++++++++++++++++++++++++ tests/tests.rs | 17 +++++ 5 files changed, 218 insertions(+), 2 deletions(-) create mode 100644 src/ping/async_ping.rs diff --git a/Cargo.toml b/Cargo.toml index ccc0989..2d052ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ping" version = "0.8.0" -authors = ["AN Long "] +authors = ["An Long "] license = "MIT" description = "Simple and naive ping implementation in Rust." repository = "https://github.com/aisk/rust-ping" @@ -9,10 +9,16 @@ categories = ["network-programming", "os"] keywords = ["ping", "system", "ICMP"] edition = "2024" +[features] +default = [] +tokio = ["dep:tokio"] + [dependencies] socket2 = { version = "0.6", features = ["all"] } thiserror = ">=1.0, <=2.1" rand = ">=0.8, <=0.9" +tokio = { version = "1", features = ["net", "rt", "time"], optional = true } [dev-dependencies] libc = "0.2" +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } diff --git a/README.md b/README.md index 249ff5e..7652721 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,33 @@ fn main() { } ``` +## Optional Tokio support + +Tokio-based asynchronous sending is available behind the optional `tokio` feature. The feature is disabled by default, so synchronous users do not pull Tokio into their dependency graph. + +```toml +[dependencies] +ping = { version = "0.9", features = ["tokio"] } +``` + +```rust +use std::time::Duration; + +#[tokio::main] +async fn main() { + let target_ip = "8.8.8.8".parse().unwrap(); + let result = ping::new(target_ip) + .timeout(Duration::from_secs(2)) + .send_async() + .await + .expect("ping failed"); + + println!("round-trip time: {:?}", result.rtt); +} +``` + +On Unix, `send_async` uses a nonblocking socket registered with Tokio's reactor, so each in-flight ping does not occupy a thread. Windows currently uses Tokio's blocking task pool as a compatibility implementation; native asynchronous Windows socket support may be added in a future release. + To perform a ping using a domain name instead of an IP address, you can use any 3rd-party DNS resolver or [`ToSocketAddrs`](https://doc.rust-lang.org/std/net/trait.ToSocketAddrs.html) from the standard library: ```rust diff --git a/src/ping.rs b/src/ping.rs index 3a67464..46a399a 100644 --- a/src/ping.rs +++ b/src/ping.rs @@ -7,6 +7,9 @@ use socket2::{Domain, Protocol, Socket, Type}; use crate::errors::Error; use crate::packet::{EchoReply, EchoRequest, ICMP_HEADER_SIZE, IcmpV4, IcmpV6, IpV4Packet}; +#[cfg(feature = "tokio")] +mod async_ping; + const TOKEN_SIZE: usize = 24; const ECHO_REQUEST_BUFFER_SIZE: usize = ICMP_HEADER_SIZE + TOKEN_SIZE; type Token = [u8; TOKEN_SIZE]; @@ -245,7 +248,10 @@ pub mod dgramsock { } } -#[deprecated(since = "0.8.0", note = "use `Ping::new` builder and `Ping::send` instead")] +#[deprecated( + since = "0.8.0", + note = "use `Ping::new` builder and `Ping::send` instead" +)] pub fn ping( addr: IpAddr, timeout: Option, diff --git a/src/ping/async_ping.rs b/src/ping/async_ping.rs new file mode 100644 index 0000000..4bb6211 --- /dev/null +++ b/src/ping/async_ping.rs @@ -0,0 +1,160 @@ +use crate::errors::Error; + +#[cfg(unix)] +use std::net::{IpAddr, SocketAddr}; +#[cfg(unix)] +use std::time::{Duration, Instant}; + +#[cfg(unix)] +use socket2::Type; +#[cfg(unix)] +use tokio::io::unix::AsyncFd; + +#[cfg(not(unix))] +use super::ping_with_socktype; +use super::{Ping, PingResult}; +#[cfg(unix)] +use super::{Token, create_socket, decode_reply, prepare_request}; + +#[cfg(unix)] +#[allow(deprecated)] +async fn ping_with_socktype_async( + socket_type: Type, + addr: IpAddr, + timeout: Option, + ttl: Option, + ident: Option, + seq_cnt: Option, + payload: Option<&Token>, + bind_device: Option<&str>, +) -> Result { + let timeout = timeout.unwrap_or(Duration::from_secs(4)); + let dest = SocketAddr::new(addr, 0); + let (request_bytes, request_payload) = prepare_request(addr, ident, seq_cnt, payload)?; + let socket = create_socket(socket_type, addr, ttl, bind_device)?; + socket.set_nonblocking(true)?; + let socket = AsyncFd::new(socket)?; + let started_at = Instant::now(); + + let operation = async { + loop { + let mut ready = socket.writable().await?; + match ready.try_io(|inner| inner.get_ref().send_to(&request_bytes, &dest.into())) { + Ok(result) => { + result?; + break; + } + Err(_would_block) => continue, + } + } + + loop { + let mut ready = socket.readable().await?; + let mut buffer = [0u8; 2048]; + let received = ready.try_io(|inner| { + // socket2 0.6 receives into MaybeUninit because the kernel may + // initialize only the returned prefix of the buffer. + inner.get_ref().recv_from(unsafe { + std::slice::from_raw_parts_mut( + buffer.as_mut_ptr() as *mut std::mem::MaybeUninit, + buffer.len(), + ) + }) + }); + let (n, src_addr) = match received { + Ok(result) => result?, + Err(_would_block) => continue, + }; + let source_ip = src_addr.as_socket().map(|s| s.ip()).unwrap_or(addr); + + let Some((reply, recv_ttl)) = decode_reply(addr, &buffer[..n]) else { + continue; + }; + if reply.payload == request_payload { + return Ok(PingResult { + rtt: started_at.elapsed(), + ident: reply.ident, + seq_cnt: reply.seq_cnt, + payload: reply.payload.to_vec(), + source: source_ip, + target: addr, + ttl: recv_ttl, + }); + } + } + }; + + tokio::time::timeout(timeout, operation) + .await + .map_err(|_| { + Error::from(std::io::Error::new( + std::io::ErrorKind::TimedOut, + "ping request timed out", + )) + })? +} + +impl Ping<'_> { + /// Sends the echo request asynchronously using Tokio. + /// + /// This method is available when the crate's `tokio` feature is enabled. + /// On Unix this uses a nonblocking socket registered with Tokio's reactor. + /// On Windows it currently falls back to Tokio's blocking thread pool. + /// All builder options and the return value are identical to + /// [`send`](Ping::send). + /// + /// ```no_run + /// # #[cfg(feature = "tokio")] + /// # #[tokio::main] + /// # async fn main() { + /// let target = "8.8.8.8".parse().unwrap(); + /// let result = ping::new(target).send_async().await.expect("ping failed"); + /// println!("{:?}", result.rtt); + /// # } + /// ``` + pub async fn send_async(&self) -> Result { + #[cfg(unix)] + return ping_with_socktype_async( + self.socket_type.into(), + self.addr, + self.timeout, + self.ttl, + self.ident, + self.seq_cnt, + self.payload, + #[cfg(any(target_os = "linux", target_os = "android"))] + self.bind_device, + #[cfg(not(any(target_os = "linux", target_os = "android")))] + None, + ) + .await; + + // Windows support currently keeps the portable blocking implementation + // off Tokio's runtime worker threads. + #[cfg(not(unix))] + { + let socket_type = self.socket_type; + let addr = self.addr; + let timeout = self.timeout; + let ttl = self.ttl; + let ident = self.ident; + let seq_cnt = self.seq_cnt; + let payload = self.payload.copied(); + + tokio::task::spawn_blocking(move || { + ping_with_socktype( + socket_type.into(), + addr, + timeout, + ttl, + ident, + seq_cnt, + payload.as_ref(), + None, + ) + }) + .await + .map_err(|_| Error::InternalError)? + } + } +} diff --git a/tests/tests.rs b/tests/tests.rs index f726795..b25a443 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -112,6 +112,23 @@ fn builder_api2() { ping::new(addr).timeout(timeout).ttl(42).send().unwrap(); } +#[cfg(feature = "tokio")] +#[tokio::test] +async fn async_builder_api() { + skip_if_no_capability!(); + let addr = "127.0.0.1".parse().unwrap(); + let timeout = Duration::from_secs(1); + let result = ping::new(addr) + .timeout(timeout) + .ttl(42) + .send_async() + .await + .unwrap(); + + assert_eq!(result.source, addr); + assert!(result.rtt <= timeout); +} + #[test] #[cfg(any(target_os = "linux", target_os = "android"))] fn bind_device() {