Unix Domain Socket通信

最近接手了一个Linux下内核编程的项目,在阅读项目原有代码的基础上,学到了很多新知识,总结一下记录在这里。

在这个项目中,客户端进程和服务器进程在同一台机器上,使用Unix Domain Socket通信。

socket是为了网络通信设计的,但是Unix Domain Socket其实是一种进程间通信的机制,实现同一主机上的进程之间的通信,使用socket的方式相比消息、信号、共享内存等进程间通信方式更加的灵活可靠,同时其语法与socket通信基本相同,因此方便使用。

整体上的代码结构与使用socket进行网络通信相似,唯一的区别在于需要为进程绑定socket文件。

环境说明

操作系统:Ubuntu 18.10(使用Linux 4.18.0-25内核)

基本数据结构

// 用于socket通信的通用地址类型
struct sockaddr {
    unsigned short sa_family;
    char sa_data[14];
};
// 用于Unix域通信的地址类型
struct sockaddr_un
{
    uint8_t sun_len;
    sa_family_t sun_family;
    char sun_path[104];
}

基本操作模块

相关变量

int rc;
int server_sock, client_sock;
int sockaddr_len;
struct sockaddr_un server_sockaddr;
struct sockaddr_un client_sockaddr;
sockaddr_len = sizeof(struct sockaddr_un);
memset(& server_sockaddr, 0, sockaddr_len);
memset(& client_sockaddr, 0, sockaddr_len);

创建socket

需要使用AF_UNIX指定socket类型为Unix Domain类型,建立面向连接的通信。

server_sock = socket(AF_UNIX, SOCK_STREAM, 0);
if (server_sock == -1) {
    printf("%s\n", "SOCKET ERROR");
    exit(1);
}

绑定socket文件

在Unix Domain Socket中,不使用IP地址+端口来表示地址,而是使用本地保存的socket文件表示地址,因此需要建立socket到地址的绑定。主要注意在服务器端和客户端都要建立到socket文件的绑定。

#define SOCK_PATH "/tmp/server.socket"
server_sockaddr.sun_family = AF_UNIX;
strcpy(server_sockaddr.sun_path, SOCK_PATH);
rc = bind(server_sock, (struct sockaddr *)& server_sockaddr, sockaddr_len);
if (rc == -1) {
    printf("%s\n", "BIND ERROR");
    close(server_sock);
    exit(1);
}

监听地址等待连接

使用listen将一个socket变为等待被动连接的socket,同时指定了等待队列的长度,从而建立起服务器端的socket。需要注意将socket文件放置于所有用户可见的位置并修改访问权限。

chmod(SOCK_PATH, 0666);
rc = listen(server_sock, 16);
if (rc == -1) {
    printf("%s\n", "LISTEN ERROR");
    close(server_sock);
    exit(1);
}

发送连接请求

在客户端,首先要获得服务器端socket地址,也就是socket文件的路径,将其写入Unix的socket地址中,直接发起连接请求。

#define SERVER_PATH "/tmp/server.socket"
server_sockaddr.sun_family = AF_UNIX;
strcpy(server_sockaddr.sun_path, SERVER_PATH);
rc = connect(client_sock, (struct sockaddr *)& server_sockaddr, sockaddr_len);
if (rc == -1) {
    printf("%s\n", "CONNECT ERROR");
    close(client_sock);
    exit(1);
}

接受连接请求

接受请求并建立到请求着的socket通信,将对方的地址保存下来。

client_sock = accept(server_sock, (struct sockaddr *)& client_sockaddr, & sockaddr_len);
if (client_sock == -1) {
    close(client_sock);
    continue;
}

接受数据

rc = recv(client_sock, & reqbuf, req_len, 0);
if (rc == -1) {
    printf("%s\n", "RECV ERROR");
    close(client_sock);
    continue;
}

发送数据

rc = send(client_sock, & reqbuf, req_len, 0);
if (rc == -1) {
    printf("%s\n", "SEND ERROR");
    close(client_sock);
    exit(1);
}

获得通信对象信息

通过getsockopt函数可以获得socket连接的属性,包括通信对象的相关信息,由于在同一主机的不同进程间通信,可以获得进程的身份凭证,包括了进程号、用户ID和组ID。

/*
Defined in Linux/socket.h
struct ucred {
    __u32    pid;
    __u32    uid;
    __u32    gid;
};
*/

struct ucred cr;
ucred_len = sizeof(struct ucred);
# 使用SO_PEERCRED可以获得对方的身份凭证
# ucred结构体中包含了用户id和进程id
if (getsockopt(client_sock, SOL_SOCKET, SO_PEERCRED, & cr, & ucred_len) == -1) {
    close(client_sock);
    continue;
}

代码框架

服务器端

int server() {
    listenSocket = socket();
    bind();
    listen(listenSocket);
    while(1){
        clientSocket = accept();
        accept();
    recv() or send();
    closesocket(clientSocket);
    }
    closesocket(listenSocket);
}

客户端

int client() {
    client_socket = socket();
    bind();
    connect(client_socket, server_sockaddr);
    recv() or send();
    closesocket(clientSocket);
}
Lei Yang
Lei Yang
PhD candidate

My research interests include visual speech recognition and semantics segmentation.