MicroServices are already a technology that every Internet developer must master. The RPC framework is one of the most important components of micro-services. Take advantage of the recent time. And looked at the source of Dubbo. Dubbo in order to achieve flexibility and decoupling, using a large number of design patterns and SPI mechanism, it is not easy to understand the code of Dubbo.
In accordance with the "Freehand Frames" series of articles, I will still minimalist implementation of an RPC framework. Help you understand the principles of the RPC framework.
Broadly speaking, a complete RPC contains a number of components, including service discovery, service governance, remote invocation, call chain analysis, gateways, and so on. I will slowly implement these functions, this article is mainly about the foundation of RPC, the implementation of remote invocation.
I believe that after reading this article you will also be able to implement a framework that can provide RPC calls.
- Call procedure for RPC
Let's take a look at the RPC call process and see what happens to the RPC call from a macro perspective.
When a call starts:
The client calls the local dynamic agent proxy
This proxy will invoke the call through the protocol to serialize the byte stream
Send a byte stream to the server via the Netty network framework
After being throttled by this byte, the server will deserialize the original call according to the Protocol and invoke the method provided by the service party using the reflection principle.
If the request has a return value, you need to serialize the result according to the protocol and return it to the caller by Netty
- Framework Overview and Technology selection
Take a look at the components of the framework:
Clinet is the caller. Servive is the provider of the service. The protocol package defines the communication protocol. Common contains a number of common logic components.
Technology selection project using MAVEN as the Package management tool, JSON acts as a serialization protocol, using the spring Boot Management object's lifecycle, Netty as a network component of NIO. So to read this article, you need to have a basic understanding of spring boot and Netty.
Here's a look at the specific implementation of each component:
- Protocol
In fact, as RPC protocol, there is only one problem to consider-how to make a call to a method can be transmitted by the network byte stream.
First we need to define the invocation of a method and return two entities:
Request:
@Data
public class Rpcrequest {
Call number
Private String RequestID;
Class name
Private String ClassName;
Method name
Private String MethodName;
Data type of request parameter
Private class<?>[] parametertypes;
Requested parameters
Private object[] Parameters;
}
Results:
@Data
public class Rpcresponse {
Call number
Private String RequestID;
Thrown exception
Private Throwable throwable;
return results
Private Object result;
}
Determined that the object needs to be serialized, it is necessary to determine the serialized protocol, implementing two methods, serialization and deserialization of two methods.
Public interface Serialization {
<T> byte[] Serialize (T obj);
<T> T Deserialize (byte[] data,class<t> clz);
}
There are a number of optional serialization protocols, such as:
The serialization method of the JDK. (not recommended, not conducive to subsequent cross-language calls)
JSON is readable, but it is slow to serialize and large in size.
Protobuf,kyro,hessian, etc. are excellent serialization frameworks and can also be selected on demand.
For simplicity and ease of debugging, we chose JSON as the serialization protocol, using Jackson as the JSON parsing framework.
/**
- @author Zhengxin
*/
Public class Jsonserialization implements serialization {
Private Objectmapper Objectmapper;
Public jsonserialization () {
This.objectmapper = new Objectmapper (); br/>}
@Override
try {
return objectmapper.writevalueasbytes (obj);
} catch (Jsonprocessingexception e) {
E.printstacktrace ();
}
return NULL;BR/>}
@Override
try {
Return Objectmapper.readvalue (DATA,CLZ);
} catch (IOException e) {
E.printstacktrace ();
}
return null;
}
}
Because Netty supports custom coder. So you only need to implement Bytetomessagedecoder and Messagetobyteencoder two interfaces. It solves the problem of serialization:
public class Rpcdecoder extends Bytetomessagedecoder {
Private class<?> CLZ;
private serialization serialization;
Public Rpcdecoder (class<?> clz,serialization serialization) {
This.clz = CLZ;
This.serialization = Serialization;br/>}
@Override
if (In.readablebytes () < 4) {
Return
}
In.markreaderindex ();
int datalength = In.readint ();
if (In.readablebytes () < datalength) {
In.resetreaderindex ();
Return
}
byte[] data = new Byte[datalength];
In.readbytes (data);
Object obj = serialization.deserialize (data, CLZ);
Out.add (obj);
}
}
public class Rpcencoder extends Messagetobyteencoder {
Private class<?> CLZ;
private serialization serialization;
Public Rpcencoder (Class<?> Clz, serialization serialization) {
This.clz = CLZ;
This.serialization = Serialization;br/>}
@Override
if (CLZ! = null) {
byte[] bytes = serialization.serialize (msg);
Out.writeint (bytes.length);
Out.writebytes (bytes);
}
}
}
Now that protocol is implemented, we can convert the invocation of the method and the return of the result into a string of byte[] arrays that can be transmitted over the network.
- Server
Server is the component responsible for processing client requests. In a highly concurrent Internet environment, the use of Nio in a non-blocking way can be relatively easy to cope with high concurrency scenarios. Netty is an excellent Nio processing framework. The key code for Server is as follows:
Netty is based on the RECOTR model. So two sets of thread bosses and workers need to be initialized. The boss is responsible for distributing the request, and the worker is responsible for performing the corresponding handler:
@Bean
Public serverbootstrap serverbootstrap () throws interruptedexception {
Serverbootstrap Serverbootstrap = new Serverbootstrap ();
Serverbootstrap.group (Bossgroup (), Workergroup ())
. Channel (Nioserversocketchannel.class)
. Handler (new Logginghandler (Loglevel.debug))
. Childhandler (Serverinitializer);
Map<channeloption<?>, object> tcpchanneloptions = Tcpchanneloptions ();
Set<channeloption<?>> KeySet = Tcpchanneloptions.keyset ();
for (@SuppressWarnings ("Rawtypes") channeloption option:keyset) {
serverbootstrap.option (option, Tcpchanneloptions.get (option));
}
return serverbootstrap;
} The operation of the
Netty is based on pipeline. So we need to register several coder implemented in protocol to Netty pipeline.
Channelpipeline pipeline = Ch.pipeline ();
//Handle the coder of the sticky packet in TCP request, the function can be self-Google
Pipeline.addlast (new Lengthfieldbasedframedecoder (65535,0,4));
Serialization and deserialization implemented in Protocol coder
Pipeline.addlast (New Rpcencoder (Rpcresponse.class,new jsonserialization ()));
Pipeline.addlast (New Rpcdecoder (Rpcrequest.class,new jsonserialization ()));
//The handler of the specific processing request is explained in detail below
Pipeline.addlast (serverhandler);
Implement the specific serverhandler used to process the real call. The
Serverhandler inherits Simplechannelinboundhandler<rpcrequest>. In short, this inboundhandler is called when the data is accepted or when the state of the Channel is changed. The method channelRead0 () is used when the handler reads the data, so we can rewrite this method to be sufficient.
@Override
protected void channelRead0 (Channelhandlercontext ctx, Rpcrequest msg) throws Exception {
Rpcresponse rpcresponse = new Rpcresponse ();
Rpcresponse.setrequestid (Msg.getrequestid ());
try{
//receives the request and starts processing the request
Object handler = handler (msg);
Rpcresponse.setresult (handler);
}catch (Throwable throwable) {
//If an exception is thrown, the exception is also stored in response
Rpcresponse.setthrowable (throwable);
Throwable.printstacktrace ();
}
//The context in which Netty is written after the operation is completed. Netty handles the return value itself.
Ctx.writeandflush (Rpcresponse);
}
Handler (msg) is actually using the Cglib Fastclass implementation, in fact, the fundamental principle, or reflection. Learning about the reflection in Java really can do whatever you wish.
Private Object Handler (Rpcrequest request) throws Throwable {
class<?> CLZ = Class.forName (Request.getclassname ());
Object Servicebean = Applicationcontext.getbean (CLZ);
class<?> ServiceClass = Servicebean.getclass ();
String methodName = Request.getmethodname ();
class<?>[] parametertypes = Request.getparametertypes ();
object[] Parameters = Request.getparameters ();
The fundamental idea is to get the class name and the method name, using reflection to implement the call
Fastclass Fastclass = fastclass.create (ServiceClass);
Fastmethod Fastmethod = Fastclass.getmethod (methodname,parametertypes);
Where the actual call takes place
Return Fastmethod.invoke (servicebean,parameters);
}
On the whole, the implementation of server is not very difficult. The core knowledge point is the use of Netty channel and the reflection mechanism of cglib.
- Client
Future
In fact, for me, the implementation of the client is much more difficult than the implementation of the server. Netty is an asynchronous framework, and all returns are based on the future and the Callback mechanism.
So before reading the following words strongly recommended, I wrote an article before the future study. Using the classic Wite and notify mechanisms, the results of asynchronous fetch requests are implemented.
/**
- @author Zhengxin
*/
public class Defaultfuture {
Private Rpcresponse Rpcresponse;
Private volatile Boolean issucceed = false;
Private Final Object object = new Object ();
Public rpcresponse getResponse (int timeout) {
Synchronized (object) {
while (!issucceed) {
try {
Wait
Object.wait (timeout);
} catch (Interruptedexception e) {
E.printstacktrace ();
}
}
return rpcresponse;
}
}
public void Setresponse (Rpcresponse response) {
if (issucceed) {
Return
}
Synchronized (object) {
This.rpcresponse = response;
This.issucceed = true;
Notiy
Object.notify ();
}
}
}
Reuse Resources
In order to improve the client throughput, there are several ways to provide the following:
Use object pooling: Building multiple clients is later saved in the object pool. But the complexity of the code and the cost of maintaining the client are high.
Reuse the channel in the Netty as much as possible.
You may have noticed earlier why you should add an ID to Rpcrequest and Rpcresponse. Because the channel in Netty is used by multiple threads. When a result is returned asynchronously, you do not know which thread returned it. At this point you can consider using a map to create an ID and a future map. This allows the requested thread to obtain the corresponding return result as long as the corresponding ID is used.
/**
- @author Zhengxin
*/
public class ClientHandler extends Channelduplexhandler {
Use map to maintain a mapping relationship between ID and future, using a thread-safe container in a multithreaded environment
Private final map<string, defaultfuture> futuremap = new concurrenthashmap<> ();br/> @Override
if (msg instanceof rpcrequest) {
Rpcrequest request = (rpcrequest) msg;
When writing data, increase the mapping
Futuremap.putifabsent (Request.getrequestid (), New Defaultfuture ());
}
Super.write (CTX, MSG, promise); br/>}
@Override
if (msg instanceof rpcresponse) {
Rpcresponse response = (rpcresponse) msg;
When you get data, put the results into the future.
Defaultfuture defaultfuture = Futuremap.get (Response.getrequestid ());
Defaultfuture.setresponse (response);
}
Super.channelread (CTX, msg);
}
Public Rpcresponse getrpcresponse (String RequestID) {
try {
Get the real results from the future.
Defaultfuture defaultfuture = Futuremap.get (RequestID);
Return Defaultfuture.getresponse (10);
}finally {
Removed from the map when finished.
Futuremap.remove (RequestID);
}
}
}
This does not inherit the Inboundhandler in the server and uses the Channelduplexhandler. As the name implies, when writing and reading data, the corresponding method will be triggered. Save the ID and future in the Map when writing. When you read the data, take the future out of the MAP and put the results into the future. The corresponding ID is required when the result is obtained.
Use transporters to encapsulate the request.
public class Transporters {
public static Rpcresponse Send (Rpcrequest request) {
Nettyclient nettyclient = new Nettyclient ("127.0.0.1", 8080);
Nettyclient.connect (Nettyclient.getinetsocketaddress ());
Rpcresponse send = nettyclient.send (request);
return send;
}
}
Implementation of dynamic Agent
The most widely known application of dynamic Agent technology is the Spring AOP, a programming implementation for facets. Dynamically add code in the original method before or after. The role of dynamic Proxy in RPC framework is to completely replace the original method and call the remote method directly.
Agent Factory class:
public class Proxyfactory {br/> @SuppressWarnings ("unchecked")
Return (T) proxy.newproxyinstance (
Interfaceclass.getclassloader (),
New Class<?>[]{interfaceclass},
New Rpcinvoker<t> (Interfaceclass)
);
}
}
The Rpcinvoker method is executed when the proxyfactory generated class is called.
public class Rpcinvoker<t> implements Invocationhandler {
Private class<t> CLZ;
Public Rpcinvoker (class<t> clz) {
This.clz = Clz;br/>}
@Override
Rpcrequest request = new Rpcrequest ();
String RequestID = Uuid.randomuuid (). toString ();
String className = Method.getdeclaringclass (). GetName ();
String methodName = Method.getname ();
class<?>[] parametertypes = Method.getparametertypes ();
Request.setrequestid (RequestID);
Request.setclassname (ClassName);
Request.setmethodname (MethodName);
Request.setparametertypes (parametertypes);
Request.setparameters (args);
return Transporters.send (Request). GetResult ();
}
}
See this invoke method, the main three functions,
Generate RequestID.
Assemble the rpcrequest.
Call transports to send the request to get the result.
Finally, the entire call chain is complete. We finally completed an RPC call.
Integration with Spring
In order for our client to be easy to use we need to consider defining a custom annotation @RpcInterface when our project accesses spring, spring scans this annotation and automatically creates a proxy object through our proxyfactory. and stored in spring's applicationcontext. This allows us to inject it directly using the @Autowired annotations.
@Target ({elementtype.type}) br/> @Retention (retentionpolicy.runtime)
}
@Configurationbr/> @Slf4j
Private ApplicationContext applicationcontext;br/> @Override
This.applicationcontext = Applicationcontext;br/>}
@Override
Reflections Reflections = New Reflections ("Com.xilidou");
Defaultlistablebeanfactory beanfactory = (defaultlistablebeanfactory) Applicationcontext.getautowirecapablebeanfactory ();
Get the interface @RpcInterfac callout
set<class<?>> Typesannotatedwith = Reflections.gettypesannotatedwith (RpcInterface.class);
for (class<?> Aclass:typesannotatedwith) {
Creates a proxy object and registers it with the spring context.
Beanfactory.registersingleton (Aclass.getsimplename (), Proxyfactory.create (AClass));
}
Log.info ("Afterpropertiesset is {}", typesannotatedwith);
}
}
Finally, our simplest RPC framework has been developed. You can test it below.
- Demo
Api
@RpcInterface
Public interface Ihelloservice {
String Sayhi (string name);
}
Server
Implementation of Ihelloserivce:
@Servicebr/> @Slf4j
@Override
Log.info (name);
Return "Hello" + name;
}
}
Start the service:
@SpringBootApplication
public class Application {
public static void Main (string[] args) throws Interruptedexception {
Configurableapplicationcontext context = Springapplication.run (Application.class);
Tcpservice Tcpservice = Context.getbean (Tcpservice.class);
Tcpservice.start ();
}
}
`
Client
@SpringBootApplication ()
public class Clientapplication {
public static void Main (string[] args) {
Configurableapplicationcontext context = Springapplication.run (Clientapplication.class);
Ihelloservice HelloService = Context.getbean (Ihelloservice.class);
System.out.println (Helloservice.sayhi ("Doudou"));
}
}
Results from running the output after:
Hello Doudou
Summarize
Finally we implemented a minimal version of RPC Remote Call module.
Freehand Frame--Implement RPC remote Call