• golang之跨语言ipc通信


    1 golang之跨语言ipc通信


    进程间通信(IPC,InterProcess Communication),指不同进程之间通过进程间通信机制来传递数据的方式,进程间通讯广义上可以是统一主机或者不同主机间进程进行通信,这里专指同一主机间的通信,不包含基于TCP/IP的各种通信机制。IPC通信方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享内存、unix domain Socket等方式。

    由于IPC通信的文章,在百度上一搜一大把,这里就不做系统性的介绍,只介绍golang与C++跨语言的进程通信方式:unix domain Socket,这种方式也是文章中介绍比较少的,文章大都集中在AF_INET类型的socket,通过127.0.0.1回环进行通信,此种方式与AF_UNIX相比,需要经过网络层处理,性能下降了一两倍,而AF_UNIX类型的socket,不经过网络层,几乎相当于进程间内存拷贝,性能非常高,且可以跨编程语言,针对需要跨语言的进程通信,推荐此种方式。其他通信方式可参考:GOLANG之IPC:进程间通信进程间通信(IPC)介绍

    1.1 unix domain Socket(unix域套接字)介绍

    Unix domain socket 最开始就是指 IPC socket通信,用于实现同一主机上的进程间通信,所以进程说的IPC socket通信机制,并不是指网络socket,socket的出现确实是为网络通讯设计的,但后来在 socket 的框架上发展出一种 IPC 机制,就是 UNIX domain socket,虽然网络 socket 也可用于同一台主机的进程间通讯(通过 loopback机制,IP地址赋值 127.0.0.1),但是此种方式需要经过网络协议栈处理,UNIX domain socket 不需要经过网络协议栈,就不涉及打包拆包、计算校验和、维护序号和应答等处理操作,只是将应用层数据从一个进程拷贝到另一个进程,IPC 机制本质上是可靠的通讯。
    IPC socket通信是全双工的,基本上可以像网络socket一样操作,可跨编程语言通信,相比其它 IPC 机制有明显的优越性。Unix domain socket 是 POSIX 标准中的一个组件,不仅是unix相关系统,linux系统和windows系统(win10版本及以上,只支持SOCK_STREAM方式)也支持,具备很好的移植特性。

    1.2 IPC SOCKET通信

    1.2.1 函数及地址定义介绍

    各编程语言用法都类似,这里以c++语言为例,对器c/s通信过程进行介绍。

    首先看下socket文件描述符创建函数:
    SOCKET PASCAL FAR socket (_In_ int af, _In_ int type, _In_ int protocol);

    unix domain socket通信与网络socket流程基本一致,首先创建socket文件描述符,其相关参数赋值如下:

    • af:family,赋值为AF_UNIX,使用 AF_UNIX,在socket地址初始化时,指定的地址是一个socket文件的路径,而不是IP地址和端口,其会在系统上创建一个socket文件,不同进程通过读写这个文件来实现通信,此文件仅仅是为了客户端与服务器连接时使用,具体数据传输是通过内存传输。注意:服务端创建socket文件,客户端连接时指定此文件路径作为连接地址,就像网络socket的ip和端口一样,服务端创建时,此路径文件必须不存在,否则会报错。
    • type: 可以赋值为SOCK_DGRAM或SOCK_STREAM,与网络socket一样,SOCK_STREAM 意味着会提供按顺序的、可靠、双向、面向连接的比特流。SOCK_DGRAM 意味着会提供定长的、不可靠、无连接的通信。在unix domain socket只是指定不同的封装格式,性能上差别不大,由于windows仅支持SOCK_STREAM,一般默认此格式。
    • protocol:可默认选择0

    UNIX domain socket 地址结构体格式用结构体 sockaddr_un 表示,这与网络socket地址结构sockaddr_in不一样,sockaddr_un定义如下:

    typedef struct sockaddr_un
    {
         ADDRESS_FAMILY sun_family;     /* AF_UNIX */
         char sun_path[UNIX_PATH_MAX];  /* pathname */
    } SOCKADDR_UN, *PSOCKADDR_UN;
    

    网络ocket 地址是IP 地址加端口号,UNIX domain socket的地址是一个 socket类型的文件在文件系统中的路径,socket文件由bind()调用创建,如果调用 bind时该文件已存在,则 bind错误返回,所以在调用 bind()时要检查socket文件是否存在,如果存在删除后再bind。

    1.2.2 UNIX domain socket服务端程序

    还是以c++为例,操作系统为windows 10,有关linux的代码也类似,都文件需要更换下,示例代码如下:

    #include 
    #include 
    #include 
    #include 
    #include //socket相关操作
    #include 
    #include 
    #include  //sockaddr_un
    #include 
    
    
    #pragma comment(lib, "ws2_32.lib")
    
    using namespace std;
    
    #define SERVER_SOCKET_FILE "D:\\server_sock" //socket文件,bind时需要检查是否存在,存在的话先删除
    
    int main()
    {
     
     /*初始化启动信息*/
     WORD sockVersion = MAKEWORD(2, 2);//调用2.2版本的socket
     WSADATA wsaData;      //WSA(Windows Sockets Asynchronous)异步套接字
    
     //将指定版本的socket与该应用程序绑定
     if (WSAStartup(sockVersion, &wsaData) != 0) //返回为0则表示初始化成功
      return 0;
     /*创建服务器UNIX domain socket*/
     /*c++ win只支持SOCK_STREAM,其他平台及语言也支持SOCK_DGRAM,如果跨编程语言最好选择SOCK_STREAM*/
     SOCKET serverSocket = ::socket(AF_UNIX, SOCK_STREAM, 0);
     if (serverSocket == INVALID_SOCKET)   //如果创建失败,则输出错误
     {
      cout << "socket error:" << WSAGetLastError() << endl;
      WSACleanup();   //中止Windows Sockets DLL的使用;与上面WSAStartup()配套使用
      return 0;
     }
     
     //定义UNIX domain socket地址
     sockaddr_un serverAddr;
     serverAddr.sun_family = AF_UNIX;
     strncpy_s(serverAddr.sun_path, SERVER_SOCKET_FILE,sizeof(SERVER_SOCKET_FILE));//指定socket文件路径
     //Socket绑定地址,这里没检查sun_path对应的文件是否存在,正式代码需要判断
     if (bind(serverSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
     {
      cout << "Bind Error!" << endl;
      closesocket(serverSocket);
      WSACleanup();
      return 0;
     }
    
     //建立监听客户端请求的信号,设置最多有5个客户端
     if (listen(serverSocket, 5) == SOCKET_ERROR)//如果建立失败
     {
      cout << "Listen Error !" << endl;
      closesocket(serverSocket);    //关闭Socket套接字
      WSACleanup();
      return 0;
     }
     cout << "listen..." << endl;
    
     //开始不断处理各个客户端请求
     while (true)
     {
      SOCKET clientSocket = INVALID_SOCKET; //初始化一个客户端socket
      sockaddr_un clientAddr;     //客户端的地址结构
      int iAddrLength = sizeof(clientAddr); //求出该结构的长度
      cout << "waiting..." << endl;
      //接收客户端的连接请求,如果客户端绑定客户端socket地址,则clientAddr的sun_path既是客户端地址
      clientSocket = accept(serverSocket, (SOCKADDR*)&clientAddr, &iAddrLength);//accept为阻塞函数
    
      //查询当前读取缓冲区
      int defTcpRcvBufSize = -1;
      socklen_t optlen2 = sizeof(defTcpRcvBufSize);
      if (getsockopt(clientSocket, SOL_SOCKET, SO_RCVBUF, (char*)&defTcpRcvBufSize, &optlen2) < 0)
      {
       break;
      }
      //配置读取缓冲区,因此测试代码需要传输图片数据,这里配置接收缓冲器为8M
      int rcvBufSize = 1024 * 1024 * 8;
      printf("you want to set socket recv buff size to %d\n", rcvBufSize);
      int optlen = sizeof(rcvBufSize);
      if (setsockopt(clientSocket, SOL_SOCKET, SO_RCVBUF, (char*)&rcvBufSize, optlen) < 0)
      {
       break;
      }
      printf("set domain socket(%d) recv buff size to %d OK!!!\n", clientSocket, rcvBufSize);
    
      //开始不断接收该客户端数据
      char* buffFromClient;   //用于接收客户端传来的数据
      buffFromClient = new char[1024 * 1024 * 8];
      char sendToClientBuff[1024];
      while (true)
      {
       memset(buffFromClient, 0, sizeof(buffFromClient));
       //recv也为阻塞函数,只有客户端发送数据过来后,程序才会往下继续走
       int iLenOfRecvData = recv(clientSocket, buffFromClient, 1024 * 1024 * 8, 0);
    
       if (iLenOfRecvData > 0)  //如果接收的数据不为空
       {
        cout <<"iLenOfRecvData:"<< iLenOfRecvData << endl;
       }
       else
       {
        cout << "connect error..." << endl;
        break;
       }
    
       memset(sendToClientBuff,1,1024)
       //发送数据到客户端
       send(clientSocket, sendToClientBuff, sizeof(sendToClientBuff), 0);
      }
      closesocket(clientSocket);//关闭与该客户端的套接字
     }
     closesocket(serverSocket);//关闭服务器套接字
     WSACleanup();
     return 0;
    }
    

    网络 socket 编程基本一样,bind服务socket地址之后 listen,表示通过 bind 的地址(也就是 socket 文件)提供服务,然后通过accept() 函数等待并获取连接连接。accept为每个连接创立新的套接字并从监听队列中移除这个连接,由于 UNIX domain socket为本地内存拷贝,当传输大尺寸文件和数据时,尽量把发送缓冲区和接收缓冲区配置大于其传输的最大值,这样可避免分包发送和组包接收带来的性能损耗,具体可通过setsockopt来配置。

    1.2.3 UNIX domain socket客户端程序

    还是以c++为例,代码运行再windows 10上,具体代码如下:

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    
    #pragma comment(lib, "ws2_32.lib")
    
    using namespace std;
    
    #define SOCKET_SERVER "D:\\server_sock"
    #define SOCKET_CLIENT "D:\\client_sock"
    
    int main()
    {
    
     /*初始化启动信息*/
     WORD sockVersion = MAKEWORD(2, 2);//调用2.2版本的socket
     WSADATA wsaData;      //WSA(Windows Sockets Asynchronous)异步套接字
    
     //将指定版本的socket与该应用程序绑定
     if (WSAStartup(sockVersion, &wsaData) != 0) //返回为0则表示初始化成功
      return 0;
     /*创建客户端Socket*/
     SOCKET clientSocket = ::socket(AF_UNIX, SOCK_STREAM, 0);
     if (clientSocket == INVALID_SOCKET)   //如果创建失败,则输出错误
     {
      cout << "socket error:" << WSAGetLastError() << endl;
      WSACleanup();   //中止Windows Sockets DLL的使用;与上面WSAStartup()配套使用
      return 0;
     }
    
     sockaddr_un clientAddr;
     clientAddr.sun_family = AF_UNIX;
     strncpy_s(clientAddr.sun_path, SOCKET_CLIENT, sizeof(SOCKET_CLIENT));
     //客户端显示绑定客户端地址,便于服务端区分客户端,也可不调用,服务端将无法获得地址,但是可通过socket句柄区分
     //此处需要检查SOCKET_CLIENT对应的socket客户端文件是否存在,如果存在要删除,这里没写
     if (bind(clientSocket, (SOCKADDR*)&clientAddr, sizeof(clientAddr)) == SOCKET_ERROR)
     {
      cout << "Bind Error!" << endl;
      closesocket(clientSocket);
      WSACleanup();
      return 0;
     }
    
    
     sockaddr_un serverAddr;
     serverAddr.sun_family = AF_UNIX;
     strncpy_s(serverAddr.sun_path, SOCKET_SERVER, sizeof(SOCKET_SERVER));
     if (connect(clientSocket, (SOCKADDR*)&serverAddr, sizeof(clientAddr)) < 0) {
      perror("connect error");
      exit(1);
     }
     while (true)
     {
      //收发数据
      char* bufrecv;   //用于接收客户端传来的数据
      bufrecv = new char[1024 * 1024 * 8];
      
      char* bufsend;   //用于接收客户端传来的数据
      bufsend = new char[1024 * 1024 * 8];
      memset(bufsend, 2, 1024 * 1024 * 8);
      send(clientSocket, bufsend, sizeof(bufsend), 0);
      memset(bufrecv, 0, 1024 * 1024 * 8);
      //recv也为阻塞函数
      int iLenOfRecvData = recv(clientSocket, bufrecv, 1024 * 1024 * 8, 0);
      if (iLenOfRecvData > 0)  //如果接收的数据不为空
      {
       cout << "iLenOfRecvData:" << iLenOfRecvData << endl;
      }
      else
      {
       cout << "connect error..." << endl;
       break;
      }
     }
     closesocket(clientSocket);//关闭服务器套接字
     WSACleanup();
     return 0;
    }
    

    编译服务端程序和客户端程序,先运行客户端程序,再运行服务端程序,即可实现两个进程的通信,以上代码基于vs2019编译实现。

    1.3 跨编程语言进程通信

    这里介绍下golang语言和c++语言,通过UNIX domain socket进行单主机不同语言通信,假设场景在边缘智能场景中,AI推理进程采用C++编写,视频抽帧进程采用golang编写,需要从视频抽帧进程传递抽帧的RGB数据到AI推理进程,此时通过UNIX domain socket进行通信,这里需要传输大数据。
    其服务端代码示例,可采用上文服务端程序实例,为c++编写,这里提供golang编写的客户端实例代码,监听的socket路径为:D:\server_sock
    代码如下:

    package main
    
    import (
     "fmt"
     "net"
     "time"
    )
    
    var quitSig chan bool
    
    func main() {
     var serverAddr *net.UnixAddr //定义UNIX domain socket地址
     serverAddr, _ = net.ResolveUnixAddr("unix", "D:\\server_sock") //服务地址
     conn, _ := net.DialUnix("unix", nil, serverAddr) //"unix"表示SOCK_STREAM方式
     defer conn.Close()
     fmt.Println("connected!")
    
     go ClientSend(conn) //开启发送线程
     b := []byte("cleint send example\n")
     conn.Write(b)
    
     <-quitSig
    }
    
    func ClientSend(conn *net.UnixConn) {
     for {
      time.Sleep(time.Second)
      //创建消息缓冲区
      buffer := make([]byte, 1024*1024*8)
      buffer[0] = '1'
      t := time.Now()
      conn.Write(buffer) //发送8M数据
      elapsed := time.Since(t)
      fmt.Println("app elapsed:", elapsed)
     }
    }
    

    golang对UNIX domain socket进行了单独的封装,比c++更加简单和清晰。
    运行c++服务端程序及golang的客户端程序,结果如下:

    跨语言

    可以看出传输8M数据,在服务端设置接收缓冲区为大于8M时,一次即可接受完,由于不经过网络协议栈,不需要考虑组包的情况,且耗时基本在毫秒级,相当于内存拷贝的速度了。

    golang进程提供了很简洁的ipc socket通信方式,如果都是golang语言编写的进程,且单主机进程通信,建议采用此方式,下面给出golang语言的服务端代码实例:

    package main
    
    import (
     "bufio"
     "fmt"
     "net"
     "time"
    )
    
    func main() {
     var servadd *net.UnixAddr
    
     servadd, _ = net.ResolveUnixAddr("unix", "D:\\server_sock")
    
     ulistener, _ := net.ListenUnix("unix", servadd)
    
     defer ulistener.Close()
    
     for {
      conn, err := ulistener.AcceptUnix()
      if err != nil {
       continue
      }
    
      fmt.Println("A client connected : " + conn.RemoteAddr().String())
      go unixrw(conn)
     }
    
    }
    
    func unixrw(conn *net.UnixConn) {
     addrStr := conn.RemoteAddr().String()
     defer func() {
      fmt.Println("disconnected :" + addrStr)
      conn.Close()
     }()
     reader := bufio.NewReader(conn)
    
     for {
      //读取客户端内容
      message, err := reader.ReadString('\n')
      if err != nil {
       return
      }
      fmt.Println(string(message))
      msg := time.Now().String() + "\n"
      b := []byte(msg)
      //将当前时间写回给客户端
      conn.Write([]byte("service : "))
      conn.Write(b)
     }
    }
    
  • 相关阅读:
    随笔-那些快乐的日子
    浅谈DAO智能合约DAPP模式系统开发技术及程序代码分析原理
    阿里大手子评:入门到大成!GitHub新上线并发编程深度解析实战PDF
    浅谈Solidity智能合约DAPP项目系统开发技术逻辑(Solidity语言框架)
    MinIO学习
    C++,没有与这些操作数匹配的“[]“运算符
    电阻值过大的原因、影响和解决方法 | 百能云芯
    软件测试技术复习
    自然语言处理之语言模型(LM)介绍
    【Redis】redis的特性和使用场景
  • 原文地址:https://blog.csdn.net/water1209/article/details/127089413