Using Netty to implement RPC

As an asynchronous event driven network application framework, Netty can be used to rapidly develop maintainable high-pe...
1. What is RPC
2. Implement RPC framework based on Netty

As an asynchronous event driven network application framework, Netty can be used to rapidly develop maintainable high-performance servers and clients. The bottom layer of Dubbo, a famous RPC framework in China, uses Netty as network communication. In this article, we will explore the nature of RPC framework and implement a simple RPC framework using Netty.

1. What is RPC

RPC (Remote Procedure Call), translated into Chinese, is Remote Procedure Call. The remote procedure is relative to the local method. It runs in a remote place rather than local. RPC can be used to call remote services like local function calls, which is a communication mode between processes. The nature of RPC calls can be shown in the following figure:

From the above description, it seems that RPC and socket are very similar. They both call remote methods in client/server mode. However, it is worth noting that RPC is not the same as socket. Socket is one of the communication means often used by RPC. RPC is implemented on the basis of socket. It needs more network and system resources than socket. In addition to socket, RPC has other communication methods, such as http, the pipeline of the operating system and other technologies to realize the call of remote programs. In Microsoft Windows system, RPC uses named pipe to communicate. If you need to understand socket related concepts, please refer to the previous article Programming with golang socket.

1.1 local method call

The local method call is the most common method in our development. A method is defined as follows:

public String sayHello(String name) { return "hello, " + name; }

Just pass in a parameter and call the sayHello method to get an output. The input parameter, the output parameter and the method body are all in the same process space, which is the local method call

1.2 Socket communication

Is there any way to communicate between different processes? The caller is in process A and needs to call method B, but method B is in process B.

The easiest way to think of

It is the use of Socket communication, using Socket can complete cross process calls, we need to agree on a process communication protocol, to transfer parameters, call functions, output parameters. After writing about Socket, we should all know that Socket is a relatively primitive way. We need to pay more attention to some details. For example, parameters and functions need to be converted into byte stream for network transmission, that is, serialization operation, and then the parameters need to be deserialized.

If RPC is to let us use Socket remote call directly on the client, it is undoubtedly a disaster. So is there any simple method that our callers don't need to pay attention to the details, just like calling local functions, just pass in parameters, call methods, and wait for the results to return? The solution to this appeal is the RPC framework, which blocks the details of the underlying network communication for users.

1.3 RPC framework

The RPC framework is used to solve the above problems. It can make the caller call remote services like calling local functions. The underlying communication details are transparent to the caller, masking all kinds of complexity, and giving the caller the ultimate experience.

When the server needs to modify the implementation within the method, the client can not perceive it completely and does not need to make any changes. This method is very convenient for cross department and cross company cooperation.

1.4 technical details of RPC call

As mentioned earlier, the RPC framework allows callers to call remote services just like they call local functions. The principle is that the RPC framework blocks the details of Socket communication, so that the caller can call remote methods just like calling local methods.

In use, the caller directly calls the local function and passes in the corresponding parameters. It does not care about other details. As for the communication details, the RPC framework is responsible for the implementation. In fact, the RPC framework adopts the proxy class mode, specifically, the dynamic proxy mode. A new class is created dynamically at runtime, that is, the proxy class. In this class, the communication details are realized, such as connection with the server, parameter serialization, result deserialization, etc.

In addition to the above-mentioned dynamic proxy, a protocol format for communication between the two parties needs to be agreed, such as the class name of the request method, the name of the requested method, the data type of the request parameters, and the requested parameters. In this way, the network transmission is carried out after serialization according to the format, and then the server decodes the request object according to the specified format after receiving the request object Know which method to call and what parameters to pass in.

Just now we mentioned network transmission. An important part of RPC framework is network transmission. Services are deployed on different hosts. How can we efficiently carry out network transmission, avoid packet loss and ensure that data is transmitted quickly and completely? In fact, it is to take advantage of our protagonist, Netty, which is a high-performance network communication framework, which is adequate for our task.

Having said so much, I would like to summarize what points the next RPC framework needs to focus on:

  • Dynamic proxy
  • communication protocol
  • serialize
  • network transmission

