Go向C发送文件描述符异常:抽象UNIX域套接字客户端挂起
抽象UNIX域套接字发送文件描述符时C客户端挂起问题排查与解决
问题场景
尝试通过抽象UNIX域套接字从Go程序向C程序发送文件描述符,但C客户端始终在recvmsg调用处挂起,无法获取目标描述符。Go代码使用标准库实现,C代码直接调用系统接口。
核心问题分析
1. 抽象套接字地址不匹配
Go的net.ResolveUnixAddr处理抽象UNIX套接字时,会自动将路径开头的@替换为null字节(\0),因此Go端实际使用的抽象套接字名称是\0myabstractsocket。而原C代码直接将包含@的字符串复制到sockaddr_un.sun_path,再手动将第一个字节设为\0,导致实际套接字名称变为\0@myabstractsocket,两边地址不匹配,无法建立有效通信。
2. UDP模式通信逻辑错误
Unixgram(数据报模式,UDP)是无连接协议,服务器需要先接收客户端的消息(哪怕是空消息)来获取客户端地址,之后才能向客户端发送数据或文件描述符。原Go代码启动后直接向自身监听地址发送消息,没有目标客户端地址,消息根本无法被C客户端接收。
修复后的代码
Go端代码
package main import ( "fmt" "net" "os" "syscall" "os/signal" ) func main() { socketAddr := "@myabstractsocket" // 创建抽象UNIX域套接字 addr, err := net.ResolveUnixAddr("unixgram", socketAddr) if err != nil { fmt.Println("解析地址失败:", err) return } conn, err := net.ListenUnixgram("unixgram", addr) if err != nil { fmt.Println("监听套接字失败:", err) return } defer conn.Close() fmt.Println("服务器监听中:", socketAddr) // 接收客户端消息,获取客户端地址 buf := make([]byte, 1) clientAddr, err := conn.ReadFromUnix(buf) if err != nil { fmt.Println("读取客户端消息失败:", err) return } fmt.Println("已获取客户端地址") // 打开要发送的文件 file, err := os.Open("file.txt") if err != nil { fmt.Println("打开文件失败:", err) return } defer file.Close() // 获取文件描述符 fileDescriptor := file.Fd() // 向客户端发送文件描述符 fdMsg := syscall.UnixRights(int(fileDescriptor)) _, _, err = conn.WriteMsgUnix(nil, fdMsg, clientAddr) if err != nil { fmt.Println("发送文件描述符失败:", err) return } fmt.Println("文件描述符发送成功") done := make(chan os.Signal, 1) signal.Notify(done, syscall.SIGINT, syscall.SIGTERM) fmt.Println("按ctrl+c退出") <-done }
C端代码
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/socket.h> #include <sys/un.h> #include <string.h> #define BUFFER_SIZE 256 int main() { // 对应Go端的@myabstractsocket(Go会自动将@转为\0) const char *socketName = "myabstractsocket"; // 创建套接字 int sockfd = socket(AF_UNIX, SOCK_DGRAM, 0); if (sockfd == -1) { perror("socket"); exit(EXIT_FAILURE); } // 配置服务器地址 struct sockaddr_un addr; memset(&addr, 0, sizeof(addr)); addr.sun_family = AF_UNIX; // 抽象套接字标识:第一个字节设为0 addr.sun_path[0] = 0; // 复制实际套接字名称到sun_path[1]开始的位置 strncpy(&addr.sun_path[1], socketName, sizeof(addr.sun_path) - 2); // 计算抽象套接字地址长度 socklen_t addrLen = offsetof(struct sockaddr_un, sun_path) + 1 + strlen(socketName); if (connect(sockfd, (struct sockaddr *)&addr, addrLen) == -1) { perror("connect"); exit(EXIT_FAILURE); } // 向服务器发送空消息,触发服务器获取客户端地址 if (send(sockfd, "", 0, 0) == -1) { perror("send"); exit(EXIT_FAILURE); } // 准备接收文件描述符 char buffer[BUFFER_SIZE]; struct msghdr msg; struct iovec iov; union { struct cmsghdr cm; char control[CMSG_SPACE(sizeof(int))]; } control_un; struct cmsghdr *cmptr; iov.iov_base = buffer; iov.iov_len = sizeof(buffer); msg.msg_iov = &iov; msg.msg_iovlen = 1; msg.msg_name = NULL; msg.msg_namelen = 0; msg.msg_control = control_un.control; msg.msg_controllen = sizeof(control_un.control); msg.msg_flags = 0; fprintf(stderr, "等待文件描述符...\n"); ssize_t recvLen = recvmsg(sockfd, &msg, 0); if (recvLen == -1) { perror("recvmsg"); exit(EXIT_FAILURE); } fprintf(stderr, "收到消息,开始提取文件描述符...\n"); // 提取文件描述符 int receivedFd = -1; for (cmptr = CMSG_FIRSTHDR(&msg); cmptr != NULL; cmptr = CMSG_NXTHDR(&msg, cmptr)) { if (cmptr->cmsg_level == SOL_SOCKET && cmptr->cmsg_type == SCM_RIGHTS) { receivedFd = *((int *)CMSG_DATA(cmptr)); break; } } if (receivedFd == -1) { fprintf(stderr, "未接收到文件描述符\n"); exit(EXIT_FAILURE); } // 测试文件描述符有效性 char testBuf[1024]; ssize_t readLen = read(receivedFd, testBuf, sizeof(testBuf) - 1); if (readLen == -1) { perror("读取接收的文件描述符失败"); close(receivedFd); exit(EXIT_FAILURE); } testBuf[readLen] = '\0'; fprintf(stderr, "从接收的文件描述符读取内容:\n%s\n", testBuf); close(receivedFd); close(sockfd); return 0; }
修复说明
- 地址匹配修正:Go端保留
@前缀让标准库自动处理,C端手动设置syn_path[0]为0并复制不含@的名称,保证两边抽象套接字名称一致。 - UDP通信逻辑修正:C端先发送空消息,Go端接收后获取客户端地址,再针对性发送文件描述符,符合无连接协议的通信流程。
- 地址长度计算优化:使用
offsetof正确计算抽象套接字的地址长度,避免原代码中错误的长度计算方式。
内容的提问来源于stack exchange,提问作者Mitar
相关产品推荐
相关产品推荐

