文章目錄
-
- 歡迎通路我的個人部落格
- 引言
- 簡介
- Unix 域套接字位址結構
- 建立 Unix 域套接字
- 綁定 Unix 域套接字
- Unix 域中的流 socket
- Unix 域中的資料報 socket
- Unix 域套接字的權限
- 使用 socketpair 建立互聯的 socket 對
- 使用 Unix 域套接字傳遞描述符
- 參考資料
歡迎通路我的個人部落格
部落格
引言
在 Linux 中有許多進行
程序間通信
的方法。今天部落客向大家介紹一種常用的程序間通信的方法 ——
Unix 域套接字
。
簡介
Unix 域套接字
是一種在本機的程序間進行通信的一種方法。雖然 Unix 域套接字的接口與
TCP 和 UDP 套接字
的接口十分相似,但是 Unix 域套接字隻能用于同一台機器的程序間通信,不能讓兩個位于不同機器的程序進行通信。正由于這個特性,Unix 域套接字可以可靠地在兩個程序間複制資料,不用像 TCP 一樣采用一些諸如 * 添加網絡報頭 、 計算檢驗和 、 産生順序号 * 等一系列保證資料完整性的操作。是以,在同一台機器上進行程序間通信時,Unix 域套接字的效率往往比 TCP 套接字的效率要高。
因為 Unix 域套接字的效率比較高,一些程式經常用 Unix 套接字代替 TCP 套接字。例如當 MySQL 的伺服器程序和用戶端程序在同一台機器上時,可以用 Unix 域套接字代替 TCP 套接字。
Unix 域套接字位址結構
在使用 TCP 套接字和 UDP 套接字時,我們需要用
struct sockaddr_in
(IPv4)定義套接字的位址結構,與之相似,Unix 域套接字使用
struct sockaddr_un
定義套接字的位址結構。
struct sockaddr_un
的定義如下(* 位于頭檔案 sys/un.h 中 *):
struct sockaddr_un
{
sa_family_t sun_family;
char sun_path[108];
};
在使用 Internet 域套接字進行程式設計時,需要将
struct sockaddr_in
的
sin_family
成員設定為
AF_INET
(IPv4)。與之類似,在使用 Unix 域套接字時,需要将 sun_family 設定為
AF_UNIX
或
AF_LOCAL
(* 這兩個宏的作用完全相同,都表示 UNIX 域 *)。
struct sockaddr_un
的第二個成員
sun_path
表示 socket 的位址。在 Unix 域中,socket 的位址用路徑名表示。例如,可以将 sun_path 設定為
/tmp/unixsock
。由于路徑名是一個字元串,是以 sun_path 必須能夠容納字元串的字元和結尾的
'\0'
。需要注意的是,标準并沒有規定 sun_path 的大小,在某些平台中,sun_path 的大小可能是 104、92 等值。是以如果需要保證可移植性,在編碼時應該使用 sun_path 的最小值。
建立 Unix 域套接字
Unix 域套接字使用
socket
函數建立,與 Internet 域套接字一樣,Unix 域套接字也有流套接字和資料報套接字兩種:
int unix_sock_fd1 = socket(AF_UNIX, SOCK_STREAM, 0); // Unix 域中的流 socket
int unix_sock_fd2 = socket(AF_UNIX, SOCK_DGRAM, 0); // Unix 域中的資料包 socket
稍後将介紹這兩種套接字的用法和差別。
綁定 Unix 域套接字
使用
bind
函數可以将一個 Unix 套接字綁定到一個位址上。綁定 Unix 域套接字時,bind 會在指定的路徑名處建立一個表示 Unix 域套接字的檔案。Unix 域套接字與路徑名是一一對應關系,即一個 Unix 域套接字隻能綁定到一個路徑名上,一個路徑名也隻能被一個套接字綁定。一般要把 Unix 域套接字綁定到一個
絕對路徑
上,例如:
struct sockaddr_un addr;
addr.sun_family = AF_LOCAL;
strcpy(addr.sun_path, "/tmp/sockaddr");
int unix_sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (bind(unix_sock_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0)
{
fprintf(stderr, "bind error\n");
}
Unix 域套接字被綁定後,可以使用 getsockname 擷取套接字綁定的路徑名:
struct sockaddr_un addr2;
socklen_t len = sizeof(addr2);
getsockname(unix_sock_fd, (struct sockaddr *)&addr2, &len);
printf("%s\n", addr2.sun_path);
當一個 Unix 域套接字不再使用時,應當調用
unlink
将其删除。
Unix 域中的流 socket
Unix 域中的流套接字與 TCP 流套接字的用法十分相似。在伺服器端,我們首先建立一個 Unix 域流套接字,将其綁定到一個路徑上,然後調用
listen
監聽用戶端連接配接,調用
accept
接受用戶端的連接配接。在用戶端,在建立一個 Unix 域流套接字之後,可以使用
connect
嘗試連接配接指定的伺服器套接字。以下是一個使用 Unix 域流套接字實作的 echo 伺服器和用戶端的例子:
// 伺服器
#include <assert.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#define UNIX_SOCKET_PATH "/tmp/echo_unix_socket"
#define BACKLOG 5
#define MSG_MAX_LENGTH 100
void echo(int client_fd);
void readLine(int fd, char *buf);
void signalHandler(int signo) // NOLINT
{
unlink(UNIX_SOCKET_PATH); // NOLINT
exit(EXIT_SUCCESS); // NOLINT
}
int main(void)
{
if (signal(SIGINT, signalHandler) == SIG_ERR) // NOLINT
{
fprintf(stderr, "signal error\n");
return -1;
}
int listen_fd = socket(AF_LOCAL, SOCK_STREAM, 0);
if (listen_fd < 0)
{
fprintf(stderr, "socket error\n");
return -1;
}
struct sockaddr_un unix_socket_addr;
memset(&unix_socket_addr, 0, sizeof(unix_socket_addr));
unix_socket_addr.sun_family = AF_LOCAL;
strcpy(unix_socket_addr.sun_path, UNIX_SOCKET_PATH);
if (bind(listen_fd, (struct sockaddr *)&unix_socket_addr, sizeof(unix_socket_addr))
< 0)
{
fprintf(stderr, "bind error\n");
return -1;
}
if (listen(listen_fd, BACKLOG) < 0)
{
fprintf(stderr, "listen error\n");
return -1;
}
for (;;)
{
int client_fd = accept(listen_fd, NULL, NULL);
if (client_fd < 0)
{
fprintf(stderr, "accept error\n");
return -1;
}
switch (fork())
{
case -1:
{
fprintf(stderr, "fork error\n");
return -1;
}
case 0:
{
echo(client_fd);
break;
}
default:
{
break;
}
}
}
return 0;
}
void echo(int client_fd)
{
char buf[MSG_MAX_LENGTH + 1] = {0};
for (;;)
{
readLine(client_fd, buf);
int msg_len = (int)strlen(buf);
if (write(client_fd, buf, msg_len) != msg_len)
{
fprintf(stderr, "write error\n");
exit(EXIT_FAILURE); // NOLINT
}
}
}
void readLine(int fd, char *buf)
{
int i = 0;
for (; i < MSG_MAX_LENGTH; i++)
{
switch (read(fd, buf + i, 1))
{
case 1:
{
break;
}
case 0:
{
exit(EXIT_FAILURE); // NOLINT
break;
}
case -1:
{
fprintf(stderr, "read error\n");
exit(EXIT_FAILURE); // NOLINT
break;
}
default:
{
assert(0);
}
}
if (buf[i] == '\n')
{
i++;
break;
}
}
buf[i] = '\0';
}
// 用戶端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#define UNIX_SOCKET_PATH "/tmp/echo_unix_socket"
#define MSG_MAX_LENGTH 100
int main(void)
{
int socket_fd = socket(AF_LOCAL, SOCK_STREAM, 0);
if (socket_fd < 0)
{
fprintf(stderr, "socker error\n");
return -1;
}
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_LOCAL;
strcpy(addr.sun_path, UNIX_SOCKET_PATH);
if (connect(socket_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0)
{
fprintf(stderr, "sonnect error\n");
return -1;
}
char buf[MSG_MAX_LENGTH + 1] = {0};
for (;;)
{
fgets(buf, MSG_MAX_LENGTH, stdin);
int len = (int)strlen(buf);
if (write(socket_fd, buf, len) != len)
{
fprintf(stderr, "write error\n");
return -1;
}
if (read(socket_fd, buf, len) != len)
{
fprintf(stderr, "read error\n");
return -1;
}
printf("%s", buf);
}
return 0;
}
Unix 域中的資料報 socket
Unix 域資料報套接字
與
UDP 套接字
類似,可以通過 Unix 域資料報套接字在程序間發送具有邊界的資料報。但由于 Unix 域資料報套接字是在本機上進行通信,是以 Unix 域資料報套接字的資料傳遞是可靠的,不會像 UDP 套接字那樣發生丢包的問題。Unix 域資料報套接字的接口與 UDP 也十分相似。在伺服器端,通常先建立一個 Unix 域資料報套接字,然後将其綁定到一個路徑上。然後調用
recvfrom
接收用戶端發送來的資料,調用
sendto
向用戶端發送資料。對于用戶端,通常是先建立一個 Unix 域資料報套接字,将這個套接字綁定到一個路徑上,然後調用
sendto
發送資料,調用
recvfrom
接收用戶端發來的資料。以下是使用 Unix 域資料報套接字實作的 echo 伺服器和用戶端:
// 伺服器
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#define UNIX_SOCKET_PATH "/tmp/echo_unix_socket"
#define MSG_MAX_LENGTH 100
void signalHandler(int signo) // NOLINT
{
unlink(UNIX_SOCKET_PATH); // NOLINT
exit(EXIT_SUCCESS); // NOLINT
}
int main(void)
{
if (signal(SIGINT, signalHandler) == SIG_ERR) // NOLINT
{
fprintf(stderr, "signal error\n");
return -1;
}
int listen_fd = socket(AF_LOCAL, SOCK_DGRAM, 0);
if (listen_fd < 0)
{
fprintf(stderr, "socket error\n");
return -1;
}
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_LOCAL;
strcpy(addr.sun_path, UNIX_SOCKET_PATH);
if (bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0)
{
fprintf(stderr, "bind error\n");
return -1;
}
char buf[MSG_MAX_LENGTH + 1] = {0};
for (;;)
{
struct sockaddr_un client_addr;
socklen_t len = sizeof(client_addr);
int msg_len = (int)recvfrom(listen_fd,
buf,
MSG_MAX_LENGTH,
0,
(struct sockaddr *)&client_addr,
&len);
if (msg_len < 0)
{
fprintf(stderr, "recvfrom error\n");
return -1;
}
if (sendto(listen_fd, buf, msg_len, 0, (struct sockaddr *)&client_addr, len) < 0)
{
fprintf(stderr, "sendto error\n");
return -1;
}
}
return 0;
}
// 用戶端
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#define UNIX_SOCKET_PATH "/tmp/echo_unix_socket"
#define MSG_MAX_LENGTH 100
#define SOCKET_PATH_MAX_LENGTH 50
void signalHandler(int signo) // NOLINT
{
char socket_path[SOCKET_PATH_MAX_LENGTH] = {0};
sprintf(socket_path, "/tmp/echo_unix_socket_%ld", (long)getpid()); // NOLINT
unlink(socket_path); // NOLINT
exit(EXIT_SUCCESS); // NOLINT
}
int main(void)
{
if (signal(SIGINT, signalHandler) == SIG_ERR) // NOLINT
{
fprintf(stderr, "signal error\n");
return -1;
}
int socket_fd = socket(AF_LOCAL, SOCK_DGRAM, 0);
if (socket_fd < 0)
{
fprintf(stderr, "socket error\n");
return -1;
}
struct sockaddr_un server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sun_family = AF_LOCAL;
strcpy(server_addr.sun_path, UNIX_SOCKET_PATH);
struct sockaddr_un client_addr;
memset(&client_addr, 0, sizeof(client_addr));
client_addr.sun_family = AF_LOCAL;
sprintf(client_addr.sun_path, "/tmp/echo_unix_socket_%ld", (long)getpid());
if (bind(socket_fd, (struct sockaddr *)&client_addr, sizeof(client_addr)) < 0)
{
fprintf(stderr, "bind error\n");
return -1;
}
char buf[MSG_MAX_LENGTH + 1] = {0};
for (;;)
{
fgets(buf, MSG_MAX_LENGTH, stdin);
int msg_len = (int)strlen(buf);
if (sendto(socket_fd,
buf,
msg_len,
0,
(struct sockaddr *)&server_addr,
sizeof(server_addr))
< 0)
{
fprintf(stderr, "sendto error\n"); // NOLINT
return -1;
}
if (recvfrom(socket_fd, buf, MSG_MAX_LENGTH, 0, NULL, NULL) < 0)
{
fprintf(stderr, "recvfrom error\n");
return -1;
}
printf("%s", buf);
}
return 0;
}
Unix 域套接字的權限
當程式調用
bind
時,會在檔案系統中的指定路徑處建立一個與套接字對應的檔案。我們可以通過控制該檔案的權限來控制程序對這個套接字的通路。當程序想要連接配接一個 Unix 域流套接字或通過一個 Unix 域資料報套接字發送資料包時,需要擁有對該套接字的
寫權限
以及對 socket 路徑名的所有目錄的
執行權限
。在調用
bind
時,會自動賦予使用者、組和其他使用者的所有權限。如果想要修改這一行為,可以在調用
bind
之前調用
umask
禁用掉某些權限。
使用 socketpair 建立互聯的 socket 對
有時我們需要在同一個程序中建立一對互相連接配接的 Unix 域 socket(* 與管道類似 *),這可以通過
socket
、
bind
、
listen
、
accept
和
connect
等調用實作。而
socketpair
提供了一個簡單友善的方法來建立一對互聯的 socket。
socketpair
建立的一對 socket 是
全雙工
的。socketpair 的函數原型如下:
#include <sys/socket.h>
int socketpair(int domain, int type, int protocol, int socketfd[2]);
socketpair 的前三個參數與
socket
函數的含義相同。由于 socketpair 隻能用于 Unix 域套接字,是以
domain
參數必須是
AF_UNIX
或
AF_LOCAL
。
type
參數可以是
SOCK_DGRAM
或
SOCK_STREAM
,分别建立一對資料報 socket 或流 socket。
protocol
參數必須是 0。
socketfd
用于傳回建立的兩個套接字檔案描述符。
通常,在調用 socketpair 建立一對套接字後會調用 fork 建立子程序,這樣父程序和子程序就可以通過這一對套接字進行程序間通信了。
使用 Unix 域套接字傳遞描述符
Unix 域套接字的一個 “特色功能” 就是在程序間
傳遞描述符
。描述符可以通過 Unix 域套接字在沒有親緣關系的程序之間傳遞。描述符是一種
輔助資料
,可以通過
sendmsg
發送,通過
recvmsg
接收。這裡的
描述符
可以是
open
、
pipe
、
mkfifo
、
socket
、
accept
等函數打開的描述符。以下是一個子程序向父程序傳遞描述符的例子:
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>
#define BUF_SIZE 1
#define TEXT_SIZE 12
void sendFd(int fd, int socket_fd);
int recvFd(int socket_fd);
int main(void)
{
int fd_pair[2] = {0};
if (socketpair(AF_LOCAL, SOCK_STREAM, 0, fd_pair) < 0)
{
fprintf(stderr, "socket error\n");
return -1;
}
pid_t pid = fork();
if (pid < 0)
{
fprintf(stderr, "fork error\n");
return -1;
}
if (pid > 0)
{
close(fd_pair[1]);
int recv_fd = recvFd(fd_pair[0]);
char text[TEXT_SIZE + 1] = {0};
if (read(recv_fd, text, TEXT_SIZE) != TEXT_SIZE)
{
fprintf(stderr, "read error\n");
return -1;
}
printf("%s", text);
if (waitpid(pid, NULL, 0) < 0)
{
fprintf(stderr, "waitpid error\n");
return -1;
}
return 0;
}
close(fd_pair[0]);
// ./hello.txt的内容為"hello world\n"
int fd = open("./hello.txt", O_RDONLY);
if (fd < 0)
{
fprintf(stderr, "open error\n");
exit(EXIT_FAILURE); // NOLINT
}
sendFd(fd, fd_pair[1]);
return 0;
}
void sendFd(int fd, int socket_fd)
{
struct msghdr msg;
memset(&msg, 0, sizeof(msg));
struct iovec iov;
union
{
struct cmsghdr cm;
char control[CMSG_SPACE(sizeof(int))];
} control_un;
struct cmsghdr *cmptr = NULL;
msg.msg_control = control_un.control;
msg.msg_controllen = sizeof(control_un.control);
cmptr = CMSG_FIRSTHDR(&msg);
cmptr->cmsg_len = CMSG_LEN(sizeof(int));
cmptr->cmsg_level = SOL_SOCKET;
cmptr->cmsg_type = SCM_RIGHTS;
*((int *)CMSG_DATA(cmptr)) = fd;
msg.msg_name = NULL;
msg.msg_namelen = 0;
char buf[BUF_SIZE] = {0};
iov.iov_base = &buf;
iov.iov_len = BUF_SIZE;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
if (sendmsg(socket_fd, &msg, 0) < 0)
{
fprintf(stderr, "sendmsg error\n");
exit(EXIT_FAILURE); // NOLINT
}
}
int recvFd(int socket_fd)
{
struct msghdr msg;
memset(&msg, 0, sizeof(msg));
char buf[BUF_SIZE] = {0};
struct iovec iov;
iov.iov_base = buf;
iov.iov_len = BUF_SIZE;
union
{
struct cmsghdr cm;
char control[CMSG_SPACE(sizeof(int))];
} control_un;
msg.msg_control = control_un.control;
msg.msg_controllen = sizeof(control_un.control);
msg.msg_name = NULL;
msg.msg_namelen = 0;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
struct cmsghdr *cmptr = CMSG_FIRSTHDR(&msg);
if (recvmsg(socket_fd, &msg, 0) < 0)
{
fprintf(stderr, "recvmsg error\n");
exit(EXIT_FAILURE); // NOLINT
}
int fd = *((int *)CMSG_DATA(cmptr));
return fd;
}
參考資料
- 《UNIX 網絡程式設計 卷 1 套接字聯網 API(第 3 版)》
- 《Linux/UNIX 系統程式設計手冊(下冊)》
- 進階程序間通信之 UNIX 域套接字 - ITtecman - 部落格園