First understand what call RPC, why want to RPC, RPC is refers to the remote procedure call (RPC), that is to say two servers A, B, an application deployed on A server, want to call functions/B on the server and application method, because is not A memory space, cannot be called directly, need to express the call through the network of semantic meaning and convey call data.



RPC functional objectives

The primary functional goal of RPC is to make it easier to build distributed computing (applications) without losing the semantic simplicity of local invocation while providing powerful remote invocation capabilities. In order to achieve this goal, RPC framework should provide a transparent call mechanism so that users do not have to explicitly distinguish between local call and remote call. In the previous brief Introduction, an implementation structure is given, which is based on stub structure. Let’s detail the implementation of the Stub structure.

RPC call classification

There are two types of RPC calls:

1. The synchronous call client waits for the call to complete and returns the result. 2. Asynchronous invocation The client does not need to wait for the return of the execution result after the invocation, but can still obtain the return result through callback notification. If the client does not care about the return of the call, it becomes a one-way asynchronous call, and the one-way call does not return the result.

The difference between asynchrony and synchronization is waiting for the server to complete and return the result.

RPC structure disassembling

A coarse-grained conceptual structure of RPC implementation is presented in The Shallow Article. Here we further detail which components it should consist of, as shown in the figure below.



The RPC server exports remote interface methods through RpcServer, and the client imports remote interface methods through RpcClient. The client invokes remote interface methods as if they were local methods. The RPC framework provides a proxy implementation of the interface, and the actual invocation is delegated to the proxy RpcProxy. The proxy encapsulates the call information and passes the call to RpcInvoker for actual execution. RpcInvoker on the client side maintains the RpcChannel with the server side through the RpcConnector, and performs protocol encoding (encode) with RpcProtocol and sends the encoded request message to the server through the channel.

RPC server receiver RpcAcceptor receives call requests from clients and performs protocol decoding (DECODE) using RpcProtocol. The decoded call information is passed to the RpcProcessor to control the call process, and finally the call is delegated to RpcInvoker to actually execute and return the call result.

RPC component Responsibilities

Above we further disassemble the components of the RPC implementation structure. Below we elaborate on the responsibility division of each component.

RpcServer is responsible for the export of remote interfaces. RpcClient is responsible for the proxy implementation of remote interfaces. RpcProxy is responsible for the proxy implementation of remote interfaces. Responsible for encoding the call information and sending the call request to the server and waiting for the call result to return to the server implementation: RpcConnector is responsible for maintaining the connection channel between the client and the server and sending data to the server. 7. RpcAcceptor is responsible for receiving the request from the client and returning the request result 8. RpcProcessor controls the invocation process on the server, including managing the invocation thread pool and timeout time. 9

Analysis of RPC Implementation

After further dismantling the components and dividing responsibilities, this paper takes the implementation of the CONCEPTUAL model of RPC framework in Java platform as an example to analyze the factors that need to be considered in the implementation in detail.

Export Remote Interface

Exporting remote interfaces means that only exported interfaces can be called remotely, while unexported interfaces cannot. A snippet of code to export an interface in Java might look like this:

DemoService demo = new … ; RpcServer server = new … ; server.export(DemoService.class, demo, options);

We can export the entire interface, or we can be more granular and export only certain methods in the interface, for example:

Server.export (demoservice. class, demo, “hi”, new class <? >[] { String.class }, options);

There is also a special call in Java called polymorphism, that is, an interface may have multiple implementations, so which is called when remote calls? The semantics of this local call are implicitly implemented through reference polymorphism provided by the JVM, so cross-process calls cannot be implicitly implemented for RPC. If the previous DemoService interface has two implementations, then we need to specially mark the different implementations when exporting the interface, for example:

DemoService demo = new … ; DemoService demo2 = new … ; RpcServer server = new … ; server.export(DemoService.class, demo, options); server.export(“demo2”, DemoService.class, demo2, options);

Demo2 is another implementation, which we mark as “demo2” to export, so the remote call also needs to pass this flag to call the correct implementation class, thus solving the semantics of polymorphic call.

Import the remote interface and client proxy

Import as opposed to export the remote interface, the client code must obtain the method or procedure definition of the remote interface in order to be able to make calls. At present, most RPC frameworks of cross-language platforms use code Generator to generate stub code according to IDL definition. In this way, the actual import process is completed at compile time through code generator. Some of the cross-language RPC frameworks I have used such as CORBAR, WebService, ICE, and Thrift do this.

Code generation is an inevitable choice for RPC frameworks of cross-language platforms, while RPC of the same language platform can be implemented by sharing interface definitions. A snippet of code to import an interface in Java might look like this:

RpcClient client = new … ; DemoService demo = client.refer(DemoService.class); demo.hi(“how are you?” );

‘import’ is the keyword in Java, so in the code snippet we refer to the import interface. The import approach here is also essentially a code generation technique, but it is generated at run time and looks cleaner than code generation at static compile time. There are at least two techniques available in Java to provide dynamic code generation: JDK dynamic proxies and bytecode generation. Dynamic proxy is more convenient to use than bytecode generation, but it is inferior to direct bytecode generation in terms of performance and bytecode generation in terms of code readability. In my opinion, it is more important to sacrifice some performance for code readability and maintainability.

Protocol codec

The client proxy needs to encode the call information before making the call, which requires consideration of what information needs to be encoded and transmitted to the server in what format for the server to complete the call. For efficiency, it is better to encode as little information as possible (transmit less data) and encode as simple rules as possible (execute efficiently). Let’s first look at what we need to encode:

