1、UNIX Domain Socket
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程序之间就是通过UNIXDomain 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()错误返回。
对比网络套接字地址结构和本地套接字地址结构:
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绑定到一个地址。
size = offsetof(struct sockaddr_un, sun_path) + strlen(un.sun_path);
#define offsetof(type, member) ((int)&((type *)0)->member)
2、本地套接字通信函数
1、socket函数
通过查询: man 7 unix 可以查到unix本地域socket通信相关信息:
#include <sys/socket.h>
#include <sys/un.h>
int socket(int domain, int type, int protocol);
函数说明: 创建本地域socket
函数参数:
domain: AF_UNIX or AF_LOCAL
type: SOCK_STREAM或者SOCK_DGRAM
protocol: 0 表示使用默认协议
函数返回值:
成功: 返回文件描述符.
失败: 返回-1, 并设置errno值.
创建socket成功以后, 会在内核创建缓冲区, 下图是客户端和服务端内核缓冲区示意图.
2、bind函数
通过man 2 bind, 可以查看bind函数的相关信息
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函数说明: 绑定套接字
函数参数:
socket: 由socket函数返回的文件描述符
addr: 本地地址
addlen: 本地地址长度
函数返回值:
成功: 返回文件描述符.
失败: 返回-1, 并设置errno值.
struct sockaddr_un {
sa_family_t sun_family; / AF_UNIX or AF_LOCAL/
char sun_path[108]; / pathname /
};*
需要注意的是: bind函数会自动创建socket文件, 若在调用bind函数之前socket文件已经存在, 则调用bind会报错, 可以使用unlink函数在bind之前先删除文件
3、本地套接字实现流程
本地套接字服务器的流程:
- 可以使用TCP的方式, 必须按照tcp的流程
- 也可以使用UDP的方式, 必须按照udp的流程
下面我们使用tcp的方式来完成本地套接字通信
服务端
tcp的本地套接字服务器流程:
- 创建套接字 socket(AF_UNIX,SOCK_STREAM,0)
- 绑定 struct sockaddr_un &强转
- 侦听 listen
- 获得新连接 accept
- 循环通信 read-write
- 关闭文件描述符 close
客户端
tcp本地套接字客户端流程:
调用socket创建套接字
调用bind函数将socket文件描述和socket文件进行绑定.
不是必须的, 若无显示绑定会进行隐式绑定,但服务器不知道谁连接了.
调用connect函数连接服务端
循环通信read-write、
关闭文件描述符 close
4、TCP实现本地套接字通信
编写代码并进行测试
测试客户端工具:
man nc
-U Specifies to use UNIX-domain sockets.
例如: nc -U sock.s
以下程序将UNIX Domain socket绑定到一个地址。
size = offsetof(struct sockaddr_un, sun_path) + strlen(un.sun_path);
#define offsetof(type, member) ((int)&((type *)0)->member)
服务端
//本地socket通信服务端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/un.h>
int main()
{
//创建socket
int lfd = socket(AF_UNIX, SOCK_STREAM, 0);
if(lfd<0)
{
perror("socket error");
return -1;
}
//删除socket文件,避免bind失败
unlink("./server.sock");
//绑定bind
struct sockaddr_un serv;
bzero(&serv, sizeof(serv));
serv.sun_family = AF_UNIX;
strcpy(serv.sun_path, "./server.sock");
int ret = bind(lfd, (struct sockaddr *)&serv, sizeof(serv));
if(ret<0)
{
perror("bind error");
return -1;
}
//监听listen
listen(lfd, 10);
//接收新的连接-accept
struct sockaddr_un client;
bzero(&client, sizeof(client));
int len = sizeof(client);
int cfd = accept(lfd, (struct sockaddr *)&client, &len);
if(cfd<0)
{
perror("accept error");
return -1;
}
printf("client->[%s]\n", client.sun_path);
int n;
char buf[1024];
while(1)
{
//读数据
memset(buf, 0x00, sizeof(buf));
n = read(cfd, buf, sizeof(buf));
if(n<=0)
{
printf("read error or client close, n==[%d]\n", n);
break;
}
printf("n==[%d], buf==[%s]\n", n, buf);
//发送数据
write(cfd, buf, n);
}
close(lfd);
return 0;
}
客户端
//本地socket通信客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/un.h>
int main()
{
//创建socket
int cfd = socket(AF_UNIX, SOCK_STREAM, 0);
if(cfd<0)
{
perror("socket error");
return -1;
}
//删除socket文件,避免bind失败
unlink("./client.sock");
//绑定bind
struct sockaddr_un client;
bzero(&client, sizeof(client));
client.sun_family = AF_UNIX;
strcpy(client.sun_path, "./client.sock");
int ret = bind(cfd, (struct sockaddr *)&client, sizeof(client));
if(ret<0)
{
perror("bind error");
return -1;
}
struct sockaddr_un serv;
bzero(&serv, sizeof(serv));
serv.sun_family = AF_UNIX;
strcpy(serv.sun_path, "./server.sock");
ret = connect(cfd, (struct sockaddr *)&serv, sizeof(serv));
if(ret<0)
{
perror("connect error");
return -1;
}
int n;
char buf[1024];
while(1)
{
memset(buf, 0x00, sizeof(buf));
n = read(STDIN_FILENO, buf, sizeof(buf));
//发送数据
write(cfd, buf, n);
//读数据
memset(buf, 0x00, sizeof(buf));
n = read(cfd, buf, sizeof(buf));
if(n<=0)
{
printf("read error or client close, n==[%d]\n", n);
break;
}
printf("n==[%d], buf==[%s]\n", n, buf);
}
close(cfd);
return 0;
}
5、网络通信中常用的其他函数
1、名字与地址转换
-
gethostbyname根据给定的主机名,获取主机信息。
过时,仅用于IPv4,且线程不安全。
-
gethostbyaddr函数。
此函数只能获取域名解析服务器的url和/etc/hosts里登记的IP对应的域名。
-
getservbyname
getservbyport
根据服务程序名字或端口号获取信息。使用频率不高。
-
getaddrinfo
getnameinfo
freeaddrinfo
可同时处理IPv4和IPv6,线程安全的。
2、套接口和地址关联
-
getsockname
根据accpet返回的sockfd,得到临时端口号
-
getpeername
根据accpet返回的sockfd,得到远端链接的端口号,在exec后可以获取客户端信息。