首页
/ gRPC C++ 实战:用 `unix-abstract` URI 在 Unix 抽象命名空间下建立 Unix Domain Socket 通信

gRPC C++ 实战:用 `unix-abstract` URI 在 Unix 抽象命名空间下建立 Unix Domain Socket 通信

2026-09-08 19:29:07作者:邵娇湘

本篇技术指南以 grpc 仓库中的 Unix Abstract Socket 示例 为核心,讲解如何在 C++ 中使用 gRPC 的 unix-abstract: URI 方案在 Unix 抽象命名空间中创建本机进程间通信(IPC)服务。读完本文,你将掌握 unix-abstract:grpc%00abstract 这类带内嵌空字符地址的写法与语义、完整可运行的服务端/客户端代码结构、基于 Bazel 的构建与运行方式,以及 gRPC 底层如何解析并绑定抽象套接字地址的实现原理。

背景:从文件系统套接字到抽象命名空间套接字

传统的 Unix Domain Socket(UDS)通过文件系统中的一个路径名来标识,例如 /tmp/grpc.sock,其地址方案在 gRPC 中写作 unix:pathunix:///absolute_path。这类套接字存在几个约束:

  • 会在文件系统上留下一个真实的路径条目,需要自行管理清理(进程退出后 socket 文件通常仍残留);
  • 文件权限直接作用于该路径,可被 chmod、可被其他用户碰巧占用路径而产生干扰。

抽象命名空间(abstract namespace)套接字是 Linux 提供的另一种 UDS 形式:套接字名称只存在于内核抽象的命名空间里,与任何文件系统路径无关,因此:

  • 不会在磁盘上产生文件条目,进程退出后名称自动消失,无需手工清理;
  • 不占用文件系统目录结构,也就没有路径冲突问题;
  • 不适用任何文件权限——任何用户/进程只要能访问该系统,就可以连接到该套接字;
  • 名称本身可以包含任意字节,包括内嵌的 NUL 字符(因为底层以长度而不是以 C 字符串 \0 结尾来定位名称)。

gRPC 将抽象套接字的地址方案统一定义为 unix-abstract:abstract_path,示例仓库的地址解析规范文档 doc/naming.md 中明确记载了该方案的约定:abstract_path 指抽象命名空间中的一个名称;名称与文件系统路径无关;由于底层实现会在名称最前面自动追加一个 \0 字节作为抽象套接字标志,因此不要abstract_path 中自行包含这个前缀空字符。

注:抽象套接字属于 Unix 系系统特性,其中“abstract namespace”目前主要在 Linux 上可用,相关地址方案仅在支持平台编译可用(源码中以 GRPC_HAVE_UNIX_SOCKET 宏开关保护,见 parse_address.cc)。

示例总览:一个内嵌 NUL 的套接字名

本示例位于 examples/cpp/unix_abstract_sockets,它把经典的 helloworld Greeter 服务架设在抽象套接字之上。示例的核心亮点是:服务端与客户端共同使用地址字符串 unix-abstract:grpc%00abstract

其中 %00 是 URI 中对 NUL 字节(0x00)的百分号编码形式。gRPC 的 URI 解析(见 percent_encoding.hpercent_encoding.cc)会将其解码为一个真实的内嵌空字符,使最终创建的抽象套接字名内嵌 \0(连同底层自动追加的前缀 \0,实际的套接字名称字节序列以两个 NUL 开头,中间夹着 grpcabstract)。这正是 lsof 中会看到该套接字被显示为 @grpc@abstract 的原因——lsof 使用 @ 符号来渲染二进制名称中的空字符。

服务端源码解析

服务端实现位于 server.cc,整体结构与 grpc C++ helloworld 示例一脉相承,唯一的关键差异在于监听地址的写法。其核心运行逻辑如下:

void RunServer() {
  std::string server_address("unix-abstract:grpc%00abstract");
  GreeterServiceImpl service;
  grpc::EnableDefaultHealthCheckService(true);
  grpc::reflection::InitProtoReflectionServerBuilderPlugin();
  ServerBuilder builder;
  builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
  builder.RegisterService(&service);
  std::unique_ptr<Server> server(builder.BuildAndStart());
  std::cout << "Server listening on " << server_address << " ... ";
  server->Wait();
}

