ZooKeeper Java Native RMI implementation Distributed Coordination Case

How to realize "cross virtual machine" call is RMI (Remote Method Invocation). For example, service A runs in JVM1 and service B runs in JVM2. Service A and service B can call each other remotely, just like calling local methods. This is RMI. In distributed systems, we can easily separate service providers and service consumers by using RMI technology, which fully reflects the weak coupling between components, and the system architecture is easier to expand.

We first quickly master the use of RMI through a simplest RMI service and call example, and then point out the limitations of RMI. Finally, the author provides a simple solution to this problem, that is, use ZooKeeper to easily solve the problems involved in the RMI call process.

Let's start with the simplest RMI example!

1 publish RMI service

To release an RMI service, we only need to do three things:

  1. Define an RMI interface: HelloService

  2. Write the implementation class of RMI interface: HelloServiceImpl

  3. Publish RMI services through JNDI

2 define an RMI interface

The RMI interface is actually an ordinary Java interface, except that the RMI interface must inherit java.rmi.Remote. In addition, the method of each RMI interface must declare to throw a java.rmi.RemoteException, as follows:

package com.yqq.zookeeper.rmi.common;

import java.rmi.Remote;
import java.rmi.RemoteException;

/**
 * @Author yqq
 * @Date 2021/10/24 16:20
 * @Version 1.0
 */
public interface HelloService extends Remote {
    String sayHello(String name) throws RemoteException;
}

Inheriting the Remote interface is actually to let the JVM know that the interface needs to be used for Remote calls. The RemoteException is thrown to let the program calling RMI service catch this exception. After all, in the process of Remote call, any strange things will happen (such as network disconnection). It should be noted that RemoteException is a "checked exception", which must be handled by using try... Catch... When calling.
Write the implementation class of RMI interface

3. Write the implementation class of RMI interface

It is a very simple thing to implement the above HelloService, but it should be noted that we must let the implementation class inherit the java.rmi.server.UnicastRemoteObject class. In addition, a constructor must be provided, and the constructor must throw a java.rmi.RemoteException exception. Since we use the RMI framework provided by the JVM, we must implement it according to this requirement, otherwise we will not be able to successfully publish RMI services. In a word: we have to play cards according to the rules!

package com.yqq.zookeeper.rmi.service;

import com.yqq.zookeeper.rmi.common.HelloService;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

/**
 * @Author yqq
 * @Date 2021/10/24 16:25
 * @Version 1.0
 */
public class HelloServiceImpl extends UnicastRemoteObject implements HelloService {
    protected HelloServiceImpl() throws RemoteException {
    }

    @Override
    public String sayHello(String name) throws RemoteException {
        return String.format("Hello %s !",name);
    }
}

In order to meet the requirements of RMI framework, we did do a lot of extra work (inherited UnicastRemoteObject class and threw RemoteException exception), but these work can not stop our determination to publish RMI services! We can easily publish RMI services through the JNDI (Java Naming and Directory Interface) API provided by the JVM.

4. Publish RMI service through JNDI

To publish RMI services, we need to tell JNDI three basic information:

  1. Domain name or IP address (host)

  2. Port number (port)

  3. Service name (service)

They constitute the URL of the RMI protocol (or "RMI address"):

rmi://:/
If we publish RMI services locally, then host is "localhost". In addition, the default port of RMI is "1099". We can also set the value of port by ourselves (as long as it does not conflict with other ports). Service is actually a unique service name based on the same host and port. We might as well use the full Java class name to represent it. In this way, it is easier to ensure the uniqueness of RMI address.
For our example, the RMI address is:

rmi://localhost:1099/com.yqq.zookeeper.rmi.server.HelloServiceImpl

We can publish RMI services by simply providing a main() method, as follows:

package com.yqq.zookeeper.rmi.service;

import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;

/**
 * @Author yqq
 * @Date 2021/10/24 17:31
 * @Version 1.0
 */
public class RmiServer {
    public static void main(String[] args) throws Exception {
        System.out.println("rmi server running");
        //Defines the port used to publish the RMI service
        int port = 1099;
        //Define url
        String url = "rmi://localhost:1099/com.yqq.zookeeper.rmi.service.HelloServiceImpl";
        //Registration service: a registry is created in JNDI
        LocateRegistry.createRegistry(port);
        //Binding service: bind the implementation class object of RMI service to url
        Naming.rebind(url,new HelloServiceImpl());
    }
}

It should be noted that we create a registry in JNDI through the LocateRegistry.createRegistry() method, and only need to provide an RMI port number. In addition, by using Naming.rebind() method to bind RMI address and RMI service implementation class, we use rebind() method here, which is equivalent to calling unbind() and bind() method of Naming successively, but using rebind() method is more pleasant, so we chose it.

Run the main() method, and the RMI service will be published automatically. The rest is to write an RMI client to call the published RMI service.

5 call RMI service

Similarly, we also use a main() method to call RMI service. Compared with publishing, the call will be simpler. We only need to know two things: 1. RMI request path and 2. RMI interface (RMI implementation class must not be required, otherwise it will be called locally). A few lines of code can call the RMI service just published, as follows:

package com.yqq.zookeeper.rmi.client;

import com.yqq.zookeeper.rmi.common.HelloService;

import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;

/**
 * @Author yqq
 * @Date 2021/10/24 18:09
 * @Version 1.0
 */
public class RmiClient {
    public static void main(String[] args) throws RemoteException, NotBoundException, MalformedURLException {
        System.out.println("rmi client running");
        //Define url
        String url = "rmi://localhost:1099/com.yqq.zookeeper.rmi.service.HelloServiceImpl";
        //Find service and return object
        HelloService helloService = (HelloService)Naming.lookup(url);
        //Call method
        String result = helloService.sayHello("James");
        System.out.println("client result:"+result);
    }
}

6 limitations of RMI services

It can be seen that with the help of JNDI, the so-called naming and directory service, we have successfully published and called RMI service. In fact, JNDI is a registry. The server puts the service object into the registry, and the client obtains the service object from the registry. On the server side, we published the RMI service and registered it in JNDI. At this time, we created a Skeleton on the server side. When the client successfully connected to JNDI for the first time and obtained the remote service object, we immediately created a Stub locally. The long-range communication is actually completed through Skeleton and Stub, Data is sent on the "transport layer" based on TCP/IP protocol. There is no doubt that RMI must be faster than WebService in theory. After all, WebService is based on HTTP, and the data carried by HTTP is transmitted through the "application layer". The transport layer is lower than the application layer, and the lower the layer, the faster.

Since RMI is faster and easier to use than WebService, why do we sometimes use WebService?

In fact, the reason is very simple. WebService can realize the call between cross language systems, while RMI can only realize the call between Java systems. In other words, RMI's cross platform performance is not as good as WebService. If our systems are developed in Java, of course, RMI service is the first choice.

It seems that RMI is really excellent. Besides not cross platform, what are the problems?
There are two main limitations:

  1. RMI uses Java's default serialization method. For systems with high performance requirements, other serialization schemes may be used (for example, Protobuf).

  2. RMI service will inevitably fail when running. For example, if RMI service cannot connect, the client will not respond.

In general, the default serialization method of Java is enough to meet our requirements. If the performance is not a problem, the second point we need to solve is to make the system HA (High Availability).

Added by hiberphoptik on Sun, 24 Oct 2021 12:31:45 +0300