Inter-Process Communication Methods in Java – Java 中的进程间通信方法

最后修改: 2023年 12月 10日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.导言

We’ve previously looked at inter-process communication (IPC) and seen some performance comparisons between different methods. In this article, we’re going to look at how we can implement some of these methods in our Java applications.

我们之前了解了进程间通信 (IPC) 并查看了不同方法之间的性能比较在本文中,我们将探讨如何在 Java 应用程序中实现其中的一些方法。

2. What Is Inter-Process Communication?

2. 什么是进程间通信?

Inter-Process Communication, or IPC for short, is a mechanism by which different processes can communicate. This can range from various processes that form the same application, to different processes running on the same computer, and other processes spread across the internet.

进程间通信,简称 IPC,是不同进程进行通信的一种机制。这包括组成同一应用程序的不同进程、在同一台计算机上运行的不同进程以及分布在互联网上的其他进程。

For example, some web browsers run each tab as a different OS process. This is done to keep them isolated from each other but does require a level of IPC between the tab process and the main browser process to keep everything working correctly.

例如,有些网络浏览器将每个标签页作为不同的操作系统进程运行。这样做是为了让它们相互隔离,但确实需要在标签页进程和主浏览器进程之间进行一定程度的 IPC,以保证一切正常运行。

Everything we look at here will be in the form of message passing. Java lacks standard support for shared memory mechanisms, though some third-party libraries can facilitate this. As such, we’ll think about a production process that sends messages to a consumption process.

我们在这里看到的所有内容都将采用消息传递的形式。Java 缺乏对共享内存机制的标准支持,不过一些第三方库可以提供便利。因此,我们将考虑一个向消费进程发送消息的生产进程。

3. File-Based IPC

3. 基于文件的 IPC

The simplest form of IPC that we can achieve in standard Java is simply using files on the local file system. One process can write a file, while the other can read from the same file. Anything that any process does using the file system outside the process boundary can be seen by all other processes on the same computer.

我们可以在标准 Java 中实现的最简单的 IPC 形式就是简单地使用本地文件系统中的文件。一个进程可以写入文件,而另一个进程可以读取同一文件。任何进程在进程边界外使用文件系统所做的任何操作都会被同一台计算机上的所有其他进程看到。

3.1. Shared Files

3.1.共享文件

We can start by having our two processes read and write the same file. Our producing process will write to a file on the file system, and later, our consuming process will read from the same file.

我们可以先让两个进程读取和写入同一个文件。我们的生产进程将写入文件系统中的一个文件,随后,我们的消费进程将从同一个文件中读取。

We do need to be careful that writing to the file and reading from the file don’t overlap. On many computers, file system operations aren’t atomic, so if the writing and reading are happening simultaneously, the consuming process may get corrupt messages. However, if we can guarantee this — for example, using filesystem locking — then shared files are a straightforward way to facilitate IPC.

我们确实需要注意,向文件写入和从文件读出的操作不要重叠。在许多计算机上,文件系统操作不是原子操作,因此如果写入和读取同时进行,消费进程可能会收到损坏的信息。但是,如果我们能保证这一点,例如使用 文件系统锁定,那么共享文件就是促进 IPC 的一种简单方法。

3.2. Shared Directory

3.2.共享目录

A step up from sharing a single, well-known file is to share an entire directory. Our producing application can write a new file into the directory every time it needs to, and our consuming application can detect the presence of a new file and react to it.

从共享单个知名文件更进一步就是共享整个目录。我们的生产应用程序可以在每次需要时向目录中写入新文件,而我们的消费应用程序可以检测到新文件的存在并作出反应。

Java has the WatchService API in NIO2 that we can use for this. Our consuming process can use it to watch our target directory, and whenever it notifies us that a new file has been created, we can react to it:

Java在NIO2中提供了WatchService API,我们可以使用它来实现这一功能。我们的消耗进程可以使用它来监视目标目录,只要它通知我们有新文件创建,我们就可以做出反应:

WatchService watchService = FileSystems.getDefault().newWatchService();

Path path = Paths.get("pathToDir");
path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE);

WatchKey key;
while ((key = watchService.take()) != null) {
    for (WatchEvent<?> event : key.pollEvents()) {
        // React to new file.
    }
    key.reset();
}

Having done this, our producing process needs to create appropriate files in this directory, and the consuming process will detect and process them.

这样,我们的生产进程就需要在该目录中创建适当的文件,而消费进程将检测并处理这些文件。

