socket本地通信
socket原本是为网络通讯设计的,但后来在socket框架的基础上发展出了一种IPC(进程通信)机制,即UNIX Domain Socket,专门用来实现使用socket实现的本地进程通信。其实socket原本便能实现本地通信功能,但使用Domain机制时,数据不需要再通过网络协议族,也无需进行拆包、计算校验和以及应答等网络通讯中涉及的一系列操作,只需将应用层的数据从本地的一个进程拷贝到另一个进程,因此相对借助网络通信机制实现的socket本地通信来说,Domain本地通信的效率和数据的正确性都得到了提升。下面就来讲解使用Domain实现本地通信的方式。
使用socket实现本地通信时,同样需要先创建socket文件,再以socket为媒介,实现数据传递。socket本地通信中也通过socket()函数来创建套接字文件,该函数的原型如下:
int socket(int domain, int type, int protocol);
其中,domain参数指定协议族,对于本地套接字来说,其值须被置为枚举值AF_UNIX;type参数指定套接字类型,本地套接字通信中的套接字类型仍可被设置为表示流式套接字通信的SOCK_STREAM,或表示数据报式套接字通信的SOCK_DGRAM;protocol参数指定具体协议,通常被设置为0;其返回值为生成的套接字描述符。
根据socket()函数中参数type的类型,本地通信的方式也分为面向连接和面向非连接这两种。当使用SOCK_STREAM作为type参数的值时,本地通信的流程与使用的接口与基于TCP协议的网络通信模型相同,其大致流程如下:
(1)调用socket()函数通信双方进程创建各自的socket文件;
(2)定义并初始化服务器端进程的地址,并使用bind()函数将其与服务器端进程绑定;
(3)调用listen()函数监听客户端进程请求;
(4)客户端调用connect()函数,根据已明确的客户端进程地址,向服务器发送请求;
(5)服务器端调用accept()函数,处理客户端进程的请求,若客户端与服务器端进程成功建立连接,则双方进程可开始通信;
(6)通信双方以数据流的形式通过已创建的连接互相发送和接收数据,进行通信;
(7)待通信结束后,通信双方各自调用close()函数关闭连接。
与socket网络通信不同的是,在本地通信中用到的套接字的结构体类型为socket sockaddr_un。
使用SOCK_DGRAM作为type参数值时,本地通信的流程与用到的接口与基于UDP协议的网络通信相同,用户可根据图10-6自行推演,此处不再赘述。值得一提的是,使用数据报实现socket本地通信的双方在理论上虽仍有可能出现信息丢失、数据包次序错乱等问题,但由于其不必再经过多层复杂的协议,这种情况出现的概率相对网络中的数据报通信要低的多。
此外,基于数据流的本地套接字通信连接时间非常短,且通信双方在建立连接后可直接交互数据,因此在socket本地通信中,基于数据报的本地套接字应用场合相对少的多。本节将以基于流的本地套接字通信为例,来展示socket本地通信的实现方法。
案例3:使用socket实现本地进程间通信。
dmserver.c //服务器
1 #include <stdlib.h>
2 #include <stdio.h>
3 #include <stddef.h>
4 #include <sys/socket.h>
5 #include <sys/un.h>
6 #include <sys/types.h>
7 #include <sys/stat.h>
8 #include <unistd.h>
9 #include <errno.h>
10 #define QLEN 10
11 //创建服务器进程,成功返回0,出错返回小于0的errno
12 int serv_listen(const char *name)
13 {
14 int fd, len, err, rval;
15 struct sockaddr_un un;
16 //创建本地domain套接字
17 if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
18 return(-1);
19 //删除套接字文件,避免因文件存在导致bind()绑定失败
20 unlink(name);
21 //初始化套接字结构体地址
22 memset(&un, 0, sizeof(un));
23 un.sun_family = AF_UNIX;
24 strcpy(un.sun_path, name);
25 len = offsetof(struct sockaddr_un, sun_path) + strlen(name);
26 if (bind(fd, (struct sockaddr *)&un, len) < 0) {
27 rval = -2;
28 goto errout;
29 }
30 if (listen(fd, QLEN) < 0) { //告知内核这是一个服务器进程
31 rval = -3;
32 goto errout;
33 }
34 return(fd);
35 errout:
36 err = errno;
37 close(fd);
38 errno = err;
39 return(rval);
40 }
41 int serv_accept(int listenfd, uid_t *uidptr)
42 {
43 int clifd, len, err, rval;
44 time_t staletime;
45 struct sockaddr_un un;
46 struct stat statbuf;
47 len = sizeof(un);
48 if ((clifd = accept(listenfd, (struct sockaddr *)&un, &len)) < 0)
49 return(-1);
50 //从调用地址获取客户端的uid
51 len -= offsetof(struct sockaddr_un, sun_path); //获取路径名长度
52 un.sun_path[len] = 0; //为路径名字符串添加终止符
53 if (stat(un.sun_path, &statbuf) < 0) {
54 rval = -2;
55 goto errout;
56 }
57 if (S_ISSOCK(statbuf.st_mode) == 0) {
58 rval = -3; //若返回值为-3,说明这不是一个socket文件
59 goto errout;
60 }
61 if (uidptr != NULL)
62 *uidptr = statbuf.st_uid; //返回uid的调用者指针
63 //到此成功获取路径名
64 unlink(un.sun_path);
65 return(clifd);
66 errout:
67 err = errno;
68 close(clifd);
69 errno = err;
70 return(rval);
71 }
72 int main(void)
73 {
74 int lfd, cfd, n, i;
75 uid_t cuid;
76 char buf[1024];
77 lfd = serv_listen("foo.socket");
78 if (lfd < 0) {
79 switch (lfd) {
80 case -3:perror("listen"); break;
81 case -2:perror("bind"); break;
82 case -1:perror("socket"); break;
83 }
84 exit(-1);
85 }
86 cfd = serv_accept(lfd, &cuid);
87 if (cfd < 0) {
88 switch (cfd) {
89 case -3:perror("not a socket"); break;
90 case -2:perror("a bad filename"); break;
91 case -1:perror("accept"); break;
92 }
93 exit(-1);
94 }
95 while (1) {
96 r_again:
97 n = read(cfd, buf, 1024);
98 if (n == -1) {
99 if (errno == EINTR)
100 goto r_again;
101 }
102 else if (n == 0) {
103 printf("the other side has been closed.\n");
104 break;
105 }
106 for (i = 0; i < n; i++)
107 buf[i] = toupper(buf[i]);
108 write(cfd, buf, n);
109 }
110 close(cfd);
111 close(lfd);
112 return 0;
113 }
dmclient.c //客户端
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <stddef.h>
4 #include <sys/stat.h>
5 #include <fcntl.h>
6 #include <unistd.h>
7 #include <sys/socket.h>
8 #include <sys/un.h>
9 #include <errno.h>
10 #define CLI_PATH "/var/tmp/" /* +5 for pid = 14 chars */
11 //创建客户端进程,成功返回0,出错返回小于0的errno
12 int cli_conn(const char *name)
13 {
14 int fd, len, err, rval;
15 struct sockaddr_un un;
16 //创建本地套接字domain
17 if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
18 return(-1);
19 //使用自定义地址填充socket地址结构体
20 memset(&un, 0, sizeof(un));
21 un.sun_family = AF_UNIX;
22 sprintf(un.sun_path, "%s%05d", CLI_PATH, getpid());
23 len = offsetof(struct sockaddr_un, sun_path) + strlen(un.sun_path);
24 unlink(un.sun_path); //避免因文件已存在导致的bind()失败
25 if (bind(fd, (struct sockaddr *)&un, len) < 0) {
26 rval = -2;
27 goto errout;
28 }
29 //使用服务器进程地址填充socket地址结构体
30 memset(&un, 0, sizeof(un));
31 un.sun_family = AF_UNIX;
32 strcpy(un.sun_path, name);
33 len = offsetof(struct sockaddr_un, sun_path) + strlen(name);
34 if (connect(fd, (struct sockaddr *)&un, len) < 0) {
35 rval = -4;
36 goto errout;
37 }
38 return(fd);
39 errout:
40 err = errno;
41 close(fd);
42 errno = err;
43 return(rval);
44 }
45 int main(void)
46 {
47 int fd, n;
48 char buf[1024];
49 fd = cli_conn("foo.socket"); //套接字文件为foo.socket
50 if (fd < 0) { //容错处理
51 switch (fd) {
52 case -4:perror("connect"); break;
53 case -3:perror("listen"); break;
54 case -2:perror("bind"); break;
55 case -1:perror("socket"); break;
56 }
57 exit(-1);
58 }
59 while (fgets(buf, sizeof(buf), stdin) != NULL) {
60 write(fd, buf, strlen(buf));
61 n = read(fd, buf, sizeof(buf));
62 write(STDOUT_FILENO, buf, n);
63 }
64 close(fd);
65 return 0;
66 }
编译以上两端代码,打开两个终端窗口,先执行服务器端程序,再执行客户端程序,同时输入需要转换的字符串,客户端对应的终端中打印的信息如下:
hello
HELLO
world
WORLD
当关闭客户端后,服务器端对应的终端打印的信息如下:
the other side has been closed.
由程序执行结果可知,服务器成功将客户端发来的字符串转换成大写,案例实现成功。
脚下留心:出错处理函数封装
本章的案例功能简单,且几乎未对系统调用函数设置容错处理,但容错处理是优质程序中必不可少的内容,因为在函数调用过程中,可能会因各种可控或不可测因素而出现各种各样的错误,所以,为使程序更为严谨,应在程序中添加容错处理代码;同时也为了使程序的主体结构更为清晰,可将功能函数与函数的错误处理功能封装到一个新的函数中。
下面将对网络编程中的常用接口及read()、write()等函数进行再次封装,为其添加出错处理功能,并将这些新函数的声明与定义分别保存在文件wrap.h和文件wrap.c中。
wrap.h //出错处理函数头文件
1 #ifndef __WRAP_H_
2 #define __WRAP_H_
3 void perr_exit(const char *s);
4 int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);
5 void Bind(int fd, const struct sockaddr *sa, socklen_t salen);
6 void Connect(int fd, const struct sockaddr *sa, socklen_t salen);
7 void Listen(int fd, int backlog);
8 int Socket(int family, int type, int protocol);
9 ssize_t Read(int fd, void *ptr, size_t nbytes);
10 ssize_t Write(int fd, const void *ptr, size_t nbytes);
11 void Close(int fd);
12 ssize_t Readn(int fd, void *vptr, size_t n);
13 ssize_t Writen(int fd, const void *vptr, size_t n);
14 static ssize_t my_read(int fd, char *ptr);
15 ssize_t Readline(int fd, void *vptr, size_t maxlen);
16 #endif
wrap.c //出错处理函数实现
1 #include <stdlib.h>
2 #include <errno.h>
3 #include <sys/socket.h>
4 //socket()
5 int Socket(int family, int type, int protocol)
6 {
7 int n;
8 if ((n = socket(family, type, protocol)) < 0)//若socket调用失败
9 perr_exit("socket error");
10 return n;
11 }
12 //bind()
13 void Bind(int fd, const struct sockaddr *sa, socklen_t salen)
14 {
15 if (bind(fd, sa, salen) < 0) //若bind()调用失败
16 perr_exit("bind error");
17 }
18 //listen()
19 void Listen(int fd, int backlog)
20 {
21 if (listen(fd, backlog) < 0) //若listen()调用失败
22 perr_exit("listen error");
23 }
24 //connect()
25 void Connect(int fd, const struct sockaddr *sa, socklen_t salen)
26 {
27 if (connect(fd, sa, salen) < 0) //若connect()调用失败
28 perr_exit("connect error");
29 }
30 //accept()
31 int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)
32 {
33 int n;
34 again:
35 if ((n = accept(fd, sa, salenptr)) < 0) { //若accept()调用失败
36 if ((errno == ECONNABORTED) || (errno == EINTR))
37 goto again;
38 else
39 perr_exit("accept error");
40 }
41 return n;
42 }
43 //close()
44 void Close(int fd)
45 {
46 if (close(fd) == -1) //关闭socket失败
47 perr_exit("close error");
48 }
49 //read()
50 ssize_t Read(int fd, void *ptr, size_t nbytes)
51 {
52 ssize_t n;
53 again:
54 if ((n = read(fd, ptr, nbytes)) == -1) { //读取数据失败
55 if (errno == EINTR)
56 goto again; //重读fd
57 else
58 return -1;
59 }
60 return n;
61 }
62 //write()
63 ssize_t Write(int fd, const void *ptr, size_t nbytes)
64 {
65 ssize_t n;
66 again:
67 if ((n = write(fd, ptr, nbytes)) == -1) { //写数据出错
68 if (errno == EINTR)
69 goto again; //重新写入
70 else
71 return -1;
72 }
73 return n;
74 }
75 void perr_exit(const char *s)
76 {
77 perror(s);
78 exit(1);
79 }