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()); } }