天天看點

Unix環境進階程式設計學習筆記(十二) 進階程序間通信基于流的管道(STREAMS-Based Pipes)UNIX 域的 SOCKET傳送檔案描述符

基于流的管道(STREAMS-Based Pipes)

所謂基于流的管道實際上就是一種全雙工管道,它必須在基于流的系統上才能實作,linux 預設對它是不支援的,而同樣的邏輯,我們通常可以用基于 UNIX domain 的 socket 來實作,是以這裡對它隻作簡單介紹。

Unix環境進階程式設計學習筆記(十二) 進階程式間通信基于流的管道(STREAMS-Based Pipes)UNIX 域的 SOCKET傳送檔案描述符

關于流機制,我在 Unix環境進階程式設計學習筆記(九) 進階IO中曾經介紹過,知道可以在流首處加入處理子產品,對于基于流的管道而言,管道的兩端都是流首,是以可以在兩端都加入處理子產品,但同時,我們也隻能在處理子產品加入的一端删除它。

有時候,為了達到在不相關的程序間通信的目的,我們可以将這種管道的一端和某個實際的檔案關聯起來,也就相當于給了它一個名字,使用 fattach 函數:

int fattach(int filedes, const char *path);           

調用程序必須擁有該檔案,且對它具有寫權限,或是調用程序具有超級使用者的權限。而一旦流管道的一端和某個檔案關聯上後,該檔案就不能再被通路了,任何對該檔案執行的打開操作,都是獲得管道的通路權,而不再是那個檔案本身。不過,對于管道關聯之前就已經打開了檔案的程序,仍然可以繼續通路該檔案,而不必受管道的影響。

使用 fdetach 函數可以解除關聯:

int fdetach(const char *path);           

在該函數被調用以後,已經獲得管道通路權的使用者将繼續擁有該管道的通路權,而之後打開該檔案的程序獲得的是對檔案本身的通路權。

UNIX 域的 SOCKET

在 Unix環境進階程式設計學習筆記(十一) 網絡IPC:套接字中,我介紹了 socket 使用,關于 domain,一共可以有4種情況,今天,我們來看一下用于同一機器下不同程序間通信的 UNIX 域。UNIX 域同時支付流和資料報接口,不同于英特網域,這兩種接口都是可靠的。

我們可以像使用普通的 socket 那樣來使用它,在 UNIX 域的限定下,其位址格式由 sockaddr_un 資料結構來呈現。在Linux 2.4.22 以及 Solaris 9 上,sockaddr_un 的定義如下:

struct sockaddr_un {
		sa_family_t 	sun_family;/* AF_UNIX */
		char 		sun_path[108];/* pathname */
};           

sun_path 成員指定了一個檔案名,當将一個 UNIX domain socket 和該位址綁定在一起之後,系統将為我們建立一個 S_IFSOCK 類型的該檔案。當該檔案已經存在時,bind 函數将失敗。當我們關閉該 socket 之後,檔案不會自動被删除,需要我們手動 unlink 它。

需要注意以幾點:

1. 在connect調用中指定的路徑名必須是一個目前綁定在某個打開的Unix域套接口上的路徑名。如果對于某個Unix域套接口的connect調用發現這個監聽套接口的隊列已滿,調用就立即傳回一個ECONNREFUSED錯誤。這一點不同于TCP,如果TCP監聽套接口隊列已滿,TCP監聽端就忽略新到達的SYN,而TCP連接配接發送端将數次發送SYN進行重試。

2. 在一個未綁定的Unix域套接口上發送資料報不會自動給這個套接口捆綁一個路徑名,這一點與UDP套接口不同。是以在用戶端,我們也必須顯示地bind一個路徑名到我們的套接口。

來看一個實際的例子。

服務端:

/*
 *author: justaipanda
 *create time:2012/09/05 21:51:27
 */

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

#define FILE_NAME "1.txt"
#define BUF_SIZE 1024

int serv_listen(const char* name) {

	int fd;
	if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
		return -1;

	unlink(name);
	struct sockaddr_un un;
	memset(&un, 0, sizeof(un));
	un.sun_family = AF_UNIX;
	strcpy(un.sun_path, name);
	int len = (int)((void*)&(un.sun_path) - (void*)&un) 
		+ strlen(un.sun_path);
	if (bind(fd, (struct sockaddr*)&un, len) < 0) {
		close(fd);
		return -2;
	}

	if (listen(fd, 128) < 0) {
		close(fd);
		return -3;
	}

	return fd;
}

int serv_accept(int listenfd) {

	int fd;
	struct sockaddr_un un;
	int len = sizeof(un);

	if ((fd = accept(listenfd, (struct sockaddr*)&un, &len)) < 0) 
		return -1;

	((char*)(&un))[len] = '\0';
	unlink(un.sun_path);

	return fd;
}

