1. 什么是Unix Domain Socket
Socket(套接字)是操作系統(tǒng)定義的一套通信方式和實(shí)現(xiàn)通信的系統(tǒng)調(diào)用成玫,比如最常用的互
聯(lián)網(wǎng)上兩臺(tái)終端之間的通信加酵。
Unix Domain Socket也是Socket的一種拳喻,專(zhuān)門(mén)用于同一臺(tái)機(jī)器的不同進(jìn)程之間進(jìn)行通信
2. 最簡(jiǎn)單的用法
在詳細(xì)介紹Unix Domain Socket各種用法的細(xì)節(jié)前,先看一個(gè)簡(jiǎn)單的例子
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <unistd.h>
#include <wait.h>
int
main(int argc, char *argv[])
{
int fds[2];
if (socketpair(AF_UNIX, SOCK_STREAM, 0, fds) != 0)
{
perror("fail to create socketpair");
return 1;
}
pid_t pid = fork();
if (pid < 0)
{
perror("fail to fork");
return 1;
}
if (pid > 0)
{
/*parent process*/
if (write(fds[1], "hi!", 3) == -1)
{
perror("fail to write");
return 1;
}
/*wait for the child process terminatation*/
wait(NULL);
} else {
/*child process*/
char buf[4] = {0};
if (read(fds[0], buf, 3) == -1)
{
perror("fail to read");
return 1;
}
printf("receive msg:%s\n", buf);
}
}
2.1 程序簡(jiǎn)單分析
- 首先通過(guò)
socketpair
函數(shù)創(chuàng)建了一對(duì)已經(jīng)連接的socket猪腕,不同進(jìn)程可以通過(guò)這對(duì)
socket進(jìn)行通信冗澈,建立后的socket如下圖所示
user process
+------------------------+
| |
| |
| fd[0] fd[1] |
+------------------------+
^ ^
| |
| |
V V
+------+ +------+
|socket|<------>|socket|
+------+ +------+
socketpair
調(diào)用成功后,會(huì)通過(guò)fds
參數(shù)返回兩個(gè)文件描述符码撰,后面通過(guò)這兩個(gè)文件描
述符進(jìn)行通信渗柿。
- 通過(guò)
fork
函數(shù)創(chuàng)建了一個(gè)子進(jìn)程 - 父進(jìn)程中利用已經(jīng)創(chuàng)建好的socket,向
fd[1]
中寫(xiě)入字符串:hi! - 子進(jìn)程利用已經(jīng)創(chuàng)建好的socket脖岛,從
fd[0]
中讀取字符串
因此程序的輸出結(jié)果是:receive msg:hi!
2.2 socketpair函數(shù)
#include <sys/socket.h>
int socketpair(int domain, int type, int protocol, int socketfd[2]);
函數(shù)作用:創(chuàng)建一對(duì)沒(méi)有名字的且已經(jīng)連接的socket
沒(méi)有名字
是相對(duì)于后面有名字的socket來(lái)說(shuō)的朵栖,名字相當(dāng)于網(wǎng)絡(luò)通信里的地址,客戶端可
以通過(guò)名字與特定的服務(wù)端進(jìn)行通信柴梆。已經(jīng)連接
的含義是在創(chuàng)建成功的情況下陨溅,可以直
接通過(guò)這兩個(gè)socketfd進(jìn)行通信。參數(shù)
除了最后一個(gè)參數(shù)绍在,前面3個(gè)參數(shù)表示創(chuàng)建socket的類(lèi)型门扇,對(duì)于網(wǎng)絡(luò)通信的socket來(lái)說(shuō)類(lèi)型
很多,但對(duì)于unix socket來(lái)說(shuō)偿渡,這些參數(shù)取值比較確定臼寄。
domain
傳AF_UNIX
或者AF_LOCAL
代表unix socket
type
可以是SOCK_STREAM
SOCK_DGRAM
或者SOCK_SEQPACKET
這幾種不同類(lèi)型是通
用的類(lèi)型,對(duì)unix socket來(lái)說(shuō)其實(shí)區(qū)別不大溜宽,例如unix socket下都是同一臺(tái)機(jī)器吉拳,
SOCK_DGRAM
類(lèi)型的socket也是可靠。
另一方面适揉,一些接口層面的區(qū)別仍然是存在的留攒,比如對(duì)于SOCK_STREAM
類(lèi)型的socket一般
的通信過(guò)程是:
- 創(chuàng)建一個(gè)socket
- 綁定一個(gè)地址
- 調(diào)用
listen
告訴內(nèi)核已經(jīng)準(zhǔn)備好接受連接請(qǐng)求 - 調(diào)用
accept
建立一個(gè)連接 - 雙方通信
對(duì)于SOCK_DGRAM
類(lèi)型的socket來(lái)說(shuō),是沒(méi)有3和4這兩步的
protocol
固定傳0
- 返回值:成功返回0, 失敗返回-1
socketpair
與pipe
的作用和用法都十分相似嫉嘀,主要區(qū)別是scokpair
調(diào)用成功返回的
一對(duì)描述符是可以互相通信的炼邀,而pipe
返回的一對(duì)描述符只能單方向通信,一個(gè)描述符
只能用于寫(xiě)剪侮,另一個(gè)描述符只能用于讀
3. 有名字的UNIX Domain Sockets
前面提到socketpair
創(chuàng)建的unix socket是沒(méi)有名字的拭宁,名字相對(duì)于網(wǎng)絡(luò)通信中的地址,
沒(méi)有地址瓣俯,其他不相關(guān)進(jìn)程也就不能和他進(jìn)行通信红淡,那么有名字的unix socket如何創(chuàng)建
呢?
下面的例子是一個(gè)服務(wù)端/客戶端通過(guò)unix socket通信的例子降铸,通信內(nèi)容非常簡(jiǎn)單,
客戶端向服務(wù)端發(fā)送一段字符串摇零,服務(wù)端接受后打印出來(lái)推掸。代碼里大部分的都是socket通
信的一些前期常規(guī)準(zhǔn)備工作,這些工作的主要步驟是:創(chuàng)建socket,綁定地址谅畅,等待連接登渣,
握手連接,連接成功進(jìn)行通信
- unix socket服務(wù)端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.h>
#include <string.h> //strcpy
#include <stddef.h> //offsetof
#include <unistd.h>
int
main(int argc, char *argv[])
{
/* 1. 創(chuàng)建socket */
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd == -1)
{
perror("fail to create socket");
return 1;
}
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
/* 綁定的地址為當(dāng)前目錄的文件名為my_unix_domain的文件*/
strcpy(addr.sun_path, "my_unix_domain");
int addrLen = offsetof(struct sockaddr_un, sun_path)
+ strlen(addr.sun_path) + 1;
/* 2. 綁定地址*/
if (bind(fd, (struct sockaddr*)&addr, addrLen) == -1)
{
perror("fail to bind");
return 1;
}
/* 3. 監(jiān)聽(tīng)客戶端連接 */
if (listen(fd, 10) == -1)
{
perror("listen");
return 1;
}
struct sockaddr_un clientAddr;
socklen_t clientLen = sizeof(clientAddr);
/* 4. 和客戶端建立連接 */
int clientFD = accept(fd, (struct sockaddr *)&clientAddr, &clientLen);
if (clientFD == -1)
{
perror("accept");
return 1;
}
/* 5. 讀取客戶端發(fā)來(lái)的內(nèi)容 */
char buf[256] = {0};
int n = read(clientFD, buf, 256);
if (n < 0)
{
perror("read");
return 1;
}
printf("receive msg:%s\n", buf);
close(fd);
close(clientFD);
/* 6. 釋放綁定的文件 */
unlink("my_unix_domain");
}
客戶端代碼
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <stddef.h>
#include <stdlib.h>
#include <unistd.h>
int
main(int argc, char *argv[])
{
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0)
{
perror("socket");
exit(1);
}
struct sockaddr_un servAddr;
servAddr.sun_family = AF_UNIX;
strcpy(servAddr.sun_path, "my_unix_domain");
int addrLen = offsetof(struct sockaddr_un, sun_path)
+ strlen(servAddr.sun_path) + 1;
if (connect(fd, (struct sockaddr *)&servAddr, addrLen) < 0)
{
perror("connect");
exit(1);
}
printf("connect to server\n");
char buf[256] = "hi~";
int n = write(fd, buf, strlen(buf));
if (n < 0)
{
perror("write");
exit(1);
}
close(fd);
return 0;
}
3.1 Unix Domain Socket地址格式
通常Unix Socket的地址有兩種情況毡泻,一種是上面提到的無(wú)名字的unix domain socket胜茧,另
一種有明確地址的,且地址和磁盤(pán)文件對(duì)應(yīng)仇味,使用下面結(jié)構(gòu)表示
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX */
char sun_path[108]; /* Pathname */
}
sun_family
字段固定為AF_UNIX呻顽,sun_path
是一個(gè)磁盤(pán)文件的路徑,以null字符結(jié)尾丹墨。
上面的例子中廊遍,綁定的地址是當(dāng)前目錄下的名字為my_unix_domain的文件,綁定成功后贩挣,
操作系統(tǒng)會(huì)在磁盤(pán)上創(chuàng)建對(duì)應(yīng)的文件喉前,如果其他socket的試圖綁定這個(gè)地址,操作系統(tǒng)檢
測(cè)到磁盤(pán)中已經(jīng)存在對(duì)應(yīng)文件王财,就會(huì)報(bào)錯(cuò)卵迂,提示地址已經(jīng)被綁定。
socket關(guān)閉后绒净,操作系統(tǒng)不會(huì)自動(dòng)清楚這個(gè)文件见咒,所以需要顯式的調(diào)用unlink函數(shù)刪除這
個(gè)文件。
在Linux系統(tǒng)中疯溺,Unix Domain Socket地址除了上面兩種情況外论颅,還有一種虛擬地址類(lèi)型,
也使用struct sockadd_un
結(jié)構(gòu)表示囱嫩,但sun_path
字段不一樣恃疯,和磁盤(pán)文件無(wú)關(guān),且
sun_path
字段的第一個(gè)字節(jié)是null('\0')墨闲。另外今妄,socket關(guān)閉后,這種虛擬的地址也會(huì)
自動(dòng)釋放
3.2 地址長(zhǎng)度的計(jì)算
上述代碼中試用的計(jì)算方式是:
offsetof(struct sockaddr_un, sun_path) + strlen(sun_path) + 1
而不是:
sizeof(sa_family_t) + strlen(sun_path) + 1
這是因?yàn)橛械南到y(tǒng)實(shí)現(xiàn)中鸳碧,在sun_family
和sun_path
字段之間可能還有其他字段盾鳞,因
次采用第一種方式,代碼移植性更好
4 傳遞輔助數(shù)據(jù)(Ancillary Data)
Unix Domain Socket還支持傳遞輔助數(shù)據(jù)瞻离,如文件句柄和認(rèn)證信息腾仅,下面的例子是傳遞了
句柄,可以看出傳遞時(shí)所用的數(shù)據(jù)結(jié)構(gòu)很復(fù)雜套利,而且必須調(diào)用sendmsg
函數(shù)
- 服務(wù)端代碼
#include <stdio.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/un.h>
#include <errno.h>
#include <string.h> //strcpy
#include <stddef.h> //offsetof
#include <unistd.h>
int NUM_FD = 1;
void
sendfd(int clientfd, int fd_to_send)
{
struct msghdr msg = { 0 };
struct cmsghdr *cmsg;
int myfds[NUM_FD]; /* Contains the file descriptors to pass */
myfds[0] = fd_to_send;
char iobuf[2] = {'f'};
iobuf[0] = 'e';
struct iovec io = {
.iov_base = iobuf,
.iov_len = sizeof(iobuf)
};
union { /* Ancillary data buffer, wrapped in a union
in order to ensure it is suitably aligned */
char buf[CMSG_SPACE(sizeof(myfds))];
struct cmsghdr align;
} u;
msg.msg_iov = &io;
msg.msg_iovlen = 1;
msg.msg_control = u.buf;
msg.msg_controllen = sizeof(u.buf);
cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof(int) * NUM_FD);
memcpy(CMSG_DATA(cmsg), myfds, NUM_FD * sizeof(int));
printf("server send msg_controllen is %d\n", msg.msg_controllen);
int send_bytes = sendmsg(clientfd, &msg, 0);
if (send_bytes == -1)
{
perror("sendmsg");
} else {
printf("send %d bytes\n", send_bytes);
}
}
int
main(int argc, char *argv[])
{
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd == -1)
{
perror("fail to create socket");
return 1;
}
struct sockaddr_un addr;
memset(&addr, 0, sizeof(struct sockaddr));
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "my_unix_domain");
int addrLen = offsetof(struct sockaddr_un, sun_path)
+ strlen(addr.sun_path) + 1;
if (bind(fd, (struct sockaddr*)&addr, addrLen) == -1)
{
perror("fail to bind");
return 1;
}
if (listen(fd, 10) == -1)
{
perror("listen");
return 1;
}
struct sockaddr_un clientAddr;
memset(&clientAddr, 0, sizeof(struct sockaddr_un));
socklen_t clientLen = sizeof(clientAddr);
int clientFD = accept(fd, (struct sockaddr *)&clientAddr, &clientLen);
if (clientFD == -1)
{
perror("accept");
return 1;
}
char buf[256] = {0};
int n = read(clientFD, buf, 256);
if (n < 0)
{
perror("read");
return 1;
}
printf("receive msg:%s\n", buf);
int fd_to_send = open("a.txt", O_RDONLY);
if (fd_to_send == -1 )
{
perror("open");
return 1;
}
sendfd(clientFD, fd_to_send);
close(fd);
close(clientFD);
unlink("my_unix_domain");
}
- 客戶端代碼
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <stddef.h>
#include <stdlib.h>
#include <unistd.h>
int
main(int argc, char *argv[])
{
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0)
{
perror("socket");
exit(1);
}
struct sockaddr_un servAddr;
servAddr.sun_family = AF_UNIX;
strcpy(servAddr.sun_path, "my_unix_domain");
int addrLen = offsetof(struct sockaddr_un, sun_path)
+ strlen(servAddr.sun_path) + 1;
if (connect(fd, (struct sockaddr *)&servAddr, addrLen) < 0)
{
perror("connect");
exit(1);
}
printf("connect to server\n");
char buf[256] = "hi~";
int n = write(fd, buf, strlen(buf));
if (n < 0)
{
perror("write");
exit(1);
}
sleep(1);
/* receive ancillary data*/
struct msghdr msg = {0};
char iobuf[1] = {'b'};
struct iovec iov[1];
iov[0].iov_base = iobuf;
iov[0].iov_len = sizeof(iobuf);
msg.msg_iov = iov;
msg.msg_iovlen = 1;
int myfds[1];
union { /* Ancillary data buffer, wrapped in a union
in order to ensure it is suitably aligned */
char buf[CMSG_SPACE(sizeof(myfds))];
struct cmsghdr align;
} u;
msg.msg_control = u.buf;
msg.msg_controllen = sizeof(u.buf);
ssize_t receive_size = recvmsg(fd, &msg, 0);
if ( receive_size == -1)
{
perror("recvmsg");
} else {
printf("client receive size is %d, receive msg_iovlen is %d, msg_controllen length is %d\n", receive_size, msg.msg_iovlen,
msg.msg_controllen);
printf("client buf is %c\n", iobuf[0]);
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
if (cmsg == NULL) {
perror("cmsg is null");
} else {
int receive_fd = *(int *)CMSG_DATA(cmsg);
char buf[1024];
if (read(receive_fd, buf, 1024) == -1)
{
perror("read");
} else {
printf("read from receive fd:%s\n", buf);
}
close(receive_fd);
}
}
close(fd);
return 0;
}