Files
OxideJS-New/crates/oxide_net/src/transport.rs
T

287 lines
10 KiB
Rust

//! 传输枚举层:`Conn` 与 `Listener` 各包装 Unix 与 TCP 两臂,统一六个常用操作,
//! `Endpoint` 描述监听目标(Unix 路径或 TCP 地址)。
//!
//! 关键约定:
//! - 两臂映射到 std 同名同语义方法,无行为分叉。
//! - `Listener::accept` 返回 `Conn` 并丢弃对端地址,现有 accept 循环不消费地址。
//! - 构造面(bind / connect)不含在本模块,调用方先用 std 原语构造句柄再包进枚举。
use std::io;
use std::io::{Read, Write};
use std::net::{SocketAddr, TcpListener, TcpStream};
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::{Path, PathBuf};
use std::time::Duration;
/// 双向连接:Unix 域套接字或 TCP 连接。
///
/// 两臂共享同一操作面,调用方无需感知具体传输。
#[derive(Debug)]
pub enum Conn {
/// Unix 域套接字臂。
Unix(UnixStream),
/// TCP 连接臂。
Tcp(TcpStream),
}
/// 监听套接字:Unix 域套接字监听者或 TCP 监听者。
#[derive(Debug)]
pub enum Listener {
/// Unix 域套接字监听者臂。
Unix(UnixListener),
/// TCP 监听者臂。
Tcp(TcpListener),
}
/// 监听端点:Unix 套接字路径或 TCP 地址。
///
/// 是 server 配置中监听目标的单一事实源;Unix 臂是文件,TCP 臂不是。
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Endpoint {
/// Unix 套接字路径臂。
Unix(PathBuf),
/// TCP 地址臂。
Tcp(SocketAddr),
}
impl Endpoint {
/// 返回套接字文件路径:仅 Unix 臂有文件,TCP 臂返回 `None`。
pub fn socket_path(&self) -> Option<&Path> {
match self {
Endpoint::Unix(path) => Some(path),
Endpoint::Tcp(_) => None,
}
}
/// 返回端点文本形态:Unix 臂为路径字符串,TCP 臂为 host:port 字符串。
pub fn describe(&self) -> String {
match self {
Endpoint::Unix(path) => path.to_string_lossy().into_owned(),
Endpoint::Tcp(addr) => addr.to_string(),
}
}
}
impl std::fmt::Display for Endpoint {
/// 端点文本形态:Unix 臂为路径字符串,TCP 臂为 host:port 字符串。
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.describe())
}
}
impl Listener {
/// 设置监听者非阻塞模式。
///
/// # 注意事项
/// - 置非阻塞后,无待接受连接时 `accept` 返回 `io::ErrorKind::WouldBlock`。
pub fn set_nonblocking(&self, nonblocking: bool) -> io::Result<()> {
match self {
Listener::Unix(l) => l.set_nonblocking(nonblocking),
Listener::Tcp(l) => l.set_nonblocking(nonblocking),
}
}
/// 接受一个连接并包装为 `Conn`,丢弃对端地址。
///
/// # 边界与前提
/// - Unix 与 TCP 臂的对端地址类型不同,本层不暴露;需要来源地址的调用方须扩展签名。
/// - 非阻塞模式下无待接受连接时返回 `WouldBlock`。
pub fn accept(&self) -> io::Result<Conn> {
match self {
// 对端地址按约定丢弃,只取连接句柄。
Listener::Unix(l) => Ok(Conn::Unix(l.accept()?.0)),
Listener::Tcp(l) => Ok(Conn::Tcp(l.accept()?.0)),
}
}
/// 取监听者绑定的本地地址文本形态。
///
/// # 边界与前提
/// - 两臂委托标准库同名方法后取文本形态(Unix 臂为套接字路径,TCP 臂为
/// host:port);TCP 臂绑 0 端口时返回操作系统分配的实际端口。
pub fn local_addr(&self) -> io::Result<String> {
match self {
Listener::Unix(l) => {
let addr = l.local_addr()?;
let text = addr.as_pathname().map(|p| p.to_string_lossy().into_owned()).unwrap_or_default();
Ok(text)
}
Listener::Tcp(l) => Ok(l.local_addr()?.to_string()),
}
}
}
impl Conn {
/// 克隆连接,返回共享同一底层套接字的新 `Conn`。
///
/// # 副作用
/// - 新句柄与原句柄共享同一套接字:写侧共享,读侧相互独立(std `try_clone` 语义)。
pub fn try_clone(&self) -> io::Result<Conn> {
match self {
Conn::Unix(s) => Ok(Conn::Unix(s.try_clone()?)),
Conn::Tcp(s) => Ok(Conn::Tcp(s.try_clone()?)),
}
}
/// 设置读超时。
///
/// # 边界与前提
/// - `None` 表示不限时(可无限阻塞)。
pub fn set_read_timeout(&self, timeout: Option<Duration>) -> io::Result<()> {
match self {
Conn::Unix(s) => s.set_read_timeout(timeout),
Conn::Tcp(s) => s.set_read_timeout(timeout),
}
}
/// 设置写超时。
///
/// # 边界与前提
/// - `None` 表示不限时(可无限阻塞)。
pub fn set_write_timeout(&self, timeout: Option<Duration>) -> io::Result<()> {
match self {
Conn::Unix(s) => s.set_write_timeout(timeout),
Conn::Tcp(s) => s.set_write_timeout(timeout),
}
}
/// 顺序写入全部字节。
///
/// # 边界与前提
/// - 空切片为无操作,恒成功。
/// - 已设超时且单次写入超时时返回 `Elapsed`(std 语义)。
pub fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
match self {
Conn::Unix(s) => s.write_all(buf),
Conn::Tcp(s) => s.write_all(buf),
}
}
}
impl Read for Conn {
/// 从连接读取字节,委托内臂。
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self {
Conn::Unix(s) => s.read(buf),
Conn::Tcp(s) => s.read(buf),
}
}
}
impl Write for Conn {
/// 向连接写入字节,委托内臂。
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self {
Conn::Unix(s) => s.write(buf),
Conn::Tcp(s) => s.write(buf),
}
}
/// 刷新连接,委托内臂。
fn flush(&mut self) -> io::Result<()> {
match self {
Conn::Unix(s) => s.flush(),
Conn::Tcp(s) => s.flush(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Read;
/// Unix 回环:临时目录 bind Unix 套接字,accept 后写读往返,六方法逐一可用。
#[test]
fn unix_loopback_roundtrip() {
let dir = std::env::temp_dir().join(format!("oxide_net_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("unix.sock");
let _ = std::fs::remove_file(&path);
let listener = UnixListener::bind(&path).unwrap();
let mut client = UnixStream::connect(&path).unwrap();
// 六方法逐一过一遍:非阻塞、accept、克隆、读写超时、write_all。
let listener = Listener::Unix(listener);
listener.set_nonblocking(false).unwrap();
let mut conn = listener.accept().unwrap();
let _peer = conn.try_clone().unwrap();
conn.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
conn.set_write_timeout(Some(Duration::from_secs(5))).unwrap();
conn.write_all(b"hello-oxide").unwrap();
let mut buf = [0u8; 11];
client.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"hello-oxide");
let _ = std::fs::remove_file(&path);
}
/// TCP 回环:bind `127.0.0.1:0`(端口 0 由操作系统分配空闲端口),
/// 经 `local_addr` 取端口,connect 后写读往返。
#[test]
fn tcp_loopback_roundtrip() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
let mut client = TcpStream::connect(addr).unwrap();
let listener = Listener::Tcp(listener);
listener.set_nonblocking(false).unwrap();
let mut conn = listener.accept().unwrap();
let _peer = conn.try_clone().unwrap();
conn.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
conn.set_write_timeout(Some(Duration::from_secs(5))).unwrap();
conn.write_all(b"hello-oxide").unwrap();
let mut buf = [0u8; 11];
client.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"hello-oxide");
}
/// 动态端口:TCP 臂绑 0 端口后 `local_addr` 文本形态含操作系统分配的非 0 端口。
#[test]
fn tcp_local_addr_dynamic_port() {
let listener = Listener::Tcp(TcpListener::bind("127.0.0.1:0").unwrap());
let text = listener.local_addr().unwrap();
let port: u16 = text.rsplit(':').next().expect("应含端口").parse().expect("端口应可解析");
assert_ne!(port, 0, "操作系统应分配非 0 端口:{text}");
}
/// Unix 臂 `local_addr` 返回套接字路径的文本形态(非空)。
#[test]
fn unix_local_addr() {
let dir = std::env::temp_dir().join(format!("oxide_net_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("unix_addr.sock");
let _ = std::fs::remove_file(&path);
let listener = Listener::Unix(UnixListener::bind(&path).unwrap());
let text = listener.local_addr().unwrap();
assert!(!text.is_empty(), "Unix 臂应得非空路径文本:{text}");
let _ = std::fs::remove_file(&path);
}
/// 非阻塞 accept 空轮:置非阻塞后无连接时 accept 应返回 `WouldBlock`(两臂各验一次)。
#[test]
fn nonblocking_accept_would_block() {
let dir = std::env::temp_dir().join(format!("oxide_net_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("unix_nb.sock");
let _ = std::fs::remove_file(&path);
let unix = Listener::Unix(UnixListener::bind(&path).unwrap());
unix.set_nonblocking(true).unwrap();
let err = unix.accept().unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::WouldBlock);
let tcp = Listener::Tcp(TcpListener::bind("127.0.0.1:0").unwrap());
tcp.set_nonblocking(true).unwrap();
let err = tcp.accept().unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::WouldBlock);
let _ = std::fs::remove_file(&path);
}
}