找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2033

积分

0

好友

285

主题
发表于 前天 23:40 | 查看: 3| 回复: 0

在使用 socket 通信时,无论是本机内部通信,还是两台机器通信,也无论是TCP的方式,还是UDP的方式,一般都要指定IP和端口号。既然常规的socket通信需要IP和端口,那么在同一台机器内部,有没有更高效的通信方式呢?

答案是肯定的。在Linux开发中,如果是同一台设备内部通信,也可以不需要IP和端口号,这就是Unix域socket通信。它实际上是通过文件系统中的特殊文件来实现进程间通信,从而避免了网络协议栈的开销。本文将通过实例演示Unix域socket的使用,涵盖UDP和TCP两种模式。

为了更好理解Unix域socket,我们不妨先回顾一下常规的TCP和UDP socket通信模型。下图清晰地展示了两种模式下的客户端与服务端交互流程:

UDP与TCP套接字通信流程对比图

1 Unix域socket基础知识

在使用IP和端口号的网络socket通信中,我们常用到 sockaddrsockaddr_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和端口,在调用 bindconnect 等函数时,再将其强制转换为 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的客户端与服务端通信示例。功能包括:

  1. 实现一对一通信。
  2. 客户端和服务端分别运行独立线程。
  3. 通信建立后,客户端定时向服务端发送消息。
    我们将分别使用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客户端流程总结

  1. 创建socket (socket, AF_UNIX, SOCK_DGRAM)。
  2. 使用 sendto 向指定的socket文件路径发送消息。

2.1.2 服务端代码

UDP服务端同样是无连接的。它需要先创建socket,然后将其绑定 (bind) 到一个特定的socket文件路径上,之后便可使用 recvfromread 来接收发往该地址的消息。

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服务端流程总结

  1. 创建socket (socket, AF_UNIX, SOCK_DGRAM)。
  2. 绑定到指定的socket文件路径 (bind)。
  3. 接收消息 (recvfrom/read)。

2.2 TCP方式

2.2.1 客户端代码

TCP是面向连接的协议。Unix域TCP客户端需要先创建socket,然后主动连接 (connect) 到服务端监听的socket文件路径。成功连接后,才能使用 sendwrite 发送数据。注意,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客户端流程总结

  1. 创建socket (socket, AF_UNIX, SOCK_STREAM)。
  2. 连接到指定的socket文件路径 (connect)。
  3. 发送消息 (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服务端流程总结

  1. 创建socket (socket, AF_UNIX, SOCK_STREAM)。
  2. 绑定到指定的socket文件路径 (bind)。
  3. 监听连接请求 (listen)。
  4. 接受客户端连接 (accept)。
  5. 接收消息 (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.socketunixTCP.socket。它们就是Unix域socket通信的“地址”载体。

3 总结

本文详细介绍了Unix域Socket通信的 C++ 实现,涵盖了UDP和TCP两种模式。其核心流程可以总结为下图,清晰地对比了两种方式下客户端与服务端的操作步骤:

Unix域socket通信UDP与TCP方式流程图

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




上一篇:微服务网关如何选型?核心架构设计与主流产品对比指南
下一篇:Java应用JAR包动态上传与热部署实现详解
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-16 04:00 , Processed in 0.256555 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表