逐行要点:

  • server_address 直接使用 unix-abstract: 前缀加抽象名称,无需任何 sockaddr_un 手工构造或 unlink 等文件系统清理逻辑
  • builder.AddListeningPort(...) 传入该 URI,ServerBuilder 会把它识别为一个本地传输地址并创建对应的监听 socket;
  • 服务端额外启用了两个实用能力:grpc::EnableDefaultHealthCheckService(true) 注册默认健康检查服务;grpc::reflection::InitProtoReflectionServerBuilderPlugin() 注册 gRPC 反射服务(这些依赖对应 BUILD 中的 //:grpc++_reflection);
  • builder.BuildAndStart() 成功返回后即打印监听地址,随后 server->Wait() 阻塞,服务端会一直运行直到被显式关闭

服务逻辑本体 GreeterServiceImpl::SayHello 仅做回显(Echo):把请求中的 name 原样写回 reply.message() 并在控制台打印。注意示例并未在回调中拼接 “Hello ” 前缀,打印结果应是原样回传。

客户端源码解析

客户端实现位于 client.cc。与普通 TCP 客户端唯一的差异同样只在 target 字符串上:

int main(int argc, char** argv) {
  absl::ParseCommandLine(argc, argv);
  absl::InitializeLog();
  std::string target_str("unix-abstract:grpc%00abstract");
  GreeterClient greeter(
      grpc::CreateChannel(target_str, grpc::InsecureChannelCredentials()));
  std::string user("arst");
  std::cout << "Sending '" << user << "' to " << target_str << " ... ";
  std::string reply = greeter.SayHello(user);
  std::cout << "Received: " << reply << std::endl;
  return 0;
}

客户端通过 grpc::CreateChannel(target_str, grpc::InsecureChannelCredentials()) 创建 Channel——channel 的目标地址与服务端监听地址必须完全一致,gRPC 的名称解析器会根据 URI scheme(此处为 unix-abstract)选择对应的解析插件并把该地址解析为本地 UDS 端点。GreeterClient 持有 Greeter::StubSayHello 封装了同步 RPC:请求体 HelloRequest.name = "arst",若 status.ok() 返回回显消息,否则打印错误码与错误消息并返回 "RPC failed"。因此运行成功时两端的输出应分别为:

  • 服务端:Echoing: arst
  • 客户端:Received: arst

RPC 定义与 Bazel 构建目标

本例复用了仓库通用示例的 helloworld.proto,其定义了 helloworld 包下的 Greeter 服务与一元 RPC SayHello,由 Bazel 规则预先生成 helloworld.grpc.pb.h 等代码(依赖 //examples/protos:helloworld_cc_grpc)。

示例的 BUILD 中声明了两个 cc_binary 目标:

  • client:仅链接 //:grpc++ 与 helloworld 生成代码;
  • server:额外链接 //:grpc++_reflection 以支持上述反射插件。

两者都依赖 abseil 的 flags 解析与日志初始化库(@com_google_absl//absl/flags:parse@com_google_absl//absl/log:initialize),与源码顶部的 absl::ParseCommandLineabsl::InitializeLog 一一对应。

构建与运行步骤

原文档给出的运行方式非常简洁,直接使用 Bazel:

  1. 先在一个终端启动服务端

    bazel run :server
    

    等待其打印 Server listening on unix-abstract:grpc%00abstract ... 后保持运行(server->Wait() 会一直阻塞到进程被关闭)。

  2. 在另一个终端运行客户端

    bazel run :client
    

    客户端会发送一次 SayHello("arst") 后立即退出;此时两个终端都能看到消息已成功发送与接收的确认输出。

  3. (可选)在服务端运行期间验证套接字确实存在

    lsof -U | grep '@grpc@abstract'
    

    lsof -U 列出系统上所有 Unix domain socket,@grpc@abstract 是抽象套接字名称(其中的 NUL 字节被 lsof 显示为 @)在输出中的可见形态,可作为“抽象套接字已建立”的实证手段。

需要说明的适用前提:以上示例基于 Bazel 构建,构建目标名称在当前示例目录下解析,因此应在 examples/cpp/unix_abstract_sockets 目录中执行;且 bazel run 之前需先完成仓库整体(如 //:grpc++helloworld_cc_grpc)的构建配置。整个能力仅在支持 Unix socket 的平台上可用。

底层实现原理:抽象地址如何被解析与绑定

unix-abstract: 看似只是个地址字符串,背后有一套完整的解析链路,核心证据集中在 src/core/lib/address_utils/parse_address.cc

  • scheme 分发grpc_parse_unix_abstract() 校验 URI scheme 必须为 unix-abstract,随后调用 grpc_core::UnixAbstractSockaddrPopulate(uri.path(), ...) 填充 sockaddrparse_address.cc);
  • 前缀 NUL 与长度寻址UnixAbstractSockaddrPopulate 的实现(parse_address.cc)先把 sun_family 设为 AF_UNIX,令 sun_path[0] = '\0',再把抽象名称以显式长度拷贝方式写入 sun_path + 1,最后按 sizeof(sun_family) + path.size() + 1 计算地址长度——这正是抽象套接字可以承载内嵌 NUL 名称的关键:它依赖长度而不是以 \0 结尾的字符串来界定名称。这也印证了 doc/naming.md 的“实现会自动前置 NUL,用户不应自己写”这一约定;
  • 名称长度限制:在拷贝前,代码对 path.size()sizeof(un->sun_path) - 1 做了上限校验,超出会返回 Path name should not have more than N characters 错误;
  • 解析器插件:在 sockaddr_resolver.cc 中,sockaddr 解析器声明自己处理 unix-abstract scheme(同时处理 unixipv4ipv6 等),客户端 Channel 因此能据 scheme 直接得到本地端点而非走 DNS;
  • 传输层与安全层:服务端 chttp2 传输用常量 kUnixAbstractUriPrefix = "unix-abstract:" 识别抽象套接字监听地址(chttp2_server.cc),本地安全连接器同样以 GRPC_ABSTRACT_UDS_URI_PATTERN "unix-abstract:" 区分抽象与文件系统类 UDS(local_security_connector.cc)。

从测试代码看,仓库对抽象套接字支持有系统性的验证:地址解析单测覆盖在 parse_address_test.cc,sockaddr 工具单测在 sockaddr_utils_test.cc,解析器单测在 sockaddr_resolver_test.cc,端到端 Posix 配置中也将 unix-abstract 作为可用的 UDS 测试形态接入(end2end_posix_config.cc)。

使用要点与注意事项

综合文档 doc/naming.md 与源码实现,实际使用 unix-abstract: 时需要注意:

  1. 不要手工加 \0 前缀:底层 UnixAbstractSockaddrPopulate 会自动在 sun_path[0] 写入 NUL,用户只需提供干净的抽象名称;如确需在名称中部嵌入 NUL,请用 URI 百分号编码(如 %00);
  2. 无权限保护:抽象套接字不继承任何文件权限语义,同机任意进程只要知道名称即可访问。用于敏感服务时应叠加 gRPC 自身的认证机制(如 TLS/mTLS),而不是依赖文件系统权限;
  3. 生命周期与可见性:抽象名称随内核命名空间存在,进程退出即被内核回收,不存在残留 socket 文件问题;但也因此“看不见摸不着”,排查时需借助 lsof -Uss -x 一类工具(lsof 将内嵌 NUL 渲染为 @);
  4. 平台限制:该特性仅在支持抽象命名空间的 Unix 平台上可用(gRPC 以 GRPC_HAVE_UNIX_SOCKET 编译开关控制,不支持的构建中相关函数直接 abort());
  5. unix: 的选择:需要文件系统权限控制、跨容器 bind mount 或跨机器语义时使用 unix:/path;追求无残留、免清理的纯本机 IPC 时使用 unix-abstract:
  6. URI 写法:同 doc/naming.md 中其他地址方案一样,unix-abstract: 之后直接跟抽象名称,不带 //

小结

examples/cpp/unix_abstract_sockets 用最小的代码改动量(仅一个目标字符串)演示了 gRPC 对 Unix 抽象命名空间套接字的完整支持:服务端 AddListeningPort("unix-abstract:grpc%00abstract", ...) 与客户端 CreateChannel 同构对称,构建后即能实现免文件系统痕迹、名称内可嵌 NUL 的本机 RPC。若需继续深入,可沿两条线索拓展:一是阅读地址规范 doc/naming.md 了解 unix:unix-abstract:vsock: 等全部本机地址方案的选择边界;二是对照 parse_address.ccsockaddr_resolver.cc 理解从 URI scheme 到 sockaddr_un 的完整解析链路,从而在自有代码中举一反三地接入其他地址类型。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391