preface:
Before learning Dubbo, let's use the currently known knowledge points (non RPC framework) to create the simplest RPC call.
In our development work, the most commonly used CS architecture is called HTTP. The browser initiates a HTTP call. Our server receives the request and returns the response.
There has always been a question haunting the author. Since there is such a simple way as HTTP call, why do we have to develop so many RCP frameworks?
You can think about it. Let's cut directly into the text to create the simplest RCP call
1. Preparation
* We prepare an interface and create its implementation class.
* The server exposes a port for receiving external requests
* The client initiates a connection to the corresponding port of the server. After the connection is successful, it initiates a call. The main content of the call is the implementation class created above, and passes the called class, specific methods and parameters to the server
* After receiving the request, the server finds the corresponding implementation class, executes the corresponding method, and returns the result to the client after execution
2. Code development
2.1 interface and implementation class
// Interface public interface StudentService { String study(String book); } // Implementation class public class UniversityStudentService implements StudentService { @Override public String study(String book) { return "universityStudent study " + book; } }
2.2 server implementation
public class ServerExport { /** Specific exposed ip port */ private String ip; private int port; public ServerExport(String ip, int port) { this.ip = ip; this.port = port; } /** Processing requests through a thread pool */ private ExecutorService executor = Executors.newFixedThreadPool(8); /** * Exposed port * @throws IOException */ public void export() throws IOException { ServerSocket serverSocket = new ServerSocket(); serverSocket.bind(new InetSocketAddress(ip, port)); while (true) { Socket socket = serverSocket.accept(); // After receiving the connection, it is encapsulated as a Task and handed over to the executor for processing executor.submit(new Task(socket)); } } private static class Task implements Runnable { private Socket socket; public Task(Socket socket) { this.socket = socket; } @Override public void run() { ObjectInputStream input = null; ObjectOutputStream output = null; try { // Get data sent by client input = new ObjectInputStream(socket.getInputStream()); // Class name String interfaceName = input.readUTF(); // Method name String methodName = input.readUTF(); // Specific parameter types and parameter values Class<?>[] paramterTypes = (Class<?>[])input.readObject(); Object[] paramters = (Object[])input.readObject(); Class<?> interfaceClass = Class.forName(interfaceName); Method method = interfaceClass.getMethod(methodName, paramterTypes); // Invoke through reflection Object result = method.invoke(interfaceClass.newInstance(), paramters); // Respond the result value to the client output = new ObjectOutputStream(socket.getOutputStream()); output.writeObject(result); } catch (IOException | ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) { e.printStackTrace(); } finally { if (null != output) { try { output.close(); } catch (IOException e) { e.printStackTrace(); } } if (null != input) { try { input.close(); } catch (IOException e) { e.printStackTrace(); } } if (null != socket) { try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } } } }
The code is not complicated. When the client wants to call specific classes and methods, it will directly transfer these basic information to the server. After receiving them one by one, the server will assemble them and call specific methods through reflection.
2.3 client implementation
public class SimpleClientImporter { public static void main(String[] args) { Socket socket = null; ObjectOutputStream output = null; ObjectInputStream input = null; try { // Connect server socket = new Socket(); socket.connect(new InetSocketAddress("localhost", 20889)); output = new ObjectOutputStream(socket.getOutputStream()); // Send call class name Class serviceClass = UniversityStudentService.class; output.writeUTF(serviceClass.getName()); // Send call method name output.writeUTF("study"); // Send the parameter type of the called method Class<?>[] paramType = new Class[1]; paramType[0] = String.class; output.writeObject(paramType); // Send the specific parameter value of the called method Object[] arg = new Object[1]; arg[0] = "math"; output.writeObject(arg); input = new ObjectInputStream(socket.getInputStream()); System.out.println(input.readObject()); } catch (Exception e) { e.printStackTrace(); } } }
After the client obtains the connection to the server, it directly operates on the ObjectOutputStream and passes all the parameters required for this call.
In this way, the server can initiate specific calls according to our customized call classes, methods, parameters and other information.
Summary: a simple RCP call is so simple. It seems that the meaning is not enough. It is really so simple to complete a simple RCP framework.
What about the specific performance? Um...
Needless to say, the server side processes requests based on traditional BIO, and the performance must be terrible;
The serialization method of the transmission object is the JDK's own serialization method. The bytes generated after serialization are also large, and the inherent serialization performance is not high.
Next, the author uses Netty to transform the calling mode of BIO
3. Rewrite RCP framework based on Netty
Before rewriting, we first change the way of passing in the class name and method name one by one. We encapsulate all parameter information into an object so that the server can understand all call information after receiving this object.
3.1 create transmission object
public class ServiceInvokeRequest implements Serializable { private static final long serialVersionUID = -349675930021881135L; private String serviceName; private String methodName; private Class<?>[] requestParamType; private Object[] args; // Omit the get set method }
3.2 server creation
public class NettyServerExport { private static int port = 20889; public static void start(){ ServerBootstrap bootstrap = new ServerBootstrap(); EventLoopGroup boss = new NioEventLoopGroup(); EventLoopGroup worker = new NioEventLoopGroup(); try { // Bootstrap basic properties bootstrap.group(boss, worker); bootstrap.channel(NioServerSocketChannel.class); bootstrap.option(ChannelOption.SO_BACKLOG, 1024); bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true); bootstrap.childOption(ChannelOption.TCP_NODELAY, true); bootstrap.childHandler(new ChannelInitializer<Channel>() { @Override protected void initChannel(Channel ch) throws Exception { // Set object codec Handler ch.pipeline().addLast(new ObjectEncoder()); ch.pipeline().addLast(new ObjectDecoder(Integer.MAX_VALUE, ClassResolvers.cacheDisabled(null))); // The most important thing is the custom Handler processing ch.pipeline().addLast(new RpcObjectServerHandler()); } }); ChannelFuture channelFuture = bootstrap.bind(port); System.out.println("server start"); channelFuture.channel().closeFuture().sync(); }catch (Exception e){ e.printStackTrace(); }finally { boss.shutdownGracefully(); worker.shutdownGracefully(); } } public static void main(String[] args) { start(); } }
3.2.1 custom RpcObjectServerHandler
public class RpcObjectServerHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { System.out.println(msg); // After reading the request from the client, it parses the corresponding parameters and initiates the call through reflection if (msg instanceof ServiceInvokeRequest) { ServiceInvokeRequest request = (ServiceInvokeRequest) msg; String interfaceName = request.getServiceName(); String methodName = request.getMethodName(); Class<?>[] paramterTypes = (Class<?>[]) request.getRequestParamType(); Object[] paramters = (Object[]) request.getArgs(); Class<?> interfaceClass = Class.forName(interfaceName); Method method = interfaceClass.getMethod(methodName, paramterTypes); // Finally, specific methods are called through reflection Object result = method.invoke(interfaceClass.newInstance(), paramters); // After the call is completed, the result value is returned to the client ctx.writeAndFlush(result); } } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { super.channelInactive(ctx); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { System.out.println(cause.getMessage()); ctx.close(); } }
After the transformation based on Netty, the client's request will be processed by ObjectDecoder to convert the specific byte array into Object;
Subsequently, a customized RpcObjectServerHandler will be used. For objects of ServiceInvokeRequest type, the specific parameters will be parsed, and the call will be initiated through reflection. Finally, the result value will be returned to the client through the ctx.writeAndFlush() method.
3.3 client creation
public class NettyClientImport<T> { public static void connect() { Bootstrap bootstrap = new Bootstrap(); EventLoopGroup worker = new NioEventLoopGroup(); try { bootstrap.group(worker); bootstrap.channel(NioSocketChannel.class); bootstrap.handler(new ChannelInitializer<Channel>() { @Override protected void initChannel(Channel ch) throws Exception { ch.pipeline().addLast(new ObjectEncoder()); ch.pipeline().addLast(new ObjectDecoder(Integer.MAX_VALUE, ClassResolvers.cacheDisabled(null))); // Using a custom RpcObjectClientHandler ch.pipeline().addLast(new RpcObjectClientHandler()); } }); ChannelFuture channelFuture = bootstrap.connect("localhost", 20889).sync(); channelFuture.channel().closeFuture().sync(); } catch (Exception e) { e.printStackTrace(); } finally { worker.shutdownGracefully(); } } public static void main(String[] args) { connect(); } }
3.3.1 RpcObjectClientHandler
public class RpcObjectClientHandler extends ChannelInboundHandlerAdapter { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { super.channelActive(ctx); // After the connection is established successfully, the request is sent // The client splices the specific information of the class, method and parameter to be called to ServiceInvokeRequest ServiceInvokeRequest request = new ServiceInvokeRequest(); Class c = UniversityStudentService.class; request.setServiceName(c.getName()); request.setMethodName("study"); Class<?>[] paramType = new Class[1]; paramType[0] = String.class; request.setRequestParamType(paramType); Object[] args = new Object[1]; args[0] = "math"; request.setArgs(args); // Finally, send the request to the server ctx.writeAndFlush(request); } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { super.channelInactive(ctx); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { // The response result of the server is received here System.out.println(msg); ctx.close(); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { System.out.println(cause.getMessage()); ctx.close(); } }
Summary: it is relatively simple for the client to initiate a call. We process it in channelActive(). After the client connects to the server, it initiates a call by splicing the request object
The final response result is obtained through the channelRead() method.
Through the above Netty transformation, it seems that the performance of our server has been improved, but the serialization method still uses the JDK native method (the author will not continue to transform, and interested partners can transform by themselves).
Is this a complete RCP framework?
Of course not. This is just an introduction. We still have a lot to do. What else? Look at the next chapter.
Reference: principle and practice of distributed service framework