由于IPC通信的文章,在百度上一搜一大把,这里就不做系统性的介绍,只介绍golang与C++跨语言的进程通信方式:unix domain Socket,这种方式也是文章中介绍比较少的,文章大都集中在AF_INET类型的socket,通过127.0.0.1回环进行通信,此种方式与AF_UNIX相比,需要经过网络层处理,性能下降了一两倍,而AF_UNIX类型的socket,不经过网络层,几乎相当于进程间内存拷贝,性能非常高,且可以跨编程语言,针对需要跨语言的进程通信,推荐此种方式。其他通信方式可参考:GOLANG之IPC:进程间通信、进程间通信(IPC)介绍
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方式)也支持,具备很好的移植特性。
各编程语言用法都类似,这里以c++语言为例,对器c/s通信过程进行介绍。
首先看下socket文件描述符创建函数:
SOCKET PASCAL FAR socket (_In_ int af, _In_ int type, _In_ int protocol);
unix domain socket通信与网络socket流程基本一致,首先创建socket文件描述符,其相关参数赋值如下:
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。
还是以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来配置。
还是以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编译实现。
这里介绍下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)
}
}