【网络编程】UNIX 域套接字(Unix Domain Sockets, UDS)

UNIX 域套接字

UNIX 域套接字 (UDS, Unix Domain Socket)是一种 本地进程间通信(IPC) 方式,适用于同一台机器上的进程之间的通信。相比 TCP/IP 套接字,UDS 效率更高、延迟更低,因为它省去了网络协议的开销。

🔹 UDS 特点

  1. 仅限本机通信(不能用于跨网络通信)。
  2. 速度快(不像 TCP/IP 需要数据封装和解析)。
  3. 支持
    • 流式套接字(SOCK_STREAM)(类似 TCP)。
    • 数据报套接字(SOCK_DGRAM)(类似 UDP)。
  4. 本地地址由文件路径标识 (如 /tmp/mysocket)。

🔹1. UNIX 域流式套接字(SOCK_STREAM)

适用于 前后台进程通信、RPC 调用、数据库访问 等场景。支持多个客户端连接,服务器能正确接收 & 发送数据。

服务器端
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>

#define SOCKET_PATH "/tmp/mysocket"

int main() {
    int server_fd, client_fd;
    struct sockaddr_un server_addr, client_addr;
    socklen_t client_len = sizeof(client_addr);
    char buffer[256];

    // 1️⃣ 创建流式套接字
    server_fd = socket(PF_UNIX, SOCK_STREAM, 0);
    if (server_fd < 0) {
        perror("❌ socket 创建失败");
        exit(EXIT_FAILURE);
    }

    // 2️⃣ 绑定本地地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sun_family = PF_UNIX;
    strncpy(server_addr.sun_path, SOCKET_PATH, sizeof(server_addr.sun_path) - 1);
    
    unlink(SOCKET_PATH); // 删除旧的套接字文件,防止 bind 失败
    if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("❌ 绑定失败");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 3️⃣ 监听连接请求
    listen(server_fd, 5);
    printf("监听 UNIX 域套接字: %s\n", SOCKET_PATH);

    // 4️⃣ 接受客户端连接
    client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);
    if (client_fd < 0) {
        perror("❌ accept 失败");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 5️⃣ 读取 & 发送数据
    read(client_fd, buffer, sizeof(buffer));
    printf("📩 收到消息: %s\n", buffer);
    write(client_fd, "✅ 服务器收到消息", 20);

    // 6️⃣ 关闭套接字
    close(client_fd);
    close(server_fd);
    unlink(SOCKET_PATH);
    return 0;
}
客户端
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>

#define SOCKET_PATH "/tmp/mysocket"

