Unix Domain Socket 使用解析之 UDP 篇

300 阅读5分钟

Unix Domain Socket(UDS)中 UDP 类型的核心概念、使用方法及在 Android 中的应用场景,帮助理解其在本地进程间通信(IPC)中的高效性:

一、Unix Domain Socket(UDS)概述

1. 核心定位

UDS 是 Linux 系统中一种基于文件系统的进程间通信(IPC)机制,用于同一主机上的进程间数据交换。与网络 Socket 相比:

  • 无需网络协议栈:直接通过文件系统路径(如 /tmp/server.sock)通信,省去网络层开销(如校验和、序列号),性能更高。

  • 支持两种模式

    • SOCK_STREAM(类似 TCP) :面向连接,可靠传输。
    • SOCK_DGRAM(类似 UDP) :无连接,不可靠传输(本文重点)。

2. 典型应用场景

  • Android Framework Native 层:如 Zygote 与 SystemServer 通信、硬件服务间的数据交互。
  • 高性能本地通信:需低延迟、高吞吐量的场景(如进程间状态同步、日志服务)。
  • 替代传统 IPC:比管道(Pipe)、消息队列(Message Queue)更灵活,支持双向通信。

二、UDP 模式核心 API 与流程

1. 关键函数与数据结构

函数作用
socket()创建 UDS 套接字,domain=AF_UNIXtype=SOCK_DGRAM(UDP 模式)
bind()绑定套接字到本地文件路径(如 server.sock
sendto()向指定地址发送数据(无连接,需指定目标地址)
recvfrom()接收数据,获取发送方地址
close()关闭套接字

数据结构 sockaddr_un

c

struct sockaddr_un {
    sa_family_t sun_family; // 固定为 AF_UNIX
    char        sun_path[108]; // 套接字文件路径(如 "server.sock")
};

2. 通信流程(服务端 vs 客户端)

服务端步骤

  1. 创建套接字socket(AF_UNIX, SOCK_DGRAM, 0)

  2. 绑定地址:指定本地文件路径(如 server.sock),需先删除已存在的文件避免冲突。

  3. 循环接收数据:通过 recvfrom() 阻塞接收客户端消息,获取发送方地址后回复。

  4. 关闭套接字:通信结束后删除套接字文件,释放资源。

客户端步骤

  1. 创建套接字:同服务端。
  2. 绑定可选地址:可绑定本地路径(如 client.sock),或不绑定(系统自动分配临时地址)。
  3. 发送数据:通过 sendto() 向服务端地址发送消息,并等待回复。
  4. 关闭套接字:通信结束后关闭。

三、示例代码解析(C/C++ 实现)

1. 服务端(udpserver.cpp)

c

#include <sys/socket.h>
#include <sys/un.h>
#include <iostream>
#include <unistd.h>

const char* SOCKET_PATH = "server.sock";

int main() {
    // 1. 创建 UDP 套接字
    int fd = socket(AF_UNIX, SOCK_DGRAM, 0);
    if (fd < 0) { perror("socket failed"); return -1; }

    // 2. 绑定本地地址
    struct sockaddr_un addr;
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strcpy(addr.sun_path, SOCKET_PATH);
    
    // 移除已存在的套接字文件
    unlink(SOCKET_PATH); 
    if (bind(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
        perror("bind failed"); return -1;
    }

    // 3. 循环接收数据
    struct sockaddr_un clientAddr;
    socklen_t clientLen = sizeof(clientAddr);
    char buf[1024] = {0};

    while (true) {
        int len = recvfrom(fd, buf, sizeof(buf), 0, 
                          (struct sockaddr*)&clientAddr, &clientLen);
        if (len < 0) { perror("recv failed"); break; }
        
        std::cout << "接收来自 " << clientAddr.sun_path << ": " << buf << std::endl;
        
        // 回复客户端
        const char* reply = "OK, received!";
        sendto(fd, reply, strlen(reply), 0, 
               (struct sockaddr*)&clientAddr, clientLen);
        
        if (strcmp(buf, "quit") == 0) { // 接收退出指令
            std::cout << "服务端退出" << std::endl;
            break;
        }
    }

    // 4. 清理资源
    close(fd);
    unlink(SOCKET_PATH);
    return 0;
}

2. 客户端(udpclient.cpp)

c

#include <sys/socket.h>
#include <sys/un.h>
#include <iostream>
#include <unistd.h>

const char* SERVER_PATH = "server.sock";
const char* CLIENT_PATH = "client.sock";

int main() {
    // 1. 创建 UDP 套接字
    int fd = socket(AF_UNIX, SOCK_DGRAM, 0);
    if (fd < 0) { perror("socket failed"); return -1; }

    // 2. 绑定客户端地址(可选,用于服务端回复)
    struct sockaddr_un clientAddr;
    memset(&clientAddr, 0, sizeof(clientAddr));
    clientAddr.sun_family = AF_UNIX;
    strcpy(clientAddr.sun_path, CLIENT_PATH);
    unlink(CLIENT_PATH); // 移除已存在文件
    bind(fd, (struct sockaddr*)&clientAddr, sizeof(clientAddr));

    // 3. 发送消息给服务端
    struct sockaddr_un serverAddr;
    memset(&serverAddr, 0, sizeof(serverAddr));
    serverAddr.sun_family = AF_UNIX;
    strcpy(serverAddr.sun_path, SERVER_PATH);

    const char* msg = "Hello, Unix UDP!";
    sendto(fd, msg, strlen(msg), 0, 
           (struct sockaddr*)&serverAddr, sizeof(serverAddr));

    // 4. 接收服务端回复
    char buf[1024] = {0};
    socklen_t serverLen = sizeof(serverAddr);
    int len = recvfrom(fd, buf, sizeof(buf), 0, 
                      (struct sockaddr*)&serverAddr, &serverLen);
    std::cout << "接收回复: " << buf << std::endl;

    // 5. 发送退出指令
    sendto(fd, "quit", 4, 0, (struct sockaddr*)&serverAddr, sizeof(serverAddr));
    
    // 6. 清理资源
    close(fd);
    unlink(CLIENT_PATH);
    return 0;
}

四、关键特性与注意事项

1. 与 TCP 模式的区别

特性UDP(SOCK_DGRAM)TCP(SOCK_STREAM)
连接性无连接(无需 connect()/listen()/accept()面向连接(需三次握手)
可靠性不可靠(可能丢包、乱序)可靠(保证顺序、重传机制)
数据边界保留消息边界(recvfrom() 一次读取完整数据包)流式无边界(需手动处理分包)
适用场景实时通信、非关键数据传输(如状态通知)可靠数据传输(如文件传输、长连接服务)

2. 注意事项

  • 文件路径管理

    • 服务端绑定前需删除已存在的套接字文件(unlink(path)),避免 bind() 失败。
    • 路径长度需小于 sun_path 数组长度(通常 108 字节)。
  • 非阻塞模式
    通过 fcntl(fd, F_SETFL, O_NONBLOCK) 设置非阻塞,避免 recvfrom() 阻塞线程。

  • 权限问题
    套接字文件权限由 umask 控制,建议设置为 0777 确保可访问。

五、在 Android 中的应用场景

1. Native 层通信

  • Zygote 与 SystemServer:通过 UDS 传递启动参数(如 zygote socket),实现进程间快速通信。
  • 硬件服务:如传感器服务(SensorService)与 HAL 层通过 UDS 传输数据,避免 Binder 开销。

2. 与 Java 层交互

Android 提供 LocalSocket/LocalServerSocket 类(Java 层),底层基于 UDS 实现,用法类似:

java

// Java 层示例(客户端)
LocalSocket socket = new LocalSocket();
LocalSocketAddress address = new LocalSocketAddress("server.sock", LocalSocketAddress.Namespace.RESERVED);
socket.connect(address);
OutputStream out = socket.getOutputStream();
out.write("Hello".getBytes());

六、总结:UDS UDP 的核心优势

  • 高效性:绕过网络协议栈,适合本地高频通信。

  • 简单性:无连接模式,代码结构比 TCP 更简洁,适合轻量级场景。

  • 灵活性:支持双向通信,可同时作为客户端和服务端。

类比说明:UDS UDP 如同「同一办公室内的对讲机」,无需通过互联网(网络协议栈),直接通过本地路径(办公室房间号)快速传递消息,适合实时且不要求可靠到达的场景(如同事间临时沟通)。在 Android 中,它是 Native 层高效 IPC 的「秘密武器」,尤其适合替代部分 Binder 通信,降低系统开销。