Of course, an excellent RPC framework needs to pay attention to more than the above points, but this article aims to make a simple RPC framework, understanding the above key points is enough

2. Implement RPC framework based on Netty

As mentioned above, several technical details of RPC framework: dynamic proxy, communication protocol, serialization and network transmission are implemented respectively.

2.1 communication protocol

The communication protocol is actually the communication rules agreed by the client and the server. Its essence is to stipulate how the client notifies the server of the information of the remote method to be called, such as the class name of the request method, the name of the requested method, the data type of the request parameter, the requested parameter and the result returned by the server. So we need to agree a communication protocol to exchange the above information.

  • Request object
@Data @ToString public class RpcRequest { /** * The ID of the request object used by the client to verify that the server request and response match */ private String requestId; /** * Class name */ private String className; /** * Method name */ private String methodName; /** * Parameter type */ private Class<?>[] parameterTypes; /** * Entering the reference */ private Object[] parameters; }
  • Response object
@Data public class RpcResponse { /** * Response ID */ private String requestId; /** * error message */ private String error; /** * Results returned */ private Object result; }

2.2 serialization

There are many serialization protocols on the market, such as jdk serialization tools (ObjectInputStream/ObjectOuputStream), protobuf, kyro, Hessian, etc. as long as the serialization method of jdk is not selected (because its performance is too poor, the generated code stream after serialization is too large), other methods can be used. Here, for convenience, JSON is selected as the serialization protocol and fastjson is used As a JSON framework.

For the convenience of subsequent extension, define the serialization interface first

public interface Serializer { /** * java Object to binary * * @param object * @return */ byte[] serialize(Object object) throws IOException; /** * Binary to java object * * @param clazz * @param bytes * @param <T> * @return */ <T> T deserialize(Class<T> clazz, byte[] bytes) throws IOException; }

We adopt the mode of JSON. Here we define the implementation class JSONSerializer:

public class JSONSerializer implements Serializer{ @Override public byte[] serialize(Object object) { return JSON.toJSONBytes(object); } @Override public <T> T deserialize(Class<T> clazz, byte[] bytes) { return JSON.parseObject(bytes, clazz); } }

If you need to use other serialization methods, you can implement the serialization interface yourself.

2.3 codec

After the protocol format and serialization method are agreed, we also need codec. The encoder converts the request object into a format suitable for transmission (generally, byte stream), and the corresponding decoder is to convert the network byte stream back to the message format of the application. Here, we implement the encoder by inheriting the abstract class messagetobyencoder provided by Netty, and implement the decoder by inheriting the abstract class ByteToMessageDecoder provided by Netty. The inheritance relationship of the above abstract classes is as follows:

  • encoder
public class RpcEncoder extends MessageToByteEncoder { private Class<?> clazz; private Serializer serializer; public RpcEncoder(Class<?> clazz, Serializer serializer) { this.clazz = clazz; this.serializer = serializer; } @Override protected void encode(ChannelHandlerContext channelHandlerContext, Object msg, ByteBuf byteBuf) throws Exception { if (clazz != null && clazz.isInstance(msg)) { byte[] bytes = serializer.serialize(msg); byteBuf.writeInt(bytes.length); byteBuf.writeBytes(bytes); } } }
  • decoder
public class RpcDecoder extends ByteToMessageDecoder { private Class<?> clazz; private Serializer serializer; public RpcDecoder(Class<?> clazz, Serializer serializer) { this.clazz = clazz; this.serializer = serializer; } @Override protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception { //Because before encoding, write an Int type, 4 bytes to indicate the length if (byteBuf.readableBytes() < 4) { return; } //Mark the current read position byteBuf.markReaderIndex(); int dataLength = byteBuf.readInt(); if (byteBuf.readableBytes() < dataLength) { byteBuf.resetReaderIndex(); return; } byte[] data = new byte[dataLength]; //Read the data in byteBuf into the data byte array byteBuf.readBytes(data); Object obj = serializer.deserialize(clazz, data); list.add(obj); } }

2.4 Netty client

Let's see how the Netty client is implemented, that is, how to use Netty to open the client. We need to pay attention to the following points:

