Handwritten RPC - crude version

Posted by tomwhalen on Fri, 07 Jan 2022 01:52:45 +0100

preface

Recently, I was accidentally isolated, had a holiday to think about it, and decided to start in the handwritten sequence. This sequence had an idea when looking at the source code of Nacous and gateway, but it has not been implemented. Take advantage of isolation.

Introduction to essential knowledge

Serializable and Deserialize

Serialization is the process of transforming the state information of an object into a form that can be stored or transmitted, that is, the process of transforming an object into a byte sequence is called object serialization;

Deserialization is the reverse process of serialization. The process of deserializing byte array into object and restoring byte sequence into object becomes the deserialization of object;

In Java, Java object serialization is provided through JDK to realize object serialization transmission, mainly through the output stream Java io. Objectoutputstream and object input stream Java io. Objectinputstream;

java.io.ObjectOutputStream: represents the object output stream. Its writeObject(Object obj) method can serialize the obj object specified by the parameter and write the resulting byte sequence to a target output stream;

java.io.ObjectInputStream: represents the object input stream. Its readObject() method reads byte sequences from the source input stream, deserializes them into an object, and returns them;

It should be noted that the serialized object needs to implement Java io. Serializable interface. Java's serialization mechanism verifies the version consistency by judging the serialVersionUID of the class. During deserialization, the JVM will compare the serialVersionUID in the transmitted byte stream with the serialVersionUID of the corresponding local entity class. If they are the same, they are considered to be consistent and can be deserialized. Otherwise, an exception with inconsistent serialization version will occur, that is, InvalidCastException.

Another thing to note is the transient keyword. The attributes modified by transient will not be serialized. If you rewrite writeobject and readobject, they can be serialized again. The case in JDK is that the array decorated with Object [] in ArryList uses transient joint words to ensure that there is no waste in the transmission process and only useful values are transmitted. The essence is to call writeobject and readobject through reflection.

What is Socket communication

Socket originally means "socket". In the field of computer communication, socket is translated as "socket". It is a convention or a way of communication between computers. Through socket, a computer can receive data from other computers or send data to other computers.

Introduction to RPC principle

What is RPC

The so-called RPC is actually generated for the communication between two processes of different hosts. Usually, the process communication between different hosts needs to consider the function of network communication in programming, so the programming will become complex. RPC is used to solve this problem. When a process on one host initiates a request for a process on another host, the kernel will transfer the request to the RPC client. After the RPC client encapsulates the message, it will be transferred to the RPC server of the target host. The RPC server will parse the message, return the original normal request and transfer it to the target process on the target host. In our opinion, it is like two processes communicating on the same host without realizing that they are on different hosts. Therefore, RPC can also be regarded as a protocol or programming framework to simplify the writing of distributed programs.

RPC basic process

  1. Rpc Client finds the method of the specific calling class through the dynamic agent through the incoming IP, port number, calling class and method parameters, serializes the requested class and method, and transmits them to the server;

  2. After the Rpc Service receives the request, it deserializes the incoming classes and methods, finds the methods of the corresponding classes through reflection, and finally serializes the returned results and returns them to the client;

  3. After receiving the return value, the Rpc Client deserializes and finally displays the results;

Hand RPC

From the basic process of RPC, we can see that the two main areas that can improve RPC performance are serialization tools and communication framework. In our whole manual series, we will promote the components step by step to high-performance components, from blocking IO to NIO, from the original JDK serialization framework to the current five flower gate serialization framework, From manually creating objects to automatically creating objects in Spring and introducing the registry, the whole process will be accompanied by knowledge introduction. Let's work together.

take the first step

In the first step, we only support the remote call of one class, using the serialization and deserialization tools carried by JDK and the way of blocking connection.

The overall project structure is divided into three parts. RPC Api is provided as Api. RPC common mainly provides public encapsulation for client and service calls. Rpc-v1 includes rpc-v1-client, which is mainly client call encapsulation. rpc-v1-service is implemented as Api and exposes corresponding methods. I will add a new version for each change in the future, which will facilitate novice learning.

Service end

The implementation of the server uses ServerSocket to listen to a port and receive connection requests circularly. If a request is sent, a thread will be created to process the call in a new thread. The core classes are RpcProxyService and ProcessorHandler. The implementation is as follows:

RpcProxyService
@Slf4j
public class RpcProxyService {

    private ExecutorService threadPool;

