How do I call someone else’s remote service?

As each service is deployed on different machines, the invocation between services is unavoidable in the process of network communication. Service consumers need to write a pile of code related to network communication for each invocation of a service, which is not only complex but also prone to error. To make network communication details transparent to users, we need to encapsulate communication details. Let’s first look at what communication details are involved in the process of the next RPC call:

  1. A service consumer (client) invocation invokes a service as a local invocation;
  2. After receiving the call, the Client stub is responsible for assembling methods and parameters into a message body that can be transmitted over the network.
  3. The Client stub finds the service address and sends the message to the server.
  4. The Server Stub decodes the message after receiving it.
  5. The Server stub invokes the local service based on the decoding result.
  6. The local service executes and returns the result to the Server stub;
  7. The Server Stub packages the returned result into a message and sends it to the consumer;
  8. The Client stub receives the message and decodes it.
  9. The service consumer gets the end result.

The goal of RPC is to encapsulate steps 2 through 8 and make these details transparent to the user.

1. How to make transparent remote service invocation?

How do you encapsulate the communication details so that a user can invoke a remote service as if it were a local call? For Java, use a proxy! Java proxies are available in two ways: 1) JDK dynamic proxies; 2) Bytecode generation. Although bytecode generation is more powerful and efficient, it is difficult to maintain the code, and most companies still choose dynamic proxy when implementing RPC frameworks. Our simplest version of the dynamic proxy approach is also natural.

2. How to encode and decode messages?

2.1 Determine the message data structure

  1. Interface name: In our example, the interface name is “HelloWorldService”. If we do not pass it, the server does not know which interface to call.
  2. Method names: There may be many methods in an interface. If you do not pass method names, the server will not know which method to call.
  3. Parameter types & Parameter values There are many parameter types, such as bool, int, long, double, string, map, list, and even struct (class) with corresponding parameter values; timeout

2.2 the serialization

From the perspective of RPC, there are three main points: 1) generality, such as whether it can support complex data structures such as Map; 2) Performance, including time complexity and space complexity. Since RPC framework will be used by almost all services of the company, if a little time can be saved in serialization, the benefits of the whole company will be considerable. Similarly, if a little memory can be saved in serialization, the network bandwidth can also be saved. 3) Scalability. For Internet companies, business changes rapidly. If the serialization protocol has good scalability and supports automatic addition of new business fields without affecting old services, it will greatly provide system flexibility. Ours is the simplest version so we use JDK serialization to handle it.

Start coding

  1. Initialization engineering

    First, create two projects: server and Client. The two modules in the server project are rpc-server-API and Rpc-server-provider respectively.

    Why does the Server project create two modules?

    The client needs to know some information about the server when calling the service on the server side. The client can rely on this module. The SDK and contract packages in our project provide this functionality. The real implementation is in the Rpc-server-provider.

  2. rpc-server-api

public interface IHelloService {
    String sayHello(String content);

    String saveUser(User user);
}
Copy the code

Request parameter class

private String className;
private String methodName;
private Object[] parameters;
Copy the code
  1. rpc-server-provider

    First, rpc-server-provider relies on rpc-server-API. We write an implementation class that implements the interface defined in the API.

    public class HelloServiceImpl implements IHelloService{
       
        @Override
        public String sayHello(String content) {
            System.out.println("request in sayHello:"+content);
            return "Say Hello:"+content; }}Copy the code

    So how do I get to remote if I write it this way? We also need to expose the service, and that requires a way to expose the service. This is a process that continues to accept requests, each socket being handed to a processorHandler.

    public class RpcProxyServer {
        ExecutorService executorService = Executors.newCachedThreadPool();
        public void publisher(Object service, int port) {
            ServerSocket serverSocket = null;
            try {
                serverSocket = new ServerSocket(port);
                while (true) {// Keep accepting requests
                    Socket socket = serverSocket.accept();//BIO
                    // Each socket is passed to a processorHandler
                    executorService.execute(newProcessorHandler(socket, service)); }}catch (IOException e) {
                e.printStackTrace();
            } finally {
                if(serverSocket ! =null) {
                    try {
                        serverSocket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
    
        }
    }
    Copy the code

    The code of processorHandler gets the request object from the socket, calls the service method with the request object, returns the result of the method execution, and writes the result to the socket.

    public class ProcessorHandler implements Runnable {
        private Socket socket;
        private Object service;
        
        public ProcessorHandler(Socket socket, Object service) {
            this.socket = socket;
            this.service = service;
        }
    
        @Override
        public void run(a) {
            try (InputStream inputStream = socket.getInputStream();
                 ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
                 ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream())) {
                // What should be in the input stream?
                // Which class is requested, method name, parameters
                RpcRequest rpcRequest = (RpcRequest) objectInputStream.readObject();
                Object result = invoke(rpcRequest); // reflection invokes the local service
                objectOutputStream.writeObject(result);
                objectOutputStream.flush();
            } catch(IOException | ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { e.printStackTrace(); }}private Object invoke(RpcRequest request) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
            // reflection calls
            Object[] args = request.getParameters(); // Get the parameters requested by the clientClass<? >[] types =new Class[args.length]; // Get the type of each argument
            for (int i = 0; i < args.length; i++) {
                types[i] = args[i].getClass();
            }
            Class clazz = Class.forName(request.getClassName()); // Load the requested class
            Method method = clazz.getMethod(request.getMethodName(), types); SayHello, saveUser finds the method in this class
            returnmethod.invoke(service, args); }}Copy the code

    You’re done. Get the service out there.

    /** * Hello world! * * /
    public class App {
        public static void main( String[] args ){
           IHelloService helloService=new HelloServiceImpl();
           RpcProxyServer proxyServer=new RpcProxyServer();
           // Publish to port 8080
           proxyServer.publisher(helloService,8080); }}Copy the code
  2. The client code is open. How can we implement calling remote methods now that we rely on server-side apis (SDK, contract package) on the client side? Similar to a server-side proxy class.

    public class RpcProxyClient {
        public <T> T clientProxy(final Class<T> interfaceCls,final String host,final int port){
            return (T)Proxy.newProxyInstance(interfaceCls.getClassLoader(),
                    newClass<? >[]{interfaceCls},newRemoteInvocationHandler(host,port)); }}Copy the code
    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 {
            // Request data wrapping
            RpcRequest rpcRequest=new RpcRequest();
            rpcRequest.setClassName(method.getDeclaringClass().getName());
            rpcRequest.setMethodName(method.getName());
            rpcRequest.setParameters(args);
            // Remote communication
            RpcNetTransport netTransport=new RpcNetTransport(host,port);
            Object result=netTransport.send(rpcRequest);
    
            returnresult; }}Copy the code

    Classes that handle network traffic

    public class RpcNetTransport {
    
        private String host;
        private int port;
    
        public RpcNetTransport(String host, int port) {
            this.host = host;
            this.port = port;
        }
    
        public Object send(RpcRequest request) {
            Object result = null;
            try (// Establish a connection
                 Socket socket = new Socket(host, port);
                 / / network socket
                 ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream());
                 ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream())) {
                
                outputStream.writeObject(request); // serialize ()
                outputStream.flush();
                result = inputStream.readObject();
            } catch (IOException | ClassNotFoundException e) {
                e.printStackTrace();
            }
            return result;
        }
    Copy the code

    Finally using this remote call:

    public class App {
        public static void main(String[] args) {
            RpcProxyClient rpcProxyClient = new RpcProxyClient();
    
            IHelloService iHelloService = rpcProxyClient.clientProxy(IHelloService.class,"localhost".8080); }}Copy the code

Finally draw a picture to summarize the process