在使用 socket 通信时,无论是本机内部通信,还是两台机器通信,也无论是TCP的方式,还是UDP的方式,一般都要指定IP和端口号。既然常规的socket通信需要IP和端口,那么在同一台机器内部,有没有更高效的通信方式呢?
答案是肯定的。在Linux开发中,如果是同一台设备内部通信,也可以不需要IP和端口号,这就是Unix域socket通信。它实际上是通过文件系统中的特殊文件来实现进程间通信,从而避免了网络协议栈的开销。本文将通过实例演示Unix域socket的使用,涵盖UDP和TCP两种模式。
为了更好理解Unix域socket,我们不妨先回顾一下常规的TCP和UDP socket通信模型。下图清晰地展示了两种模式下的客户端与服务端交互流程:

1 Unix域socket基础知识
在使用IP和端口号的网络socket通信中,我们常用到 sockaddr 和 sockaddr_in 结构体。它们大小相同(16字节),且都有 family 属性,但内部组织方式不同:
sockaddr 用一个14字节的数组 sa_data 来存放地址信息。
sockaddr_in 则将这14字节拆分为端口 (sin_port)、IP地址 (sin_addr) 和填充字段 (sin_zero)。
#include <netinet/in.h>
struct sockaddr {
unsigned short sa_family; // 2 bytes address family, AF_xxx
char sa_data[14]; // 14 bytes of protocol address
};
// IPv4 AF_INET sockets:
struct sockaddr_in {
short sin_family; // 2 bytes e.g. AF_INET, AF_INET6
unsigned short sin_port; // 2 bytes e.g. htons(3490)
struct in_addr sin_addr; // 4 bytes see struct in_addr, below
char sin_zero[8]; // 8 bytes zero this if you want to
};
struct in_addr {
unsigned long s_addr; // 4 bytes load with inet_pton()
};
这两个结构体包含的信息本质相同,但用法有惯例:我们通常用 sockaddr_in 来填充类型、IP和端口,在调用 bind、connect 等函数时,再将其强制转换为 sockaddr* 类型。
那么,Unix域socket使用什么结构体呢?除了通用的 sockaddr,它还有一个专属的结构体 sockaddr_un:
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX */
char sun_path[UNIX_PATH_MAX]; /* pathname */
};
与 sockaddr_in 类比,sockaddr_un 同样简洁,只有协议族(固定为 AF_UNIX)和路径名。这个路径名就是用于通信的“地址”,它对应文件系统中的一个特殊socket文件。
2 编程测试
本次测试的目标是实现一个Unix域socket的客户端与服务端通信示例。功能包括:
- 实现一对一通信。
- 客户端和服务端分别运行独立线程。
- 通信建立后,客户端定时向服务端发送消息。
我们将分别使用UDP(数据报)和TCP(流)两种传输方式来演示。
首先,是程序所需的头文件和宏定义:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <thread>
#include <string>
#include "printUtil.h"
#define UNIX_UDP_SOCKET_ADDR "unixUDP.socket"
#define UNIX_TCP_SOCKET_ADDR "unixTCP.socket"
#define BUF_SIZE 100
using namespace std;
2.1 UDP方式
2.1.1 客户端代码
Unix域socket的UDP客户端程序遵循无连接模型。作为客户端,只需创建一个socket,然后向目标地址(这里是文件路径)调用 sendto 发送消息即可。关键点在于创建socket时,第二个参数需指定为 SOCK_DGRAM,表示数据报服务。
void UdpClientThread()
{
int sockfd = socket(AF_UNIX, SOCK_DGRAM, 0);
if (sockfd < 0)
{
PRINT("create socket fail\n");
return;
}
PRINT("create socketfd:%d\n", sockfd);
struct sockaddr_un addr;
memset (&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, UNIX_UDP_SOCKET_ADDR);
while(1)
{
static int i = 0;
std::string str("helloUDP" + std::to_string(++i));
sendto(sockfd, str.c_str(), str.length(), 0, (struct sockaddr *)&addr, sizeof(addr));
sleep(1);
}
}
Unix域UDP客户端流程总结:
- 创建socket (
socket, AF_UNIX, SOCK_DGRAM)。
- 使用
sendto 向指定的socket文件路径发送消息。
2.1.2 服务端代码
UDP服务端同样是无连接的。它需要先创建socket,然后将其绑定 (bind) 到一个特定的socket文件路径上,之后便可使用 recvfrom 或 read 来接收发往该地址的消息。
void UdpServerThread()
{
int sockfd = socket(AF_UNIX, SOCK_DGRAM, 0);
if (sockfd < 0)
{
PRINT("create socket fail\n");
return;
}
PRINT("create socketfd:%d\n", sockfd);
struct sockaddr_un addr;
memset (&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, UNIX_UDP_SOCKET_ADDR);
if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)))
{
PRINT("bind fail\n");
return;
}
size_t size = 0;
char buf[BUF_SIZE] = {0};
while(1)
{
size = recvfrom(sockfd, buf, BUF_SIZE, 0, NULL, NULL);
//size = read(sockfd, buf, BUF_SIZE);
if (size > 0)
{
PRINT("recv:%s\n", buf);
}
}
}
Unix域UDP服务端流程总结:
- 创建socket (
socket, AF_UNIX, SOCK_DGRAM)。
- 绑定到指定的socket文件路径 (
bind)。
- 接收消息 (
recvfrom/read)。
2.2 TCP方式
2.2.1 客户端代码
TCP是面向连接的协议。Unix域TCP客户端需要先创建socket,然后主动连接 (connect) 到服务端监听的socket文件路径。成功连接后,才能使用 send 或 write 发送数据。注意,socket类型应指定为 SOCK_STREAM。
void TcpClientThread()
{
//------------socket
int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sockfd < 0)
{
PRINT("create socket fail\n");
return;
}
PRINT("create socketfd:%d\n", sockfd);
struct sockaddr_un addr;
memset (&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, UNIX_TCP_SOCKET_ADDR);
sleep(2);//wait server ready
//------------connect
if (connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)))
{
PRINT("connect fail\n");
return;
}
PRINT("connect ok\n");
while(1)
{
static int i = 0;
std::string str("helloTCP" + std::to_string(++i));
//------------send
send(sockfd, str.c_str(), str.length(), 0);
//write(sockfd, str.c_str(), str.length());
sleep(1);
}
}
Unix域TCP客户端流程总结:
- 创建socket (
socket, AF_UNIX, SOCK_STREAM)。
- 连接到指定的socket文件路径 (
connect)。
- 发送消息 (
send/write)。
2.2.2 服务端代码
TCP服务端的工作稍多几步。它需要创建socket并绑定路径,之后进入监听状态 (listen),等待客户端的连接请求。当有客户端连接时,accept 会返回一个新的socket描述符用于与此客户端通信,随后便可通过该描述符接收数据。
void TcpServerThread()
{
//------------socket
int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sockfd < 0)
{
PRINT("create socket fail\n");
return;
}
PRINT("create socketfd:%d\n", sockfd);
struct sockaddr_un addr;
memset (&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, UNIX_TCP_SOCKET_ADDR);
//------------bind
if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)))
{
PRINT("bind fail\n");
return;
}
PRINT("bind ok\n");
//------------listen
if (listen(sockfd, 5))
{
PRINT("listen fail\n");
return;
}
PRINT("listen ok\n");
//------------accept
int clientfd = accept(sockfd, NULL, NULL);
PRINT("accept clientfd:%d\n", clientfd);
if (clientfd > 0)
{
size_t size = 0;
char buf[BUF_SIZE] = {0};
while(1)
{
//------------recv
size = recv(clientfd, buf, BUF_SIZE, 0);
//size = read(clientfd, buf, BUF_SIZE);
if (size > 0)
{
PRINT("recv:%s\n", buf);
}
sleep(1);
}
}
PRINT("end\n");
}
Unix域TCP服务端流程总结:
- 创建socket (
socket, AF_UNIX, SOCK_STREAM)。
- 绑定到指定的socket文件路径 (
bind)。
- 监听连接请求 (
listen)。
- 接受客户端连接 (
accept)。
- 接收消息 (
recv/read)。
2.3 一种调试打印技巧
为了方便调试,让每条打印信息都自动带上其所在的函数名,我们可以定义一个 PRINT 宏。它是对 printf 的封装,能自动添加 [函数名] 的前缀。以下是 printUtil.h 文件的内容:
#ifndef __PRINTUTIL_H_
#define __PRINTUTIL_H_
#define FIRST(...) FIRST_HELPER(__VA_ARGS__, throwaway)
#define FIRST_HELPER(first, ...) first
#define REST(...) REST_HELPER(NUM(__VA_ARGS__), __VA_ARGS__)
#define REST_HELPER(qty, ...) REST_HELPER2(qty, __VA_ARGS__)
#define REST_HELPER2(qty, ...) REST_HELPER_##qty(__VA_ARGS__)
#define REST_HELPER_ONE(first)
#define REST_HELPER_TWOORMORE(first, ...) , __VA_ARGS__
#define NUM(...) \
SELECT_10TH(__VA_ARGS__, TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE,\
TWOORMORE, TWOORMORE, TWOORMORE, TWOORMORE, ONE, throwaway)
#define SELECT_10TH(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, ...) a10
//自定义打印格式
#define PRINT(...) printf("[%s] " FIRST(__VA_ARGS__), __func__ REST(__VA_ARGS__))
#endif
2.4 测试结果
在 main 函数中,我们启动UDP和TCP的服务端与客户端线程。注意,程序开始时会使用 unlink 删除可能已存在的socket文件,确保每次运行环境干净。
int main()
{
unlink(UNIX_UDP_SOCKET_ADDR);
unlink(UNIX_TCP_SOCKET_ADDR);
thread th1(UdpServerThread);
thread th2(UdpClientThread);
thread th3(TcpServerThread);
thread th4(TcpClientThread);
th1.join();
th2.join();
th3.join();
th4.join();
PRINT("hello\n");
}
程序运行后,输出结果如下。可以看到,UDP和TCP的客户端都能成功发送消息,服务端也能正确接收。
[UdpServerThread] create socketfd:3
[TcpServerThread] create socketfd:5
[TcpClientThread] create socketfd:6
[TcpServerThread] bind ok
[TcpServerThread] listen ok
[UdpClientThread] create socketfd:4
[UdpServerThread] recv:helloUDP1
[UdpServerThread] recv:helloUDP2
[TcpClientThread] connect ok
[TcpServerThread] accept clientfd:7
[TcpServerThread] recv:helloTCP1
[UdpServerThread] recv:helloUDP3
[TcpServerThread] recv:helloTCP2
[UdpServerThread] recv:helloUDP4
[TcpServerThread] recv:helloTCP3
[UdpServerThread] recv:helloUDP5
[TcpServerThread] recv:helloTCP4
[UdpServerThread] recv:helloUDP6
[TcpServerThread] recv:helloTCP5
[UdpServerThread] recv:helloUDP7
[TcpServerThread] recv:helloTCP6
[UdpServerThread] recv:helloUDP8
[UdpServerThread] recv:helloUDP9
[TcpServerThread] recv:helloTCP7
此外,程序运行后会在当前目录生成两个socket文件:unixUDP.socket 和 unixTCP.socket。它们就是Unix域socket通信的“地址”载体。
3 总结
本文详细介绍了Unix域Socket通信的 C++ 实现,涵盖了UDP和TCP两种模式。其核心流程可以总结为下图,清晰地对比了两种方式下客户端与服务端的操作步骤:

通过Unix域Socket进行本地进程间通信,最大的优势在于它绕过了复杂的网络协议栈,通信效率更高,且不再需要分配和管理IP地址与端口号,只需指定一个文件系统路径即可。这使得它成为Linux/Unix系统上高性能本地IPC(进程间通信)的理想选择之一。如果你想深入探讨更多网络编程或系统编程的细节,欢迎在 云栈社区 交流分享。