c/c++ Socket+共享内存实现本机进程间通信

一、UNIX 域 Socket 特点

  1. 只能在本机使用
    • 不能跨机器通信,只能在 Linux/Unix 系统同一台主机上。
  2. 高性能
    • 因为数据不走网卡,也不经过网络协议栈,速度比 TCP/UDP 快。
  3. 通过文件表示
    • UNIX Socket 会对应一个 文件路径 ,比如你的示例里:

      c
      复制编辑
      #define SOCK_PATH "/data/.../ads_demo.sock"

    • 进程通过这个文件"找到对方",然后就能通信。

  4. 支持面向连接和无连接
    • 面向连接(SOCK_STREAM)类似 TCP
    • 无连接(SOCK_DGRAM)类似 UDP

二、 项目框架

  • socket_demo/
    ├── include/
    │ ├── common.h # 公共头文件
    │ ├── client.h
    │ └── server.h
    ├── src/
    │ ├── client.cpp
    │ ├── server.cpp
    │ └── common.cpp
    ├── CMakeLists.txt
    └── README.md

Server 做的事情

  1. 初始化信号处理
    • 捕获 SIGINT 和 SIGTERM,用于优雅退出。
  2. 清理残留 socket 文件
    • 防止上次异常退出导致 UNIX socket 文件还存在。
  3. 创建共享内存
    • shm_open 创建/打开共享内存
    • ftruncate 设置大小
    • mmap 映射到进程地址空间
  4. 建立 UNIX 域 Socket 并监听
    • socket(AF_UNIX)
    • bind 绑定路径
    • listen 等待 client 连接
  5. 等待客户端连接
    • accept 阻塞直到 client 连接
  6. 主循环处理消息
    • 收到控制消息(PUSH / EXIT / 其他)
    • PUSH
      • server 从共享内存读取数据
      • 处理后写回共享内存
      • 通过 socket 发送 PULL 控制消息通知 client
    • EXIT:退出循环
  7. 清理资源
    • 关闭 socket、取消映射共享内存、删除共享内存和 socket 文件

Client 做的事情

  1. 连接 Server 的 UNIX socket
  2. 映射共享内存(同名)
  3. 写数据到共享内存
  4. 通过 socket 发送 PUSH 消息
  5. 等待 server 响应 PULL 消息
  6. 从共享内存读取 server 的返回数据
  7. 循环或结束
  8. 清理资源

server.c

cpp 复制代码
#include <stdio.h>      // printf、perror 等标准 I/O
#include <stdlib.h>     // exit、EXIT_xxx、malloc/free 等
#include <string.h>     // memset、strncpy、strncmp 等
#include <stdbool.h>    // 引入 bool / true / false
#include <signal.h>     // signal、SIGINT、SIGTERM
#include <unistd.h>     // close、unlink、read/write、ftruncate 等
#include <fcntl.h>      // O_CREAT、O_RDWR 等 open/shm_open 标志位
#include <sys/mman.h>   // shm_open、mmap、munmap、shm_unlink
#include <sys/socket.h> // socket、bind、listen、accept、send、recv
#include <sys/un.h>     // UNIX 域套接字 sockaddr_un、AF_UNIX
#include <sys/stat.h>   // 权限位(0666 等),有时配合 umask 使用
#include "common_net.h" // 你自定义的公共协议: SHM_NAME/SHM_SIZE/SOCK_PATH/CTRL_MSG_xxx/shm_packet_t/CTRL_MSG_MAX 等

static int shm_fd = -1;         // 共享内存对象的文件描述符
static void *shm_addr = NULL;   // 映射后的地址
static int listen_fd = -1;      // 监听用的 UNIX 域 socket fd
static int conn_fd = -1;        // 已接受连接的客户端 fd
static volatile sig_atomic_t g_stop = 0; 
// 信号安全的停止标志volatile sig_atomic_t:保证在信号处理函数里对它的写入是原子的、类型安全的(避免数据竞争)。
// 初始化为无效值或空指针,便于清理阶段判断是否需要释放。


