UNIX 域套接字实现本地进程间通信

发布于:2025-07-13 ⋅ 阅读:(21) ⋅ 点赞:(0)

🚀 使用 UNIX 域套接字 (AF_UNIX) 实现高效进程通信

在 Linux 和其他类 UNIX 系统中,进程间通信 (IPC) 的方法有很多种,例如管道、消息队列、共享内存等。然而,当你的应用程序需要在 同一台机器上的不同进程间进行高效、低延迟的数据交换时,UNIX 域套接字 (AF_UNIX) 往往是最佳选择。
它的工作方式类似于网络套接字,但所有数据传输都发生在内核内部,避免了网络协议栈的开销,因此速度更快、效率更高。


💡 为什么选择 UNIX 域套接字?

在深入代码之前,我们先来聊聊 AF_UNIX 的优势:

  1. 高性能:数据在内核中直接传递,无需经过网络层,减少了数据复制和上下文切换,显著提升了通信速度。
  2. 低延迟:由于没有网络开销,通信延迟极低,非常适合对实时性要求高的应用。
  3. 安全性:UNIX 域套接字在文件系统中表现为一个特殊文件。这意味着你可以使用标准的文件权限来控制哪些用户或进程可以访问它,提供了比某些其他 IPC 机制更细粒度的安全控制。
  4. API 熟悉度:如果你熟悉网络套接字(AF_INET),那么AF_UNIX 的 API会让你感到非常亲切。socket(), bind(), listen(), accept(), connect(), read(), write() 等函数都通用,降低了学习成本

🛠️ 如何工作?(核心概念)

UNIX 域套接字的运作方式很直观:

  1. 文件路径作为地址:与网络套接字使用 IP 地址和端口号不同,AF_UNIX 套接字使用文件系统路径作为其唯一的标识符。服务器会绑定到一个特定的文件路径(例如 /tmp/my_socket),客户端则通过这个路径来连接服务器。

  2. 服务器端:
    创建一个 AF_UNIX 类型的套接字。
    将套接字绑定到文件系统中的一个路径。这个操作会在指定路径下创建一个特殊的套接字文件。
    开始监听传入的连接请求。
    接受客户端的连接,每次接受都会返回一个新的套接字文件描述符,用于与该客户端单独通信。
    使用 read()write() 进行数据传输。

  3. 客户端:
    创建一个 AF_UNIX 类型的套接字。
    连接到服务器绑定的文件路径。
    使用read() write() 进行数据传输。


🧑‍💻 代码实战:构建一个简单的 Echo 服务器

下面将通过 C 语言代码,一步步实现一个 UNIX 域套接字服务器和对应的客户端。服务器将接收客户端发送的消息,并原样返回(Echo)。

  1. 服务器端 (server.c)
    服务器的职责是创建、绑定、监听并接受连接,然后处理数据
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h> // 包含 sockaddr_un 结构体
#include <unistd.h>

#define SOCKET_PATH "/tmp/my_unix_socket" // 服务器将绑定的套接字文件路径
#define BUFFER_SIZE 256

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

    // 1. 创建UNIX域套接字 (AF_UNIX, 流式套接字 SOCK_STREAM)
    server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (server_fd == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    // 2. 设置服务器地址结构体
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sun_family = AF_UNIX;
    // 将套接字路径复制到 sun_path 字段
    strncpy(server_addr.sun_path, SOCKET_PATH, sizeof(server_addr.sun_path) - 1);

    // 3. 重要的清理步骤:删除旧的套接字文件(如果存在)
    // 如果服务器上次异常退出,可能留下这个文件,导致绑定失败
    unlink(SOCKET_PATH); 

    // 4. 将套接字绑定到指定的文件路径
    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 5. 开始监听连接请求,队列长度为 5
    if (listen(server_fd, 5) == -1) {
        perror("listen");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    printf("🚀 服务器已启动,监听在 %s...\n", SOCKET_PATH);

    while (1) {
        client_len = sizeof(client_addr);
        // 6. 接受新的客户端连接,这是一个阻塞调用
        client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len);
        if (client_fd == -1) {
            perror("accept");
            continue; // 继续等待下一个连接
        }

        printf("🤝 接受到一个新连接。\n");

        // 7. 与客户端进行数据通信(Echo 功能)
        while ((bytes_read = read(client_fd, buffer, sizeof(buffer) - 1)) > 0) {
            buffer[bytes_read] = '\0'; // 确保字符串以 null 结尾
            printf("➡️ 接收到客户端消息: %s", buffer);

            // 将收到的消息原样回传给客户端
            if (write(client_fd, buffer, bytes_read) == -1) {
                perror("write");
                break; // 写入失败则退出循环
            }
            printf("⬅️ 已回复客户端。\n");
        }

        if (bytes_read == -1) {
            perror("read");
        } else if (bytes_read == 0) {
            printf("🔴 客户端已关闭连接。\n");
        }

        // 8. 关闭与当前客户端的连接
        close(client_fd);
    }

    // 9. 关闭服务器监听套接字,并清理套接字文件(通常在程序退出时执行)
    close(server_fd);
    unlink(SOCKET_PATH); 
    return 0;
}
  1. 客户端 (client.c)
    客户端负责创建套接字并连接到服务器,然后发送和接收数据。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h> // 包含 sockaddr_un 结构体
