Unix Domain Socket 使用解析之 TCP 篇

348 阅读5分钟

Unix Domain Socket(UDS)中 TCP 类型的核心概念、使用流程及在 Android 中的应用场景,解析其与 UDP 模式的差异及高效通信原理:

一、UDS TCP 模式的核心定位与优势

1. 什么是 UDS TCP?

UDS(Unix Domain Socket)是 Linux 系统中用于本地进程间通信(IPC)的高效机制,TCP 模式(SOCK_STREAM)提供面向连接的可靠数据传输,类似网络 TCP 但无需经过网络协议栈,直接通过文件系统路径(如 /tmp/server.sock)通信。
核心优势

  • 可靠性:基于流式传输,保证数据顺序和完整性,支持流量控制和错误重传。
  • 高效性:无需网络层开销(如 IP 协议、校验和),性能优于传统网络 Socket。
  • 适用场景:需稳定传输的场景,如文件传输、长连接服务、复杂协议交互。

2. 与 UDS UDP 的区别

特性TCP(SOCK_STREAM)UDP(SOCK_DGRAM)
连接性面向连接(需 connect()/accept()无连接(直接 sendto()/recvfrom()
可靠性可靠(保证顺序,重传丢失数据)不可靠(可能丢包、乱序)
数据边界无边界(流式,需手动分包)有边界(每次 recvfrom() 读取完整数据包)
典型场景文件传输、实时聊天、配置同步状态通知、心跳包、非关键数据传输

二、TCP 模式核心流程与 API

1. 服务端关键流程

  1. 创建套接字

    c

    int fd = socket(AF_UNIX, SOCK_STREAM, 0); // AF_UNIX 表示 UDS,SOCK_STREAM 为 TCP 模式
    
  2. 绑定本地地址

    • 需指定一个文件路径(如 server.sock),绑定前需删除已存在的文件以避免冲突。

    c

    struct sockaddr_un addr;
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strcpy(addr.sun_path, "server.sock");
    unlink("server.sock"); // 删除已存在文件
    bind(fd, (struct sockaddr*)&addr, sizeof(addr));
    
  3. 监听连接

    c

    listen(fd, 12); // 第二个参数为最大等待连接数
    
  4. 接受连接

    c

    struct sockaddr_un clientAddr;
    socklen_t clientLen = sizeof(clientAddr);
    int connFd = accept(fd, (struct sockaddr*)&clientAddr, &clientLen); // 阻塞等待客户端连接
    
  5. 数据读写

    c

    char buf[1024];
    ssize_t readLen = recv(connFd, buf, sizeof(buf), 0); // 读取数据
    send(connFd, "OK", 2, 0); // 发送回复
    
  6. 关闭连接

    c

    close(connFd);
    close(fd);
    unlink("server.sock"); // 清理套接字文件
    

2. 客户端关键流程

  1. 创建套接字

    c

    int fd = socket(AF_UNIX, SOCK_STREAM, 0);
    
  2. 绑定可选本地地址(非必需,系统可自动分配):

    c

    struct sockaddr_un clientAddr;
    strcpy(clientAddr.sun_path, "client.sock");
    unlink("client.sock");
    bind(fd, (struct sockaddr*)&clientAddr, sizeof(clientAddr));
    
  3. 连接服务端

    c

    struct sockaddr_un serverAddr;
    strcpy(serverAddr.sun_path, "server.sock");
    connect(fd, (struct sockaddr*)&serverAddr, sizeof(serverAddr)); // 阻塞连接服务端
    
  4. 数据读写

    c

    send(fd, "Hello", 5, 0); // 发送数据
    recv(fd, buf, sizeof(buf), 0); // 接收回复
    
  5. 关闭连接

    c

    close(fd);
    unlink("client.sock"); // 若绑定了本地地址,需清理文件
    

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

1. 服务端(tcpserver.cpp)

c

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

const char* SOCKET_PATH = "/tmp/localserver.sock";

int main() {
    // 1. 创建 TCP 套接字
    int fd = socket(AF_UNIX, SOCK_STREAM, 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. 开始监听
    if (listen(fd, 5) < 0) { // 最大 5 个等待连接
        perror("listen failed"); return -1;
    }

    // 4. 接受连接
    struct sockaddr_un clientAddr;
    socklen_t clientLen = sizeof(clientAddr);
    int connFd = accept(fd, (struct sockaddr*)&clientAddr, &clientLen);
    if (connFd < 0) { perror("accept failed"); return -1; }

    // 5. 循环通信
    char buf[128] = {0};
    while (recv(connFd, buf, sizeof(buf), 0) > 0) {
        std::cout << "接收: " << buf << std::endl;
        send(connFd, "OK", 2, 0); // 回复确认
        memset(buf, 0, sizeof(buf));
    }

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

2. 客户端(tcpclient.cpp)

c

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

const char* SERVER_PATH = "/tmp/localserver.sock";

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

    // 2. 连接服务端
    struct sockaddr_un serverAddr;
    memset(&serverAddr, 0, sizeof(serverAddr));
    serverAddr.sun_family = AF_UNIX;
    strcpy(serverAddr.sun_path, SERVER_PATH);
    if (connect(fd, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) < 0) {
        perror("connect failed"); return -1;
    }

    // 3. 发送数据并接收回复
    const char* msg = "Hello, TCP!";
    send(fd, msg, strlen(msg), 0);
    char buf[128] = {0};
    if (recv(fd, buf, sizeof(buf), 0) > 0) {
        std::cout << "接收回复: " << buf << std::endl;
    }

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

四、关键注意事项

  1. 套接字文件管理

    • 服务端绑定前必须删除已存在的套接字文件(unlink(path)),否则 bind() 会失败。
    • 文件权限由 umask 控制,建议设置为 0777 确保可访问。
  2. 阻塞与非阻塞模式

    • 默认 accept()/connect()/recv()/send() 为阻塞模式,可通过 fcntl(fd, F_SETFL, O_NONBLOCK) 设置非阻塞。
  3. 数据分包处理

    • TCP 是流式协议,需手动处理分包(如通过头部标识消息长度),避免一次 recv() 读取不完整数据。
  4. 连接队列长度

    • listen(int fd, int backlog) 中的 backlog 表示最大未接受连接数,需根据并发量合理设置(如 5-10)。

五、在 Android 中的应用场景

1. Native 层通信

  • Zygote 与 SystemServer:Android 系统启动时,Zygote 通过 UDS TCP 向 SystemServer 发送启动参数,确保可靠传输。
  • 硬件服务与 Framework:如摄像头服务(CameraService)通过 UDS 与 HAL 层建立长连接,传输图像数据。

2. 替代 Binder 的场景

  • 需高吞吐量或避免 Binder 复杂开销时(如内部测试工具、日志服务),UDS TCP 提供更轻量级的可靠通信。

3. Java 层适配

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

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 TCP 的核心价值

  • 可靠高效:适合本地进程间需要稳定传输的场景,如文件传输、配置同步。

  • 流程清晰:基于连接的模式,代码结构规范,易于调试和维护。

  • 系统级应用:在 Android 底层服务中广泛使用,是理解系统启动和 IPC 机制的关键。

类比说明:UDS TCP 如同「办公室内的有线电话」,需先建立连接(拨号),通话过程中数据按顺序可靠传输,适合需要稳定沟通的场景(如重要文件传输)。对比 UDP 的「对讲机」模式,TCP 更适合对可靠性要求高的任务,是 Android 系统中 Native 层通信的重要工具。