概述

在这个教程中,我们将学习如何使用Unix域套接字通道。我们将探讨一些理论基础知识、优缺点,并构建一个简单的Java客户端-服务器应用,使用Unix域套接字通道交换文本消息。我们还将研究如何利用Unix域套接字与数据库连接。

1. Unix域套接字通道

传统的进程间通信涉及基于TCP/IP的套接字,由IP地址和端口号定义,用于互联网或私有网络上的通信。相比之下,Unix域套接字仅限于同一物理主机上进程之间的通信。几十年来,它们一直是Unix操作系统的一部分,但最近已被添加到Microsoft Windows中,因此不再局限于Unix系统。

Unix域套接字通过文件系统路径名进行标识,看起来与其他文件名相似,例如*/folder/socketC:\folder\socket。与TCP/IP连接相比,它们具有更快的设置时间、更高的数据传输速率,且接受远程连接时没有安全风险。然而,主要缺点是只限于单个物理主机的通信。

请注意,只要我们在共享卷上创建套接字,我们甚至可以在同一系统上的容器之间使用Unix域套接字进行通信。

2. 套接字配置

由于Unix域套接字基于文件系统路径名,首先我们需要为套接字文件定义一个路径,并将其转换为UnixDomainSocketAddress

Path socketPath = Path
  .of(System.getProperty("user.home"))
  .resolve("baeldung.socket");
UnixDomainSocketAddress socketAddress = UnixDomainSocketAddress.of(socketPath);

在示例中,我们在用户的主目录下创建名为baeldung.socket的套接字文件。

需要记住的是,在每次关闭服务器后,我们需要删除套接字文件:

Files.deleteIfExists(socketPath);

不幸的是,它不会自动删除,我们无法再使用它进行后续连接。尝试使用相同的路径将导致异常,提示该地址已使用:

java.net.BindException: Address already in use

3. 接收消息

接下来,我们可以启动一个服务器,从套接字通道接收消息。

首先,我们需要创建一个具有Unix协议的服务器套接字通道:

ServerSocketChannel serverChannel = ServerSocketChannel
  .open(StandardProtocolFamily.UNIX);

然后,我们需要将其绑定到先前创建的套接字地址:

serverChannel.bind(socketAddress);

现在我们可以等待第一个客户端连接:

SocketChannel channel = serverChannel.accept();

当客户端连接时,消息将以字节缓冲的形式接收。为了处理输入并打印每条消息到控制台,我们需要建立一个无限循环:

while (true) {
    readSocketMessage(channel)
      .ifPresent(message -> System.out.printf("[Client message] %s", message));
    Thread.sleep(100);
}

在上述示例中,readSocketMessage方法负责将套接字通道的缓冲区转换为String

private Optional<String> readSocketMessage(SocketChannel channel) throws IOException {
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    int bytesRead = channel.read(buffer);
    if (bytesRead < 0)
        return Optional.empty();

    byte[] bytes = new byte[bytesRead];
    buffer.flip();
    buffer.get(bytes);
    String message = new String(bytes);
    return Optional.of(message);
}

需要注意的是,服务器必须先于客户端启动。如示例所示,它只能接受一个客户端连接。

4. 发送消息

发送消息比接收消息稍微简单一些。

我们只需要设置一个具有Unix协议的套接字通道,并将其连接到我们的套接字地址:

SocketChannel channel = SocketChannel
  .open(StandardProtocolFamily.UNIX);
channel.connect(socketAddress);

现在我们可以准备一个文本消息:

String message = "Hello from Baeldung Unix domain socket article";

将其转换为字节缓冲:

ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.clear();
buffer.put(message.getBytes());
buffer.flip();

然后将整个数据写入套接字:

while (buffer.hasRemaining()) {
    channel.write(buffer);
}

最后,服务器日志将显示以下输出:

[Client message] Hello from Baeldung Unix domain socket article!

5. 连接到数据库

Unix域套接字可用于连接数据库。许多流行发行版,如MongoDB或PostgreSQL,都提供了默认配置,可以直接使用。

例如,MongoDB会在/tmp/mongodb-27017.sock创建一个Unix域套接字,我们可以在创建MongoClient连接时直接使用:

MongoClient mongoClient = new MongoClient("/tmp/mongodb-27017.sock");

一个要求是向项目添加jnr.unixsocket依赖:

<dependency>
    <groupId>com.github.jnr</groupId>
    <artifactId>jnr-unixsocket</artifactId>
    <version>0.38.13</version>
</dependency>

另一方面,PostgreSQL允许我们使用Unix域套接字遵循JDBC标准。因此,我们在创建连接时只需提供一个额外的socketFactory参数:

String dbUrl = "jdbc:postgresql://databaseName?socketFactory=org.newsclub.net.unix.
  AFUNIXSocketFactory$FactoryArg&socketFactoryArg=/var/run/postgresql/.s.PGSQL.5432";
Connection connection = DriverManager
  .getConnection(dbUrl, "dbUsername", "dbPassword")

socketFactory参数应指向一个扩展java.net.SocketFactory的类。这个类将负责创建Unix域套接字,而不是TCP/IP套接字。

在示例中,我们使用了来自junixsocket库AFUNIXSocketFactory类:

<dependency>
  <groupId>com.kohlschutter.junixsocket</groupId>
  <artifactId>junixsocket-core</artifactId>
  <version>2.4.0</version>
</dependency>

6. 总结

在这篇教程中,我们了解了如何使用Unix域套接字通道。我们涵盖了使用Unix域套接字发送和接收消息的方法,以及如何使用Unix域套接字连接数据库。如往常一样,所有源代码可在GitHub上找到。


« 上一篇: Java Weekly, 第414期