// 捕获 SIGINT/SIGTERM 时仅设置标志(异步信号安全):不要在信号处理函数里做耗时/非可重入的操作
static void on_sigint(int sig) { g_stop = 1; }

//=============================固定长度收发(TCP 风格、但你这儿用的是 UNIX 域 SOCK_STREAM)===================
// 读取Socket上固定大小的消息(简化处理:期望每次刚好读到一个消息)
static bool recv_fixed(int fd, void *buf, size_t len) {
    size_t got = 0;
    while (got < len) {   //为什么循环?:流式套接字不保证一次 send/recv 就把指定长度全部传完,必须循环直至满足长度。
        ssize_t r = recv(fd, (char*)buf + got, len - got, 0);
        if (r <= 0) return false;    // 0=对端关闭;<0=出错
        got += (size_t)r;    // 处理"短读"
    }
    return true;
}

// 发送固定大小消息
static bool send_fixed(int fd, const void *buf, size_t len) {
    size_t sent = 0;
    while (sent < len) {
        ssize_t r = send(fd, (const char*)buf + sent, len - sent, 0);
        if (r <= 0) return false;   // 0 很少见;<0 出错
        sent += (size_t)r;   // 处理"短写"
    }
    return true;
}


//=====================================================================================================
int main(void) {

// ====================安装信号处理器,支持 Ctrl+C 或服务管理器优雅退出。=======================
    signal(SIGINT, on_sigint);
    signal(SIGTERM, on_sigint);

// 1) ==================清理上次残留的socket文件(若程序异常退出可能残留)=====================
    unlink(SOCK_PATH);

// 2) =================创建/初始化共享内存(创建方一般是Server)=======================
    shm_fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666); //O_CREAT|O_RDWR:如果没有则创建,有则打开;读写模式。
    if (shm_fd < 0) {
        perror("shm_open");
        return 1;
    }

    //必须:新创建的 POSIX 共享内存大小是 0,需要 ftruncate 扩展至 SHM_SIZE。 如果共享内存已存在且更小,也需扩容;若更大,不会自动缩小(可根据需求处理)。
    if (ftruncate(shm_fd, SHM_SIZE) == -1) {  
        perror("ftruncate");
        return 1;
    }

    // 把共享内存"映射"到当前进程虚拟地址空间,返回可读写指针。
    // MAP_SHARED:写入对其他 mmap 了同一对象的进程可见。
    // 注意:返回后你就可以把 shm_fd 关掉(映射仍有效)。代码里选择保留到最后一并关闭,也可以。
    shm_addr = mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
    if (shm_addr == MAP_FAILED) {
        perror("mmap");
        return 1;
    }

    // 可选:把共享内存初始化为 0,避免读到脏内容。
    memset(shm_addr, 0, SHM_SIZE);
    printf("[SERVER] Shared memory ready: %s (%d bytes)\n", SHM_NAME, SHM_SIZE);

// 3)================================ 建立UNIX域socket监听(AF_UNIX,文件形式)========================
        
    //  AF_UNIX + SOCK_STREAM:本机面向连接、字节流语义(类似 TCP,但走文件路径)。
    // 优点:本机通信延迟低,权限控制细,避免网络栈开销。
    listen_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (listen_fd < 0) {
        perror("socket");
        return 1;
    }

    //sockaddr_un 的路径字段SOCK_PATH必须以 NUL 结尾,所以 strncpy(..., n-1) 是对的。
    // 路径长度有限(典型 108 字节),太长会失败。
    struct sockaddr_un addr;
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SOCK_PATH, sizeof(addr.sun_path) - 1);


   //bind 将 socket 与路径绑定;
    if (bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
        perror("bind");
        return 1;
    }

    //listen 进入监听状态,backlog=1 表示排队上限。// 改进:可调大一点 backlog;可在 bind 前设置 umask 控制 socket 节点权限。
    if (listen(listen_fd, 1) < 0) {
        perror("listen");
        return 1;
    }
    printf("[SERVER] Listening on %s\n", SOCK_PATH);


