进程间Socket通信,还在用localhost?来看看UDS

UNIX Domain Socket (UDS)

概念

UNIX Domain Socket(UDS,Unix域套接字)是一种在同一台主机上进行进程间通信(IPC,Inter-Process Communication)的机制, 它提供了一种类似于网络套接字(如 TCP/IP 套接字用于网络通信)的接口, 但仅用于本地通信,也就是在同一操作系统内核管理下的不同进程之间交互数据。

socket API原本是为网络通讯设计的,但后来在socket的框架上发展出一种IPC机制,就是UNIX Domain Socket。 虽然网络socket也可用于同一台主机的进程间通讯(通过loopback地址127.0.0.1), 但是UNIX Domain Socket用于IPC更有效率:

  • 不需要经过网络协议栈
  • 不需要打包拆包、计算校验和、维护序号和应答等

只是将应用层数据从一个进程拷贝到另一个进程。UNIX域套接字与TCP套接字相比较,在同一台主机的传输速度前者是后者的两倍。 这是因为,IPC机制本质上是可靠的通讯,而网络协议是为不可靠的通讯设计的。 UNIX Domain Socket也提供面向流和面向数据包两种API接口,类似于TCP和UDP.

特点
  • 高效性
    • 由于通信发生在本地主机的内核空间内,相比于通过网络协议栈(如 TCP/IP 进行本地回环通信时仍要经过多层网络相关处理)的通信方式 少了很多网络协议相关的开销,比如不需要经过网卡驱动、网络协议的封装与解析等步骤,数据传输效率更高,延迟更低。 例如,在频繁进行小数据量交互的场景中,UNIX Domain Socket 能快速地将数据从一个进程传递到另一个进程。
  • 安全性
    • 可以通过文件系统的权限机制来控制对套接字的访问。 创建 UNIX Domain Socket 时会在文件系统中有对应的一个抽象的“文件”表示(实际上并非传统意义的磁盘文件), 可以像设置普通文件权限那样,设置哪些用户、用户组能够读写该套接字,以此来保障通信的安全性,防止未经授权的进程接入进行非法通信。
  • 灵活性
    • 支持多种通信模式,比如面向连接的字节流(类似 TCP)通信模式, 通过 SOCK_STREAM 类型创建套接字, 通信双方可以像使用 TCP 连接那样进行可靠的、有序的数据传输; 还支持面向消息的无连接通信模式(类似 UDP),使用 SOCK_DGRAM 类型创建,以数据报的形式进行消息传递。
使用场景
  • 不同本地进程间协同工作
    • 例如在一个多媒体处理系统中,有一个音频采集进程负责从声卡获取音频数据, 还有一个音频编码进程将采集到的原始音频数据进行编码压缩以便存储或传输。 这两个进程运行在同一台主机上,就可以通过 UNIX Domain Socket 来高效地传递音频数据, 实现紧密协作,满足实时性要求较高的音频处理需求。

MySQL官方文档 中写到

If the host is not specified or is localhost, a connection to the local host occurs:

  • On Windows, the client connects using shared memory, if the server was started with the shared_memory system variable enabled to support shared-memory connections.
  • On Unix, MySQL programs treat the host name localhost specially, in a way that is likely different from what you expect compared to other network-based programs: the client connects using a Unix socket file. The --socket option or the MYSQL_UNIX_PORT environment variable may be used to specify the socket name.

翻译成中文就是:

如果未指定主机或主机为 localhost 时,将发生与本地主机的连接:

  • 在 Windows 上,客户端使用共享内存进行连接,如果 服务器是使用 shared_memory系统 变量以支持共享内存连接。
  • 在 Unix 上,MySQL 程序将主机名 localhost 的特殊性,与其他基于网络的程序相比,其方式可能与您预期的不同: 客户端使用 Unix 套接字文件进行连接。--socket 选项或 MYSQL_UNIX_PORT 环境变量可用于指定套接字名称。

