preface
During the interview, you are often asked RPC related questions, such as: what do you say about the principle of RPC implementation, what should you consider to implement an RPC framework, what is the process of initiating a request based on the RPC framework, and so on. So this time I will summarize the relevant knowledge points of a wave of RPC and explain it in advance. This article is only to answer some interview questions, so it is only to explain the principle and will not dig into the details.
Registration Center
RPC (Remote Procedure Call) is translated into Chinese as $\ color{red} {Remote Procedure Call} $. The role of RPC framework is to realize that when calling remote methods, it can be the same as calling local methods, so that developers can focus more on business development without considering the details of network programming.
How can RPC framework be implemented so that developers do not pay attention to details such as network programming?
First, we distinguish between two roles: a service provider and a service caller. The service caller actually executes the corresponding method on the service provider's machine through dynamic proxy, load balancing, network call and other mechanisms. After the service provider completes the method execution, the execution result is transmitted back to the service provider through the network.
The general process is as follows:
But now the services are deployed in clusters, so how can the service caller know the changes in the service provider's cluster in real time, such as the IP address of the service provider has changed, or how can the traffic be switched in time when the service is restarted?
This requires $\ color{red} {registry} $to work. We can regard the registry as a server, and then each service as a client. Each client needs to register itself in the registry. Then, when a service caller wants to call another service, it needs to obtain the information of the service provider from the registry, It mainly obtains the server IP address list and port information of the service provider.
After obtaining these information, the service caller caches it locally and maintains a long connection with the registry. When there is any change in the service provider, the registry can notify the service caller in real time, and the caller can update its locally cached information in time (regular polling can also be used).
After obtaining the server IP address information, the service caller selects an IP address according to its own load balancing policy, and then initiates the request for network call.
So what is the network call initiated by the network client?
You can use JDK's native BIO live NIO to implement a set of network communication modules, but here we recommend directly using the powerful network communication framework Netty. It is a network communication framework based on NIO, which supports high concurrency, perfect encapsulation, good performance and fast transmission.
Netty is not the main content of this article, so we won't talk about it here.
Client call procedure
Because we know that data is transmitted in binary form in the network, it needs to be serialized when the caller passes the called parameters. The service provider also needs to deserialize when receiving parameters.
Network protocol
Since the caller needs to serialize, the service provider needs to deserialize, so both parties should determine a protocol. What parameters the caller transmits, the service provider will parse according to this protocol, and the result will be parsed according to this protocol when returning the result.
So what is the structure of this Agreement and what is it like?
Because this protocol can be customized, we give an example in JSON for convenience:
{ "interfaces": "interface=com.jimoer.rpc.test.producer.TestService;method=printTest;parameter=com.jiomer.rpc.test.producer.TestArgs", "requestId": "3", "parameter": { "com.jiomer.rpc.test.producer.TestArgs": { "age": 20, "name": "Jimoer" } } }
First, the first parameter interfaces is to let the service provider know which interface the caller wants to call, which method in the interface, and what type of method parameters are.
The second parameter is a unique id of the current request. When multiple threads request a method at the same time, this id is used to distinguish. In the future, this id can be used as the basis for link tracking or log management.
The third parameter is the parameter value in the actual calling method. What is the specific type and what is the value of each attribute.
call
The following is also a simple example to illustrate the calling process. We use part of the code and part of the text to string the whole calling process.
// Define the URL of the request String tcpURL = "tcp://testProducer/TestServiceImpl"; // Define interface request TestService testService = ProxyFactory.create(TestService.class, tcpURL); // Assembly request parameters TestArgs testArgs = new TestArgs(20,"Jimoer"); // Request execution via dynamic proxy String result = testService.printTest(testArgs);
By looking at the above code, we can see that the core of the whole calling process is in the ProxyFactory.create() method. The main process in this method is to dynamically generate the actual proxy object of the interface, and then use the interface of Netty to initiate network requests.
Proxy.newProxyInstance(getClass().getClassLoader(), interfaces.getClass().getInterfaces(), new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // Step 1: get the address list of the calling service ListregistryInfos = interfacesMethodRegistryList.get(clazz); if (registryInfos == null) { throw new RuntimeException("The service provider could not be found"); } // Step 2: select an address through its own load balancing strategy RegistryInfo registryInfo = loadBalancer.choose(registryInfos); // Step 3: Netty's network request processing ChannelHandlerContext ctx = channels.get(registryInfo); // Step 4: generate a unique identifier according to the full pathname and method of the interface class String identify = InvokeUtils.buildInterfaceMethodIdentify(clazz, method); String requestId; // Step 5: ensure the uniqueness of the generated requestId by locking synchronized (ApplicationContext.this) { requestIdWorker.increment(); requestId = String.valueOf(requestIdWorker.longValue()); } // Step 6: organization parameters JSONObject jsonObject = new JSONObject(); jsonObject.put("interfaces", identify); jsonObject.put("parameter", param); jsonObject.put("requestId", requestId); System.out.println("Send to server JSON Is:" + jsonObject.toJSONString()); // $$separator between multiple messages String msg = jsonObject.toJSONString() + "$$"; ByteBuf byteBuf = Unpooled.buffer(msg.getBytes().length); byteBuf.writeBytes(msg.getBytes()); // Step 7: call here ctx.writeAndFlush(byteBuf); // Here, the thread will be blocked until the service provider processes the request, returns the result, and then wakes up. waitForResult(); return result; } });
The execution process is roughly divided into these steps:
- Gets the address list of the calling service.
- Select an address through its own load balancing strategy.
- Netty's network request processing (select a Channel).
- The unique identification is generated according to the full pathname and method of the interface class.
- Ensure the uniqueness of the generated requestId by locking.
- Organization request parameters.
- Initiate the call.
- The thread blocks until the service provider returns a result.
- Fill in the return result and return it to the caller.
Server processing
As mentioned above, after the service caller initiates a network request, it will block until the service provider returns data. Therefore, after the service provider processes the logic of the calling method, it still needs to wake up the blocked calling thread.
When processing the request, the service provider first obtains the data through Netty, then deserializes it, then obtains the method to be called according to the protocol, and then calls it through reflection.
The return entry of Netty is in the following logic
@Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { try { String message = (String) msg; if (messageCallback != null) { // Put the received message into the callback method messageCallback.onMessage(message); } } finally { ReferenceCountUtil.release(msg); } }
After receiving the response message, Netty's client first returns the result to the caller, and then releases the previous blocked calling thread after processing.
client.setMessageCallback(message -> { // Here, the message returned by the acquiring server is pushed into the queue first RpcResponse response = JSONObject.parseObject(message, RpcResponse.class); System.out.println("Received a response:" + response); String interfaceMethodIdentify = response.getInterfaceMethodIdentify(); String requestId = response.getRequestId(); // Set unique identification String key = interfaceMethodIdentify + "#" + requestId; Invoker invoker = inProgressInvoker.remove(key); // Set the result to the proxy object invoker.setResult(response.getResult()); // Blocked threads before locking and releasing. synchronized (ApplicationContext.this) { ApplicationContext.this.notifyAll(); } });
setResult() method
@Override public void setResult(String result) { synchronized (this) { this.result = JSONObject.parseObject(result, returnType); notifyAll(); } }
The above steps are like this. Put the unique ID of the previous request into the returned information, then set the result to the proxy object, return the result, and then wake up the previous call blocking thread.
summary
In fact, the whole RPC request process is as follows (excluding asynchronous calls):
Make a summary and describe an RPC request process in Vernacular:
First, both the caller and the service provider should register in the registry;
- The service caller serializes the request parameter object into binary data, generates the proxy object through the dynamic proxy, selects an address of the service provider pulled from the registry using Netty through the proxy object, and then initiates the network request.
- The service provider receives binary data from the TCP channel, deserializes the binary data according to the defined RPC network protocol, divides the interface address and parameter object, and then finds the interface through reflection to execute the call.
- Then, the service provider serializes the call execution results and returns them to the TCP channel.
- After the service caller obtains the response binary data, it is de sequenced into a result object.
In this way, an RPC network call is completed. In fact, after the later framework is extended, the functions of current limiting, fusing, service degradation, serialization diversity extension, service monitoring, link tracking and so on should be considered. These will be expanded later. That's it this time.