// 4) =========================================等待客户端连接=======================================
// 接受第一个客户端,返回已连接的 fd。
// 可选:循环 accept 支持多个客户端;目前代码是"单连接模式"。

    conn_fd = accept(listen_fd, NULL, NULL);
    if (conn_fd < 0) {
        perror("accept");
        return 1;
    }
    printf("[SERVER] Client connected.\n");


// 5) ======================================主循环:控制协议与共享内存读写==================================
    char ctrl[CTRL_MSG_MAX]; // 是固定长度控制消息缓冲区,避免"粘包/拆包"问题。
    uint32_t seq = 0; //用于响应包的序号(示例用途)。

    while (!g_stop) {

         //收到一条固定长度控制消息;失败表示对端关闭或出错,直接退出循环。
        if (!recv_fixed(conn_fd, ctrl, sizeof(ctrl))) {  
            printf("[SERVER] client disconnected or recv error.\n");
            break;
        }

        // 关键点:这里比较的是 整个固定缓冲区 与 CTRL_MSG_PUSH(你在 client 端也用 send_fixed(..., sizeof(ctrl)) 发送同样长度,才能完全相等)。
        // 若 client 端只发 "PUSH" 的长度(而不是 CTRL_MSG_MAX),这儿会匹配失败(尾部有未定义字节)。
        // 因此**两端必须约定"控制消息固定长度"**并按同一长度收发。


        //接收到"PUSH"
        if (strncmp(ctrl, CTRL_MSG_PUSH, sizeof(ctrl)) == 0) {
            // 客户端写完共享内存,服务端读取
            shm_packet_t *pkt = (shm_packet_t*)shm_addr; //从共享内存中获取结构体
            size_t header = sizeof(shm_packet_t);     //data 是"柔性数组成员"(uint8_t data[];),sizeof(shm_packet_t) 只包含头部,不含数据区。
            if (header + pkt->data_len <= SHM_SIZE) {    //读取时做越界检查:header + data_len <= SHM_SIZE。???????????????
                printf("[SERVER] <- PUSH: seq=%u len=%u data=\"%.*s\"\n",   //"%.*s" 用于打印指定长度的字符串数据(即便包含 \0 也能按长度显示)
                       pkt->seq, pkt->data_len, pkt->data_len, pkt->data);
            } else {
                printf("[SERVER] <- PUSH: invalid length!\n");
            }

            // 回写一条响应到共享内存,并通知客户端PULL
            const char *resp = "ACK from SERVER";
            size_t resp_len = strlen(resp);
            size_t header2 = sizeof(shm_packet_t);
            if (header2 + resp_len <= SHM_SIZE) { //进行越界检查,避免写爆共享内存。
                shm_packet_t *out = (shm_packet_t*)shm_addr;
                out->seq = ++seq;
                out->data_len = (uint32_t)resp_len;
                memcpy(out->data, resp, resp_len); // //向共享内存写数据
                // 通知客户端
                char msg[CTRL_MSG_MAX] = {0}; //固定长度控制消息缓冲区,服务端写共享内存 → 通过 socket 发送固定长度 "PULL" 通知对端去读。
                strncpy(msg, CTRL_MSG_PULL, sizeof(msg)-1); //strncpy(..., n-1) 保证以 \0 结尾;随后 send_fixed 以 固定长度 整块发送。
                send_fixed(conn_fd, msg, sizeof(msg));
                printf("[SERVER] -> PULL: seq=%u len=%u data=\"%s\"\n", out->seq, out->data_len, resp);
            } else {
                printf("[SERVER] response too long for SHM!\n");
            }
        //接收到"EXIT"则退出主循环;
        } else if (strncmp(ctrl, CTRL_MSG_EXIT, sizeof(ctrl)) == 0) {
            printf("[SERVER] <- EXIT\n");
            break;

        //其他无效控制消息打印出来(用于调试协议不一致问题)。
        } else {
            printf("[SERVER] <- UNKNOWN CTRL: \"%.*s\"\n", (int)sizeof(ctrl), ctrl);
        }
    }