  • Write the startup method and specify that the transmission uses Channel
  • Specify ChannelHandler to read and write data in network transmission
  • Add codec
  • Add failure retry mechanism
  • Add a method to send a request message
@Slf4j public class NettyClient { private EventLoopGroup eventLoopGroup; private Channel channel; private ClientHandler clientHandler; private String host; private Integer port; public NettyClient(String host, Integer port) { this.host = host; this.port = port; } public void connect() throws InterruptedException { clientHandler = new ClientHandler(); eventLoopGroup = new NioEventLoopGroup(); //Startup class Bootstrap bootstrap = new Bootstrap(); bootstrap.group(eventLoopGroup) //Specifies the Channel used by the transport .channel(NioSocketChannel.class) .option(ChannelOption.SO_KEEPALIVE, true) .option(ChannelOption.TCP_NODELAY, true) .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); //Add encoder pipeline.addLast(new RpcEncoder(RpcRequest.class, new JSONSerializer())); //Add decoder pipeline.addLast(new RpcDecoder(RpcResponse.class, new JSONSerializer())); //Request processing class pipeline.addLast(clientHandler); } }); /** * Get Netty connections synchronously */ channel = bootstrap.connect(host, port).sync().channel(); } /** * send message * * @param request * @return */ public RpcResponse send(final RpcRequest request) { try { channel.writeAndFlush(request).await(); } catch (InterruptedException e) { e.printStackTrace(); } return clientHandler.getRpcResponse(request.getRequestId()); } @PreDestroy public void close() { eventLoopGroup.shutdownGracefully(); channel.closeFuture().syncUninterruptibly(); } }

Our focus on data processing is on the ClientHandler class, which inherits the ChannelDuplexHandler class and can process both outbound and inbound data.

public class ClientHandler extends ChannelDuplexHandler { /** * Using Map to maintain the mapping relationship between request object ID and response result Future */ private final Map<String, DefaultFuture> futureMap = new ConcurrentHashMap<>(); @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (msg instanceof RpcResponse) { //Get response object RpcResponse response = (RpcResponse) msg; DefaultFuture defaultFuture = futureMap.get(response.getRequestId()); //Write the result to DefaultFuture defaultFuture.setResponse(response); } super.channelRead(ctx, msg); } @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { if (msg instanceof RpcRequest) { RpcRequest request = (RpcRequest) msg; //Before sending the request object, first save the request ID and build a mapping relationship with the response Future futureMap.putIfAbsent(request.getRequestId(), new DefaultFuture()); } super.write(ctx, msg, promise); } /** * Get response results * * @param requestId * @return */ public RpcResponse getRpcResponse(String requestId) { try { DefaultFuture future = futureMap.get(requestId); return future.getRpcResponse(10); } finally { //After successful acquisition, remove it from the map futureMap.remove(requestId); } } }

As can be seen from the above implementation, we have defined a Map to maintain the mapping relationship between the request ID and the response result. The purpose is for the client to verify whether the server's response matches the request. Because Netty's channel may be used by multiple threads, when the result returns, you don't know which thread it is returned from, so you need a mapping relationship.

Our results are encapsulated in the DefaultFuture, because Netty is an asynchronous framework, and all returns are based on the Future and Callback mechanism. We customize Future here to implement "asynchronous call" on the client side.

public class DefaultFuture { private RpcResponse rpcResponse; private volatile boolean isSucceed = false; private final Object object = new Object(); public RpcResponse getRpcResponse(int timeout) { synchronized (object) { while (!isSucceed) { try { 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; object.notify(); } } }

2.5 Netty server

The implementation of the Netty server is similar to that of the client, but it should be noted that after decoding the request, local functions need to be called by proxy. Here is the server-side implementation.

@Slf4j public class NettyServer implements InitializingBean { private ServerHandler serverHandler; private EventLoopGroup boss; private EventLoopGroup worker; private Integer serverPort; public NettyServer(ServerHandler serverHandler, Integer serverPort) { this.serverHandler = serverHandler; this.serverPort = serverPort; } @Override public void afterPropertiesSet() throws Exception { //Using zookeeper as a registry is not covered in this paper and can be ignored ServiceRegistry registry = null; if (Objects.nonNull(serverPort)) { start(registry); } } public void start(ServiceRegistry registry) throws Exception { //Thread pool responsible for handling client connections boss = new NioEventLoopGroup(); //Thread pool responsible for processing read and write operations worker = new NioEventLoopGroup(); ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(boss, worker) .channel(NioServerSocketChannel.class) .option(ChannelOption.SO_BACKLOG, 1024) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); //Add decoder pipeline.addLast(new RpcEncoder(RpcResponse.class, new JSONSerializer())); //Add encoder pipeline.addLast(new RpcDecoder(RpcRequest.class, new JSONSerializer())); //Add request processor pipeline.addLast(serverHandler); } }); bind(serverBootstrap, serverPort); } /** * If port binding fails, port number + 1, rebind */ public void bind(final ServerBootstrap serverBootstrap, int port) { serverBootstrap.bind(port).addListener(future -> { if (future.isSuccess()) { log.info("port[ {} ] Binding succeeded", port); } else { log.error("port[ {} ] Binding failed", port); bind(serverBootstrap, port + 1); } }); } @PreDestroy public void close() throws InterruptedException { boss.shutdownGracefully().sync(); worker.shutdownGracefully().sync(); log.info("close Netty"); } }

The following is the server-side core code, which handles the Handler class for read-write operations:

@Slf4j public class ServerHandler extends SimpleChannelInboundHandler<RpcRequest> implements ApplicationContextAware { private ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } @Override protected void channelRead0(ChannelHandlerContext ctx, RpcRequest msg) { RpcResponse rpcResponse = new RpcResponse(); rpcResponse.setRequestId(msg.getRequestId()); try { Object handler = handler(msg); log.info("Get return result: {} ", handler); rpcResponse.setResult(handler); } catch (Throwable throwable) { rpcResponse.setError(throwable.toString()); throwable.printStackTrace(); } ctx.writeAndFlush(rpcResponse); } /** * The server uses the proxy to process the request * * @param request * @return */ private Object handler(RpcRequest request) throws ClassNotFoundException, InvocationTargetException { //use Class.forName To load the class file Class<?> clazz = Class.forName(request.getClassName()); Object serviceBean = applicationContext.getBean(clazz); log.info("serviceBean: {}", serviceBean); Class<?> serviceClass = serviceBean.getClass(); log.info("serverClass:{}", serviceClass); String methodName = request.getMethodName(); Class<?>[] parameterTypes = request.getParameterTypes(); Object[] parameters = request.getParameters(); //Using CGLIB Reflect FastClass fastClass = FastClass.create(serviceClass); FastMethod fastMethod = fastClass.getMethod(methodName, parameterTypes); log.info("Start calling CGLIB Proxy execution method..."); return fastMethod.invoke(serviceBean, parameters); } }

2.6 client agent

The client uses Java dynamic proxy (all RPC interfaces are required to have implemented interfaces). For details about dynamic proxy, please refer to the previous article Understand dynamic agent thoroughly . The client Java dynamic proxy is implemented as follows:

@Slf4j public class RpcClientDynamicProxy<T> implements InvocationHandler { private Class<T> interfaceClazz; private String host; private Integer port; public RpcClientDynamicProxy(Class<T> interfaceClazz, String host, Integer port) { this.interfaceClazz = interfaceClazz; this.host = host; this.port = port; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 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); log.info("Request content: {}", request); //Open the Netty client and connect directly //Here, the host and port of the server are specified directly, and the normal RPC framework will get it from the registry NettyClient nettyClient = new NettyClient(host, port); log.info("Start connecting to the server:{}", new Date()); nettyClient.connect(); RpcResponse send = nettyClient.send(request); log.info("Return result of request call:{}", send.getResult()); return send.getResult(); } @SuppressWarnings("unchecked") public T getProxy() { return (T) Proxy.newProxyInstance( interfaceClazz.getClassLoader(), new Class<?>[], this ); } }

In the proxy method, encapsulate the request object, build the NettyClient object, open the client and send the request message.

2.7 RPC remote call test

All the above code is the implementation of RPC. If we want to use our own RPC framework, we can type the above code into a jar package and import it in the client side and server side respectively. In order to simulate this process, I take all the above codes as a single module, and then define two modules to implement the server and the client.

The netty RPC server module relies on the netty RPC module, and the netty RPC client module relies on the netty RPC server and netty RPC modules. Here's the pom configuration:

