so eazy uses Netty and dynamic proxy to implement a simple RPC with one click

Keywords: Java Programming Back-end Programmer architecture

RPC (remote procedure call) remote procedure call

RPC is used to communicate between the Java processes of two hosts in distributed applications. When host A calls the methods of host B, the process is simple, just like calling the methods in its own process.
The responsibility of RPC framework is to encapsulate the details of the underlying call. As long as the client calls the method, it can obtain the response of the service provider, which is convenient for developers to write code.
The bottom layer of RPC uses TCP protocol, server and client and point-to-point communication.

effect

In the application scenario of RPC, the client calls the code of the server

The client needs to have a corresponding api interface to send the method name, method parameter type, specific parameters, etc. to the server

The server needs to have a specific implementation of the method. After receiving the request from the client, it calls the corresponding method according to the information and returns the response to the client

Flow chart demonstration

code implementation

First, the client should know the interface of the server, then encapsulate a request object and send it to the server

To call a method, you need to have: method name, method parameter type, specific parameters, and class name of the execution method

View Code

The response (method call result) returned by the server to the client is also encapsulated with an object

View Code

  • If you need to return each response to the corresponding request in a multi-threaded call, you can add an ID for identification

To transmit objects over the network, you need to serialize them first. jackson tool is used here

<dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.11.4</version>
</dependency>

View Code

  • In the process of deserialization, you need to specify the type to be converted. The server receives the request and the client receives the response. The two types are different, so you need to specify the type in subsequent transmission

After having the data to be transmitted, use Netty to start the network service for transmission

Server

Bind the port number and open the connection

public class ServerNetty {

    public static void connect(int port) throws InterruptedException {

        EventLoopGroup workGroup = new NioEventLoopGroup();
        EventLoopGroup bossGroup = new NioEventLoopGroup();

        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.channel(NioServerSocketChannel.class)
                .group(bossGroup,workGroup)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        /**
                         * Add the data processor of the custom protocol and specify the received data type
                         * Add server processor
                         */
                        ch.pipeline().addLast(new NettyProtocolHandler(RpcRequest.class));

                        ch.pipeline().addLast(new ServerHandler());
                    }
                });

        bootstrap.bind(port).sync();
    }
}

Two data processors are bound in Netty

One is the data processor. The server receives a request - > calls a method - > returns a response. These processes are executed in the data processor

public class ServerHandler extends SimpleChannelInboundHandler {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {

        RpcRequest rpcRequest = (RpcRequest)msg;

        // Get the parameters required to use reflection
        String methodName = rpcRequest.getMethodName();
        Class[] paramTypes = rpcRequest.getParamType();
        Object[] args = rpcRequest.getArgs();
        String className = rpcRequest.getClassName();

        //Get object from registry container
        Object object = Server.hashMap.get(className);

        Method method = object.getClass().getMethod(methodName,paramTypes);
        //Reflection call method
       String result = (String) method.invoke(object,args);


        // Encapsulate the response results and send them back
        RpcResponse rpcResponse = new RpcResponse();
        rpcResponse.setCode(200);
        rpcResponse.setResult(result);

        ctx.writeAndFlush(rpcResponse);
    }
}
  • Here, there is a pre operation to get objects from the hash table: put the objects that may be called remotely into the container and wait for use

One is a custom TCP protocol processor. In order to solve the common problems of TCP: packet sticking and unpacking caused by the size mismatch between the data packet sent by the client and the data buffer received by the server.

/**
 * Custom TCP protocol for network transmission
 * When sending: add two magic numbers to the transmitted byte stream as the header, then calculate the length of the data, add the data length to the header, and finally the data
 * When receiving: after two magic numbers are identified, the next one is the header. Finally, the byte array corresponding to the length is used to receive data
 */
public class NettyProtocolHandler extends ChannelDuplexHandler {

    private static final byte[] MAGIC = new byte[]{0x15,0x66};

    private Class decodeType;

    public NettyProtocolHandler() {
    }