// 6) ===================================清理资源=============================================
    //关闭连接、监听 fd,并删除 Unix 域 socket 文件,避免残留。
    if (conn_fd >= 0) close(conn_fd);
    if (listen_fd >= 0) close(listen_fd);
    unlink(SOCK_PATH); // 清理socket文件


    //解除映射、关闭共享内存对象的 fd。
    if (shm_addr && shm_addr != MAP_FAILED) munmap(shm_addr, SHM_SIZE);
    if (shm_fd >= 0) close(shm_fd);

    // 注意:是否立即删除共享内存?
    // 方案A:显式删除,避免残留
    shm_unlink(SHM_NAME);

    printf("[SERVER] Exit.\n");
    return 0;
}

client.c

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>     // shm_open, mmap
#include <sys/socket.h>   // socket, connect, send, recv
#include <sys/un.h>       // sockaddr_un
#include "common_net.h"

static int shm_fd = -1;
static void *shm_addr = NULL;
static int sock_fd = -1;
static volatile sig_atomic_t g_stop = 0;

static void on_sigint(int sig) { g_stop = 1; }

static bool recv_fixed(int fd, void *buf, size_t len) {
    size_t got = 0;
    while (got < len) {
        ssize_t r = recv(fd, (char*)buf + got, len - got, 0);
        if (r <= 0) return false;
        got += (size_t)r;
    }
    return true;
}
static bool send_fixed(int fd, const void *buf, size_t len) {
    size_t sent = 0;
    while (sent < len) {
        ssize_t r = send(fd, (const char*)buf + sent, len - sent, 0);
        if (r <= 0) return false;
        sent += (size_t)r;
    }
    return true;
}

int main(void) {
    signal(SIGINT, on_sigint);
    signal(SIGTERM, on_sigint);

    // 1) 打开共享内存(由Server已创建)
    shm_fd = shm_open(SHM_NAME, O_RDWR, 0666);
    if (shm_fd < 0) {
        perror("shm_open");
        fprintf(stderr, "Make sure SERVER is running (it creates the shm).\n");
        return 1;
    }
    shm_addr = mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
    if (shm_addr == MAP_FAILED) {
        perror("mmap");
        return 1;
    }
    printf("[CLIENT] Shared memory opened: %s\n", SHM_NAME);

    // 2) 连接服务器的UNIX域socket
    sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sock_fd < 0) {
        perror("socket");
        return 1;
    }
    struct sockaddr_un addr;
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SOCK_PATH, sizeof(addr.sun_path) - 1);

    if (connect(sock_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
        perror("connect");
        fprintf(stderr, "Make sure SERVER is listening on %s\n", SOCK_PATH);
        return 1;
    }
    printf("[CLIENT] Connected to server.\n");

    // 3) 向共享内存写入一条消息,然后通过socket发PUSH通知
    const char *msg = "Hello from CLIENT";
    size_t msg_len = strlen(msg);

    shm_packet_t *pkt = (shm_packet_t*)shm_addr;
    size_t header = sizeof(shm_packet_t);
    if (header + msg_len > SHM_SIZE) {
        fprintf(stderr, "message too long for SHM\n");
        return 1;
    }
    pkt->seq = 1;
    pkt->data_len = (uint32_t)msg_len;
    memcpy(pkt->data, msg, msg_len);

    // 通过socket通知服务端
    char ctrl[CTRL_MSG_MAX] = {0};
    strncpy(ctrl, CTRL_MSG_PUSH, sizeof(ctrl)-1);
    if (!send_fixed(sock_fd, ctrl, sizeof(ctrl))) {
        fprintf(stderr, "[CLIENT] send PUSH failed.\n");
        return 1;
    }
    printf("[CLIENT] -> PUSH: seq=%u len=%u data=\"%s\"\n", pkt->seq, pkt->data_len, msg);

    // 4) 等待服务端回写共享内存并通过socket发PULL通知
    if (!recv_fixed(sock_fd, ctrl, sizeof(ctrl))) {
        fprintf(stderr, "[CLIENT] recv ctrl failed.\n");
        return 1;
    }
    if (strncmp(ctrl, CTRL_MSG_PULL, sizeof(ctrl)) == 0) {
        shm_packet_t *in = (shm_packet_t*)shm_addr;
        if (sizeof(shm_packet_t) + in->data_len <= SHM_SIZE) {
            printf("[CLIENT] <- PULL: seq=%u len=%u data=\"%.*s\"\n",
                   in->seq, in->data_len, in->data_len, in->data);
        } else {
            printf("[CLIENT] <- PULL: invalid length!\n");
        }
    } else {
        printf("[CLIENT] <- UNKNOWN CTRL: \"%.*s\"\n", (int)sizeof(ctrl), ctrl);
    }

    // 5) 发送EXIT让服务端优雅退出(演示)
    memset(ctrl, 0, sizeof(ctrl));
    strncpy(ctrl, CTRL_MSG_EXIT, sizeof(ctrl)-1);
    send_fixed(sock_fd, ctrl, sizeof(ctrl));

    // 6) 清理
    if (sock_fd >= 0) close(sock_fd);
    if (shm_addr && shm_addr != MAP_FAILED) munmap(shm_addr, SHM_SIZE);
    if (shm_fd >= 0) close(shm_fd);

    printf("[CLIENT] Exit.\n");
    return 0;
}

