Java RPC framework based on Netty/Zookeeper implementation

A small RPC framework based on Spring Boot Starter. This RPC framework is not written to repeat the wheel, but for the purpose of learning, by writing a RPC framework to achieve knowledge learning and application purposes. Simple RPC framework (Danran-RPC), the bottom layer uses Netty for network communication, using Zookeeper as the registry. This project can be packaged with Maven and run directly with other projects. Another warehouse:

  • https://gitee.com/lengdanran/danran-rpc-debug
  • https://github.com/lengdanran/danran-rpc-debug

Integrated debugging warehouse for the project, in which you can debug danran-RPC source code

Quick learning

Example code: gitee.com/lengdanran/…

  • https://gitee.com/lengdanran/danran-rpc.git
  • https://github.com/lengdanran/danran-rpc.git

Generate local Maven dependencies

Clone the code locally from the above git repository address, then go to the project poM directory, and run the Maven installation command:

mvn clean install
Copy the code

The service provider-consumer introduces this Maven dependency simultaneously

Importing packaged maven dependencies, because the RPC framework uses a logging framework that conflicts with Springboot itself, is a good ideadanran-rpcLog dependencies are removed to avoid program failure to start.

<dependency>
    <groupId>danran.rpc</groupId>
    <artifactId>danran-rpc-spring-boot-starter</artifactId>
    <version>1.0.1</version>
    <! -- Remove log conflicts -->
    <exclusions>
        <exclusion>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
        </exclusion>
        <exclusion>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
        </exclusion>
    </exclusions>
</dependency>
Copy the code

Configure registries for both service providers and consumers (replace with your own ZK address)

Add the following configuration to your project’s application.properties

danran.rpc.register-address=121.4.195.203:2181 # Zk address
danran.rpc.protocol=danran
danran.rpc.server-port=6666
Copy the code

Service provider usage examples

package provider.service;

import common.api.BookService;
import common.entity.Book;
import danran.rpc.annotation.RPCService;
import org.springframework.beans.factory.annotation.Autowired;
import provider.mapper.BookMapper;

import java.util.List;

/ * * *@Classname BookServiceImpl
 * @Description TODO
 * @Date2021/8/24 now *@Created by ASUS
 */
@RPCService
public class BookServiceImpl implements BookService {

    @Autowired
    private BookMapper bookMapper;

    / * * *@returnAll book information */
    @Override
    public List<Book> getAllBooks(a) {
        returnbookMapper.getAllBooks(); }}Copy the code

By adding the @rpcService annotation to a specific service implementation class, the class can be registered with ZooKeeper as a service provider so that the consumer can discover the service. After adding the annotation, it will be injected into the Spring container as a Component of Spring. (No longer need to add @Component annotation)

Service Consumer Example

package consumer.controller;

import common.api.BookService;
import common.entity.Book;
import danran.rpc.annotation.InjectService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/ * * *@Classname ConsumerController
 * @Description TODO
 * @Date 2021/8/24 19:04
 * @Created by ASUS
 */
@RestController
@RequestMapping("/consumer")
public class ConsumerController {

    @InjectService
    private BookService bookService;

    @GetMapping("/get_all_books")
    public List<Book> getAllBooks(a) {
        returnbookService.getAllBooks(); }}Copy the code

Using the @InjectService annotation, you can start service discovery and then automatically inject proxy objects for remote services.

Sample run results

Start two Springboot Web services, the Provider provides the implementation of querying the database, and the consumer remotely invokes the service.

  • Provider Startup Logs

  • Consumer Startup log

Start the Postman interface debugging tool and access the service interface:

Interface returns data successfully, check program log:

RPC principle

define

Remote Procedure Call (RPC), simply understood, means that a local service is required, and the specific service is provided by another independent server. We can notify the server to execute the corresponding service through the network or other means, and then return the information we care about.

RPC: Call to check the results

Local: you

Remote service: your child’s homeroom teacher

Now you want to know the result of your child’s final exam, and the result is in the homeroom teacher, you call the homeroom teacher, and the homeroom teacher checks and tells you what the result is, which is an RPC process.