    public NettyProtocolHandler(Class decodeType){
        this.decodeType = decodeType;
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        ByteBuf in = (ByteBuf) msg;
        //Receive response object
        Object dstObject;

        byte[] header = new byte[2];
        in.readBytes(header);

        byte[] lenByte = new byte[4];
        in.readBytes(lenByte);

        int len = ByteUtils.Bytes2Int_BE(lenByte);

        byte[] object = new byte[len];
        in.readBytes(object);

        dstObject = JsonSerialization.deserialize(object, decodeType);
        //Give it to the next data processor
        ctx.fireChannelRead(dstObject);

    }

    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {

        ByteBuf byteBuf = Unpooled.buffer();

        //Write magic number
        byteBuf.writeBytes(MAGIC);

        byte[] object = JsonSerialization.serialize(msg);

        //The data length is converted to a byte array and written
        int len = object.length;

        byte[] bodyLen = ByteUtils.int2bytes(len);

        byteBuf.writeBytes(bodyLen);

        //Write object
        byteBuf.writeBytes(object);

        ctx.writeAndFlush(byteBuf);
    }
}
  • The data processor is used by both the server and the client, which is equivalent to a protocol that both parties have agreed to abide by for data transmission
  • The object is serialized and deserialized here, so the deserialization type is specified in this processor
  • To send the length of data, you need a tool to convert integer type to byte type

Conversion data tool class

View Code

client

Encapsulate Netty's operation, and finally return a CHANLE type to send data

public class ClientNetty {

    public static Channel connect(String host,int port) throws InterruptedException {

        InetSocketAddress address = new InetSocketAddress(host,port);

        EventLoopGroup workGroup = new NioEventLoopGroup();

        Bootstrap bootstrap = new Bootstrap();
            bootstrap.channel(NioSocketChannel.class)
                    .group(workGroup)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {

                            //Custom protocol handler (the client receives a response)
                            ch.pipeline().addLast(new NettyProtocolHandler(RpcResponse.class));
                            //Processing data handler
                            ch.pipeline().addLast(new ClientHandler());
                        }
                    });

            Channel channel = bootstrap.connect(address).sync().channel();

            return channel;
    }
}

The data processor is responsible for receiving the response and putting the response results into future, which will be used in subsequent dynamic agents

public class ClientHandler extends SimpleChannelInboundHandler {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {

        RpcResponse rpcResponse = (RpcResponse) msg;

        //The normal return code of the server is 200
        if(rpcResponse.getCode() != 200){
            throw new Exception();
        }

        //Put the results into the future
        RPCInvocationHandler.future.complete(rpcResponse.getResult());
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        super.exceptionCaught(ctx, cause);
    }
}

To make the client call the remote method like calling the local method, it needs a proxy object for the client to call, and let the proxy object call the implementation of the server.

Proxy object construction

public class ProxyFactory {

    public static Object getProxy(Class<?>[] interfaces){

        return Proxy.newProxyInstance(ProxyFactory.class.getClassLoader(),
                interfaces,
                new RPCInvocationHandler());
    }
}

Method execution of client proxy object

After sending the request to the server, it will be blocked until there are results in the future.

public class RPCInvocationHandler implements InvocationHandler {


    static public CompletableFuture future;
    static Channel channel;

    static {
        future = new CompletableFuture();
        //Turn on netty network service
        try {
            channel = ClientNetty.connect("127.0.0.1",8989);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        RpcRequest rpcRequest = new RpcRequest();

        rpcRequest.setArgs(args);
        rpcRequest.setMethodName(method.getName());
        rpcRequest.setParamType(method.getParameterTypes());
        rpcRequest.setClassName(method.getDeclaringClass().getSimpleName());

       channel.writeAndFlush(rpcRequest);
        //A blocking operation, waiting for the result of network transmission
       String result = (String) future.get();

        return result;
    }
}
  • Here, static is used to modify future and CHANLE, without considering that the client connects multiple servers and makes multiple remote calls
  • You can use a hash table to store chanles corresponding to different servers. You can get them from the hash table every time you call
  • The hash table is used to store the future corresponding to different request s, and the result of each response corresponds to it

client

The interface you need to have to make a remote call

public interface OrderService {

    public String buy();
}

Pre operation and test code

public class Client {

    static OrderService orderService;

    public static void main(String[] args) throws InterruptedException {

        //Create a proxy object for the class making the remote call
        orderService = (OrderService) ProxyFactory.getProxy(new Class[]{OrderService.class});

        String result = orderService.buy();

        System.out.println(result);
    }
}

Server

To accept remote calls, you need to have specific implementation classes

public class OrderImpl implements OrderService {

    public OrderImpl() {
    }

    @Override
    public String buy() {
        System.out.println("call buy method");
        return "call buy Method success";
    }
}

Pre operation and test code

public class Server {

   public static HashMap<String ,Object> hashMap = new HashMap<>();

    public static void main(String[] args) throws InterruptedException {
        //Turn on netty network service
        ServerNetty.connect(8989);

        //Register the services to be opened in the hash table in advance
        hashMap.put("OrderService",new OrderImpl());

    }
}

results of enforcement

Posted by kylecooper on Mon, 08 Nov 2021 17:30:31 -0800