int main() {
	
	int fd;
	if ((fd = serv_listen(FILE_NAME)) < 0) {
		printf("listen error!\n");
		exit(fd);
	}

	while(1) {

		int sclient = serv_accept(fd);
		if (sclient < 0) {
			printf("accept error!\n");
			exit(sclient);
		}

		char buffer[BUF_SIZE];
		ssize_t len = recv(sclient, buffer, BUF_SIZE, 0);
		if (len < 0) {
			printf("recieve error!\n");
			close(sclient);
			continue;
		}

		buffer[len] = '\0';
		printf("receive[%d]:%s\n", (int)len, buffer);
		if (len > 0) {
			if('q' == buffer[0]) {
				printf("server over!\n");
				exit(0);
			}
				
			char* buffer2 = "I'm a server!";
			len = send(sclient, buffer2, strlen(buffer2), 0);
			if (len < 0)
				printf("send error!\n");
		}
		close(sclient);
	}

	return 0;
}
           

用戶端:

/*
 *author: justaipanda
 *create time:2012/09/06 10:36:43
 */

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

#define FILE_NAME "1.txt"
#define CLI_PATH "/var/tmp/"
#define BUF_SIZE 1024

int cli_conn(const char* name) {

	int fd;
	if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
		return -1;

	struct sockaddr_un un;
	memset(&un, 0, sizeof(un));
	un.sun_family = AF_UNIX;
	sprintf(un.sun_path, "%s%05d", CLI_PATH, getpid());
	unlink(un.sun_path);
	int len = (int)((void*)&(un.sun_path) - (void*)&un) 
		+ strlen(un.sun_path);
	if (bind(fd, (struct sockaddr*)&un, len) < 0) {
		close(fd);
		return -2;
	}

	if(chmod(un.sun_path, S_IRWXU) < 0) {
		close(fd);
		return -3;
	}

	memset(&un, 0, sizeof(un));
	un.sun_family = AF_UNIX;
	strcpy(un.sun_path, name);
	len = (int)((void*)&(un.sun_path) - (void*)&un) 
		+ strlen(un.sun_path);
	if (connect(fd, (struct sockaddr*)&un, len) < 0) {
		close(fd);
		return -4;
	}

	return fd;
}

int main() {

	int fd;
	if ((fd = cli_conn(FILE_NAME)) < 0) {
		printf("connect error!\n");
		exit(fd);
	}

	char buffer[BUF_SIZE];
	printf("input:");
	scanf("%s", buffer);
	int len = send(fd, buffer, strlen(buffer), 0);
	if (len < 0) {
		printf("send error!\n");
		exit(len);
	}

	len = recv(fd, buffer, BUF_SIZE, 0);
	if (len < 0) {
		printf("recieve error!\n");
		exit(len);
	}

	buffer[len] = '\0';
	printf("receive[%d]:%s\n", (int)len, buffer);

	return 0;
}
           

對于關聯的程序,我們也可以使用 socketpair 函數簡化工作:

int socketpair(int domain, int type, int protocol, int sockfd[2]);           

這個函數的功能實際上就相當于建立了一個全雙工的管道,其中,domain 隻能被指定為 AF_UNIX。

傳送檔案描述符

許多時候,如果可以在程序間傳遞檔案描述符,這将極大的簡化我們程式的設計。

所謂傳送檔案描述符實際上是指——讓接收程序打開一個檔案描述符,該描述符的值不一定和發送程序發送的檔案描述符相同,但它們都指向同一檔案表。

如果發送程序關閉了檔案描述符,這并不會真的關閉檔案或裝置,因為系統認為,接收程序仍然打開着檔案,即使此時,接收程序可能還未收到該檔案描述符。

傳送檔案描述符一般可以有兩種方式:基于流的管道和 UNIX domain socket,對于前者,這裡不多下文筆,主要講後者。利用 UNIX domain socket 傳遞檔案描述符需要使用前面講過的 sendmsg 和 recvmsg 函數(參見Unix環境進階程式設計學習筆記(十一) 網絡IPC:套接字)。利用 msghdr 結構體中的 msg_constrol 成員傳遞描述符。該成員指向 cmsghdr 結構:

struct cmsghdr {
	socklen_t cmsg_len;/* data byte count, including header */
	int cmsg_level; /* originating protocol */
	int cmsg_type;/* protocol-specific type */
	/* followed by the actual control message data */
};
           

為了發送檔案描述符,将 cmsg_len 設定為 cmsghdr 結構的長度再加上一個整型(描述符)的長度,cmsg_level 字段設定為 SOL_SOCKET,cmsg_type 字段設定為 SCM_RIGHTS,用以指明我們在傳送通路權限。

對于這個結構體的操作宏:

unsigned char *CMSG_DATA(struct cmsghdr *cp);
//Returns: pointer to data associated with cmsghdr structure

struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *mp);
//Returns: pointer to first cmsghdr structure associated with the msghdr structure, or NULL if none exists

struct cmsghdr *CMSG_NXTHDR(struct msghdr *mp, struct cmsghdr *cp);
//Returns: pointer to next cmsghdr structure associated with the msghdr structure given the current cmsghdr structure, or NULL if we're at the last one

unsigned int CMSG_LEN(unsigned int nbytes);
//Returns: size to allocate for data object nbytes large
           

下面看一個使用 UNIX domain socket 傳遞檔案描述符的例子,該例子是由前面的例子修改而來,其中 serv_listen,serv_accept,以及 cli_conn 函數和原來一樣,就不再重複了。

服務端:

int send_fd(int fd, int fd_to_send) {

	struct msghdr msg;
	msg.msg_name = NULL;
	msg.msg_namelen = 0;

	struct cmsghdr *cmptr = NULL;
	char buffer[BUF_SIZE];
	struct iovec iov;

	if (fd_to_send >= 0) {
		int cmsg_len = CMSG_LEN(sizeof(int));
		cmptr = malloc(cmsg_len);

		cmptr->cmsg_level = SOL_SOCKET;
		cmptr->cmsg_type = SCM_RIGHTS;
		cmptr->cmsg_len = cmsg_len;
		*(int*)CMSG_DATA(cmptr) = fd_to_send;

		msg.msg_control = cmptr;
		msg.msg_controllen = cmsg_len;

		sprintf(buffer, "OK!");
	}
	else {

		if (-1 == fd_to_send)
			sprintf(buffer, "cannot open file!");
		else
			sprintf(buffer, "wrong command!");
		
		msg.msg_control = NULL;
		msg.msg_controllen = 0;
	}

	msg.msg_iov = &iov;
	msg.msg_iovlen = 1;
	iov.iov_base = buffer;
	iov.iov_len = strlen(buffer);

	sendmsg(fd, &msg, 0);
	if (cmptr)
		free(cmptr);
	return 0;
}

int main() {
	
	int fd;
	if ((fd = serv_listen(FILE_NAME)) < 0) {
		printf("listen error!\n");
		exit(fd);
	}

	while(1) {

		int sclient = serv_accept(fd);
		if (sclient < 0) {
			printf("accept error!\n");
			exit(sclient);
		}

		char buffer[BUF_SIZE];
		ssize_t len = recv(sclient, buffer, BUF_SIZE, 0);
		if (len < 0) {
			printf("recieve error!\n");
			close(sclient);
			continue;
		}

		buffer[len] = '\0';
		printf("receive[%d]:%s\n", (int)len, buffer);
		if (len > 0) {
			if('q' == buffer[0]) {
				printf("server over!\n");
				exit(0);
			}
			else if ('f' == buffer[0]) {
				int new_fd = open(buffer + 2, O_RDWR);
				send_fd(sclient, new_fd);
			}
			else
				send_fd(sclient, -2);
		}
		close(sclient);
	}

	return 0;
}           

用戶端:

int recv_fd(int fd, char *buffer, size_t size) {

	struct cmsghdr *cmptr;
	int cmsg_len = CMSG_LEN(sizeof(int));
	cmptr = malloc(cmsg_len);

	struct iovec iov;
	iov.iov_base = buffer;
	iov.iov_len = size;

	struct msghdr msg;
	msg.msg_name = NULL;
	msg.msg_namelen = 0;
	msg.msg_iov = &iov;
	msg.msg_iovlen = 1;
	msg.msg_control = cmptr;
	msg.msg_controllen = cmsg_len;

	int len = recvmsg(fd, &msg, 0);
	if (len < 0) {
		printf("receve message error!\n");
		exit(0);
	}
	else if (len == 0) {
		printf("connection closed by server!\n");
		exit(0);
	}

	buffer[len] = '\0';
	int cfd = -1;
	if (cmptr->cmsg_type != 0)
		cfd = *(int*)CMSG_DATA(cmptr);
	free(cmptr);
	return cfd;
}

int main() {

	int fd;
	if ((fd = cli_conn(FILE_NAME)) < 0) {
		printf("connect error!\n");
		exit(fd);
	}

	char buffer[BUF_SIZE];
	printf("input:");
	fgets(buffer, BUF_SIZE, stdin);
	buffer[strlen(buffer) - 1] = '\0';
	int len = send(fd, buffer, strlen(buffer), 0);
	if (len < 0) {
		printf("send error!\n");
		exit(len);
	}

	int cfd = recv_fd(fd, buffer, BUF_SIZE);
	printf("data:%s\n", buffer);
	if (cfd >= 0) {
		printf("received open file:%d\n", cfd);
	}

	return 0;
}           

繼續閱讀