#include <unistd.h>

#define SOCKET_PATH "/tmp/my_unix_socket" // 服务器的套接字文件路径
#define BUFFER_SIZE 256

int main() {
    int client_fd;
    struct sockaddr_un server_addr;
    char buffer[BUFFER_SIZE];
    int bytes_read;

    // 1. 创建UNIX域套接字
    client_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (client_fd == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    // 2. 设置服务器地址结构体
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sun_family = AF_UNIX;
    strncpy(server_addr.sun_path, SOCKET_PATH, sizeof(server_addr.sun_path) - 1);

    // 3. 连接到服务器
    if (connect(client_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("connect");
        close(client_fd);
        exit(EXIT_FAILURE);
    }

    printf("✅ 成功连接到服务器。\n");

    // 4. 与服务器进行数据通信
    while (1) {
        printf("请输入消息(输入 'exit' 退出): ");
        if (fgets(buffer, sizeof(buffer), stdin) == NULL) {
            break; // 读取失败或EOF
        }

        // 检查用户是否输入 'exit'
        if (strcmp(buffer, "exit\n") == 0) {
            break;
        }

        // 将消息发送给服务器
        if (write(client_fd, buffer, strlen(buffer)) == -1) {
            perror("write");
            break;
        }

        // 从服务器接收回复
        bytes_read = read(client_fd, buffer, sizeof(buffer) - 1);
        if (bytes_read == -1) {
            perror("read");
            break;
        } else if (bytes_read == 0) {
            printf("🚫 服务器已关闭连接。\n");
            break;
        }
        buffer[bytes_read] = '\0'; // 确保字符串以 null 结尾
        printf("⬅️ 收到服务器回复: %s", buffer);
    }

    // 5. 关闭客户端套接字
    close(client_fd);
    printf("👋 客户端已退出。\n");
    return 0;
}

🖥️ 编译与运行

在你的 Linux 终端中,按照以下步骤编译和运行:

编译服务器和客户端:

gcc server.c -o server
gcc client.c -o client

启动服务器:
打开一个终端窗口,运行服务器程序。

./server

你应该会看到输出 🚀 服务器已启动,监听在 /tmp/my_unix_socket…

启动客户端:
打开另一个终端窗口,运行客户端程序。

./client

客户端会显示 ✅ 成功连接到服务器。

现在,你可以在客户端终端输入消息,服务器会接收到并将其原样返回给客户端!


🧹 清理注意事项

UNIX 域套接字文件 (/tmp/my_unix_socket 在我们的例子中) 会在服务器绑定时创建。如果服务器非正常退出 (例如,被 Ctrl+C 中断),这个文件可能不会被自动删除。下次你尝试启动服务器时,可能会遇到 “Address already in use” (地址已被占用) 的错误。

这就是为什么在服务器代码中,我们特意加入了 unlink(SOCKET_PATH); 这一行。它确保在绑定新套接字之前,删除任何可能残留的旧套接字文件,从而避免启动失败。


网站公告

今日签到

点亮在社区的每一天
去签到