Unix Domain Socket (UDS)

开篇故事

想象你在一家大公司工作。如果两个部门需要通信,传统方式是打电话(网络 Socket)——即使他们在同一栋楼里。但更高效的方式是直接走到对方办公室(Unix Domain Socket)——因为他们在同一台机器上,无需经过外部网络。

Unix Domain Socket(UDS)是同一台机器上进程间通信(IPC)的高效方式。它使用文件系统路径作为地址,比 TCP/IP 快得多,因为数据不需要经过网络协议栈。


本章适合谁

如果你想学习:

  • 如何在同一台机器上实现高效的进程间通信
  • Unix Domain Socket 的工作原理
  • 如何设计自定义二进制协议

本章适合你。UDS 是本地 IPC 的首选方案。


你会学到什么

完成本章后,你可以:

  1. 理解 Unix Domain Socket 的核心概念
  2. 使用 std::os::unix::net 创建 UDS 服务端
  3. 使用 std::os::unix::net 创建 UDS 客户端
  4. 实现自定义二进制协议(长度前缀 + 负载)
  5. 使用父进程编排多个子进程

前置要求


依赖安装

UDS 使用 Rust 标准库,无需额外依赖:

use std::os::unix::net::{UnixListener, UnixStream};

第一个例子

最简单的 UDS 服务端:

use std::fs;
use std::os::unix::net::UnixListener;
use std::io;

fn main() -> io::Result<()> {
    let socket_path = "/tmp/hello.socket";
    
    // 清理已存在的 socket 文件
    let _ = fs::remove_file(socket_path);
    
    // 创建并绑定 socket
    let listener = UnixListener::bind(socket_path)?;
    println!("UDS 服务端启动在 {}", socket_path);
    
    // 接受连接
    match listener.accept() {
        Ok((stream, _addr)) => {
            println!("客户端已连接");
            // 处理客户端请求...
        }
        Err(e) => eprintln!("接受连接失败: {}", e),
    }
    
    // 清理
    fs::remove_file(socket_path)?;
    Ok(())
}

💡 注意:此代码需要服务端和客户端配合运行。请使用 cargo run --bin uds_server 和 cargo run --bin uds_client 进行完整测试。

完整示例:


原理解析

UDS 架构概览

┌─────────────────────────────────────────────────────────────────────┐
│                    Unix Domain Socket 架构                           │
│                                                                      │
│   ┌──────────────┐         ┌──────────────┐         ┌──────────────┐│
│   │   Parent     │         │   Server     │         │   Client     ││
│   │  (编排进程)   │────────→│  (UDS 监听)   │←────────│  (UDS 连接)   ││
│   │              │  spawn   │              │  accept  │              ││
│   └──────────────┘         └──────┬───────┘         └──────────────┘│
│                                   │                                  │
│   通信协议:                                                                │
│   ┌─────────────────────────────────────────────────────────────────┐ │
│   │ 4 字节大端长度前缀 + UTF-8 负载                                   │ │
│   │ ┌─────────────┐ ┌───────────────────────────────────────────┐   │ │
│   │ │ Length (4B) │ │ Payload (UTF-8 string)                    │   │ │
│   │ │ 0x00000005  │ │ "hello"                                   │   │ │
│   │ └─────────────┘ └───────────────────────────────────────────┘   │ │
│   └─────────────────────────────────────────────────────────────────┘ │
│                                                                      │
│   Socket 路径: /tmp/hello.socket                                     │
└─────────────────────────────────────────────────────────────────────┘

自定义二进制协议

协议格式:

┌─────────────┬──────────────────────────────────────────┐
│ Length (4B) │ Payload (UTF-8 string)                   │
│ 大端序       │ 可变长度                                  │
└─────────────┴──────────────────────────────────────────┘

为什么使用长度前缀?

  • 接收方知道要读取多少字节
  • 避免粘包问题
  • 支持可变长度消息

UDS 服务端实现

use std::fs;
use std::io::{self, Read, Write};
use std::os::unix::net::{UnixListener, UnixStream};

fn handle_client(mut stream: UnixStream) -> io::Result<()> {
    loop {
        // 1. 读取长度前缀(4 字节大端序)
        let mut length_bytes = [0u8; 4];
        stream.read_exact(&mut length_bytes)?;
        let length = u32::from_be_bytes(length_bytes) as usize;

        // 2. 读取负载
        let mut payload = vec![0u8; length];
        stream.read_exact(&mut payload)?;
        let message = String::from_utf8(payload).expect("Invalid UTF-8");

        // 3. 处理消息(反转字符串)
        let response = if message == "done" {
            String::from("ok")
        } else {
            message.chars().rev().collect()
        };

        // 4. 发送响应
        let resp_payload = response.as_bytes();
        stream.write_all(&u32::to_be_bytes(resp_payload.len() as u32))?;
        stream.write_all(resp_payload)?;
        stream.flush()?;

        if message == "done" {
            break;
        }
    }
    Ok(())
}

fn main() -> io::Result<()> {
    let socket_path = "/tmp/hello.socket";
    
    // 清理已存在的 socket 文件
    let _ = fs::remove_file(socket_path);
    
    // 创建并绑定 socket
    let listener = UnixListener::bind(socket_path)?;
    println!("服务端启动在 {}", socket_path);
    
    // 接受连接并处理
    match listener.accept() {
        Ok((stream, _addr)) => handle_client(stream)?,
        Err(e) => eprintln!("接受连接失败: {}", e),
    }
    
    // 清理
    fs::remove_file(socket_path)?;
    Ok(())
}

💡 注意:此代码需要服务端配合运行。请使用 cargo run --bin uds_server 进行完整测试。

UDS 客户端实现

use std::io::{self, Read, Write};
use std::os::unix::net::UnixStream;