也就是说

  • 数据库服务器与本地应用通信
    • 像 MySQL 数据库服务器,当它运行在本地主机上,并且本地有一些应用程序需要与之交互获取或存储数据时, 就可以使用 UNIX Domain Socket 进行通信。 相比使用网络套接字进行本地回环通信(比如指定 127.0.0.1 地址通信), 使用 UNIX Domain Socket 能减少开销,提升通信速度,优化整体性能。
与网络套接字对比
  • 通信范围
    • 网络套接字(如基于 TCP/IP 的套接字)主要用于不同主机之间通过网络进行通信, 能够跨越局域网、广域网等,让分布在不同物理位置的主机上的进程互相通信; 而 UNIX Domain Socket 仅局限于同一台主机内部的进程间通信,不能用于跨主机通信。
  • 底层实现开销
    • 网络套接字通信时,数据要经过多层网络协议栈处理,从应用层数据往下依次经过传输层、网络层、数据链路层等进行封装, 然后通过物理网络传输到目标主机后再层层解析还原数据;UNIX Domain Socket 因为是本地通信, 在内核空间内直接通过简单的拷贝等操作就能完成数据传递,避免了大量网络相关的复杂处理开销。

在OSI第几层?

UNIX Domain Socket 通常可以被看作是位于传输层

与传输层特点相符之处
  • 提供端到端通信服务

    • 在 OSI 模型中,传输层的主要职责是为不同主机(在网络套接字情况下) 或者同一主机内不同进程(对于 UNIX Domain Socket 这种本地通信机制)之间提供端到端的通信服务。 UNIX Domain Socket 能够在同一台主机上实现进程到进程的通信连接,就如同 TCP 在不同主机间建立可靠连接来传输数据那样, 保障数据可以从一个进程准确地发送到另一个目标进程,起到了类似传输层在网络通信中保障端到端数据传递的作用。
  • 支持不同的通信模式

    • 传输层有面向连接(如 TCP)和面向无连接(如 UDP)等不同的通信协议模式供上层应用根据需求选择。 UNIX Domain Socket 同样支持这两种模式,通过 SOCK_STREAM 可以创建面向连接的字节流通信方式,提供可靠的、有序的数据传输,类似 TCP 使用 SOCK_DGRAM 能创建面向消息的无连接通信模式,类似 UDP 的数据报通信,这与传输层根据不同业务场景提供多样化通信模式的特性相契合。
  • 功能抽象层面相似

    • 以 TCP 为例,它在网络通信中隐藏了网络底层复杂的物理链路、网络拓扑等细节,让上层应用只需要关心发送和接收数据, 不用去管数据具体是怎么通过网线、路由器等物理设备以及 IP 路由等网络层操作来传输的。 UNIX Domain Socket 也是如此,对于同一主机内的进程来说,它隐藏了内核空间内数据交互的底层细节, 比如具体的内存拷贝、内核调度等操作,让进程只聚焦在利用它进行数据的收发, 就像进程在使用传输层服务一样,从功能抽象层面来看处于类似的层级位置。
不同观点
  • 和网络传输层的差异
    • 虽然功能上有相似性,但传统的传输层(如 TCP、UDP)是面向网络通信设计的,要遵循网络相关标准和协议, 涉及到跨主机、跨网络的通信场景,要配合网络层的 IP 地址等进行寻址以及应对复杂的网络环境(如丢包重传策略要考虑网络拥塞等网络相关情况)。 而 UNIX Domain Socket 仅用于本地主机内进程通信,它更多地利用了操作系统内核提供的本地通信机制,不需要进行网络层面的寻址、路由等操作, 其通信范围局限在主机内部,所以从这个角度来说它和面向广泛网络环境的传统传输层协议还是存在一定区别。

不过总体而言,从其主要提供的进程间通信服务、通信模式以及在整个网络通信抽象层次中的定位来看, 将 UNIX Domain Socket 归为类似传输层的位置是比较合理的一种理解方式, 它在本地通信领域扮演着类似网络传输层保障数据可靠或按需传递的角色。

