RPC remote procedure call can be said to be the basis of distributed system. This paper will demonstrate what happened to an ordinary RPC call through Java.
Before writing the code, suppose we have a commodity class:
public class Product implements Serializable { private Integer id; private String name; public Product(Integer id, String name) { this.id = id; this.name = name; } //toString() //get set method }
There is a commodity service interface:
public interface IProductService { Product getProductById(Integer id); }
The service end has the implementation class of commodity service interface:
public class ProductServiceImpl implements IProductService { @Override public Product getProductById(Integer id) { //In fact, you should query the database to obtain data, which is simplified below return new Product(id, "mobile phone"); } }
Next, we send a commodity id to the server through the client. After the server obtains the id, it obtains the commodity information through the commodity service class and returns it to the client.
public class Client { public static void main(String[] args) throws Exception { //establish Socket Socket socket = new Socket("127.0.0.1", 8888); //Get output stream ByteArrayOutputStream baos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(baos); //Put goods Id It is transmitted to the server through the network dos.writeInt(123); socket.getOutputStream().write(baos.toByteArray()); socket.getOutputStream().flush(); //Read the commodity information returned by the server DataInputStream dis = new DataInputStream(socket.getInputStream()); Integer id = dis.readInt(); //commodity id String name = dis.readUTF(); //Trade name Product product = new Product(id, name);//Generate products through the product information returned by the server System.out.println(product); //Close the stream resource. For the convenience of reading, it is not done try-catch handle dos.close(); baos.close(); socket.close(); } } public class Server { private static boolean running = true; public static void main(String[] args) throws Exception { //Establish server Socket ServerSocket ss = new ServerSocket(8888); //Continuously listen and process client requests while (running) { Socket socket = ss.accept(); process(socket); socket.close(); } ss.close(); } private static void process(Socket socket) throws Exception { InputStream is = socket.getInputStream(); OutputStream os = socket.getOutputStream(); DataInputStream dis = new DataInputStream(is); DataOutputStream dos = new DataOutputStream(os); //Read from the client id Integer id = dis.readInt(); //Call service class to generate goods IProductService service = new ProductServiceImpl(); Product product = service.getProductById(id); //Write the product information back to the client dos.writeInt(id); dos.writeUTF(product.getName()); dos.flush(); dos.close(); dis.close(); os.close(); is.close(); } }
The above is the original simple version of RPC remote call. You can see that the networking code is written dead in the client. The network code is coupled with getProductById(). If you want to change other methods to remote call, you have to write the networking code again, which is very troublesome.
In actual use, various remote calls will be written. For example, the IProductService interface may be extended as follows:
public interface IProductService { Product getProductById(Integer id); Product getProductByName(String name); Product getMostExpensiveProduct(); }
We can't write a piece of network connection code for each method. We have to think of a way to embed a common network connection code for all methods. How should it be embedded? Here we can use the proxy mode. In Java, many excellent frameworks use proxy mode for code embedding, such as Mybatis. It embeds the code of JDBC connection part around the sql statement through the proxy mode, so that we can focus on writing sql. First, the code of the server needs to be modified. Since multiple methods share a set of networked code, we need to identify which method is called on the server.
public class Server { private static boolean running = true; public static void main(String[] args) throws Exception { //...... } private static void process(Socket socket) throws Exception { //Get input stream, output stream InputStream is = socket.getInputStream(); OutputStream os = socket.getOutputStream(); ObjectInputStream ois = new ObjectInputStream(is); ObjectOutputStream oos = new ObjectOutputStream(os); //Get the method name of this remote call String methodName = ois.readUTF(); //Get the parameter type of this remote call method Class[] parameterTypes = (Class[]) ois.readObject(); //Get the specific parameter object Object[] args = (Object[]) ois.readObject(); //Create an instance of the commodity service class (you can continue to optimize here in the future) IProductService service = new ProductServiceImpl(); //Call the corresponding method according to the remotely obtained method name and parameters Method method = service.getClass().getMethod(methodName, parameterTypes); Product product = (Product) method.invoke(service, args); //Write the results back to the client oos.writeObject(product); oos.close(); ois.close(); socket.close(); } }
Then on the client side, we create a new proxy class and provide a getStub method to get the proxy class. The dynamic proxy using JDK requires three parameters, one is the class loader, one is the class class of the interface, and the last is the InvocationHandler instance. The logic behind JDK dynamic proxy is as follows: the JVM will dynamically create a proxy class object according to the class class of the interface. This proxy object implements the incoming interface, that is, it has the implementation of all methods in the interface. The specific implementation of the method can be specified by the user, that is, calling the invoke method of InvocationHandler. In the invoke method, there are three parameters: proxy proxy class, method called by method and args called by method. We can enhance the specific implementation method in the invoke method. In this case, we make a network call.
public class Stub { public static IProductService getStub() { InvocationHandler h = new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //And server establishment Socket connect Socket socket = new Socket("127.0.0.1", 8888); ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream()); //Get the method name of the remote call String methodName = method.getName(); //Get the parameter type of the remote call method Class[] parametersTypes = method.getParameterTypes(); //Pass the method name to the server oos.writeUTF(methodName); //Pass the method parameter type to the server oos.writeObject(parametersTypes); //Pass the method parameters to the server oos.writeObject(args); oos.flush(); //Gets the return result of the remote call ObjectInputStream ois = new ObjectInputStream(socket.getInputStream()); Product product = (Product) ois.readObject(); ois.close(); oos.close(); socket.close(); return product; } }; Object o = Proxy.newProxyInstance(IProductService.class.getClassLoader(), new Class[]{IProductService.class}, h); return (IProductService) o; } }
This new version is better than the first version, but it can still be optimized. Now our agent can only return the implementation class of IProductService. We have to find a way to make it return any type of Service implementation class. The idea is similar to that of calling methods remotely. When calling methods remotely, we pass the method name, parameter type and parameters to the server; Now to dynamically create a Service class, we can pass the name of the Service interface to the server. After the server gets the name of the remote interface, it can find the corresponding Service implementation class from the Service registry. As for how to register the Service implementation class to the Service registry, here is an idea: you can consider using spring annotation injection. This is similar to the spring code we usually write. After creating the Service implementation class, we will add the annotation @ Service, so that we can traverse the Bean using @ Service and find the corresponding implementation class after receiving the remote call.
summary
If you want to build a simple RPC framework, you need to solve the following four points:
- Agency problem
- Service instantiation problem
- Serialization problem
- Communication problems
This paper focuses on the first two, the agent problem and the service instantiation problem. Let's review it below:
- Firstly, the problem of networking of each remote method is solved through the agent mode
- The service instantiation problem is solved by passing the method name, method parameter type, parameter, and the directly passed interface name mentioned later. The principle behind this is reflection, and passing these parameters is also for reflection.