Remember, though, that most filesystem operations aren’t atomic. We must ensure that the file creation event is only triggered when the file is completely written. This is commonly done by writing the file into a temporary directory and then moving it into the target directory when finished.

但请记住,大多数文件系统操作都不是原子操作。我们必须确保只有在文件完全写入时才会触发文件创建事件。通常的做法是先将文件写入临时目录,完成后再将其移入目标目录。

On most filesystems, a “move file” or “rename file” action is considered atomic as long as it happens within the same filesystem.

在大多数文件系统中,只要 “移动文件 “或 “重命名文件 “操作发生在同一文件系统中,就会被视为原子操作。

3.3. Named Pipes

3.3.命名管道

So far, we’ve used complete files to pass our messages between processes. This requires that the producing process has written the entire file before the consuming process reads it.

到目前为止,我们一直使用完整文件在进程间传递信息。这就要求生产进程在消费进程读取之前写完整个文件。

Named Pipes are a particular type of file we can use here. Named pipes are entries on the file system but don’t have any storage behind them. Instead, they act as a pipeline between writing and reading processes.

命名管道是我们可以在此使用的一种特殊文件类型。命名管道是文件系统中的条目,但背后没有任何存储空间。相反,它们充当了写入和读取进程之间的管道。

We start by having our consuming process open the named pipe for reading. Because this named pipe is presented as a file on the filesystem, we do this using standard file IO mechanisms:

首先,我们让消耗进程打开命名管道进行读取。由于命名管道是作为文件系统上的一个文件呈现的,因此我们使用标准的文件 IO 机制来完成这项工作:

BufferedReader reader = new BufferedReader(new FileReader(file));

String line;
while ((line = reader.readLine()) != null) {
    // Process read line
}

Everything that’s written to this named pipe will then be immediately read by this consuming process. This means that our production process needs to open this file and write it as normal.

写入该命名管道的所有内容都会立即被消费进程读取。这就意味着,我们的生产进程需要打开这个文件,并像往常一样写入。

Unfortunately, we don’t have a mechanism to create these named pipes in Java. Instead, we need to use standard OS commands to create the file system entry before our program can use it. Exactly how we do this varies by operating system. For example, on Linux, we’d use the mkfifo command:

遗憾的是,我们没有在 Java 中创建这些命名管道的机制。相反,我们需要使用标准的操作系统命令来创建文件系统条目,然后我们的程序才能使用它。具体方法因操作系统而异。例如,在 Linux 中,我们将使用 mkfifo 命令:

$ mkfifo /tmp/ipc-namedpipe

And then, we can use /tmp/ipc-namedpipe in our consuming and producing processes.

然后,我们可以在消费和生产流程中使用 /tmp/ipc-namedpipe

4. Network-Based IPC

4. 基于网络的 IPC

Everything we’ve seen has revolved around the two processes sharing the same filesystem. This means that they need to be running on the same computer. However, in some cases, we wish to have our processes communicate with each other regardless of the computer they’re running on.

我们所看到的一切都围绕着两个进程共享同一个文件系统展开。这意味着它们必须运行在同一台计算机上。但是,在某些情况下,我们希望进程之间能够相互通信,而不管它们运行在哪台计算机上。

We can achieve this by using network-based IPC instead. Essentially, this is just running a network server in one process and a network client in another.

我们可以通过使用基于网络的 IPC 来实现这一目标。从本质上讲,这只是在一个进程中运行网络服务器,在另一个进程中运行网络客户端。

4.1. Simple Sockets

4.1.简单套接字

The most obvious example of implementing network-based IPC is to use simple network sockets. We can either use the sockets support in the JDK or rely on libraries such as Netty or Grizzly.

实现基于网络的 IPC 最明显的例子就是使用简单的网络套接字。我们可以使用 JDK 中的 套接字支持,或者依赖 Netty 或 Grizzly 等库。

Our consuming process would run a network server that listens on a known address. It can then handle incoming connections and process messages as any network server would:

我们的消耗进程将运行一个网络服务器,在已知地址上监听。这样,它就可以像任何网络服务器一样处理传入连接和信息:

try (ServerSocket serverSocket = new ServerSocket(1234)) {
    Socket clientSocket = serverSocket.accept();

    PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
    BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

    String line;
    while ((line = in.readLine()) != null) {
        // Process read line
    }
} 

The producing processes can then send network messages to this to facilitate our IPC:

然后,生产进程可以向其发送网络信息,以方便我们的 IPC:

try (Socket clientSocket = new Socket(host, port)) {
    PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
    BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

    out.println(msg);
}