vs TCP

UNIX Domain Socket和TCP在可靠性方面有一定的相似性,但也存在差异,以下是具体的对比分析:

UNIX Domain Socket的可靠性情况
  • 面向连接的字节流模式(SOCK_STREAM类型)下可靠
    • 当使用 SOCK_STREAM 类型创建UNIX Domain Socket时,它提供了类似TCP面向连接的可靠通信机制。 在此模式下,内核会确保数据按照发送的顺序准确无误地传递到接收端进程,并且会进行差错控制,比如检测数据传输过程中是否出现错误, 一旦发现错误会尝试纠正或者通知发送方重发数据,保障数据的完整性和准确性。 例如,在本地的一个文件服务器进程与客户端进程通过这种模式的UNIX Domain Socket通信传输文件内容时, 接收端能够可靠地接收到完整且正确的文件数据,就像通过TCP连接传输文件一样可靠。
  • 面向消息的无连接模式(SOCK_DGRAM类型)下不可靠
    • 若使用 SOCK_DGRAM 类型创建UNIX Domain Socket,其通信方式类似UDP,是面向消息的无连接通信。 在这种模式下,发送方只管发送数据报,不会去确认接收方是否成功接收到了消息,也不保证数据报的顺序与发送顺序一致, 有可能出现数据丢失或者乱序等情况,所以这种模式是不可靠的通信方式。 比如本地的一个简单监控系统中,某个采集进程通过这种无连接模式向显示进程发送一些实时状态数据报, 可能会有部分数据报丢失,但在某些对数据完整性要求不那么高的场景下也能满足需求。
两者可靠性对比总结
  • 相同点
    • 在各自面向连接的通信模式(UNIX Domain Socket的 SOCK_STREAM 模式和TCP)下, 都能保证数据的可靠传输,即保障数据的完整性、准确性以及按顺序传递, 从这个角度来说,在对应可靠通信模式下它们为上层应用提供了类似的可靠通信服务体验。
  • 不同点
    • UNIX Domain Socket主要用于本地主机内进程间通信,其可靠通信模式下的差错控制等机制是在内核空间基于本地环境实现的, 相对来说不用考虑复杂网络环境下的诸多因素(如网络拥塞、不同链路的丢包特性等),实现相对简洁高效; 而TCP是面向网络通信的,要应对各种各样复杂的网络状况,其可靠性保障机制更加复杂和完善,涉及大量针对网络特点的算法和处理流程, 像要通过不同网络节点、适应不同网络带宽和延迟等情况来确保数据可靠传输,通信开销也相对更大一些。

总体而言,UNIX Domain Socket在面向连接模式下具备可靠性特点,和TCP在可靠性功能的实现目标上有共通之处, 但由于应用场景不同,在可靠性保障机制的复杂程度等方面存在差异。

UDS 原理

  1. 服务器首先拿到一个socket结构体,和一个unix域相关的unix_proto_data结构体。
  2. 服务器bind一个文件。对于操作系统来说,就是新建一个文件,然后把文件路径信息存在unix_proto_data中。
  3. listen
  4. 客户端通过同样的文件路径调用connect去连接服务器。这时候客户端的结构体插入服务器的连接队列,等待处理。
  5. 服务器调用accept摘取队列的节点,然后新建一个通信socket进行通信。

unix域通信本质还是基于内存之间的通信,客户端和服务器都维护一块内存,然后实现全双工通信, 而unix域的文件路径,只不过是为了让客户端进程可以找到服务端进程。 而通过connect和accept让客户端和服务器对应的结构体关联起来,后续就可以互相往对方维护的内存里写东西了。就可以实现进程间通信。

Go Code

 

Go

代码解读

复制代码

