概述

今天给大家讲解网络编程中的一个内容——Unix 本地套接字。

发现很多人不知道或者不太了解 Unix 本地套接字这个概念,这也难怪,socket API 原本就是为多台主机之间网络通信设计的,并且这种网络 socket 同样支持单台主机上的进程间通信,当然这样做的话,仍然需要 IP 地址和端口号(通过 loopback 地址 127.0.0.1)。Unix本地套接字,其实就是一种专门用于本地(也就是单个主机上的)网络通信的一种方法,它所用的 API 跟我们之前用的网络 socket API 是一样的。

本文要介绍的 Unix 本地套接字是在 socket 的框架上发展出一种 IPC 机制,即 UNIX Domain Socket,也就是 UNIX 本地套接字,或者称为 UNIX 域套接字。相比于网络 socket 和 IPC,Unix本地套接字有其自身的特点和优势,下面我们就来看一下吧。

虽然在很多教材中经常把Unix本地套接字放在网络编程里面进行讲解,但实际上,这种通信方式更类似于我们之前所学的IPC(进程间通信)的方式,比如无名管道(pipe)、有名管道(mkfifo)。但是,Unix域套接字所提供的控制方式会更多一些,比如说TCP(字节流套接字)提供等待连接的功能,UDP(数据报套接字)提供帧同步功能,同时也是全双工的(比如使用 socketpair 创建的流管道中的两个描述符都是既可读又可写的)。

TCP 和 UDP

首先,我们回顾一下,TCP 和 UDP 套接字的服务端和客户端,从 socket 的创建,到连接,到数据传输,再到关闭 socket 的整个流程。

TCP 是一种面向连接的字节流套接字,所以服务端需要通过 listen() 转变为被动 socket,通过 accept() 等待连接。

图1. TCP 客户/服务器通信流程

而对于 UDP 来说,就比较简单了,因为它是一种无连接的数据报套接字,实际上,客户/服务端的概念也弱化了。

图2. UDP 通信流程

Unix 本地套接字 API

前面我们说过,Unix 域套接字所使用的 API 其实跟我们之前用的 socket API 是一样的,并且对于 TCP 和 UDP,其工作流程跟上图的网络 socket 完全一样。

那么,下面我们就来看一下如何通过这些 API 来创建并使用我们的 Unix 本地套接字,以及它们之间有什么区别,然后再通过一个简单的示例程序来体验一下。

创建套接字

首先是 socket 的创建。同样使用 socket() 这个函数。

但是它的第一个参数 domain 不再是 AF_INET 或者 PF_INET,而是 AF_UNIX,表示的就是 Unix 域本地套接字。那 AF_LOCAL 又是什么呢?这其实是历史原因造成的,我们的主题是“Unix本地套接字”或者“Unix域套接字”,但实际上我们用是Linux,对吧。那其实,为了消除它对 Unix 操作系统的依赖,在 POSIX 标准中,早就已经将 AF_UNIX 变成 AF_LOCAL 了,但是尽管如此,我们依然习惯使用“Unix域”这个称谓,因此,更推荐大家使用 AF_LOCAL

第二个参数的话,跟 Internet 域套接字一样,可以是 SOCK_STREAMSOCK_DGRAMSOCK_RAW,但实际上,几乎没见过使用原始套接字的。所以一般来说 Unix 提供两类套接字,也就是字节流套接字(类似于TCP)和数据报套接字(类似于UDP)。

第三个参数 Protocol,显然,因为第二个参数 type 不是原始套接字,所以 protocol 一般填 0 就可以了。

绑定地址

创建完套接字,接下来就是通过 bind() 函数绑定地址,但对于 Unix 本地套接字来说,绑定的地址就不是原来的“IP地址 + 端口号”了,而是一个有效的路径。

本地套接字的地址结构体 sockaddr_un 的后缀是 _un,表示 Unix,而不是原来的 sockaddr_in(Internet)。我们来看一下这个Unix域套接字的地址结构体中包含哪些内容:

Unix 本地套接字的地址结构体中包含两个成员,其中 sun_family 表示协议族,填 AF_LOCALAF_UNIX 即可;sun_path 表示一个路径名。

从这里面可以很明显得看出 Unix 域套接字与原来的 网络套接字的区别,Unix 域中用于标识客户和服务器的协议地址是普通文件系统中的路径名,而这个文件就称为套接字文件

