Java16的Unix域Socket教程 - nipafx


Java 16的套接字Socket/服务器套接字通道API可以使用Unix域套接字在同一主机上进行更快,更安全的进程间通信。
Java的SocketChannelServerSocketChannelAPI提供对套接字的阻塞和多路复用非阻塞访问。在Java 16之前,它仅限于TCP / IP套接字Socket,现在也可以访问Unix域套接字Socket了。
与TCP / IP回送连接相比,Unix域套接字具有一些优点:

  • 因为它们只能用于同一主机上的通信,所以打开它们而不是使用TCP / IP套接字不会冒接受远程连接的风险。
  • 访问控制与基于文件的机制一起应用,这些机制由操作系统进行了详细,易于理解和实施。
  • Unix域套接字比TCP / IP环回连接具有更快的建立时间和更高的数据吞吐量。

请注意,只要您在共享卷上创建套接字,您甚至可以使用Unix域套接字在同一系统上的容器之间进行通信。
Unix域套接字由文件系统路径名寻址,您可以将它们用于同一主机上的进程间通信。Unix域套接字在基于Unix的操作系统(Linux,MacOS)和Windows 10和Windows Server 2019上受支持(尽管其名称为Windows 10和Windows Server 2019)。
这是有关如何使用API​​的快速教程。
 
访问Unix域套接字
如前所述,Unix域套接字基于路径名,因此我们需要的第一件事是可以将其转换为套接字地址的路径。这可以是任何路径,但是为了确保我们具有必需的权限:
Path socketFile = Path
    .of(System.getProperty("user.home"))
    .resolve(
"server.socket");
UnixDomainSocketAddress address =
    UnixDomainSocketAddress.of(socketFile);

下一步是在该地址上启动服务器和客户端。要创建它们,我们需要将新方法传递给相应的静态工厂方法

// server
ServerSocketChannel serverChannel = ServerSocketChannel
    .open(StandardProtocolFamily.UNIX);
serverChannel.bind(address);

// client
SocketChannel channel = SocketChannel
    .open(StandardProtocolFamily.UNIX);

我们可以发送消息之前的第三步也是最后一步是客户端和服务器相互连接:

// server
SocketChannel channel = serverChannel.accept();

// client
channel.connect(address);

完整客服代码:

// in Server.java
public static void main(String[] args)
         throws IOException, InterruptedException {
    Path socketFile = Path
        .of(System.getProperty(
"user.home"))
        .resolve(
"server.socket");
    
// in case the file is left over from the last run,
    
// this makes the demo more robust
    Files.deleteIfExists(socketFile);
    UnixDomainSocketAddress address =
        UnixDomainSocketAddress.of(socketFile);

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

    System.out.println(
"[INFO] Waiting for client to connect...");
    SocketChannel channel = serverChannel.accept();
    System.out.println(
"[INFO] Client connected");

    
// start receiving messages
}

// in Client.java
public static void main(String[] args)
         throws IOException, InterruptedException {
    Path socketFile = Path
        .of(System.getProperty(
"user.home"))
        .resolve(
"server.socket");
    UnixDomainSocketAddress address =
        UnixDomainSocketAddress.of(socketFile);

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

    
// start receiving messages
}

 
传递消息
服务器和客户端都可以发送和接收消息,但是为了简单起见,我们将要从客户端发送到服务器。
为了使客户端发送一条消息,我们需要创建一个ByteBuffer,用消息的字节填充它,翻转它以进行发送,然后写入通道:

// in Client.java
private static void writeMessageToSocket(
        SocketChannel socketChannel, String message)
        throws IOException {
    ByteBuffer buffer= ByteBuffer.allocate(1024);
    buffer.clear();
    buffer.put(message.getBytes());
    buffer.flip();
    while(buffer.hasRemaining()) {
        socketChannel.write(buffer);
    }
}

现在,我们使用此方法向服务器发送一些消息:
// in Client.java
public static void main(String[] args)
        throws IOException, InterruptedException {

    
// as above

    Thread.sleep(3_000);
    writeMessageToSocket(channel,
"Hello");
    Thread.sleep(1_000);
    writeMessageToSocket(channel,
"Unix domain sockets");
}

在接收端,我们执行类似的步骤,但相反:从通道读取,翻转字节,将其转换为消息:

// in Server.java
private static Optional<String> readMessageFromSocket(
        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);
}
// in Server.java
public static void main(String[] args)
        throws IOException, InterruptedException {

    
// as above

    while (true) {
        readMessageFromSocket(channel)
            .ifPresent(System.out::println);
        Thread.sleep(100);
    }
}

这将创建一个无限循环,该循环每100毫秒检查一次是否有新消息写入套接字,如果有,则将其输出。这意味着服务器现在将无限期运行,直到您通过按CTRL-C在终端中将其关闭为止。
如果像以前一样按顺序启动服务器和客户端,现在您将看到客户端发送的消息已由服务器打印到输出中。
 
现实生活中的复杂性
上面提供的代码只是介绍了如何通过套接字实现通信的表面。您会注意到,始终始终需要首先启动服务器,它只能接受一个连接,一旦客户端放弃该连接,它就永远无法创建新的连接(尝试启动多次),并且该服务器正在运行无限期,直到被迫关闭。它不会进行清理(例如删除创建的文件),而客户端也不会进行清理(通过关闭连接)。
 
总结
在本教程中,我们使用套接字通道API在具有Unix域套接字的同一主机上建立进程间通信,这些套接字已添加到Java 16中预先存在的API中。新的代码路径可归结为:
  • 创建一个 UnixDomainSocketAddress
  • 创建ServerSocketChannel并SocketChannel与StandardProtocolFamily.UNIX
  • 绑定服务器并将客户端连接到该地址

Unix域套接字比TCP / IP环回连接更安全,更高效,并且在所有基于Unix的操作系统以及现代Windows版本上均受支持。
如果您想深入探讨该主题,请查看Michael McMahon在Inside Java上的文章