Computer 1 want to invoke a service of 2, make an RPC call to the communication middleware, after communication middleware will ask marshalling to request is sent to the computer 2 through the network communication middleware, reads the request packets from the network communication middleware, solutions group, then the computer 2 internal local call, after the call, A return data is obtained, and the communication middleware marshals the response data and sends it to computer 1 over the network.

There is a problem: how does computer 1 (the consumer) know the address of the remote service?

Service discovery

To solve this problem, you can use a third party, that is, the person who manages the service, once there is a service request, tell the other party the address of the service you requested.

Therefore, the current popular RPC framework Dubbo adopts the following architecture:

Mainly include consumers, registry, the provider tripartite structure character, consumers through the service registry to subscribe to their care, registry will be registered service address and relevant information is returned to the consumers, consumers through specific protocol sends the data to the socket in the finally sent to the network by the network card, finally get the service provider’s response data.

Danran xml-rpc implementation

File structure

  • Annotation: Contains the definition of InjectService and RPCService annotations, which are used to inject and publish services
  • Client: Implementation of client service discovery, service proxy, and network communication
  • Common: The common module of the framework that contains the definition of the protocol, serialization, and common Entity
  • Config: automatic configuration class for Spring container startup
  • Properties: user-defined parameter
  • Server: Registration, publishing, and startup of RPC services

Implementation component

The implementation of Danran-RPC refers to the implementation of Dubbo, mainly using the following components:

  • Network communication — Netty
  • Registries – Zookeeper (different registries should be implemented later: Redis, Nacos, Eureka, etc.)
  • Proxy – Java dynamic proxy
  • The IOC – Spring

The overall architecture

Start the process

Client package implementation

According to the principle of RPC, the client needs to group and send the request data through the network communication middleware and get the corresponding response data.

NetClient

The interface defines the network request client, defines the network request specification, can be implemented by different network communication framework, users can customize the implementation class, can be integrated into the Danran-RPC framework. Netty is used here.

public byte[] sendRequest(byte[] data, Service service) throws InterruptedException {
    String[] ip_port = service.getAddress().split(":");
    String ip = ip_port[0];
    int port = Integer.parseInt(ip_port[1]);

    SendHandler sendHandler = new SendHandler(data);
    byte[] rsp;
    // Configure the client
    EventLoopGroup group = new NioEventLoopGroup();
    try {
        Bootstrap bootstrap = new Bootstrap();
        // Initialize the channel
        bootstrap.group(group).channel(NioSocketChannel.class).option(ChannelOption.TCP_NODELAY, true).handler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel ch) { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(sendHandler); }});// Start the connection
        bootstrap.connect(ip, port).sync();
        rsp = (byte[]) sendHandler.rspData();
        logger.info("Send Request and get the response: {}", rsp);
    } finally {
        // Release thread group resources
        group.shutdownGracefully();
    }
    return rsp;
}
Copy the code

ClientProxyFactory

This is the client proxy factory, a proxy class for creating remote services that encapsulates the marshalling and unmarshalling of request and response data. The factory class contains the service discovery instance object and the concrete network layer implementation, which can be used with the service discovery instance to discover services and send data. The getProxy(Class

clazz) method returns the clazz proxy object. The core code is:

return (T) this.objectCache.computeIfAbsent(
    clazz, 
    cls -> newProxyInstance(
        cls.getClassLoader(), 
        newClass<? >[]{cls},new ClientInvocationHandler(cls)));
Copy the code

This code builds a proxy object based on the Clazz Class, which is implemented as ClientInvocationHandler. JDK dynamic proxy is used to achieve the InvocationHandler interface.

The key is the implementation of the invoke() method. Call a method of a proxied object by reflection.

First, it will get the proxied object’s full name through the proxied object clazz, and the service discovery instance will go to the service registry to find the corresponding remote service with the obtained full name.

List<Service> services = serviceDiscovery.getServices(serviceName);
Copy the code

