一、本地Socket到底是什么?

咱们先聊聊本地Socket。说白了,它就是同一台机器上两个进程之间通信的"管道"。你想想看,网络Socket是用来跨机器通信的,而本地Socket呢?就是给本机上的进程们互相递个话。

我记得刚入行那会儿,有个同事非要用TCP回环地址(127.0.0.1)做进程间通信。我说你干嘛不用本地Socket?他还不信。后来压测的时候,本地Socket比TCP回环快了将近一倍,他才服气。

1.1 地址结构:sockaddr_un

本地Socket的地址结构长这样,我直接上代码:

#include <sys/un.h>

struct sockaddr_un {
    sa_family_t sun_family;  // 地址族,填 AF_UNIX 或 AF_LOCAL
    char        sun_path[];  // 路径名,比如 "/tmp/my_socket"
};

这里有个坑,我踩过。sun_path的长度是有限的,不同系统不一样。Linux上是108个字符,BSD上是104个。你要是路径写太长,直接截断,连个警告都没有。

避坑指南:我曾经在一个项目里,把socket文件路径写成了嵌套目录,结果bind()一直失败。后来才发现,父目录必须存在,而且进程要有写权限。嗯,这个坑我记了好几年。

二、两种工作模式

2.1 本地流式Socket(SOCK_STREAM)

流式Socket,就是面向连接的。像打电话一样,先拨号,再通话,最后挂断。它保证数据有序、不丢失、不重复。

我个人的习惯是,如果两个进程需要频繁交换大量数据,就用流式。比如数据库客户端和服务器之间,用流式就特别合适。

// 服务端示例
int sfd = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "/tmp/mysocket");

bind(sfd, (struct sockaddr*)&addr, sizeof(addr));
listen(sfd, 5);

int cfd = accept(sfd, NULL, NULL);
// 然后就可以用 read/write 通信了

2.2 本地数据报Socket(SOCK_DGRAM)

数据报Socket,像发快递。不需要建立连接,直接扔数据包。但它不保证顺序,也不保证一定到达。

什么时候用数据报?我举个例子。监控系统里,各个模块上报心跳包,丢一两个无所谓,但要求实时性高。这时候数据报就比流式合适。

// 发送端
int sfd = socket(AF_UNIX, SOCK_DGRAM, 0);
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "/tmp/dgram_socket");

sendto(sfd, "hello", 5, 0, 
       (struct sockaddr*)&addr, sizeof(addr));

三、本地Socket vs 网络Socket

我整理了个表格,方便你对比:

对比项 本地Socket 网络Socket
地址族 AF_UNIX / AF_LOCAL AF_INET / AF_INET6
地址形式 文件路径 IP:端口
跨机器 不行 可以
性能 高(不走网络协议栈) 相对低
安全性 依赖文件权限 依赖防火墙等

为什么会这样?因为本地Socket的数据传输,压根不经过网络协议栈。数据直接从用户态拷贝到另一个用户态,中间没有TCP/IP那堆开销。你想想看,这能不快吗?

四、应用场景

我这些年做过的项目里,本地Socket最常见的几个场景:

  • 数据库通信:比如MySQL、PostgreSQL的本地连接,用的就是本地Socket
  • 容器间通信:Docker里同一个宿主机上的容器,用本地Socket比走网络快得多
  • 多媒体框架:比如PulseAudio,用本地Socket传递音频数据
  • 系统服务:systemd的journald日志服务,内部通信全用本地Socket

我的建议:只要两个进程在同一台机器上,优先考虑本地Socket。别图省事直接用TCP回环,性能差距真的很大。

五、性能优势到底有多大?

我做过一个简单的压测。同样传输1MB数据,本地Socket比TCP回环快了大概30%-50%。原因有三:

  1. 没有协议栈开销:TCP的三次握手、四次挥手全免了
  2. 数据拷贝少:内核态到用户态的拷贝次数更少
  3. 没有网络延迟:哪怕回环接口,也有微秒级的延迟

嗯,这里要注意。性能优势在高频小数据包场景下特别明显。如果你一次传几MB的大块数据,差距反而没那么大,因为瓶颈在内存拷贝上。

六、综合案例:本地日志收集器

我写个完整的例子,展示本地Socket怎么用。这个案例是一个简单的日志收集系统:

/* log_server.c - 日志服务端 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>

#define SOCKET_PATH "/tmp/log_collector"
#define BUFFER_SIZE 1024

int main() {
    int server_fd, client_fd;
    struct sockaddr_un addr;
    char buffer[BUFFER_SIZE];
    
    // 1. 创建socket
    server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (server_fd == -1) {
        perror("socket");
        exit(1);
    }
    
    // 2. 绑定地址
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1);
    
    // 如果socket文件已存在,先删除
    unlink(SOCKET_PATH);
    
    if (bind(server_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
        perror("bind");
        exit(1);
    }
    
    // 3. 监听
    if (listen(server_fd, 5) == -1) {
        perror("listen");
        exit(1);
    }
    
    printf("日志服务器启动,等待连接...\n");
    
    // 4. 接受连接
    client_fd = accept(server_fd, NULL, NULL);
    if (client_fd == -1) {
        perror("accept");
        exit(1);
    }
    
    // 5. 读取日志
    while (1) {
        memset(buffer, 0, BUFFER_SIZE);
        int n = read(client_fd, buffer, BUFFER_SIZE - 1);
        if (n <= 0) break;
        
        printf("[收到日志] %s", buffer);
    }
    
    close(client_fd);
    close(server_fd);
    unlink(SOCKET_PATH);
    
    return 0;
}
/* log_client.c - 日志客户端 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>

#define SOCKET_PATH "/tmp/log_collector"

int main() {
    int sock_fd;
    struct sockaddr_un addr;
    char message[] = "这是一条测试日志\n";
    
    // 1. 创建socket
    sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sock_fd == -1) {
        perror("socket");
        exit(1);
    }
    
    // 2. 连接服务器
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1);
    
    if (connect(sock_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
        perror("connect");
        exit(1);
    }
    
    // 3. 发送日志
    write(sock_fd, message, strlen(message));
    printf("日志已发送\n");
    
    close(sock_fd);
    return 0;
}

运行方式:先启动服务端 ./log_server,再启动客户端 ./log_client。服务端会打印出客户端发送的日志。

七、知识体系总览

我把本章的核心知识点画了张图,方便你理解整体结构:

本地Socket知识体系 本地Socket sockaddr_un 流式(SOCK_STREAM) / 数据报(SOCK_DGRAM) vs 网络Socket 应用场景 性能优势(无协议栈开销) 日志收集器案例 核心要点: 1. 同一台机器上进程间通信的首选方案 2. 比TCP回环快30%-50% 3. 注意socket文件路径和权限

好了,本地Socket的核心内容就这些。记住一句话:同机通信,首选本地Socket。下一章咱们聊聊有名管道,那也是个好东西。


公众号:蓝海资料掘金营,微信deep3321