这里要强调一下的是,Unix 本地套接字关联的这个路径名应该是一个绝对路径名,而不是一个相对路径名。为什么呢?因为解析相对路径依赖于调用者的当前工作目录,也就是说,要是服务器绑定了一个相对路径名,那么客户端也得在与服务端相同的目录中才能成功调用connect(连接)或者sendto(发送)这样一些函数。显然,这样就会导致程序出现异常情况,所以建议大家最好使用一个绝对路径名。

这个路径名,其实还要分为两种,一种是我们上面所提到的普通路径名,另一种是抽象路径名。普通路径名是一个正常的字符串,也就是说,sun_path 字段是以空字符(’\0’)结尾的。而抽象路径名,sun_path 字段的第一个字节需要设置成 NULL(’\0’),所以在计算抽象路径名的长度的时候就要特别小心了,否则在解析抽象路径名时就有可能出现异常情况,因为抽象路径名不是像解析普通路径名那样,解析到第一个 NULL 就可以停止了。

使用抽象路径名的好处是,因为不会再在文件系统中创建文件了,所以对于抽象路径名来说,就不需要担心与文件系统中已存在的文件产生名字冲突的问题了,也不需要在使用完套接字之后删除附带产生的这个文件了,当套接字被关闭之后会自动删除这个抽象名。

其他API

其他的一些 API,比如 listen()、accept()、connect(),以及数据通信用的 read()、write()、recv()、send()、recvfrom()、sendto()、recvmsg()、sendmsg(),用法跟网络 socket 基本一样,主要是地址结构体需要注意一下。

另外,在 Unix 本地套接字的使用中,还经常用到这些 API:

(1)用于创建类似于无名管道(pipe)的本地套接字

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

(2)当不再需要这个 Unix 域套接字时,应删除路径名对应的文件

int unlink(const char *pathname);
int remove(const char *pathname);

注意,如果是抽象路径名,就不需要在使用完本地套接字后手动删除对应的套接字文件,因为当本地套接字被关闭之后,内核会自动删除这个抽象名。

(3)获取本地套接字的地址信息

int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

示例

TCP(字节流套接字)

在这个例子中,我们使用绝对路径名"/tmp/unix.str"来实现一个字节流的本地套接字,服务端接收数据,客户端发送数据。

【unixstr_serv.c】

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>#define UNIXSTR_PATH "/tmp/unix.str"
#define LISTENQ 5
#define BUFFER_SIZE 256int main(void)
{int listenfd, connfd;socklen_t len;struct sockaddr_un servaddr, cliaddr;if(-1 == (listenfd = socket(AF_LOCAL, SOCK_STREAM, 0))){perror("socket");exit(EXIT_FAILURE);}unlink(UNIXSTR_PATH);bzero(&servaddr, sizeof(servaddr));servaddr.sun_family = AF_LOCAL;strcpy(servaddr.sun_path, UNIXSTR_PATH);if(-1 == bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr))){perror("bind");exit(EXIT_FAILURE);}listen(listenfd, LISTENQ);len = sizeof(cliaddr);if(-1 == (connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &len))){perror("accept");exit(EXIT_FAILURE);}char buf[BUFFER_SIZE];while(1){bzero(buf, sizeof(buf));if(read(connfd, buf, BUFFER_SIZE) == 0) break;printf("Receive: %s", buf);}close(listenfd);close(connfd);unlink(UNIXSTR_PATH);return 0;
}

【unixstr_cli.c】

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>#define UNIXSTR_PATH "/tmp/unix.str"
#define LISTENQ 5
#define BUFFER_SIZE 256int main(void)
{int sockfd;struct sockaddr_un servaddr;sockfd = socket(AF_LOCAL, SOCK_STREAM, 0);bzero(&servaddr, sizeof(servaddr));servaddr.sun_family = AF_LOCAL;strcpy(servaddr.sun_path, UNIXSTR_PATH);connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));char buf[BUFFER_SIZE];while(1){bzero(buf, sizeof(BUFFER_SIZE));printf(">> ");if(fgets(buf, BUFFER_SIZE, stdin) == NULL){break;}write(sockfd, buf, strlen(buf));}close(sockfd);return 0;
}

有兴趣的童鞋可以自己编译、执行,看看运行效果,我们这里来看一下 /tmp/unix.str 这个文件吧。

rudy@ubuntu12:/tmp$ ls -l unix.str
srwxrwxr-x 1 rudy rudy 0 10月 26 11:58 unix.str

