c/c++ UNIX 域Socket和共享内存实现本机通信
一、UNIX 域Socket 特点
- 只能在本机使用
- 不能跨机器通信,只能在 Linux/Unix 系统同一台主机上。
- 高性能
- 因为数据不走网卡,也不经过网络协议栈,速度比 TCP/UDP 快。
- 通过文件表示
- UNIX Socket 会对应一个 文件路径,比如你的示例里:
c
复制编辑
#define SOCK_PATH "/data/.../ads_demo.sock" - 进程通过这个文件“找到对方”,然后就能通信。
- UNIX Socket 会对应一个 文件路径,比如你的示例里:
- 支持面向连接和无连接
- 面向连接(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 做的事情
- 初始化信号处理:
- 捕获 SIGINT 和 SIGTERM,用于优雅退出。
- 清理残留 socket 文件:
- 防止上次异常退出导致 UNIX socket 文件还存在。
- 创建共享内存:
- shm_open 创建/打开共享内存
- ftruncate 设置大小
- mmap 映射到进程地址空间
- 建立 UNIX 域 Socket 并监听:
- socket(AF_UNIX)
- bind 绑定路径
- listen 等待 client 连接
- 等待客户端连接:
- accept 阻塞直到 client 连接
- 主循环处理消息:
- 收到控制消息(PUSH / EXIT / 其他)
- PUSH:
- server 从共享内存读取数据
- 处理后写回共享内存
- 通过 socket 发送 PULL 控制消息通知 client
- EXIT:退出循环
- 清理资源:
- 关闭 socket、取消映射共享内存、删除共享内存和 socket 文件
Client 做的事情
- 连接 Server 的 UNIX socket
- 映射共享内存(同名)
- 写数据到共享内存
- 通过 socket 发送 PUSH 消息
- 等待 server 响应 PULL 消息
- 从共享内存读取 server 的返回数据
- 循环或结束
- 清理资源
server.c
#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");}// 回写一条响应到共享内存,并通知客户端PULLconst 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
#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域socketsock_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
#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
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
设计要点 & 常见坑
- 为什么要“共享内存 + Socket”组合?
- 共享内存:大数据零拷贝(如图像/点云/矩阵),极快
- Socket:结构化通知与控制(谁写好了、谁该读、何时退出、异常处理)
- 组合后既快又可控,是业界常用范式
- 共享内存生命周期
- shm_open(O_CREAT|O_RDWR) + ftruncate 由创建方(通常Server)执行
- 所有进程 mmap 后即可读写
- 不 shm_unlink 就会残留,建议 Server 优雅退出时调用 shm_unlink;
想“进程退出自动释放”效果:创建后立即 shm_unlink,所有进程关闭后会被内核销毁(如同文件 unlink 机制)
- 并发/同步
- 示例用“消息通知”隐式同步(谁先写谁发消息)
- 复杂情形建议加环形队列 + 原子变量或**POSIX信号量(sem_open)**做互斥/同步
- 错误处理与健壮性
- 真实工程中务必检查 send/recv 的返回值,并处理对端断开
- 注意 SHM 中 data_len 的 边界,防御性检查避免越界
- 权限与安全
- /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 之间的通信(以太网)