UNIX域協議
UNIX域套接字與TCP相比, 在同一臺主機上, UNIX域套接字更有效率, 幾乎是TCP的兩倍(由於UNIX域套接字不需要經過網絡協議棧,不需要打包/拆包,計算校驗和,維護序號和應答等,只是將應用層數據從一個進程拷貝到另一個進程, 而且UNIX域協議機制本質上就是可靠的通訊, 而網絡協議是爲不可靠的通訊設計的).
UNIX域套接字可以在同一臺主機上各進程之間傳遞文件描述符;
UNIX域套接字與傳統套接字的區別是用路徑名來表示協議族的描述;
UNIX域套接字也提供面向流和麪向數據包兩種API接口,類似於TCP和UDP,但是面向消息的UNIX套接字也是可靠的,消息既不會丟失也不會順序錯亂。
使用UNIX域套接字的過程和網絡socket十分相似, 也要先調用socket創建一個socket文件描述符, family指定爲AF_UNIX, type可以選擇SOCK_DGRAM/SOCK_STREAM;
UNIX域套接字地址結構:
- #define UNIX_PATH_MAX 108
- struct sockaddr_un
- {
- sa_family_t sun_family; /* AF_UNIX */
- char sun_path[UNIX_PATH_MAX]; /* pathname */
- };
基於UNIX域套接字的echo-server/client程序
- /**Server端**/
- void echoServer(int sockfd);
- int main()
- {
- signal(SIGCHLD, sigHandlerForSigChild);
- int listenfd = socket(AF_UNIX, SOCK_STREAM, 0);
- if (listenfd == -1)
- err_exit("socket error");
- char pathname[] = "/tmp/test_for_unix";
- unlink(pathname);
- struct sockaddr_un servAddr;
- servAddr.sun_family = AF_UNIX;
- strcpy(servAddr.sun_path, pathname);
- if (bind(listenfd, (struct sockaddr *)&servAddr, sizeof(servAddr)) == -1)
- err_exit("bind error");
- if (listen(listenfd, 128) == -1)
- err_exit("listen error");
- while (true)
- {
- int connfd = accept(listenfd, NULL, NULL);
- if (connfd == -1)
- err_exit("accept error");
- pid_t pid = fork();
- if (pid == -1)
- err_exit("fork error");
- else if (pid > 0)
- close(connfd);
- else if (pid == 0)
- {
- close(listenfd);
- echoServer(connfd);
- close(connfd);
- exit(EXIT_SUCCESS);
- }
- }
- }
- void echoServer(int sockfd)
- {
- char buf[BUFSIZ];
- while (true)
- {
- memset(buf, 0, sizeof(buf));
- int recvBytes = read(sockfd, buf, sizeof(buf));
- if (recvBytes < 0)
- {
- if (errno == EINTR)
- continue;
- else
- err_exit("read socket error");
- }
- else if (recvBytes == 0)
- {
- cout << "client connect closed..." << endl;
- break;
- }
- cout << buf ;
- if (write(sockfd, buf, recvBytes) == -1)
- err_exit("write socket error");
- }
- }
- /**Client端代碼**/
- void echoClient(int sockfd);
- int main()
- {
- int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
- if (sockfd == -1)
- err_exit("socket error");
- char pathname[] = "/tmp/test_for_unix";
- struct sockaddr_un servAddr;
- servAddr.sun_family = AF_UNIX;
- strcpy(servAddr.sun_path, pathname);
- if (connect(sockfd, (struct sockaddr *)&servAddr, sizeof(servAddr)) == -1)
- err_exit("connect error");
- echoClient(sockfd);
- }
- void echoClient(int sockfd)
- {
- char buf[BUFSIZ] = {0};
- while (fgets(buf, sizeof(buf), stdin) != NULL)
- {
- if (write(sockfd, buf, strlen(buf)) == -1)
- err_exit("write socket error");
- memset(buf, 0, sizeof(buf));
- int recvBytes = read(sockfd, buf, sizeof(buf));
- if (recvBytes == -1)
- {
- if (errno == EINTR)
- continue;
- else
- err_exit("read socket error");
- }
- cout << buf ;
- memset(buf, 0, sizeof(buf));
- }
- }
UNIX域套接字編程注意點
1.bind成功將會創建一個文件,權限爲0777 & ~umask
2.sun_path最好用一個/tmp目錄下的文件的絕對路徑, 而且server端在指定該文件之前首先要unlink一下;
3.UNIX域協議支持流式套接口(需要處理粘包問題)與報式套接口(基於數據報)
4.UNIX域流式套接字connect發現監聽隊列滿時,會立刻返回一個ECONNREFUSED,這和TCP不同,如果監聽隊列滿,會忽略到來的SYN,這導致對方重傳SYN。
傳遞文件描述符
socketpair
- #include <sys/types.h>
- #include <sys/socket.h>
- int socketpair(int domain, int type, int protocol, int sv[2]);
創建一個全雙工的流管道
參數:
domain: 協議家族, 可以使用AF_UNIX(AF_LOCAL)UNIX域協議, 而且在Linux上, 該函數也就只支持這一種協議;
type: 套接字類型, 可以使用SOCK_STREAM
protocol: 協議類型, 一般填充爲0;
sv: 返回的套接字對;
socketpair 函數跟pipe 函數是類似: 只能在具有親緣關係的進程間通信,但pipe 創建的匿名管道是半雙工的,而socketpair 可以認爲是創建一個全雙工的管道。
可以使用socketpair 創建返回的套接字對進行父子進程通信, 如下例:
- int main()
- {
- int sockfds[2];
- if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockfds) == -1)
- err_exit("socketpair error");
- pid_t pid = fork();
- if (pid == -1)
- err_exit("fork error");
- // 父進程, 只負責數據的打印
- else if (pid > 0)
- {
- close(sockfds[1]);
- int iVal = 0;
- while (true)
- {
- cout << "value = " << iVal << endl;
- write(sockfds[0], &iVal, sizeof(iVal));
- read(sockfds[0], &iVal, sizeof(iVal));
- sleep(1);
- }
- }
- // 子進程, 只負責數據的更改(+1)
- else if (pid == 0)
- {
- close(sockfds[0]);
- int iVal = 0;
- while (read(sockfds[1], &iVal, sizeof(iVal)) > 0)
- {
- ++ iVal;
- write(sockfds[1], &iVal, sizeof(iVal));
- }
- }
- }
sendmsg/recvmsg
- #include <sys/types.h>
- #include <sys/socket.h>
- ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
- ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
它們與sendto/send 和 recvfrom/recv 函數類似,只不過可以傳輸更復雜的數據結構,不僅可以傳輸一般數據,還可以傳輸額外的數據,如文件描述符。
- //msghdr結構體
- struct msghdr
- {
- void *msg_name; /* optional address */
- socklen_t msg_namelen; /* size of address */
- struct iovec *msg_iov; /* scatter/gather array */
- size_t msg_iovlen; /* # elements in msg_iov */
- void *msg_control; /* ancillary data, see below */
- size_t msg_controllen; /* ancillary data buffer len */
- int msg_flags; /* flags on received message */
- };
- struct iovec /* Scatter/gather array items */
- {
- void *iov_base; /* Starting address */
- size_t iov_len; /* Number of bytes to transfer */
- };
msghdr結構體成員解釋:
1)msg_name :即對等方的地址指針,不關心時設爲NULL即可;
2)msg_namelen:地址長度,不關心時設置爲0即可;
3)msg_iov:是結構體iovec 的指針, 指向需要發送的普通數據, 見下圖。
成員iov_base 可以認爲是傳輸正常數據時的buf;
成員iov_len 是buf 的大小;
4)msg_iovlen:當有n個iovec 結構體時,此值爲n;
5)msg_control:是一個指向cmsghdr 結構體的指針(見下圖), 當需要發送輔助數據(如控制信息/文件描述符)時, 需要設置該字段, 當發送正常數據時, 就不需要關心該字段, 並且msg_controllen可以置爲0;
6)msg_controllen:cmsghdr 結構體可能不止一個(見下圖):
7)flags: 不用關心;
- //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 unsigned char cmsg_data[]; */
- };
爲了對齊,可能存在一些填充字節(見下圖),跟系統的實現有關,但我們不必關心,可以通過一些函數宏來獲取相關的值,如下:
- #include <sys/socket.h>
- struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *msgh);
- //獲取輔助數據的第一條消息
- struct cmsghdr *CMSG_NXTHDR(struct msghdr *msgh, struct cmsghdr *cmsg); //獲取輔助數據的下一條信息
- size_t CMSG_ALIGN(size_t length);
- size_t CMSG_SPACE(size_t length);
- size_t CMSG_LEN(size_t length); //length使用的是的(實際)數據的長度, 見下圖(兩條填充數據的中間部分)
- unsigned char *CMSG_DATA(struct cmsghdr *cmsg);
進程間傳遞文件描述符
- /**示例: 封裝兩個函數send_fd/recv_fd用於在進程間傳遞文件描述符**/
- int send_fd(int sockfd, int sendfd)
- {
- // 填充 name 字段
- struct msghdr msg;
- msg.msg_name = NULL;
- msg.msg_namelen = 0;
- // 填充 iov 字段
- struct iovec iov;
- char sendchar = '\0';
- iov.iov_base = &sendchar;
- iov.iov_len = 1;
- msg.msg_iov = &iov;
- msg.msg_iovlen = 1;
- // 填充 cmsg 字段
- struct cmsghdr cmsg;
- cmsg.cmsg_len = CMSG_LEN(sizeof(int));
- cmsg.cmsg_level = SOL_SOCKET;
- cmsg.cmsg_type = SCM_RIGHTS;
- *(int *)CMSG_DATA(&cmsg) = sendfd;
- msg.msg_control = &cmsg;
- msg.msg_controllen = CMSG_LEN(sizeof(int));
- // 發送
- if (sendmsg(sockfd, &msg, 0) == -1)
- return -1;
- return 0;
- }
- int recv_fd(int sockfd)
- {
- // 填充 name 字段
- struct msghdr msg;
- msg.msg_name = NULL;
- msg.msg_namelen = 0;
- // 填充 iov 字段
- struct iovec iov;
- char recvchar;
- iov.iov_base = &recvchar;
- iov.iov_len = 1;
- msg.msg_iov = &iov;
- msg.msg_iovlen = 1;
- // 填充 cmsg 字段
- struct cmsghdr cmsg;
- msg.msg_control = &cmsg;
- msg.msg_controllen = CMSG_LEN(sizeof(int));
- // 接收
- if (recvmsg(sockfd, &msg, 0) == -1)
- return -1;
- return *(int *)CMSG_DATA(&cmsg);
- }
- int main()
- {
- int sockfds[2];
- if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockfds) == -1)
- err_exit("socketpair error");
- pid_t pid = fork();
- if (pid == -1)
- err_exit("fork error");
- // 子進程以只讀方式打開文件, 將文件描述符發送給子進程
- else if (pid == 0)
- {
- close(sockfds[1]);
- int fd = open("read.txt", O_RDONLY);
- if (fd == -1)
- err_exit("open error");
- cout << "In child, fd = " << fd << endl;
- send_fd(sockfds[0], fd);
- }
- // 父進程從文件描述符中讀取數據
- else if (pid > 0)
- {
- close(sockfds[0]);
- int fd = recv_fd(sockfds[1]);
- if (fd == -1)
- err_exit("recv_fd error");
- cout << "In parent, fd = " << fd << endl;
- char buf[BUFSIZ] = {0};
- int readBytes = read(fd, buf, sizeof(buf));
- if (readBytes == -1)
- err_exit("read fd error");
- cout << buf;
- }
- }
分析:
我們知道,父進程在fork 之前打開的文件描述符,子進程是可以共享的,但是子進程打開的文件描述符,父進程是不能共享的,上述程序就是舉例在子進程中打開了一個文件描述符,然後通過send_fd 函數將文件描述符傳遞給父進程,父進程可以通過recv_fd 函數接收到這個文件描述符。先建立一個文件read.txt 後輸入幾個字符,然後運行程序;
注意:
(1)只有UNIX域協議才能在本機進程間傳遞文件描述符;
(2)進程間傳遞文件描述符並不是傳遞文件描述符的值(其實send_fd/recv_fd的兩個值也是不同的), 而是要在接收進程中創建一個新的文件描述符, 並且該文件描述符和發送進程中被傳遞的文件描述符指向內核中相同的文件表項.