1 socket IPC(本地套接字domain)
1.1 本地通信的方法
- 1)pipe,mkfifo,两者实现最简单。
- 2)mmap 非血缘关系进程间。
- 3)信号,开销小。
- 4)domain,稳定性最好。注意,在本节domain称之为本地套接字。与之对应的是网络套接字,两者需要区分一下。
1.2 本地套接字domain注意点
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文件描述符,domain参数可以是AF_UNIX,AF_LOCAL(并且绑定时address family一般指定为AF_UNIX,两者是对应赋相同值),type可以选择SOCK_DGRAM或SOCK_STREAM,protocol参数仍然指定为0即可。
即:
int socket(int domain, int type, int protocol);
/**
* 参1:可以是AF_UNIX,AF_LOCAL,网络一般是AF_INET。
* 参2:可以是SOCK_DGRAM或SOCK_STREAM,和网络一样。
* 参3:都是传0默认协议即可。
**/
UNIX Domain Socket与网络socket编程最明显的不同在于地址格式不同,用结构体sockaddr_un表示,网络编程的socket地址是IP地址加端口号,而UNIX Domain Socket的地址是一个socket类型的文件在文件系统中的路径,这个socket文件由bind()调用创建,如果调用bind()时该文件已存在,则bind()错误返回。
//对比网络套接字地址结构和本地套接字地址结构:
struct sockaddr_in {
__kernel_sa_family_t sin_family; /* Address family */ 地址结构类型
__be16 sin_port; /* Port number */ 端口号
struct in_addr sin_addr; /* Internet address */ IP地址
};
struct sockaddr_un {
__kernel_sa_family_t sun_family; /* AF_UNIX */ 地址结构类型
char sun_path[UNIX_PATH_MAX]; /* pathname */ socket文件名(含路径)
};
以下程序将UNIX Domain socket绑定到一个地址。
#define offsetof(type, member) ((int)&((type *)0)->MEMBER) //用于求某个结构体类型的成员的偏移地址,type为结构体类型,member为结构体类型的内部成员
socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un serv_addr;
serv_addr.sun_family = AF_UNIX;
strcpy(serv_addr.sun_path, "mysocket")
int len = offsetof(struct sockaddr_un, serv_addr.sun_path) + strlen(serv_addr.sun_path);//实际len = 2 + strlen(serv_addr.sun_path),即offsetof(serv_addr, serv_addr.sun_path)求出大小为2。
unlink("mysocket"); //若mysocket文件已存在,则先删除在bind创建,否则存在会使bind失败
bind(sfd, (struct sockaddr *)&serv_addr, len);//bind后会创建mysocket文件。
跳过两字节是因为下图的sockaddr_un路径名长度是不确定的,需要跳过16位的AF_UNIX才能求出len,然后求出sockaddr_un类型变量的长度给bind。注意不要直接写2,要使用offsetof函数,否则可能出错。
2 本地套接字通信案例
本地套接字通信案例客户端需要注意的是:
- 1)客户端对比网络通信,必须要显示bind,并且需要初始化两次地址结构,一个是客户端本身的文件名,一个是服务器的文件名,这两个文件相当于网络套接字的缓冲区。
- 2)注意connect时是连接服务器的地址结构,而不是客户端本身的。
- 3)下面的代码案例只能连接一个客户端,多个不行。注意:代码在共享目录执行时可能在bind时出现操作不允许,与共享目录的权限有关,换个普通目录即可。
注意下面的函数是经过封装出错处理,大家按正常理解即可。
server.c
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <strings.h>
#include <string.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <sys/un.h>
#include <stddef.h>
#include "wrap.h"
#define SERV_ADDR "serv.socket" //缓冲区文件名,伪文件,不占用磁盘空间
int main(void)
{
int lfd, cfd, len, size, i;
struct sockaddr_un servaddr, cliaddr;
char buf[4096];
lfd = Socket(AF_UNIX, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sun_family = AF_UNIX;
strcpy(servaddr.sun_path,SERV_ADDR);
len = offsetof(struct sockaddr_un, sun_path) + strlen(servaddr.sun_path); /* servaddr total len */
unlink(SERV_ADDR); /* 确保bind之前serv.sock文件不存在,bind会创建该文件 */
Bind(lfd, (struct sockaddr *)&servaddr, len); /* 参3不能是sizeof(servaddr) */
Listen(lfd, 20);
printf("Accept ...\n");
while (1) {
len = sizeof(cliaddr);
cfd = Accept(lfd, (struct sockaddr *)&cliaddr, (socklen_t *)&len);
len -= offsetof(struct sockaddr_un, sun_path); /* 得到文件名的长度 */
cliaddr.sun_path[len] = '\0'; /* 确保打印时,没有乱码出现 */
printf("client bind filename %s\n", cliaddr.sun_path);
while ((size = read(cfd, buf, sizeof(buf))) > 0) {
for (i = 0; i < size; i++)
buf[i] = toupper(buf[i]);
write(cfd, buf, size);
}
close(cfd);
}
close(lfd);
return 0;
}
client.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <strings.h>
#include <string.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <sys/un.h>
#include <stddef.h>
#include "wrap.h"
#define SERV_ADDR "serv.socket"
#define CLIE_ADDR "clie.socket"
int main(void)
{
int cfd, len;
struct sockaddr_un servaddr, cliaddr;
char buf[4096];
cfd = Socket(AF_UNIX, SOCK_STREAM, 0);
bzero(&cliaddr, sizeof(cliaddr));
cliaddr.sun_family = AF_UNIX;
strcpy(cliaddr.sun_path,CLIE_ADDR);
len = offsetof(struct sockaddr_un, sun_path) + strlen(cliaddr.sun_path); /* 计算客户端地址结构有效长度 */
unlink(CLIE_ADDR);
Bind(cfd, (struct sockaddr *)&cliaddr, len); /* 对比网络套接字,客户端需要显示bind, 不能依赖隐式绑定*/
/* 注意本地套接字通信需要初始化两次地址结构 */
bzero(&servaddr, sizeof(servaddr)); /* 构造server 地址 */
servaddr.sun_family = AF_UNIX;
strcpy(servaddr.sun_path,SERV_ADDR);
len = offsetof(struct sockaddr_un, sun_path) + strlen(servaddr.sun_path); /* 计算服务器端地址结构有效长度 */
Connect(cfd, (struct sockaddr *)&servaddr, len);
while (fgets(buf, sizeof(buf), stdin) != NULL) {
write(cfd, buf, strlen(buf));
len = read(cfd, buf, sizeof(buf));
write(STDOUT_FILENO, buf, len);
}
close(cfd);
return 0;
}
结果:
3 本地套接字和网络套接字流程的ser和cli对比