    public RpcProxyService() {
        int corePoolSize = 5;
        int maximumPoolSize = 200;
        long keepAliveTime = 60;
        BlockingQueue<Runnable> workingQueue = new ArrayBlockingQueue<>(100);
        ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("socket-pool-").build();
        threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, workingQueue, threadFactory);
    }
    
    /**
     * Expose the method and start listening immediately after the service is registered
     *
     * @param service
     * @param port
     */
    public void register(Object service, int port) {

        try (ServerSocket serverSocket = new ServerSocket(port);) {
            Socket socket;
            while ((socket = serverSocket.accept()) != null) {
                log.info("Client connection IP Is:" + socket.getInetAddress());
                threadPool.execute(new ProcessorHandler(socket, service));
            }
        } catch (IOException e) {
            log.error("Connection exception", e);
        }
    }
}
ProcessorHandler
@Slf4j
public class ProcessorHandler implements Runnable {

    private Socket socket;

    private Object service;

    public ProcessorHandler(Socket socket, Object service) {
        this.service = service;
        this.socket = socket;
    }

    @Override
    public void run() {

        try (ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream())) {
            ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream());
            //Read parameters from input stream
            RpcRequest rpcRequest = (RpcRequest) inputStream.readObject();
            //Get method by reflection
            Method method = service.getClass().getMethod(rpcRequest.getMethodName(), rpcRequest.getParamTypes());
            //Execution method
            Object result = method.invoke(service, rpcRequest.getParameters());
            outputStream.writeObject(RpcResponse.ok(result));
            outputStream.flush();
        } catch (IOException | ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException exception) {
            //Packaging can be performed again here to classify the abnormalities
            log.error("An error occurred while calling", exception);
        }

    }
}

Client side

The Client side generates the proxy object through RpcClientProxy dynamic proxy (JDK dynamic proxy), and then determines the specific class and method to call by executing the invoke of RemoteInvocationHandler, that is, build the RpcRequest object, and finally initiate the remote call through RpcClient.

RpcClientProxy
public class RpcClientProxy {

    public <T> T getProxy(Class<T> interfaceClass, String host, int port) {
        return (T) Proxy.newProxyInstance(interfaceClass.getClassLoader(),
                new Class<?>[]{interfaceClass},
                new RemoteInvocationHandler(host, port));
    }
}
RemoteInvocationHandler
public class RemoteInvocationHandler implements InvocationHandler {

    private String host;

    private int port;

    public RemoteInvocationHandler(String host, int port) {
        this.host = host;
        this.port = port;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //Construct request parameters
        RpcRequest rpcRequest = RpcRequest.builder()
                .interfaceName(method.getDeclaringClass().getName())
                .methodName(method.getName())
                .parameters(args)
                .paramTypes(method.getParameterTypes())
                .build();
        //Send request
        RpcClient rpcClient = new RpcClient();
        return ((RpcResponse) rpcClient.send(rpcRequest, host, port)).getData();
    }
}
RpcClient
@Slf4j
public class RpcClient {

    public Object send(RpcRequest rpcRequest, String host, int port) {
        try (Socket socket = new Socket(host, port)) {
            ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream());
            ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream());
            //serialize
            outputStream.writeObject(rpcRequest);
            outputStream.flush();
            return inputStream.readObject();
        } catch (IOException | ClassNotFoundException e) {
            log.error("An exception occurred while calling", e);
            return null;
        }
    }
}

Common end

At present, the Common end is encapsulated with input and output parameters, and the code is as follows:

RpcRequest
@Data
@Builder
public class RpcRequest implements Serializable {

    /**
     * Interface name
     */
    private String interfaceName;

    /**
     * Method name
     */
    private String methodName;

    /**
     * parameter
     */
    private Object[] parameters;

    /**
     * Parameter type
     */
    private Class<?>[] paramTypes;

}
RpcResponse
@Data
public class RpcResponse<T> implements Serializable {

    /**
     * Status code
     */
    private Integer code;

    /**
     * Reminder information
     */
    private String message;

    /**
     * Return information
     */
    private T data;


    public static <T> RpcResponse<T> ok(T data) {
        RpcResponse<T> rpcResponse = new RpcResponse<>();
        rpcResponse.setCode(ResponseCode.SUCCESS.getCode());
        rpcResponse.setData(data);
        rpcResponse.setMessage(rpcResponse.getMessage());
        return rpcResponse;
    }

    public static <T> RpcResponse<T> error(int code, String message) {
        RpcResponse<T> rpcResponse = new RpcResponse<>();
        rpcResponse.setCode(code);
        rpcResponse.setMessage(message);
        return rpcResponse;
    }

}

I have uploaded the whole code github , for beginners, you must conduct joint debugging to understand the overall RPC process.

Welcome to pay attention and praise!

Topics: Java rpc Network Protocol