int main() {
    int client_fd;
    struct sockaddr_un server_addr;
    char buffer[256] = "你好,服务器!";

    // 1️⃣ 创建流式套接字
    client_fd = socket(PF_UNIX, SOCK_STREAM, 0);
    if (client_fd < 0) {
        perror("❌ socket 创建失败");
        exit(EXIT_FAILURE);
    }

    // 2️⃣ 连接服务器
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sun_family = PF_UNIX;
    strncpy(server_addr.sun_path, SOCKET_PATH, sizeof(server_addr.sun_path) - 1);

    if (connect(client_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("❌ 连接失败");
        close(client_fd);
        exit(EXIT_FAILURE);
    }

    // 3️⃣ 发送 & 接收数据
    write(client_fd, buffer, strlen(buffer));
    read(client_fd, buffer, sizeof(buffer));
    printf("📩 服务器回复: %s\n", buffer);

    // 4️⃣ 关闭套接字
    close(client_fd);
    return 0;
}

🔹 2. UNIX 域数据报套接字(SOCK_DGRAM)

适用于 非连接通信(类似 UDP) ,如日志采集、进程间通知。

服务器端
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>

#define SOCKET_PATH "/tmp/mysocket_dgram"
#define BUFFER_SIZE 256

int main() {
    int server_fd;
    struct sockaddr_un server_addr, client_addr;
    socklen_t client_len = sizeof(client_addr);
    char buffer[BUFFER_SIZE];

    // 1️⃣ 创建数据报套接字
    server_fd = socket(PF_UNIX, SOCK_DGRAM, 0);
    if (server_fd < 0) {
        perror("❌ socket 创建失败");
        exit(EXIT_FAILURE);
    }

    // 2️⃣ 绑定本地地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sun_family = PF_UNIX;
    strncpy(server_addr.sun_path, SOCKET_PATH, sizeof(server_addr.sun_path) - 1);

    unlink(SOCKET_PATH);
    if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("❌ 绑定失败");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    printf("监听数据报套接字: %s\n", SOCKET_PATH);

    // 3️⃣ 接收数据
    recvfrom(server_fd, buffer, BUFFER_SIZE, 0, (struct sockaddr*)&client_addr, &client_len);
    printf("📩 收到消息: %s\n", buffer);

    // 4️⃣ 发送回复
    sendto(server_fd, "✅ 服务器收到消息", 20, 0, (struct sockaddr*)&client_addr, client_len);

    // 5️⃣ 关闭套接字
    close(server_fd);
    unlink(SOCKET_PATH);
    return 0;
}
客户端
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>

#define SERVER_SOCKET_PATH "/tmp/mysocket_dgram"
#define CLIENT_SOCKET_PATH "/tmp/mysocket_dgram_client"
#define BUFFER_SIZE 256

int main() {
    int client_fd;
    struct sockaddr_un server_addr, client_addr;
    char buffer[BUFFER_SIZE] = "你好,数据报服务器!";

    // 1️⃣ 创建数据报套接字
    client_fd = socket(PF_UNIX, SOCK_DGRAM, 0);
    if (client_fd < 0) {
        perror("❌ socket 创建失败");
        exit(EXIT_FAILURE);
    }

    // 2️⃣ 绑定客户端地址(可选)
    memset(&client_addr, 0, sizeof(client_addr));
    client_addr.sun_family = PF_UNIX;
    strncpy(client_addr.sun_path, CLIENT_SOCKET_PATH, sizeof(client_addr.sun_path) - 1);
    unlink(CLIENT_SOCKET_PATH);
    bind(client_fd, (struct sockaddr*)&client_addr, sizeof(client_addr));

    // 3️⃣ 发送数据
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sun_family = PF_UNIX;
    strncpy(server_addr.sun_path, SERVER_SOCKET_PATH, sizeof(server_addr.sun_path) - 1);

    sendto(client_fd, buffer, strlen(buffer), 0, (struct sockaddr*)&server_addr, sizeof(server_addr));

    // 4️⃣ 接收服务器回复
    recvfrom(client_fd, buffer, BUFFER_SIZE, 0, NULL, NULL);
    printf("📩 服务器回复: %s\n", buffer);

    // 5️⃣ 关闭套接字
    close(client_fd);
    unlink(CLIENT_SOCKET_PATH);
    return 0;
}
编译 & 运行 UNIX 域套接字:
  1. 编译服务器端和客户端

使用 gcc 进行编译:

bash 复制代码
gcc unix_socket_server.c -o unix_socket_server
gcc unix_socket_client.c -o unix_socket_client

如果没有编译错误,将会生成 unix_socket_serverunix_socket_client 可执行文件。

  1. 运行服务器
bash 复制代码
./unix_socket_server

预期输出

bash 复制代码
监听 UNIX 域套接字: /tmp/mysocket

此时服务器将等待客户端连接。

  1. 运行客户端

在另一个终端窗口运行:

bash 复制代码
./unix_socket_client

预期输出

bash 复制代码
📩 服务器回复: ✅ 服务器收到消息

服务器端的终端应显示:

bash 复制代码
📩 收到消息: 你好,服务器!
  1. 测试多个客户端

多个终端 运行:

bash 复制代码
./unix_socket_client

服务器端应能处理多个客户端连接,每次连接后显示:

bash 复制代码
📩 收到消息: 你好,服务器!
  1. 测试过程中可能出现的异常情况

🚨 5.1 服务器未启动时运行客户端

bash 复制代码
./unix_socket_client

预期错误:

bash 复制代码
❌ connect 失败: No such file or directory

解决方案:先运行 ./unix_socket_server

🚨 5.2 服务器端 bind() 失败

如果服务器 未正常退出 ,则 bind() 可能失败:

bash 复制代码
❌ bind 失败: Address already in use

解决方案:删除旧的套接字文件:

bash 复制代码
rm -f /tmp/mysocket

然后重新运行服务器:

bash 复制代码
./unix_socket_server

🚨 5.3 Too many open files

如果服务器长时间运行,可能会遇到:

bash 复制代码
❌ accept 失败: Too many open files

✅ 解决方案:

  • 设置更高的文件描述符限制:
bash 复制代码
ulimit -n 65535
  • 确保 close(client_fd); 在服务器端被正确调用。
  1. 关闭服务器

按 Ctrl + C 终止服务器。

如果服务器异常退出,建议手动删除 /tmp/mysocket 文件:

bash 复制代码
rm -f /tmp/mysocket

关于如何处理UNIX域套接字的错误

UNIX 域套接字错误处理指南

📌 1. 常见错误及解决方案

在使用 UNIX 域套接字(Unix Domain Sockets, UDS) 进行 本地 IPC 通信 时,可能会遇到各种错误。以下是常见错误及其解决方法。

🚨 1.1 bind() 失败 - Address already in use

  • 发生原因
    • 旧的套接字文件仍然存在 ,导致 bind() 失败。
    • 上次程序异常退出,未正确 unlink() 套接字文件。

✅ 解决方案:

bind() 之前 删除旧的套接字文件

c 复制代码
unlink("/tmp/mysocket");  // 删除已有的套接字文件

如下示例:

c 复制代码
struct sockaddr_un server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sun_family = PF_UNIX;
strcpy(server_addr.sun_path, "/tmp/mysocket");

unlink("/tmp/mysocket");  // 删除旧文件,避免 "Address already in use"

if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
    perror("❌ bind 失败");
    exit(EXIT_FAILURE);
}

🚨 1.2 connect() 失败 - No such file or directory

  • 发生原因
    • 服务器套接字文件不存在bind() 失败或者未运行)。
    • 路径拼写错误 (如 /tmp/mysocket 被写成 /tmp/mysocke)。

✅ 解决方案:
检查服务器是否成功创建了套接字文件

bash 复制代码
ls -l /tmp/mysocket  # 确保服务器创建了该文件

确保路径正确

c 复制代码
struct sockaddr_un client_addr;
memset(&client_addr, 0, sizeof(client_addr));
client_addr.sun_family = PF_UNIX;
strcpy(client_addr.sun_path, "/tmp/mysocket");  // 确保路径正确

if (connect(sockfd, (struct sockaddr*)&client_addr, sizeof(client_addr)) < 0) {
    perror("❌ connect 失败");
    exit(EXIT_FAILURE);
}

🚨 1.3 accept() 失败 - Too many open files

  • 发生原因
    • 文件描述符使用过多 ,达到系统 ulimit 限制(默认 1024)。
    • 调用 close() 失败,导致文件描述符泄漏。

✅ 解决方案:
查询当前进程的文件描述符限制

bash 复制代码
ulimit -n

临时增加文件描述符限制

bash 复制代码
ulimit -n 65535

在服务器端正确关闭 accept() 返回的 client_fd

c 复制代码
int client_fd = accept(server_fd, NULL, NULL);
if (client_fd < 0) {
    perror("❌ accept 失败");
} else {
    // 处理客户端请求...
    close(client_fd);  // 关闭连接,防止文件描述符泄漏
}

🚨 1.4 recv() / send() 失败 - Broken pipe

  • 发生原因
    • 客户端突然断开连接 ,但服务器仍然尝试 send() 数据。
    • SIGPIPE 信号未被处理,导致进程崩溃。

✅ 解决方案:
忽略 SIGPIPE 信号

c 复制代码
signal(SIGPIPE, SIG_IGN);  // 忽略 SIGPIPE,防止进程崩溃

检测 send() 返回值

c 复制代码
ssize_t bytes_sent = send(sockfd, data, strlen(data), 0);
if (bytes_sent < 0) {
    perror("❌ send 失败");
}

检测 recv() 返回值

c 复制代码
ssize_t bytes_received = recv(sockfd, buffer, sizeof(buffer), 0);
if (bytes_received == 0) {
    printf("客户端已断开连接\n");
} else if (bytes_received < 0) {
    perror("❌ recv 失败");
}