— Call code — 1. Interface methods include interface name and method name 2. Method parameters include parameter type and parameter value 3. Call attribute includes call attribute information, such as call attachment implicit parameter, call timeout time, etc. — Return code — 1. Return result Return value defined in the interface method 2. Return code Exception return code 3. Return exception information Invoke exception information

In addition to the above necessary call information, we may also need some meta-information to facilitate codec and possible future extensions. So our encoded message is divided into two parts, one is the meta information and the other is the necessary information for the call. When designing an RPC protocol message, we put the meta information in the protocol header and the necessary information in the protocol body. The following is a conceptual RPC protocol message design format:



— Message header — Magic: protocol magic, designed for decoding header size: protocol header length, designed for extension Version: protocol version, designed for compatibility ST: message body serialization type HB: Heartbeat message flag, designed for long-link transport layer heartbeat OW: Status Code: status code of the response message Reserved: reserved for byte alignment Message ID: message ID Body size: The length of the message body — the body of the message — adopts the serialization encoding, which is commonly used in the following formats: XML, such as Webservie SOAP JSON, jSON-RPC binary, such as thrift. hession; Kryo etc.

After the format is determined, the encoding and decoding is simple. Because the header length is certain, we are more concerned about the serialization method of the message body. Serialization we are concerned with three aspects: 1. Serialization and deserialization efficiency, the faster the better. 2. The smaller the serialized byte length, the better. 3. Compatibility of serialization and deserialization, whether the interface parameter object is compatible if a field is added. The above three points sometimes can not have it both ways, which involves the specific serialization library implementation details, will not be further analyzed in this article.

Transport services

After protocol encoding, it is natural to transmit the encoded RPC request message to the server, and the server returns the result message or confirmation message to the client after execution. The application scenario of RPC is essentially a reliable request response message flow, similar to HTTP. Therefore, the LONG-connection TCP protocol is more efficient. Unlike HTTP, we define a unique ID for each message at the protocol level, so it is easier to reuse the connection.

Since long connections are used, the first question is how many connections do you need between the client and server? In fact, there is no difference between single-connection and multi-connection. For applications with a small amount of data transfer, single-connection is sufficient. The biggest difference between single-connection and multi-connection is that each connection has its own private send and receive buffer. Therefore, when a large amount of data is transmitted, it is more efficient to spread the data among different connection buffers. So, if you don’t have enough data transfers to keep the single-connection buffer saturated all the time, then using multiple connections doesn’t yield any noticeable improvement and can add to the overhead of connection management.

The connection is initiated and maintained by the client. If the client and server are directly connected, the connection is not interrupted (except for physical link faults). If the connection between client and server passes through some load forwarding devices, the connection may be interrupted by these intermediate devices when the connection is inactive for a period of time. It is necessary to periodically send heartbeat data to each connection to maintain the connection. Heartbeat messages are internal messages used by RPC framework library. There is also a special heartbeat bit in the preceding protocol header structure, which is used to mark heartbeat messages and is transparent to business applications.

Execution call

All the client stub does is encode the message and transmit it to the server, while the actual invocation takes place on the server. The Server Stub is divided into two components: RpcProcessor and RpcInvoker. One is responsible for controlling the call process and the other is responsible for the actual call. Let’s take Java as an example of what it takes to implement these two components.

Dynamic interface calls that implement code in Java are currently typically called through reflection. In addition to the native JDK reflection, some third-party libraries provide better performance reflection calls, so RpcInvoker encapsulates the implementation details of reflection calls.

What factors should be considered for control of the call process, and what call control services should be provided by RpcProcessor? The following points are put forward to inspire thinking:

1. Efficiency improvement Each request should be executed as soon as possible, so we can’t create threads to execute each request, we need to provide thread pool services. 2. Resource isolation When we export multiple remote interfaces, how to avoid a single interface call occupying all thread resources and causing other interfaces to execute blocking. 3. Timeout Control If an interface is running slowly and the client has timed out and given up waiting, it is meaningless for the server thread to continue executing.

RPC Exception Handling

No matter how much RPC tries to disguise remote calls as local calls, they are still quite different, and there are some exceptions that are never encountered when making local calls. Before we talk about exception handling, let’s compare some differences between local calls and RPC calls: 1. Local calls are always executed, while remote calls are not, and the call message may not be sent to the server for network reasons. 2. Local calls only throw exceptions declared by the interface, while remote calls also throw other exceptions from the RPC framework runtime. 3. The performance difference between local and remote calls can be significant, depending on the proportion of RPC inherent consumption. It is these differences that make RPC more of a consideration. When a remote interface is called to throw an exception, the exception may be a business exception or a runtime exception (such as a network outage) thrown by the RPC framework. A business exception indicates that the server has performed the call, which may not have performed properly for some reason, while an RPC runtime exception indicates that the server may not have performed the call at all, and the exception handling strategy for the caller naturally needs to be distinguished.

Since the inherent consumption of RPC is several orders of magnitude higher than that of local calls, the inherent consumption of local calls is in the nanosecond level, whereas the inherent consumption of RPC is in the millisecond level. It is not appropriate to export the remote interface to be serviced by a separate process for too light a computing task. It is only worthwhile to export the remote interface to be serviced by a separate process if the time spent on the computing task is much higher than the inherent consumption of RPC.

conclusion

So far we have presented a conceptual framework for RPC implementation and analyzed in detail some implementation details that need to be considered. No matter how elegant the concept of RPC is, “there are still several snakes hidden in the grass”, only a deep understanding of the nature of RPC can be better applied.

The last

Welcome to pay attention to my public number [Java small melon brother sharing platform], the article will be updated in it, sorting out the data will be placed in it