显然,文件类型为“s”,代表套接字文件,也就是 S_IFSOCK 类型。

UDP(数据报套接字)

类似于上面 TCP 的例子,我们使用绝对路径名"/tmp/unix.dg"来实现一个数据报的本地套接字,一端接收数据,一端发送数据。

【unixdg_serv.c】

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>#define UNIXDG_PATH "/tmp/unix.dg"
#define BUFFER_SIZE 256int main(void)
{int sockfd;struct sockaddr_un servaddr, cliaddr;sockfd = socket(AF_LOCAL, SOCK_DGRAM, 0);unlink(UNIXDG_PATH);bzero(&servaddr, sizeof(servaddr));servaddr.sun_family = AF_LOCAL;strcpy(servaddr.sun_path, UNIXDG_PATH);bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));socklen_t len = sizeof(cliaddr);char buf[BUFFER_SIZE];while(1){bzero(buf, BUFFER_SIZE);if(0 == recvfrom(sockfd, buf, BUFFER_SIZE, 0, (struct sockaddr *)&cliaddr, &len)){break;}printf("recvfrom: %s", buf);}close(sockfd);unlink(UNIXDG_PATH);return 0;
}

【unixdg_cli.c】

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>#define UNIXDG_PATH "/tmp/unix.dg"
#define BUFFER_SIZE 256int main(void)
{int sockfd;struct sockaddr_un servaddr, cliaddr;socklen_t len;sockfd = socket(AF_LOCAL, SOCK_DGRAM, 0);/* local address */bzero(&cliaddr, sizeof(cliaddr));cliaddr.sun_family = AF_LOCAL;strcpy(cliaddr.sun_path, UNIXDG_PATH);bind(sockfd, (struct sockaddr *)&cliaddr, sizeof(cliaddr));/* remote address */bzero(&servaddr, sizeof(servaddr));servaddr.sun_family = AF_LOCAL;strcpy(servaddr.sun_path, UNIXDG_PATH);len = sizeof(servaddr);char buf[BUFFER_SIZE];while(1){bzero(buf, BUFFER_SIZE);printf(">> ");if(fgets(buf, BUFFER_SIZE, stdin) == NULL){break;}sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&servaddr, len);}close(sockfd);return 0;
}

需要注意的是,跟网络套接字不一样,对于 Unix 域套接字的 UDP 客户端,必须 bind 一个路径名到 UDP 套接字,以使得 UDP 服务器有发送应答的目的地。

总结

通过上面简单的示例,我们可以看到 Unix 本地套接字跟 Internet 套接字虽然使用相同的 API,但用法上又有些不同,跟IPC(比如管道、消息队列、共享内存等)相比,也有不同。我们可以简单地把 Unix 本地套接字看成是 socket 和管道的混合体。

可以这么说,Unix 本地套接字的优势体现在它所使用的 API 几乎等同于网络 socket(客户/服务器)使用的 API,但是与客户端和服务端都在同一主机上的 TCP 相比,Unix 本地字节流套接字有性能上的优势。在单个主机,使用 Unix 域套接字来替代 Internet 域套接字是有好处的。

最后,总结一下:

  1. Socket 同样可以用于本地通信。
  2. 创建套接字时使用本地协议 AF_LOCAL
  3. 分为流式套接字(SOCK_STREAM)和数据报套接字(SOCK_DGRAM) 。
  4. 和其他进程间通信方式相比,Unix 本地套接字使用方便,效率也高 。因为它不需要经过网络协议栈、不需要打包拆包、不需要计算校验和、不需要维护序号和应答等、只是将应用层数据从一个进程拷贝到另一个进程…
  5. 常用于前后台进程通信,比如 X Window。
  6. 另外,Unix 本地套接字可用于传递文件描述符、传递用户凭证等场景。