🚨 1.5 recvfrom() 失败 - Invalid argument

  • 发生原因
    • 在数据报(SOCK_DGRAM)模式下,未正确绑定地址。
    • 调用 recvfrom() 时,传递了错误的 socklen_t 值。

✅ 解决方案:
确保服务器端 bind() 了正确的地址

c 复制代码
struct sockaddr_un server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sun_family = PF_UNIX;
strcpy(server_addr.sun_path, "/tmp/mysocket_dgram");

unlink("/tmp/mysocket_dgram");  // 防止地址被占用
if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
    perror("❌ bind 失败");
    exit(EXIT_FAILURE);
}

确保 recvfrom() 传递正确的 socklen_t 值

c 复制代码
struct sockaddr_un client_addr;
socklen_t client_len = sizeof(client_addr);
recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr*)&client_addr, &client_len);
UNIX 域套接字完整错误处理示例
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/socket.h>
#include <sys/un.h>

#define SOCKET_PATH "/tmp/mysocket"

void handle_sigpipe(int signo) {
    printf("SIGPIPE 信号,客户端可能已断开\n");
}

int main() {
    int server_fd, client_fd;
    struct sockaddr_un server_addr, client_addr;
    socklen_t client_len = sizeof(client_addr);
    char buffer[256];

    // 1️⃣ 忽略 SIGPIPE,防止 send() 失败时进程崩溃
    signal(SIGPIPE, handle_sigpipe);

    // 2️⃣ 创建套接字
    server_fd = socket(PF_UNIX, SOCK_STREAM, 0);
    if (server_fd < 0) {
        perror("❌ socket 创建失败");
        exit(EXIT_FAILURE);
    }

    // 3️⃣ 绑定地址
    unlink(SOCKET_PATH);
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sun_family = PF_UNIX;
    strncpy(server_addr.sun_path, SOCKET_PATH, sizeof(server_addr.sun_path) - 1);

    if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("❌ bind 失败");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 4️⃣ 监听连接
    listen(server_fd, 5);
    printf("监听 UNIX 域套接字: %s\n", SOCKET_PATH);

    while (1) {
        client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);
        if (client_fd < 0) {
            perror("❌ accept 失败");
            continue;
        }

        // 5️⃣ 读取数据
        ssize_t bytes_received = read(client_fd, buffer, sizeof(buffer));
        if (bytes_received > 0) {
            printf("📩 收到消息: %s\n", buffer);
            write(client_fd, "✅ 服务器收到消息", 20);
        } else if (bytes_received == 0) {
            printf("客户端已断开连接\n");
        } else {
            perror("❌ recv 失败");
        }
        close(client_fd);
    }

    close(server_fd);
    unlink(SOCKET_PATH);
    return 0;
}

正确处理 UNIX 域套接字错误,可以提高通信稳定性,防止进程崩溃;建议使用 unlink() 处理 bind() 失败忽略 SIGPIPE 处理 send() 失败

以上。仅供学习与分享交流,请勿用于商业用途!转载需提前说明。

我是一个十分热爱技术的程序员,希望这篇文章能够对您有帮助,也希望认识更多热爱程序开发的小伙伴。
感谢!

相关推荐
不做菜鸟的网工17 小时前
BGP特性
网络协议
AlfredZhao1 天前
生产环境里,为什么不建议把普通端口直接暴露到公网?
linux·https·443·80
戴为沐2 天前
Linux内存扩容指南
linux
zylyehuo2 天前
Linux 彻底且安全地删除文件
linux
明月_清风3 天前
开发者网络概念全扫盲:一篇搞定
后端·网络协议
刘马想放假3 天前
Modbus 全栈技术解析:TCP、RTU、ASCII、RTU over TCP
数据结构·网络协议
用户805533698033 天前
主线 U-Boot 上 RK3506:和闭源 rkbin 拔河的三个隐性契约
linux·嵌入式
用户034095297913 天前
linux fcitx 5 雾凇拼音 设置在中文输入法下仍然输入英文标点
linux
王二端茶倒水4 天前
一套可落地的无线运营方案,不能只管 AP,还要管用户、计费和运维
网络协议
162723816084 天前
EtherCAT 分布式时钟(DC)原理与配置实战:把多轴真正"对齐到同一时刻"
网络协议