Notably, compared to our file-based IPC, we can more easily send messages in both directions.

值得注意的是,与基于文件的 IPC 相比,我们可以更轻松地双向发送信息。

4.2. JMX

4.2 JMX.

Using network sockets works well enough, but there’s a lot of complexity that we need to manage ourselves. As an alternative, we can also use JMX. This technically still uses network-based IPC, but it abstracts the networking away from us, so we’re working only in terms of the MBeans.

使用网络套接字的效果很好,但我们需要自己管理的复杂性也很多。作为替代方案,我们还可以使用JMX这在技术上仍然使用基于网络的 IPC,但它将网络抽象化了,因此我们只需在 MBeans 中工作。

As before, we’d need a server running on our consuming process. However, this server is now our standard MBeanServer from the JVM rather than anything we do ourselves.

和以前一样,我们需要在消耗进程中运行一个服务器。不过,这个服务器现在是来自 JVM 的标准 MBeanServer 而不是我们自己做的任何事情。

We’d first need to define our MBean itself:

我们首先需要定义 MBean 本身:

public interface IPCTestMBean {
    void sendMessage(String message);
}

class IPCTest implements IPCTestMBean {
    @Override
    public void sendMessage(String message) {
        // Process message
    }
}

Then, we can provide this to the MBeanServer within the JVM:

然后,我们可以将其提供给 JVM 中的 MBeanServer

ObjectName objectName = new ObjectName("com.baeldung.ipc:type=basic,name=test");

MBeanServer server = ManagementFactory.getPlatformMBeanServer();
server.registerMBean(new IPCTest(), objectName);

At this point, we’ve got our consumer ready.

此时,我们已经为消费者做好了准备。

We can then use a JMXConnectorFactory instance to send messages to this server from our producing system:

然后,我们可以使用 JMXConnectorFactory 实例从生产系统向该服务器发送消息:

JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:1234/jmxrmi");
try (JMXConnector jmxc = JMXConnectorFactory.connect(url, null)) {
    ObjectName objectName = new ObjectName("com.baeldung.ipc:type=basic,name=test");

    IPCTestMBean mbeanProxy = JMX.newMBeanProxy(jmxc.getMBeanServerConnection(), objectName, IPCTestMBean.class, true);
    mbeanProxy.sendMessage("Hello");
}

Note that for this to work, we need to run our consumer with some additional JVM arguments to expose JMX on a well-known port:

请注意,要实现这一点,我们需要使用一些额外的 JVM 参数来运行我们的消费者,以便在一个众所周知的端口上公开 JMX

-Dcom.sun.management.jmxremote=true
-Dcom.sun.management.jmxremote.port=1234
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false

We then need to use this within the URL within the client for it to connect to the correct server.

然后,我们需要在客户端的 URL 中使用该 URL,以便连接到正确的服务器。

5. Messaging Infrastructure

5.消息传送基础设施

Everything we’ve seen so far is a relatively simple means of IPC. At a certain point, this stops working as well. For example, it assumes that there is only one process consuming messages — or that the producers know exactly which consumer to talk to.

到目前为止,我们看到的都是相对简单的 IPC 方法。到了一定程度,这种方法也就失效了。例如,它假定只有一个进程在消费消息,或者生产者确切知道要与哪个消费者对话。

If we need to go beyond this, we can integrate with dedicated messaging infrastructure using something like JMS, AMPQ, or Kafka.

如果我们需要更进一步,我们可以使用类似 JMSAMPQKafka的功能与专用消息传递基础架构集成。

Obviously, this is on a much larger scale than we’ve been covering here — this would allow an entire suite of producing and consuming systems to pass messages between each other. However, if we need this kind of scale, then these options do exist.

显然,这比我们在这里讨论的规模要大得多–这将允许一整套生产和消费系统在彼此间传递信息。不过,如果我们需要这样的规模,这些选项确实存在。

6. Conclusion

6.结论

We’ve seen several different means of IPC between processes and how we can implement them ourselves. This has covered a range of scales, from sharing an individual file to an enterprise-level scale.

我们已经了解了进程间 IPC 的几种不同方式,以及我们如何自己实现它们。这涵盖了从共享单个文件到企业级规模的各种规模。

Next time you need to have multiple processes communicating with each other, why not consider some of these options?

下一次,当您需要让多个流程相互通信时,为什么不考虑一下这些选项呢?

As always, all the code from this article can be found over on GitHub.

本文的所有代码都可以在 GitHub 上找到