Write Java RPC from scratch (01) based on websocket implementation

created at 10-08-2021 views: 3

RPC

problem to be solved

RPC mainly aims to solve two problems:

  • Solve the problem of calling between services in a distributed system.
  • When making a remote call, it must be as convenient as a local call, so that the caller cannot perceive the logic of the remote call.

In this section, let's learn how to implement the simplest rpc call based on websocket, and then we will implement a version based on netty4.

Complete process

Complete process

The Client on the left corresponds to Service A in front, and the Server on the right corresponds to Service B.

Let's explain in detail step by step below.

steps

1)In the application layer code of Service A, the add method of an implementation class of Calculator is called, hoping to perform an addition operation;

2)This Calculator implementation class does not directly implement the calculator's addition, subtraction, multiplication, and division logic internally, but instead obtains the result of the operation by remotely calling the RPC interface of Service B, so it is called Stub;

3)How does Stub establish remote communication with Service B? At this time, you will use the remote communication tool, which is the Run-time Library in the figure. This tool will help you realize the function of remote communication. For example, Java's Socket is such a library. Of course, you can also use Http-based Protocol HttpClient, or other communication tools, can be used, RPC does not specify which protocol you want to use for communication;

4)Stub establishes communication with Service B by calling the method provided by the communication tool, and then sends the request data to Service B. It should be noted that since the underlying network communication is based on a binary format, the data sent by the stub to the communication tool must also be binary, such as calculator.add(1,2), you must put the parameter values ​​1 and 2 To a Request object (of course, this Request object not only contains this information, but also includes other information such as which RPC interface of which service is to be called), and then serialize it into binary, and then pass it to the communication tool class. This will also be in the following code Reflected in realization;

5)The binary data is transmitted to Service B. Of course, Service B also has its own communication tool, which receives binary requests through this communication tool;

6)Since the data is binary, it is natural to deserialize the binary data into the request object, and then hand the request object to the Stub of Service B for processing;

7)Like the previous Service A Stub, the Stub here is also a "fake". It is only responsible for parsing the request object, knowing which RPC interface the caller wants to call, and what are the incoming parameters. Then pass these parameters to the corresponding RPC interface, which is the actual implementation class of Calculator for execution. Obviously, if it is Java, then reflection must be used here.

8)After the RPC interface is executed, the execution result is returned. Now it is Service B's turn to send the data to Service A. How to send it? The same reason, the same process, but now Service B becomes Client and Service A becomes Server: Service B deserialization execution result -> transfer to Service A -> Service A deserialization execution result -> will The result is returned to Application, complete.

Simple implementation

Suppose service A wants to call a method of service B.

Because they are not in the same memory, they cannot be used directly. How can a function similar to Dubbo be achieved?

There is no need to use HTTP level communication, just use TCP protocol.

common

Common modules define common objects.

Rpc constant

public interface RpcConstant {

     /**
      * address
      */
     String ADDRESS = "127.0.0.1";

     /**
      * The port number
      */
     int PORT = 12345;

}

Request for participation

public class RpcCalculateRequest implements Serializable {

     private static final long serialVersionUID = 6420751004355300996L;

     /**
      * Parameter one
      */
     private int one;

     /**
      * Parameter two
      */
     private int two;

     //getter & setter & toString()
}

Service interface

public interface Calculator {

     /**
      * Calculate addition
      * @param one parameter one
      * @param two parameter two
      * @return return result
      */
     int add(int one, int two);

}

server

Implementation of the service interface

public class CalculatorImpl implements Calculator {

    @Override
    public int add(int one, int two) {
        return one + two;
    }

}

start service

public static void main(String[] args) throws IOException {
    Calculator calculator = new CalculatorImpl();
    try (ServerSocket listener = new ServerSocket(RpcConstant.PORT)) {
        System.out.println("Server start:" + RpcConstant.ADDRESS + ":" + RpcConstant.PORT);
        while (true) {
            try (Socket socket = listener.accept()) {
                // Deserialize the request
                ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
                Object object = objectInputStream.readObject();
                System.out.println("Request is: "+ object);
                // call service
                int result = 0;
                if (object instanceof RpcCalculateRequest) {
                    RpcCalculateRequest calculateRpcRequest = (RpcCalculateRequest) object;
                    result = calculator.add(calculateRpcRequest.getOne(), calculateRpcRequest.getTwo());
                }
                // return result
                ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
                objectOutputStream.writeObject(result);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

Start log:

Server start: 127.0.0.1:12345

client

Client call

public static void main(String[] args) {
    Calculator calculator = new CalculatorProxy();
    int result = calculator.add(1, 2);
    System.out.println(result);
}

Computed proxy class

public class CalculatorProxy implements Calculator {

    @Override
    public int add(int one, int two) {
        try {
            Socket socket = new Socket(RpcConstant.ADDRESS, RpcConstant.PORT);

            // serialize the request
            RpcCalculateRequest calculateRpcRequest = new RpcCalculateRequest(one, two);
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());

            // send the request to the service provider
            objectOutputStream.writeObject(calculateRpcRequest);

            // Deserialize the response body
            ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
            Object response = objectInputStream.readObject();

            if (response instanceof Integer) {
                return (Integer) response;
            } else {
                throw new RuntimeException();
            }
        } catch (IOException | ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }
}

Call log

client side

3

server side

Server start: 127.0.0.1:12345
Request is: RpcCalculateRequest{one=1, two=2}
created at:10-08-2021
edited at: 10-08-2021: