golang之跨语言ipc通信

1 golang之跨语言ipc通信

文章目录

  • 1 golang之跨语言ipc通信
    • 1.1 unix domain Socket(unix域套接字)介绍
    • 1.2 IPC SOCKET通信
      • 1.2.1 函数及地址定义介绍
    • 1.2.2 UNIX domain socket服务端程序
      • 1.2.3 UNIX domain socket客户端程序
    • 1.3 跨编程语言进程通信

进程间通信(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的客户端程序,结果如下:

golang之跨语言ipc通信_第1张图片

可以看出传输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)
 }
}

你可能感兴趣的:(golang,golang,IPC,unix域套接字,UDS,SOCKET)