fn main() -> io::Result<()> {
    let socket_path = "/tmp/hello.socket";
    let messages = vec!["hello", "world", "rust", "done"];

    // 连接到服务端
    let mut stream = UnixStream::connect(socket_path)?;

    for msg in messages {
        // 1. 发送:长度前缀 + 负载
        let payload = msg.as_bytes();
        stream.write_all(&u32::to_be_bytes(payload.len() as u32))?;
        stream.write_all(payload)?;
        stream.flush()?;

        // 2. 接收:长度前缀 + 响应
        let mut length_bytes = [0u8; 4];
        stream.read_exact(&mut length_bytes)?;
        let length = u32::from_be_bytes(length_bytes) as usize;
        let mut resp_payload = vec![0u8; length];
        stream.read_exact(&mut resp_payload)?;
        let resp_str = String::from_utf8(resp_payload).expect("Invalid UTF-8");
        
        println!("收到响应: {}", resp_str);
    }

    Ok(())
}

💡 注意:此代码需要服务端配合运行。请使用 cargo run --bin uds_client 进行完整测试。

父进程编排

use std::fs;
use std::io;
use std::process::{Command, Stdio};
use std::thread;
use std::time::Duration;

fn main() -> io::Result<()> {
    let socket_path = "/tmp/hello.socket";
    
    // 确保 socket 文件干净
    let _ = fs::remove_file(socket_path);
    
    // 启动服务端
    let mut server = Command::new("./uds_server")
        .stdout(Stdio::inherit())
        .spawn()?;
    
    // 等待服务端绑定
    thread::sleep(Duration::from_millis(500));
    
    // 启动客户端
    let mut client = Command::new("./uds_client")
        .stdout(Stdio::inherit())
        .spawn()?;
    
    // 等待完成
    client.wait()?;
    server.wait()?;
    
    // 清理
    let _ = fs::remove_file(socket_path);
    
    Ok(())
}

💡 注意:此代码需要服务端和客户端配合运行。请使用 cargo run --bin uds_parent 进行完整测试。

UDS vs TCP 对比

特性UDSTCP
通信范围同一台机器跨网络
性能更快(无需网络协议栈)较慢
地址格式文件系统路径IP:Port
安全性文件权限控制防火墙/ACL
适用场景本地 IPC远程通信

常见错误

错误 1: 未清理已存在的 socket 文件

// ❌ 错误:直接绑定,可能失败
let listener = UnixListener::bind("/tmp/hello.socket")?;

// ✅ 正确:先清理
let _ = fs::remove_file("/tmp/hello.socket");
let listener = UnixListener::bind("/tmp/hello.socket")?;

错误 2: 未处理 UTF-8 转换错误

// ❌ 错误:直接 unwrap
let message = String::from_utf8(payload).unwrap();

// ✅ 正确:处理错误
let message = match String::from_utf8(payload) {
    Ok(s) => s,
    Err(e) => {
        eprintln!("Invalid UTF-8: {}", e);
        return;
    }
};

错误 3: 未等待服务端绑定

// ❌ 错误:立即启动客户端
let mut server = Command::new("./uds_server").spawn()?;
let mut client = Command::new("./uds_client").spawn()?; // 可能连接失败!

// ✅ 正确:等待服务端就绪
let mut server = Command::new("./uds_server").spawn()?;
thread::sleep(Duration::from_millis(500)); // 或使用更复杂的就绪检测
let mut client = Command::new("./uds_client").spawn()?;

动手练习

练习 1: 修改协议为 JSON

将当前的字符串反转协议改为 JSON 格式:

// TODO: 定义请求/响应结构体
// #[derive(Serialize, Deserialize)]
// struct Request { action: String, data: String }
// struct Response { result: String }

// TODO: 修改 handle_client 使用 JSON 序列化/反序列化
点击查看答案
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
struct Request { action: String, data: String }

#[derive(Serialize, Deserialize)]
struct Response { result: String }

fn handle_client(mut stream: UnixStream) -> io::Result<()> {
    loop {
        let mut length_bytes = [0u8; 4];
        stream.read_exact(&mut length_bytes)?;
        let length = u32::from_be_bytes(length_bytes) as usize;
        
        let mut payload = vec![0u8; length];
        stream.read_exact(&mut payload)?;
        
        let request: Request = serde_json::from_slice(&payload).expect("Invalid JSON");
        
        let response = Response {
            result: request.data.chars().rev().collect(),
        };
        
        let resp_payload = serde_json::to_vec(&response).expect("Serialization failed");
        stream.write_all(&u32::to_be_bytes(resp_payload.len() as u32))?;
        stream.write_all(&resp_payload)?;
        stream.flush()?;
    }
    Ok(())
}

小结

核心要点:

  1. UDS 是同一台机器上进程间通信的高效方式
  2. 自定义协议 使用长度前缀避免粘包问题
  3. 父进程编排 使用 Command 启动和管理子进程
  4. 清理 socket 文件 是必要的,否则绑定会失败

关键术语:

English中文说明
Unix Domain SocketUnix 域套接字本地 IPC 机制
Length Prefix长度前缀避免粘包问题
Parent Process父进程编排子进程的进程
Child Process子进程被父进程启动的进程
Big-Endian大端序网络字节序

下一步:


术语表

English中文
Unix Domain SocketUnix 域套接字
IPC进程间通信
Length Prefix长度前缀
Payload负载/消息体
Big-Endian大端序
Parent Process父进程
Child Process子进程
Bind绑定
Accept接受连接
Connect连接

完整示例:


继续学习

💡 记住:UDS 比 TCP 快得多,因为数据不需要经过网络协议栈。在同一台机器上的进程间通信,优先选择 UDS!