  • netty-rpc
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>netty-explore</artifactId> <groupId>com.zhuoli.service</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>netty-rpc</artifactId> <dependencies> <!-- https://mvnrepository.com/artifact/org.springframework/spring-context --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.3.RELEASE</version> </dependency> </dependencies> </project>
  • netty-rpc-server
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>netty-explore</artifactId> <groupId>com.zhuoli.service</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>netty-rpc-server</artifactId> <dependencies> <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>2.2.4.RELEASE</version> </dependency> <dependency> <groupId>com.zhuoli.service</groupId> <artifactId>netty-rpc</artifactId> <version>1.0-SNAPSHOT</version> <scope>compile</scope> </dependency> </dependencies> </project>
  • netty-rpc-client
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>netty-explore</artifactId> <groupId>com.zhuoli.service</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>netty-rpc-client</artifactId> <dependencies> <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>2.2.4.RELEASE</version> </dependency> <dependency> <groupId>com.zhuoli.service</groupId> <artifactId>netty-rpc</artifactId> <version>1.0-SNAPSHOT</version> <scope>compile</scope> </dependency> <dependency> <groupId>com.zhuoli.service</groupId> <artifactId>netty-rpc-server</artifactId> <version>1.0-SNAPSHOT</version> <scope>compile</scope> </dependency> </dependencies> </project>

2.7.1 RPC server