Linux网络编程——Unix本地套接字相关推荐

  1. 【Linux网络编程】UDP 套接字编程

    [Linux网络编程]UDP 套接字编程 [1]用户数据报协议(UDP) UDP是一个简单的传输层协议,不保证UDP数据报会到达其最终目的地,不保证各个数据报的先后顺序跨网络后保持不变,也不保证每个数 ...

  2. Linux网络编程:原始套接字的魔力【续】

    如何从链路层直接发送数据帧        本来以为这部分都弄完了,结果有朋友反映说看了半天还是没看到如何从链路层直接发送数据.因为上一篇里面提到的是从链路层"收发"数据,结果只&q ...

  3. [Linux网络编程学习笔记]套接字地址结构

    好久没有看那Linux网络编程这本书了,今天看到了重点部分-TCP套接字.下面先来看看套接字的地址结构 Linux系统的套接字可以支持多种协议,每种不同的协议都是用不同的地址结构.在头文件<li ...

  4. 【Linux网络编程】原始套接字实例:MAC 头部报文分析

    通过<Linux网络编程--原始套接字编程>得知,我们可以通过原始套接字以及 recvfrom( ) 可以获取链路层的数据包,那我们接收的链路层数据包到底长什么样的呢? MAC 头部(有线 ...

  5. 【Linux网络编程】原始套接字编程

    原始套接字编程和之前的 UDP 编程差不多,无非就是创建一个套接字后,通过这个套接字接收数据或者发送数据.区别在于,原始套接字可以自行组装数据包(伪装本地 IP,本地 MAC),可以接收本机网卡上所有 ...

  6. 【Linux网络编程】原始套接字能干什么?

    通常情况下程序员接所接触到的套接字(Socket)为两类: (1)流式套接字(SOCK_STREAM):一种面向连接的 Socket,针对于面向连接的TCP 服务应用: (2)数据报式套接字(SOCK ...

  7. 【Linux网络编程】原始套接字实例:发送 UDP 数据包

    以太网报文格式: 详细的说明,请看<MAC 头部报文分析>. IP 报文格式: 详细的说明,请看<IP 数据报格式详解>. UDP 报文格式: 详细的说明,请看<UDP ...

  8. 【Linux网络编程】原始套接字实例:MAC 地址扫描器

    如果 A (192.168.1.1 )向 B (192.168.1.2 )发送一个数据包,那么需要的条件有 ip.port.使用的协议(TCP/UDP)之外还需要 MAC 地址,因为在以太网数据包中 ...

  9. Linux网络编程 | 基本UCP套接字编程

    文章目录 通信模型 UDP数据读写 实现完整的UDP服务器端客户端程序 服务器端 客户端 在网络编程中,UDP运用非常广泛.很多网络协议是基于UDP来实现的,如SNMP等. UDP是无连接的.不可靠的 ...

最新文章

  1. VS2015 error MSB6006: “cmd.exe”已退出 代码为 3
  2. node.js项目中常量的配置 - 个人文章 - SegmentFault 思否
  3. 变量在内存中的值[c][code]
  4. threejs 物体根据相机位置显示_认识Threejs
  5. java笔试题及答案
  6. 16套51单片机开发板资料共享下载,拼命整理
  7. oracle awr 定期,Oracle 每天自动生成AWR报告
  8. 负压电源设计 TPS54160 负压 Level Shifting Control for an Inverting Buck-boost
  9. 「数据架构」什么是数据流程图(DFD)?如何绘制DFD?
  10. 越南语常用的40句口语
  11. StringUtils中isNotEmpty和isNotBlank及isBlank()和isEmpty()区别
  12. 怎么搭建一个高性能服务器
  13. Maltego注册问题
  14. Effective Java (3rd Editin) 读书笔记:1 创建和销毁对象
  15. 在群辉上搭建git服务器
  16. 【Open3d报错】 无法使用open3d显示图像
  17. python使用循环求斐波那契的第n项_深市收盘价如何确定,沪深股市收盘价怎样确定...
  18. 小米兰亭pro ttf_探索 让未来多一种可能 小米5G新品发布会直播
  19. 百词斩2018年秋招面试题
  20. 【附源码】计算机毕业设计JAVA罪犯信息管理系统

热门文章

  1. 廖雪峰Git教程笔记与总结 -- Git简介、常用命令、分支管理
  2. 带外数据:TCP紧急模式分析
  3. 【青梅快讯】Greenplum 最新版本6.20.3已正式发布
  4. css中伪类和伪元素有什么不一样
  5. 毕业设计 STM32自动泊车系统 - 智能小车 自动停车
  6. 专享策略04 | 商品通用套利模型(二)
  7. iOS 如何适应 iPhone 5s/6/6 Plus 三种屏幕的尺寸?
  8. Win10关闭蓝牙省电模式的两种方法
  9. 大数据获取与预处理-会计欺诈检测
  10. RSA加密解密算法工具_JAVA