common_net.h

cpp 复制代码
#ifndef ADS_COMMON_H
#define ADS_COMMON_H

#include <stdint.h>

#define SHM_NAME        "/ads_demo_shm"     // POSIX共享内存名(会出现在 /dev/shm/ads_demo_shm)
#define SHM_SIZE        4096                // 共享内存大小
#define SOCK_PATH       "/data/standard_sdk/personal/xal61637/ADS_FUNC_TEST/data/ads_demo.sock"// UNIX域Socket路径(文件形式)

// 控制消息(通过Socket传输)------简单起见用定长字符串
#define CTRL_MSG_MAX    64
#define CTRL_MSG_PUSH   "PUSH"   // 客户端通知服务器"我往共享内存写好了"
#define CTRL_MSG_PULL   "PULL"   // 服务器通知客户端"我往共享内存写好了"
#define CTRL_MSG_EXIT   "EXIT"   // 请求对方退出

// 协议约定:客户端将数据写到共享内存中的 shm_packet_t,然后发一条 "PUSH" 告知服务端"可以读了"。
//共享内存中的数据格式(示例:简单的报头 + 文本)
typedef struct {
    uint32_t seq;            // 序号,演示数据变化
    uint32_t data_len;       // 有效数据长度(<= SHM_SIZE - sizeof(header))
    char     data[];         // 柔性数组成员,紧随其后
} shm_packet_t;

#endif // ADS_COMMON_H

cmakeLists.txt

cpp 复制代码
cmake_minimum_required(VERSION 3.10)
project(SocketDemo)

set(CMAKE_CXX_STANDARD 17)
include_directories(include)

add_executable(client src/client.cpp src/common.cpp)
add_executable(server src/server.cpp src/common.cpp)

target_link_libraries(server PRIVATE rt) 
target_link_libraries(client PRIVATE rt)

三、 共享内存和 socket