After obtaining the specific remote Service, a RequestWrap class is generated that encapsulates the Service name, proxied method name, proxied method parameter type, and proxied method parameter. After that, the encapsulated request will be marshalled according to the protocol to get data, and finally the request will be sent through NetClient at the network layer. Finally, the response data is obtained from the network layer implementation, the ResponseWrap is obtained from the ungroup response, and the specific response data is obtained from the response encapsulation.

ServiceDiscovery — ServiceDiscovery

Service discovery will query the remote service registration information from the registry, and then return. In Danran-RPC, Zookeeper is currently implemented as the registry, so the specific implementation class is ZkDiscovery. The specific implementation process of service discovery is as follows:

The basis for service discovery is exactly the same as the basis for service registration, ensuring that registered services will be discovered.

  • Concatenate the ZNode path based on the provided service name (default is full class name)
  • Get the names of all the children in this path from Zookeeper (the names of the children are JSON strings of the specific remote service information following URLEncode)
  • Decoded to the service provider and returned based on the received service offering information

Server package implementation

This package is the implementation of RPC service registration and service injection package, including the register module and service startup module.

The register module

This module provides a ServiceRegister interface at the top, which is used to define service registration specifications. Users can implement this interface, customize the registration specifications and logic, Danran xml-rpc concrete implementation class for ZookeeperExportServiceRegister, provide a Zookeeper registry implementation.

ZookeeperExportServiceRegister

This class (hereafter referred to as ZESR) implements the registration interface ServiceRegister and provides the Zookeeper registration implementation.

ZESR starts a zkClient based on the Zookeeper address provided by the user.

Service register — register()

  • Gets the host address of the service provider
  • A user provides a service port, port, from which the service is exposed
  • Encapsulating service wrapper classesService
  • Invoking service exposureexportService()

Service exposure — exportService()

  • Get the Service name (full class name of the Service implementation interface) from the Service
  • Service address concatenation (logically consistent with service discovery)
  • Concrete services (including the service address of the service provider, as well as the service implementation interface)
{
  	"address":"192.168.25.1:6666"."name":"common.api.BookService"."protocol":"danran"
}
Copy the code
  • Register the service with Zookeeper – create the node
/*** * service exposed **@paramService Specifies the exposed service */
private void exportService(Service service) {
    String serviceName = service.getName();
    String uri = JSON.toJSONString(service);
    try {
        uri = URLEncoder.encode(uri, "UTF-8");
    } catch (UnsupportedEncodingException e) {
        e.printStackTrace();
    }
    // Abstract services locate nodes
    String servicePath = "/rpc/" + serviceName + "/service";
    logger.info("Abstract service [" + serviceName + "] registered address ==" + servicePath);

    if(! zkClient.exists(servicePath)) { zkClient.createPersistent(servicePath,true);
    }
    // Address of a specific service instance
    String uriPath = servicePath + "/" + uri;
    logger.info("Specific service instance registered node ==" + uriPath);

    if (zkClient.exists(uriPath)) zkClient.delete(uriPath);// If so, delete the update
    zkClient.createEphemeral(uriPath);// Create a transient node
}
Copy the code

RPC handler — DefaultRpcProcessor

This class defines the specific process of RPC Service startup, implements Spring ApplicationListener, supports Service startup exposure and automatic Service injection.

Internal contains three members:

This class does two things:

  • The user to use@RPCServiceService publication of the tag
  • The user to use@InjectServiceService injection of the tag

The SPRing-supplied IOC container is used here to fetch beans from the container for operation.

RPC Service Startup
  1. Pull everything from the Spring container@RPCServiceMark the bean
  2. Iterate over the resulting list of beans, each of which is an instance of the provider object of the actual service
  3. Get the bean’s Class object, Clazz, and use Clazz to get its implemented interface (which is consistent with the client interface to associate the client with the server).
  4. The actual service provider is wrapped in the parent class (interface), and then the service registry module is invoked to register with Zookeeper
  5. Start the RPC service implementation

With services

Inject the service that the user marked with @InjectService.

The Spring container is also used to obtain beans in the container, and then determine whether each bean has a service that needs to be injected. Once an attribute is found, the attribute is injected into a proxy object through reflection. Once the client initiates a service call, the invoke() method of the proxy object will be triggered to execute the proxy execution. Get the required return data.