4. UNIX Domain Socket IPC

socket API原本是為網絡通訊設計的,但後來在socket的框架上發展出一種IPC機制,就是UNIX Domain Socket。雖然網絡socket也可用於同一台主機的進程間通訊(通過loopback地址127.0.0.1),但是UNIX Domain Socket用於IPC更有效率:不需要經過網絡協議棧,不需要打包拆包、計算校驗和、維護序號和應答等,只是將應用層數據從一個進程拷貝到另一個進程。這是因為,IPC機制本質上是可靠的通訊,而網絡協議是為不可靠的通訊設計的。UNIX Domain Socket也提供面向流和麵向數據包兩種API介面,類似於TCP和UDP,但是面向消息的UNIX Domain Socket也是可靠的,消息既不會丟失也不會順序錯亂。

UNIX Domain Socket是全雙工的,API介面語義豐富,相比其它IPC機制有明顯的優越性,目前已成為使用最廣泛的IPC機制,比如X Window伺服器和GUI程序之間就是通過UNIX Domain Socket通訊的。

使用UNIX Domain Socket的過程和網絡socket十分相似,也要先調用socket()創建一個socket檔案描述符,address family指定為AF_UNIX,type可以選擇SOCK_DGRAM或SOCK_STREAM,protocol參數仍然指定為0即可。

UNIX Domain Socket與網絡socket編程最明顯的不同在於地址格式不同,用結構體sockaddr_un表示,網絡編程的socket地址是IP地址加連接埠號,而UNIX Domain Socket的地址是一個socket類型的檔案在檔案系統中的路徑,這個socket檔案由bind()調用創建,如果調用bind()時該檔案已存在,則bind()錯誤返回。

以下程序將UNIX Domain socket綁定到一個地址。

#include <stdlib.h>
#include <stdio.h>
#include <stddef.h>
#include <sys/socket.h>
#include <sys/un.h>

int main(void)
{
	int fd, size;
	struct sockaddr_un un;

	memset(&un, 0, sizeof(un));
	un.sun_family = AF_UNIX;
	strcpy(un.sun_path, "foo.socket");
	if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
		perror("socket error");
		exit(1);
	}
	size = offsetof(struct sockaddr_un, sun_path) + strlen(un.sun_path);
	if (bind(fd, (struct sockaddr *)&un, size) < 0) {
		perror("bind error");
		exit(1);
	}
	printf("UNIX domain socket bound\n");
	exit(0);
}

注意程序中的offsetof宏,它在stddef.h標頭檔中定義:

#define offsetof(TYPE, MEMBER) ((int)&((TYPE *)0)->MEMBER)

offsetof(struct sockaddr_un, sun_path)就是取sockaddr_un結構體的sun_path成員在結構體中的偏移,也就是從結構體的第幾個位元組開始是sun_path成員。想一想,這個宏是如何實現這一功能的?

該程序的運行結果如下。

$ ./a.out
UNIX domain socket bound
$ ls -l foo.socket
srwxrwxr-x 1 user        0 Aug 22 12:43 foo.socket
$ ./a.out
bind error: Address already in use
$ rm foo.socket
$ ./a.out
UNIX domain socket bound

以下是伺服器的listen模組,與網絡socket編程類似,在bind之後要listen,表示通過bind的地址(也就是socket檔案)提供服務。

#include <stddef.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.h>

#define QLEN 10

/*
 * Create a server endpoint of a connection.
 * Returns fd if all OK, <0 on error.
 */
int serv_listen(const char *name)
{
	int                 fd, len, err, rval;
	struct sockaddr_un  un;

	/* create a UNIX domain stream socket */
	if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
		return(-1);
	unlink(name);   /* in case it already exists */

	/* fill in socket address structure */
	memset(&un, 0, sizeof(un));
	un.sun_family = AF_UNIX;
	strcpy(un.sun_path, name);
	len = offsetof(struct sockaddr_un, sun_path) + strlen(name);

	/* bind the name to the descriptor */
	if (bind(fd, (struct sockaddr *)&un, len) < 0) {
		rval = -2;
		goto errout;
	}
	if (listen(fd, QLEN) < 0) { /* tell kernel we're a server */
		rval = -3;
		goto errout;
	}
	return(fd);

errout:
	err = errno;
	close(fd);
	errno = err;
	return(rval);
}

以下是伺服器的accept模組,通過accept得到客戶端地址也應該是一個socket檔案,如果不是socket檔案就返回錯誤碼,如果是socket檔案,在建立連接後這個檔案就沒有用了,調用unlink把它刪掉,通過傳出參數uidptr返回客戶端程序的user id。

#include <stddef.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.h>

int serv_accept(int listenfd, uid_t *uidptr)
{
	int                 clifd, len, err, rval;
	time_t              staletime;
	struct sockaddr_un  un;
	struct stat         statbuf;

	len = sizeof(un);
	if ((clifd = accept(listenfd, (struct sockaddr *)&un, &len)) < 0)
		return(-1);     /* often errno=EINTR, if signal caught */

	/* obtain the client's uid from its calling address */
	len -= offsetof(struct sockaddr_un, sun_path); /* len of pathname */
	un.sun_path[len] = 0;           /* null terminate */

	if (stat(un.sun_path, &statbuf) < 0) {
		rval = -2;
		goto errout;
	}

	if (S_ISSOCK(statbuf.st_mode) == 0) {
		rval = -3;      /* not a socket */
		goto errout;
	}

	if (uidptr != NULL)
		*uidptr = statbuf.st_uid;   /* return uid of caller */
	unlink(un.sun_path);        /* we're done with pathname now */
	return(clifd);

errout:
	err = errno;
	close(clifd);
	errno = err;
	return(rval);
}

以下是客戶端的connect模組,與網絡socket編程不同的是,UNIX Domain Socket客戶端一般要顯式調用bind函數,而不依賴系統自動分配的地址。客戶端bind一個自己指定的socket檔案名的好處是,該檔案名可以包含客戶端的pid以便伺服器區分不同的客戶端。

#include <stdio.h>
#include <stddef.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.h>

#define CLI_PATH    "/var/tmp/"      /* +5 for pid = 14 chars */

/*
 * Create a client endpoint and connect to a server.
 * Returns fd if all OK, <0 on error.
 */
int cli_conn(const char *name)
{
	int                fd, len, err, rval;
	struct sockaddr_un un;

	/* create a UNIX domain stream socket */
	if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
		return(-1);

	/* fill socket address structure with our address */
	memset(&un, 0, sizeof(un));
	un.sun_family = AF_UNIX;
	sprintf(un.sun_path, "%s%05d", CLI_PATH, getpid());
	len = offsetof(struct sockaddr_un, sun_path) + strlen(un.sun_path);

	unlink(un.sun_path);        /* in case it already exists */
	if (bind(fd, (struct sockaddr *)&un, len) < 0) {
		rval = -2;
		goto errout;
	}

	/* fill socket address structure with server's address */
	memset(&un, 0, sizeof(un));
	un.sun_family = AF_UNIX;
	strcpy(un.sun_path, name);
	len = offsetof(struct sockaddr_un, sun_path) + strlen(name);
	if (connect(fd, (struct sockaddr *)&un, len) < 0) {
		rval = -4;
		goto errout;
	}
	return(fd);

errout:
	err = errno;
	close(fd);
	errno = err;
	return(rval);
}

下面是自己動手時間,請利用以上模組編寫完整的客戶端/伺服器通訊的程序。