[manual RPC framework] spring boot + netty4 implements the RPC framework
Thread model 1: traditional blocking I/O service model
Model features:
Use blocking IO mode to obtain input data
Each link requires an independent thread to complete data input, business processing and data return.
Problem analysis:
When the number of concurrent threads is large, a large number of threads will be created, occupying a large amount of system resources
After the connection is created, if the current thread has no data to read temporarily, the thread will block the read operation, resulting in a waste of thread resources.
Thread model 2: Reactor mode
For the two shortcomings of the traditional blocking I/O service model, the solutions are as follows:
Based on I/O reuse model: multiple connections share a blocking object. Applications only need to wait on one blocking object without blocking all connections. When a connection has new data to process, the operating system notifies the application that the thread returns from the blocking state and starts business processing. The corresponding name of reactor: 1. Reactor mode 2. Dispatcher mode 3. Notifier mode
Reuse thread resources based on thread pool: it is no longer necessary to create threads for each connection. The business processing tasks after the connection are allocated to threads for processing. One thread can process the business of multiple connections.
Single Reactor single thread
model analysis
Advantages: the model is simple. There are no problems of multithreading, process communication and competition. All of them are completed in one thread
Disadvantages: performance problem. There is only one thread, which can not give full play to the performance of multi-core CPU. When the Handler handles the business on a connection, the whole process cannot handle other connection events, which can easily lead to performance bottlenecks
Disadvantages: reliability problems, unexpected thread termination, or entering an endless loop will make the communication module of the whole system unavailable, unable to receive and process external messages, resulting in node failure
Usage scenario: the number of clients is limited, and the business processing is very fast. For example, the time complexity of Redis in business processing is O(1)
Single Reactor multithreading
model analysis
Advantages: it can make full use of the processing power of multi-core cpu
Disadvantages: multithreaded data sharing and access are complex. reactor handles the listening and response of all events. When running in a single thread, it is prone to performance bottlenecks in high concurrency scenarios
Master slave Reactor multithreading
model analysis
Advantages: the data interaction between the parent thread and the child thread is simple, and the responsibilities are clear. The parent thread only needs to receive new connections, and the child thread completes the subsequent business processing.
Advantages: the data interaction between the parent thread and the child thread is simple. The Reactor main thread only needs to pass the new connection to the child thread, and the child thread does not need to return data
Disadvantages: high programming complexity
Combined with examples: this model is widely used in many projects, including Nginx master-slave Reactor multi process model, Memcached master-slave multithreading, and Netty master-slave multithreading model
First, implement simple Netty communication
Server example
public static void main(String[] args) { //Create a connection thread group with 1 threads. Only handle connection requests NioEventLoopGroup boss = new NioEventLoopGroup(1); //Create a worker thread group. The number of threads is assumed to be the number of cpu cores * 2 by default. Processing and client business processing NioEventLoopGroup worker = new NioEventLoopGroup(); //Create a Server-side startup object ServerBootstrap serverBootstrap = new ServerBootstrap(); //Configure thread group serverBootstrap.group(boss, worker) //NioServerSocketChannel is used as the channel implementation of the server .channel(NioServerSocketChannel.class) //Initialize the processor to the worker thread group .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel socketChannel) throws Exception { socketChannel.pipeline() //Add codec for string .addLast(new StringDecoder()) .addLast(new StringEncoder()) //Add the codec of the object. Classresolvers.weakcachingcurrentresolver sets the weak reference WeakReferenceMap cache class loader to prevent memory overflow .addLast(new ObjectDecoder(ClassResolvers.weakCachingConcurrentResolver(this.getClass().getClassLoader()))) .addLast(new ObjectEncoder()) //Add custom business processor .addLast(new SimpleChannelInboundHandler<Object>() { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { log.info("Client connection... Client address:{}", ctx.channel().remoteAddress()); } @Override protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object o) throws Exception { log.info("Data received by the server:{}", o.toString()); //100 million AI code String str = o.toString(); str = str.replace("Do you", ""); str = str.replace("?", "!"); str = str.replace("? ", "! "); channelHandlerContext.writeAndFlush(str); } }); } }); //Start and listen ChannelFuture channelFuture = serverBootstrap.bind(8888).syncUninterruptibly(); //Monitor closed channel channelFuture.channel().closeFuture(); }
Client example
public static void main(String[] args) { //Setting up client worker threads NioEventLoopGroup worker = new NioEventLoopGroup(); //Create client startup object Bootstrap bootstrap = new Bootstrap(); bootstrap.group(worker) //Channel connector .channel(NioSocketChannel.class) //Initialize the processor to the worker thread group .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel socketChannel) throws Exception { socketChannel.pipeline() //Add codec for string .addLast(new StringDecoder()) .addLast(new StringEncoder()) //Add the codec of the object. Classresolvers.weakcachingcurrentresolver sets the weak reference WeakReferenceMap cache class loader to prevent memory overflow .addLast(new ObjectDecoder(ClassResolvers.weakCachingConcurrentResolver(this.getClass().getClassLoader()))) .addLast(new ObjectEncoder()) //Add custom business processor .addLast(new SimpleChannelInboundHandler<Object>() { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { ctx.writeAndFlush("Ha ha ha"); } @Override protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object o) throws Exception { log.info("Data received by client:{}", o.toString()); } }); } }); ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8888).syncUninterruptibly(); //The client needs to enter information to create a scanner Scanner scanner = new Scanner(System.in); while (scanner.hasNextLine()) { String msg = scanner.nextLine(); //Send to the server through channel channel.writeAndFlush(msg + "\r\n"); } channelFuture.channel().closeFuture(); }
Start up and try, but it should be noted that you have to start the server first~
Spring boot + netty4 implement rpc framework
Well, let's get to the point. Let's use our knowledge to implement a simple rpc framework
In brief, RPC (Remote Procedure Call) Remote Procedure Call is simply understood as a node requesting services provided by another node. Call between two services is like calling a local method.
RPC sequence diagram:
RPC process:
[Client] initiates the call [[Client] data coding [Client] sends encoded data to the server [Server] receives the data sent by the client [The server] decodes the data [The server] processes the message business and returns the result value [[server] encode the result value [Server] returns the encoded result value to the client [Client] receive result value [[Client] decoding result value [Client] process the returned data business
Introduce dependency
<dependencies> <!-- SpringBoot rely on --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <!-- Spring Container context --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> </dependency> <!-- Spring to configure --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- Netty4 --> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.58.Final</version> </dependency> <!-- tool --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.5.8</version> </dependency> </dependencies>
Write server
Custom message protocol:
/** * @author zc * @date 2021/3/1 17:43 */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public class RpcMessage implements Serializable { private static final long serialVersionUID = 430507739718447406L; /** * interface Interface name */ private String name; /** * Method name */ private String methodName; /** * Parameter type */ private Class<?>[] parTypes; /** * parameter */ private Object[] pars; /** * Result value */ private Object result; }
Custom Rpc annotation:
/** * @author zc * @date 2021/3/2 15:36 */ @Target(value = {ElementType.TYPE, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface RpcServer { }
Define the ServerHandle business processor:
/** * Netty Server End Handle processing class, message body RpcMessage * Implement the ApplicationContextAware interface: this interface can load and obtain all spring bean s. * Beans that implement this interface will automatically inject ApplicationContext when the spring container is initialized * * @author ZC * @date 2021/3/1 22:15 */ @Slf4j @ChannelHandler.Sharable public class ServerHandle extends SimpleChannelInboundHandler<RpcMessage> implements ApplicationContextAware { private Map<String, Object> serviceMap; /** * setApplicationAware is automatically executed when the class is loaded by the Spring container * * @param applicationContext Spring context * @throws BeansException Abnormal information */ @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { //Get all Beans collections with @ RpcServer annotation from Spring container, map < name (object type, object full pathname), instance object > Map<String, Object> beansWithAnnotation = applicationContext.getBeansWithAnnotation(RpcServer.class); log.info("cover@RpcServer Annotation loaded Bean: {}", beansWithAnnotation); if (beansWithAnnotation.size() > 0) { Map<String, Object> map = new ConcurrentHashMap<>(16); for (Object o : beansWithAnnotation.values()) { //Gets the interface Class implemented by the instance object Class<?> anInterface = o.getClass().getInterfaces()[0]; //Get the interface class name as the Key and the instance object as the Value map.put(anInterface.getName(), o); } //Using variables to catch map s serviceMap = map; } } @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { log.info("The client is connected: {}", ctx.channel().remoteAddress()); super.channelActive(ctx); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { log.error("Abnormal information"); cause.printStackTrace(); super.exceptionCaught(ctx, cause); } @Override protected void channelRead0(ChannelHandlerContext channelHandlerContext, RpcMessage rpcMessage) throws Exception { log.info("Messages sent by the client:{}", rpcMessage); //Get instance object from Map Object service = serviceMap.get(rpcMessage.getName()); //Get call method Method method = service.getClass().getMethod(rpcMessage.getMethodName(), rpcMessage.getParTypes()); method.setAccessible(true); //Reflection calls the instance object method to get the return value Object result = method.invoke(service, rpcMessage.getPars()); rpcMessage.setResult(JSONUtil.toJsonStr(result)); log.info("Message back to client:{}", rpcMessage); //The Netty server writes the data to the Channel and sends it to the client. At the same time, a listener is added. When all data packets are sent, the Channel is closed channelHandlerContext.writeAndFlush(rpcMessage).addListener(ChannelFutureListener.CLOSE); } }
Define the NettyServer side:
/** * Netty Server * * @author zc * @date 2021/2/24 13:23 **/ @Slf4j public class NettyServer { /** * server End processor */ private final ServerHandle serverHandle; /** * Server channel */ private Channel channel; /** * constructor * * @param serverHandle server processor */ public NettyServer(ServerHandle serverHandle) { this.serverHandle = serverHandle; } /** * start-up * * @param port Boot port */ public void start(int port) { EventLoopGroup boss = new NioEventLoopGroup(1); EventLoopGroup worker = new NioEventLoopGroup(); try { ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(boss, worker) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel socketChannel) throws Exception { socketChannel.pipeline() .addLast(new ObjectDecoder(ClassResolvers.weakCachingConcurrentResolver(this.getClass().getClassLoader()))) .addLast(new ObjectEncoder()) .addLast(serverHandle); } }); final ChannelFuture channelFuture = serverBootstrap.bind(port).syncUninterruptibly(); log.info("Server start-port: {}", port); channel = channelFuture.channel(); channel.closeFuture().syncUninterruptibly(); } catch (Exception e) { boss.shutdownGracefully(); worker.shutdownGracefully(); } } /** * Close current channel */ public void stop() { channel.close(); } }
Custom rpc configuration attribute class:
/** * @author zc * @date 2021/3/4 23:38 */ @Component @ConfigurationProperties(prefix = "netty") @Data public class NettyRpcProperties { private int serverPort; }`
To create a Server side startup configuration class:
/** * NettyServer Server configuration class * * @author zc * @date 2021/3/1 18:24 */ @Slf4j @Configuration @EnableConfigurationProperties(NettyRpcProperties.class) public class ServerBeanConfig { private final NettyRpcProperties nettyRpcProperties; @Autowired public ServerBeanConfig(NettyRpcProperties nettyRpcProperties) { this.nettyRpcProperties = nettyRpcProperties; } /** * Configure ServerHandle * * @return ServerHandle Processing class */ @Bean public ServerHandle serverHandle() { return new ServerHandle(); } /** * Configuring NettyServer * * @param handle ServerHandle Processing class * @return NettyServer */ @Bean public NettyServer nettyServer(ServerHandle handle) { NettyServer nettyServer = new NettyServer(handle); // nettyServer.start(nettyRpcProperties.getServerPort()); return nettyServer; } /** * Solve the problem that the SpringBoot port cannot listen */ @Component static class NettyServerStart implements ApplicationRunner { private final NettyServer nettyServer; private final NettyRpcProperties properties; @Autowired NettyServerStart(NettyServer nettyServer, NettyRpcProperties properties) { this.nettyServer = nettyServer; this.properties = properties; } @Override public void run(ApplicationArguments args) throws Exception { log.info("===============ApplicationRunner"); if (nettyServer != null) { nettyServer.start(properties.getServerPort()); } } } }
Inject Spring container
At this time, there are two ways to make the configuration automatically injected into the Spring container take effect:
Automatic injection
stay resource Create under directory META-INF Directories, creating spring.factories file Write in the document org.springframework.boot.autoconfigure.EnableAutoConfiguration=${Package path:xxx.xxx.xxx}.${Configuration class: ServerBeanConfig} After configuration, in SpringBoot The configuration class is automatically loaded at startup.
Injection by annotation
/** * Custom SpringBoot startup annotation * Inject ServerBeanConfig configuration class * * @author ZC * @date 2021/3/1 23:48 */ @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @ImportAutoConfiguration({ServerBeanConfig.class}) public @interface EnableNettyServer { }
Write client
Create client processor ` ClientHandle
/** * @author zc * @date 2021/3/2 15:19 */ @Slf4j @ChannelHandler.Sharable public class ClientHandle extends SimpleChannelInboundHandler<RpcMessage> { /** * Define the message Map, with the connection Channel as the key and the message return value as the value */ private final ConcurrentMap<Channel, RpcMessage> rpcMessageConcurrentMap; public ClientHandle(ConcurrentMap<Channel, RpcMessage> rpcMessageConcurrentMap) { this.rpcMessageConcurrentMap = rpcMessageConcurrentMap; } @Override protected void channelRead0(ChannelHandlerContext channelHandlerContext, RpcMessage rpcMessage) throws Exception { log.info("The client receives the server message:{}", rpcMessage); rpcMessageConcurrentMap.put(channelHandlerContext.channel(), rpcMessage); } }
Create client startup class NettyClient
/** * @author ZC * @date 2021/3/1 23:30 */ @Slf4j public class NettyClient { private Channel channel; /** * Store the mapping relationship between the request number and the response object */ private final ConcurrentMap<Channel, RpcMessage> rpcMessageConcurrentMap = new ConcurrentHashMap<>(); public RpcMessage send(int port, final RpcMessage rpcMessage) { //The client needs an event loop group EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap bootstrap = new Bootstrap(); bootstrap.group(group) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel socketChannel) throws Exception { socketChannel.pipeline() .addLast(new ObjectDecoder(ClassResolvers.weakCachingConcurrentResolver(this.getClass().getClassLoader()))) .addLast(new ObjectEncoder()) .addLast(new ClientHandle(rpcMessageConcurrentMap)); } }); final ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", port).syncUninterruptibly(); log.info("Successfully connected to the server: " + channelFuture.channel().remoteAddress()); channel = channelFuture.channel(); channel.writeAndFlush(rpcMessage); log.info("Data sent successfully:{}", rpcMessage); channel.closeFuture().syncUninterruptibly(); return rpcMessageConcurrentMap.get(channel); } catch (Exception e) { log.error("client exception", e); return null; } finally { group.shutdownGracefully(); //Remove the direct mapping relationship between the request number and the response object rpcMessageConcurrentMap.remove(channel); } } public void stop() { channel.close(); } }
Define Netty client Bean post processor
/** * Netty Client Bean post processor * Implement the Spring post processor interface: BeanPostProcessor * After the Bean object is instantiated and dependency injected, add our own logic before and after calling the initialization method. Note that it is triggered after Bean instantiation and dependency injection * * @author ZC * @date 2021/3/2 23:00 */ @Slf4j public class NettyClientBeanPostProcessor implements BeanPostProcessor { private final NettyClient nettyClient; public NettyClientBeanPostProcessor(NettyClient nettyClient) { this.nettyClient = nettyClient; } /** * After instantiation and dependency injection, complete some customized initialization tasks before calling the displayed initialization * Note: method return value cannot be null * If NULL is returned, the null pointer exception will be reported in the subsequent initialization method, or the Bean instance object cannot be obtained through the getBean() method * Because the post processor took the bean instance from the Spring IoC container, the object was not put back into the IoC container again */ @Override public Object postProcessBeforeInitialization(Object bean, @Nullable String beanName) throws BeansException { //Get instance Class Class<?> beanClass = bean.getClass(); do { //Get all fields of this class Field[] fields = beanClass.getDeclaredFields(); for (Field field : fields) { //Judge whether the field has @ RpcServer if (field.getAnnotation(RpcServer.class) != null) { field.setAccessible(true); try { //Get the proxy object of this class through JDK dynamic proxy Object o = Proxy.newProxyInstance(field.getType().getClassLoader(), new Class[]{field.getType()}, new ClientInvocationHandle(nettyClient)); //Inject the proxy class into this field field.set(bean, o); log.info("Create proxy class ===>>> {}", beanName); } catch (IllegalAccessException e) { log.error(e.getMessage()); } } } } while ((beanClass = beanClass.getSuperclass()) != null); return bean; } /** * Execute when instantiation, dependency injection and initialization are completed * Note: method return value cannot be null * If NULL is returned, the null pointer exception will be reported in the subsequent initialization method, or the Bean instance object cannot be obtained through the getBean() method * Because the post processor took the bean instance from the Spring IoC container, the object was not put back into the IoC container again */ @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { // You can perform different processing operations according to the beanName return bean; } /** * JDK Dynamic agent processor */ static class ClientInvocationHandle implements InvocationHandler { private final NettyClient nettyClient; public ClientInvocationHandle(NettyClient nettyClient) { this.nettyClient = nettyClient; } /** * Proxy method call * * @param proxy proxy class * @param method method * @param args parameter * @return Return value */ @Override public Object invoke(Object proxy, Method method, Object[] args) { //Assembling Netty parameters RpcMessage rpcMessage = RpcMessage.builder() .name(method.getDeclaringClass().getName()) .methodName(method.getName()) .parTypes(method.getParameterTypes()) .pars(args) .build(); //Call Netty to send data RpcMessage send = nettyClient.send(1111, rpcMessage); log.info("Received server data:{}, Return result value ====>>>>{}", send, send.getResult()); return send.getResult(); } } }
Define client configuration classes
/** * @author zc * @date 2021/3/1 18:24 */ @Configuration public class ClientBeanConfig { @Bean public NettyClient nettyClient() { return new NettyClient(); } @Bean public NettyClientBeanPostProcessor nettyClientBeanPostProcessor(NettyClient nettyClient) { return new NettyClientBeanPostProcessor(nettyClient); } }
Finally, like the server, inject the Spring container
/** * @author ZC * @date 2021/3/1 23:48 */ @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @ImportAutoConfiguration({ClientBeanConfig.class}) public @interface EnableNettyClient { }
So far, our spring boot + netty4 has implemented the simplest rpc framework pattern; Then we can reference our own rpc dependencies.
Finally, execute the maven command
mvn install
Netty RPC examples example
Interface service
There's nothing in pom...
Define an interface
/** * @author zc * @date 2021/3/1 17:55 */ public interface Test1Api { void test(); void test(int id, String name); String testStr(int id); Object testObj(); }
RPC server server
Normal SpringBoot project
Introducing pom
<!-- custom rpc rely on --> <dependency> <groupId>cn.happyloves.rpc</groupId> <artifactId>netty-rpc</artifactId> <version>0.0.1</version> </dependency> <!-- Interface dependency --> <dependency> <groupId>cn.happyloves.netty.rpc.examples.api</groupId> <artifactId>rpc-api</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency>
Configuration properties
# apply name spring.application.name=rpc-server # Application service WEB access port server.port=8080 netty.server-port=1111
Create an entity class
/** * @author ZC * @date 2021/3/2 23:59 */ @Data public class Account implements Serializable { private static final long serialVersionUID = 667178018106218163L; private Integer id; private String name; private String username; private String password; }
Create Server to implement Test1Api interface
/** * @author ZC * @date 2021/3/2 23:59 */ @Slf4j @Service @RpcServer public class TestServiceImpl implements Test1Api { @Override public void test() { log.info("111111111"); } @Override public void test(int id, String name) { log.info("222222222,{},{}", id, name); } @Override public String testStr(int id) { log.info("33333333333333333,{}", id); return "33333333333333333 " + id; } @Override public Object testObj() { log.info("444444444444444444"); Account account = new Account(); account.setName("Zhang San"); return account; } }
Finally, @ enablenetyserver is added to the SpringBoot boot class
/** * @author ZC * @date 2021/3/2 23:55 */ @EnableNettyServer @SpringBootApplication public class RpcServerApplication { public static void main(String[] args) { SpringApplication.run(RpcServerApplication.class, args); } }
RPC server client
Introducing pom dependency
<dependency> <groupId>cn.happyloves.rpc</groupId> <artifactId>netty-rpc</artifactId> <version>0.0.1</version> </dependency> <dependency> <groupId>cn.happyloves.netty.rpc.examples.api</groupId> <artifactId>rpc-api</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency>
Create Controller
/** * @author ZC * @date 2021/3/3 0:04 */ @RestController public class ClientController { @RpcServer private Test1Api testServiceImpl; @GetMapping("/test1") public void test() { testServiceImpl.test(); } @GetMapping("/test2") public void test(int id, String name) { testServiceImpl.test(id, name); } @GetMapping("/test3") public String testStr(int id) { return testServiceImpl.testStr(id); } @GetMapping("/test4") public Object testObj() { return testServiceImpl.testObj(); } }
Finally, add the annotation @ EnableNettyClient on the startup class
@EnableNettyClient @SpringBootApplication public class RpcClientApplication { public static void main(String[] args) { SpringApplication.run(RpcClientApplication.class, args); } }
First run the server, then run the client, and then call the client interface to see that the server can receive the message from the client, then the server processes and returns, and the client receives and returns...
At this point, a small demo is completed.
Of course, there are still many needs to be handled in the future. For example, in the current demo, each communication between the client needs to create an instance to connect, the service registration, the client and the server are the same application, and so on. This will be improved slowly later
Continue to support Remi sauce~~
Article source: https://www.jianshu.com/p/f94a0d971b7b