Basic RPC implementation based on Java

RPC remote procedure call can be said to be the basis of distributed system. This paper will demonstrate what happened to an ordinary RPC call through Java.
Before writing the code, suppose we have a commodity class:
public class Product implements Serializable {

    private Integer id;
    private String name;

    public Product(Integer id, String name) {
        this.id = id;
        this.name = name;
    }

    //toString()
    
    //get set method
There is a commodity service interface:
public interface IProductService {

    Product getProductById(Integer id);
} 
The service end has the implementation class of commodity service interface:
public class ProductServiceImpl implements IProductService {
    @Override
    public Product getProductById(Integer id) {
        //In fact, you should query the database to obtain data, which is simplified below
        return new Product(id, "mobile phone");
    }
} 
Next, we send a commodity id to the server through the client. After the server obtains the id, it obtains the commodity information through the commodity service class and returns it to the client.
public class Client {

    public static void main(String[] args) throws Exception {
        //establish Socket
        Socket socket = new Socket("127.0.0.1", 8888);
        //Get output stream
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(baos);
        //Put goods Id It is transmitted to the server through the network
        dos.writeInt(123);

        socket.getOutputStream().write(baos.toByteArray());
        socket.getOutputStream().flush();

        //Read the commodity information returned by the server
        DataInputStream dis = new DataInputStream(socket.getInputStream());
        Integer id = dis.readInt();     //commodity id
        String name = dis.readUTF();    //Trade name
        Product product = new Product(id, name);//Generate products through the product information returned by the server

        System.out.println(product);
        
        //Close the stream resource. For the convenience of reading, it is not done try-catch handle
        dos.close();
        baos.close();
        socket.close();
    }
}

public class Server {
    private static boolean running = true;

    public static void main(String[] args) throws Exception {
        //Establish server Socket
        ServerSocket ss = new ServerSocket(8888);
        //Continuously listen and process client requests
        while (running) {
            Socket socket = ss.accept();
            process(socket);
            socket.close();
        }
        ss.close();
    }

    private static void process(Socket socket) throws Exception {
        InputStream is = socket.getInputStream();
        OutputStream os = socket.getOutputStream();
        DataInputStream dis = new DataInputStream(is);
        DataOutputStream dos = new DataOutputStream(os);

        //Read from the client id
        Integer id = dis.readInt();
        //Call service class to generate goods
        IProductService service = new ProductServiceImpl();
        Product product = service.getProductById(id);
        //Write the product information back to the client
        dos.writeInt(id);
        dos.writeUTF(product.getName());
        dos.flush();

        dos.close();
        dis.close();
        os.close();
        is.close();
    }
} 
The above is the original simple version of RPC remote call. You can see that the networking code is written dead in the client. The network code is coupled with getProductById(). If you want to change other methods to remote call, you have to write the networking code again, which is very troublesome.
In actual use, various remote calls will be written. For example, the IProductService interface may be extended as follows:
public interface IProductService {

    Product getProductById(Integer id);
    
    Product getProductByName(String name);
    
    Product getMostExpensiveProduct();
} 
We can't write a piece of network connection code for each method. We have to think of a way to embed a common network connection code for all methods. How should it be embedded? Here we can use the proxy mode. In Java, many excellent frameworks use proxy mode for code embedding, such as Mybatis. It embeds the code of JDBC connection part around the sql statement through the proxy mode, so that we can focus on writing sql. First, the code of the server needs to be modified. Since multiple methods share a set of networked code, we need to identify which method is called on the server.
public class Server {

    private static boolean running = true;

    public static void main(String[] args) throws Exception {
        //......
    }

    private static void process(Socket socket) throws Exception {
        //Get input stream, output stream
        InputStream is = socket.getInputStream();
        OutputStream os = socket.getOutputStream();
        ObjectInputStream ois = new ObjectInputStream(is);
        ObjectOutputStream oos = new ObjectOutputStream(os);
        //Get the method name of this remote call
        String methodName = ois.readUTF();
        //Get the parameter type of this remote call method
        Class[] parameterTypes = (Class[]) ois.readObject();
        //Get the specific parameter object
        Object[] args = (Object[]) ois.readObject();
        
        //Create an instance of the commodity service class (you can continue to optimize here in the future)
        IProductService service = new ProductServiceImpl();
        //Call the corresponding method according to the remotely obtained method name and parameters
        Method method = service.getClass().getMethod(methodName, parameterTypes);
        Product product = (Product) method.invoke(service, args);
        //Write the results back to the client
        oos.writeObject(product);

        oos.close();
        ois.close();
        socket.close();
    }
} 
Then on the client side, we create a new proxy class and provide a getStub method to get the proxy class. The dynamic proxy using JDK requires three parameters, one is the class loader, one is the class class of the interface, and the last is the InvocationHandler instance. The logic behind JDK dynamic proxy is as follows: the JVM will dynamically create a proxy class object according to the class class of the interface. This proxy object implements the incoming interface, that is, it has the implementation of all methods in the interface. The specific implementation of the method can be specified by the user, that is, calling the invoke method of InvocationHandler. In the invoke method, there are three parameters: proxy proxy class, method called by method and args called by method. We can enhance the specific implementation method in the invoke method. In this case, we make a network call.
public class Stub {

    public static IProductService getStub() {

        InvocationHandler h = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                //And server establishment Socket connect
                Socket socket = new Socket("127.0.0.1", 8888);
                ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
                //Get the method name of the remote call
                String methodName = method.getName();
                //Get the parameter type of the remote call method
                Class[] parametersTypes = method.getParameterTypes();
                //Pass the method name to the server
                oos.writeUTF(methodName);
                //Pass the method parameter type to the server
                oos.writeObject(parametersTypes);
                //Pass the method parameters to the server
                oos.writeObject(args);
                oos.flush();
                //Gets the return result of the remote call
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
                Product product = (Product) ois.readObject();

                ois.close();
                oos.close();
                socket.close();
                return product;
            }
        };
        Object o = Proxy.newProxyInstance(IProductService.class.getClassLoader(), new Class[]{IProductService.class}, h);
        return (IProductService) o;
    }
} 
This new version is better than the first version, but it can still be optimized. Now our agent can only return the implementation class of IProductService. We have to find a way to make it return any type of Service implementation class. The idea is similar to that of calling methods remotely. When calling methods remotely, we pass the method name, parameter type and parameters to the server; Now to dynamically create a Service class, we can pass the name of the Service interface to the server. After the server gets the name of the remote interface, it can find the corresponding Service implementation class from the Service registry. As for how to register the Service implementation class to the Service registry, here is an idea: you can consider using spring annotation injection. This is similar to the spring code we usually write. After creating the Service implementation class, we will add the annotation @ Service, so that we can traverse the Bean using @ Service and find the corresponding implementation class after receiving the remote call.  

summary

If you want to build a simple RPC framework, you need to solve the following four points:
  1. Agency problem
  2. Service instantiation problem
  3. Serialization problem
  4. Communication problems
This paper focuses on the first two, the agent problem and the service instantiation problem. Let's review it below:
  1. Firstly, the problem of networking of each remote method is solved through the agent mode
  2. The service instantiation problem is solved by passing the method name, method parameter type, parameter, and the directly passed interface name mentioned later. The principle behind this is reflection, and passing these parameters is also for reflection.

Posted on Thu, 02 Dec 2021 23:27:28 -0500 by boushley