设计要点 & 常见坑

  1. 为什么要"共享内存 + Socket"组合?
  • 共享内存:大数据零拷贝(如图像/点云/矩阵),极快
  • Socket:结构化通知与控制(谁写好了、谁该读、何时退出、异常处理)
  • 组合后既快又可控,是业界常用范式
  1. 共享内存生命周期
  • shm_open(O_CREAT|O_RDWR) + ftruncate 由创建方(通常Server)执行
  • 所有进程 mmap 后即可读写
  • 不 shm_unlink 就会残留 ,建议 Server 优雅退出时调用 shm_unlink;
    想"进程退出自动释放"效果:创建后立即 shm_unlink,所有进程关闭后会被内核销毁(如同文件 unlink 机制)
  1. 并发/同步
  • 示例用"消息通知"隐式同步(谁先写谁发消息)
  • 复杂情形建议加环形队列 + 原子变量或**POSIX信号量(sem_open)**做互斥/同步
  1. 错误处理与健壮性
  • 真实工程中务必检查 send/recv 的返回值,并处理对端断开
  • 注意 SHM 中 data_len 的 边界,防御性检查避免越界
  1. 权限与安全
  • /tmp/xxx.sock 对权限敏感,必要时用 umask/chmod
  • SHM 权限 0666 仅示例,实际按需求收紧

对比

1. 通信范围

  • 共享内存
    • 只能在同一台机器上的不同进程之间通信(IPC,进程间通信)。
    • 本质上是让两个进程访问同一段物理内存。
    • 跨机器不能用。
  • 网络套接字(Socket)
    • 不限于本机,既可以本机进程之间通信 ,也可以跨网络通信(不同服务器之间)。
    • TCP/UDP 都是基于 Socket 的。

📌 简单类比

共享内存像两个人用同一个笔记本写字,必须坐在同一张桌子上;

Socket 像两个人通过电话交流,可以隔着世界另一边说话。

2. 速度

  • 共享内存
    • 最快的 IPC 方式之一,因为数据不经过内核缓冲区拷贝(直接在物理内存上操作)。
    • 适合大量数据、高频率的通信,比如视频帧、传感器数据。
  • Socket
    • 会经过操作系统内核协议栈,多次数据拷贝,比共享内存慢很多。
    • 延迟比共享内存大。

3. 实现难度

  • 共享内存
    • 数据结构需要自己管理,比如写指针、读指针、同步锁。
    • 如果两个进程同时写同一个区域,没有锁会乱。
  • Socket
    • 协议栈帮你做好了数据收发的顺序和可靠性(尤其是 TCP)。
    • 不需要自己管理读写位置,但需要考虑网络延迟、丢包等问题(UDP)。

4. 数据存储特性

  • 共享内存
    • 只要你不 shm_unlink(),即使进程退出,数据仍然存在,其他进程还可以访问。
    • 必须手动删除,否则会一直占用内存。
  • Socket
    • 数据发出去后就没了(除非自己写到文件)。
    • 连接断了,数据就丢。

5. 典型使用场景

  • 共享内存
    • 摄像头视频流处理(DMS、ADAS 数据)
    • 大型科学计算中不同进程共享数据集
  • Socket
    • 远程服务调用(比如 HTTP 请求)
    • 游戏服务器与客户端通信
    • 车载 ECU 之间的通信(以太网)
相关推荐
小米里的大麦29 分钟前
026 inode 与软硬链接
linux
₯㎕星空&繁华2 小时前
Linux-地址空间
linux·运维·服务器·经验分享·笔记
小米里的大麦2 小时前
023 基础 IO —— 重定向
linux
John.Lewis2 小时前
数据结构初阶(15)排序算法—交换排序(快速排序)(动图演示)
c语言·数据结构·排序算法
Cx330❀3 小时前
【数据结构初阶】--排序(三):冒泡排序、快速排序
c语言·数据结构·经验分享·算法·排序算法
lsnm4 小时前
【LINUX网络】HTTP协议基本结构、搭建自己的HTTP简单服务器
linux·运维·服务器·c语言·网络·c++·http
杜大帅锅4 小时前
Linux搭建ftp服务器
linux·运维·服务器
运维自动化&云计算4 小时前
Centos虚拟机硬盘报错,根分区满,已用显示为负40G
linux·运维·centos