package main import ( "io" "log" "net" "os" ) func main() { // 定义UDS文件路径 socketPath := "/var/tmp/qqiu" // 删除已存在的同名套接字文件(避免绑定失败) _ = os.Remove(socketPath) // 创建监听套接字 l, err := net.Listen("unix", socketPath) if err != nil { log.Fatal("listen error:", err) } defer l.Close() defer os.Remove(socketPath) log.Println("Server is listening on", socketPath) for { // 接受客户端连接 conn, err := l.Accept() if err != nil { log.Println("accept error:", err) continue } go handleConnection(conn) } } func handleConnection(conn net.Conn) { defer conn.Close() // 创建缓冲区用于读取数据 // 创建一个动态增长的字节切片 var message []byte buffer := make([]byte, 1024) for { n, err := conn.Read(buffer) if err != nil { if err != io.EOF { log.Println("读取错误:", err) } break } // 将读取的数据追加到message中 message = append(message, buffer[:n]...) // 如果读取的数据少于缓冲区大小,说明消息已经读取完毕 if n < len(buffer) { break } } log.Printf("Received from client: %s\n", string(message)) // 向客户端发送响应数据 response := []byte("Message received successfully!") _, err := conn.Write(response) if err != nil { log.Println("write error:", err) return } }

 

Go

代码解读

复制代码

package main import ( "log" "net" ) func main() { socketPath := "/var/tmp/qqiu" // 连接服务端 conn, err := net.Dial("unix", socketPath) if err != nil { log.Fatal("dial error:", err) } defer conn.Close() message := []byte("Hello from client!") _, err = conn.Write(message) if err != nil { log.Println("write error:", err) return } buffer := make([]byte, 1024) n, err := conn.Read(buffer) if err != nil { log.Println("read error:", err) return } log.Printf("Received from server: %s\n", buffer[:n]) }

Java Code

since JDK16

 

Java

代码解读

复制代码

import java.io.IOException; import java.net.StandardProtocolFamily; import java.net.UnixDomainSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; public class UDSServerExample { public static void main(String[] args) { try { // 1. 创建ServerSocketChannel对象,指定使用UNIX Domain Socket ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(StandardProtocolFamily.UNIX); // 2. 配置为非阻塞模式(可选,但在很多实际场景中常用) serverSocketChannel.configureBlocking(false); // 3. 绑定套接字到指定的文件路径(这里就是UDS对应的抽象文件位置) serverSocketChannel.bind(UnixDomainSocketAddress.of("/var/tmp/qqiu")); System.out.println("Server is listening on UNIX Domain Socket..."); while (true) { // 4. 监听客户端连接请求,非阻塞模式下如果没有连接则返回null SocketChannel socketChannel = serverSocketChannel.accept(); if (socketChannel!= null) { // 处理客户端连接 ByteBuffer buffer = ByteBuffer.allocate(1024); // 5. 从客户端读取数据到缓冲区 int bytesRead = socketChannel.read(buffer); if (bytesRead > 0) { buffer.flip(); byte[] data = new byte[bytesRead]; buffer.get(data); System.out.println("Received from client: " + new String(data)); } socketChannel.close(); } } } catch (IOException e) { e.printStackTrace(); } } }

 

Java

代码解读

复制代码

import java.io.IOException; import java.net.UnixDomainSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; public class UDSClientExample { public static void main(String[] args) { try { // 1. 创建SocketChannel对象,指定使用UNIX Domain Socket并连接到服务端 SocketChannel socketChannel = SocketChannel.open(UnixDomainSocketAddress.of("/var/tmp/qqiu")); // 2. 配置为非阻塞模式(可选) socketChannel.configureBlocking(false); ByteBuffer buffer = ByteBuffer.wrap("Hello from client".getBytes()); // 3. 向服务端发送数据 socketChannel.write(buffer); buffer.clear(); // 4. 可以选择接收服务端返回的数据(如果有) int bytesRead = socketChannel.read(buffer); if (bytesRead > 0) { buffer.flip(); byte[] data = new byte[bytesRead]; buffer.get(data); System.out.println("Received from server: " + new String(data)); } socketChannel.close(); } catch (IOException e) { e.printStackTrace(); } } }

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值