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);
}