  • Server RPC interface:
package com.zhuoli.service.netty.explore.netty.rpc.server.contract; public interface HelloService { String hello(String name); }
  • Server RPC interface implementation:
package com.zhuoli.service.netty.explore.netty.rpc.server.impl; import com.zhuoli.service.netty.explore.netty.rpc.server.contract.HelloService; import org.springframework.stereotype.Service; @Service public class HelloServiceImpl implements HelloService { @Override public String hello(String name) { return "hello, " + name; } }
  • Server startup portal
@SpringBootApplication @Slf4j public class RpcServerApplicationContext { @Value("$") private Integer port; public static void main(String[] args) throws Exception { SpringApplication.run(RpcServerApplicationContext.class, args); log.info("Server started successfully"); } @Bean public NettyServer nettyServer() { return new NettyServer(serverHandler(), port); } @Bean public ServerHandler serverHandler() { return new ServerHandler(); } }

2.7.2 RPC client

  • Client calls RPC:
@SpringBootApplication @Slf4j public class NettyRpcClientApplicationContext { public static void main(String[] args) throws Exception { SpringApplication.run(NettyRpcClientApplicationContext.class, args); //The server host and port are directly specified here HelloService helloService = new RpcClientDynamicProxy<>(HelloService.class, "127.0.0.1", 3663).getProxy(); String result = helloService.hello("zhuoli"); log.info("Response results“: {}", result); } }

Start the server side and client side respectively, and the server side log:

Client log:

We have implemented a very simple RPC framework based on Netty, which is far from the mature RPC framework, and even the basic registry has not been implemented. However, through this practice, I can say that I have a deeper understanding of RPC and understand what aspects an RPC framework needs to pay attention to at the end. In the future, when we use a mature RPC framework, such as Dubbo, we can We should have a clear idea and understand that the underlying layer is using Netty as the basic communication framework. If you look at the source code of the open source RPC framework, it is relatively easy.

Reference link:

1. Implementation of simple RPC framework based on Netty

2. The basic principle of RPC and how to implement RPC with Netty

30 June 2020, 00:00 | Views: 6626

Add new comment

For adding a comment, please log in
or create account

0 comments