29 Add Meal HTTP Protocol JSON Rpcdubbo Cross Language Is So Simple

29 Add Meal HTTP Protocol JSONRPCDubbo Cross-Language is So Simple #

In the previous lesson, when introducing the implementation of Protocol and Invoker, we focused on AbstractProtocol and DubboInvoker. In fact, there is another branch of implementation for Protocol called AbstractProxyProtocol, as shown in the following diagram:

Lark20201103-162545.png

Inheritance diagram of AbstractProxyProtocol

From the diagram, we can see that the Protocol implementations of gRPC, HTTP, WebService, Hessian, Thrift, and other protocols all inherit from the abstract class AbstractProxyProtocol.

In today’s diverse technology stack, many companies use languages such as Node.js, Python, Rails, Go, etc. to develop web applications, while many services are implemented using Java technology stack. This leads to a significant demand for cross-language invocation. As an RPC framework, Dubbo naturally hopes to achieve cross-language invocation. Currently, Dubbo uses the “HTTP Protocol + JSON-RPC” approach to achieve this, where both the HTTP protocol and JSON are naturally cross-language standards with mature libraries available in various languages.

Now let’s focus on analyzing Dubbo’s support for the HTTP protocol. First, I will introduce the basics of JSON-RPC and help you get started quickly through an example. Then I will explain the specific implementation of HttpProtocol in Dubbo, which combines the HTTP protocol with JSON-RPC to achieve cross-language invocation.

JSON-RPC #

The HTTP protocol supported by Dubbo is actually based on the JSON-RPC protocol.

JSON-RPC is a cross-language remote procedure call protocol based on JSON. The XML-RPC and WebService protocols supported by modules such as dubbo-rpc-xml and dubbo-rpc-webservice in Dubbo, like JSON-RPC, are all text-based protocols. However, JSON-RPC has a simpler and more compact format compared to XML, WebService, etc. Compared to binary protocols such as Dubbo and Hessian, JSON-RPC is easier to debug and implement. Therefore, JSON-RPC is a very excellent remote procedure call protocol.

In the Java ecosystem, there are many mature JSON-RPC frameworks, such as jsonrpc4j and jpoxy. Among them, jsonrpc4j is lightweight, easy to use, and can be used independently or seamlessly integrated with Spring, making it very suitable for Spring-based projects.

Now let’s take a look at the basic format of a JSON-RPC request:

{
    "id": 1,
    "method": "sayHello",
    "params": [
        "Dubbo json-rpc"
    ]
}

The fields in a JSON-RPC request have the following meanings:

  • The id field is used to uniquely identify a remote procedure call.
  • The method field specifies the name of the method to be called.
  • The params array represents the parameters passed to the method. If the method doesn’t take any parameters, an empty array is passed.

After the JSON-RPC server receives a call request, it will look for the corresponding method and invoke it. Then it formats the return value of the method into the following format and returns it to the client:

{
    "id": 1,
    "result": "Hello Dubbo json-rpc",
    "error": null
}

The fields in a JSON-RPC response have the following meanings:

  • The id field is used to uniquely identify a remote procedure call, and its value is consistent with the id field in the request.
  • The result field records the return value of the method. If there is no return value, it returns null. If an error occurs during the invocation, it returns null as well.
  • The error field represents the exception information when an error occurs during the invocation. If the method executes without exceptions, this field is null.

Basic Usage of jsonrpc4j #

Dubbo uses the jsonrpc4j library to implement the JSON-RPC protocol. Now let’s use jsonrpc4j to write a simple JSON-RPC server example and client example. Through these two examples, we will demonstrate the basic usage of jsonrpc4j.

First, we need to create the domain classes and service interface that both the server and client require. Let’s start by creating a User class as the most basic data object:

public class User implements Serializable {

    private int userId;
    private String name;
    private int age;

    // Getter/setter methods and toString() method for the above fields are omitted

}

Next, let’s create a UserService interface as the service interface. It defines 5 methods for creating User, querying User and related information, and deleting User:

public interface UserService {
    User createUser(int userId, String name, int age); 
    User getUser(int userId);
    String getUserName(int userId);
    int getUserId(String name);
    void deleteAll();
}

UserServiceImpl is the implementation class of the UserService interface. It manages User objects using an ArrayList collection. The specific implementation is as follows:

public class UserServiceImpl implements UserService {
    // Manages all User objects
    private List<User> users = new ArrayList<>(); 

    @Override
    public User createUser(int userId, String name, int age) {
        System.out.println("createUser method");
        User user = new User();
        user.setUserId(userId);
        user.setName(name);
        user.setAge(age);
        users.add(user); // Create a new User object and add it to the users collection
        return user;
    }

    @Override
    public User getUser(int userId) {
        System.out.println("getUser method");
        return users.stream().filter(u -> u.getUserId() == userId).findAny().get();
    }

    @Override
    public String getUserName(int userId) {
        System.out.println("getUserName method");
        return getUser(userId).getName();
    }

    @Override
    public int getUserId(String name) {
        System.out.println("getUserId method");
        return users.stream().filter(u -> u.getName().equals(name)).findAny().get().getUserId();
    }

    @Override
    public void deleteAll() {
        System.out.println("deleteAll");
        users.clear(); // Clear the users collection
    }
}

} }

The core of the entire user management business is roughly like this. Now let’s see how the server associates UserService with JSON-RPC.

First, we create the RpcServlet class, which is a subclass of HttpServlet and overrides the service() method of HttpServlet. As we know, when HttpServlet receives GET and POST requests, it will eventually call its service() method for processing. HttpServlet will also wrap the HTTP request and response into HttpServletRequest and HttpServletResponse and pass them into the service() method. In the implementation of RpcServlet, a JsonRpcServer is created, and the HTTP request is delegated to JsonRpcServer for processing in the service() method:

public class RpcServlet extends HttpServlet {

    private JsonRpcServer rpcServer = null;

    public RpcServlet() {

        super();

        // JsonRpcServer will invoke methods in UserServiceImpl according to json-rpc requests

        rpcServer = new JsonRpcServer(new UserServiceImpl(), UserService.class);

    }

    @Override

    protected void service(HttpServletRequest request,

                            HttpServletResponse response) throws ServletException, IOException {

        rpcServer.handle(request, response);

    }

}

Finally, we create a JsonRpcServer as the entry class for the server. In its main() method, Jetty is started as the web container. The implementation is as follows:

public class JsonRpcServer {

    public static void main(String[] args) throws Throwable {

        // Server listens on port 9999

        Server server = new Server(9999);

        // Associate with an existing context

        WebAppContext context = new WebAppContext();

        // Set the descriptor location

        context.setDescriptor("/dubbo-demo/json-rpc-demo/src/main/webapp/WEB-INF/web.xml");

        // Set the Web content context path

        context.setResourceBase("/dubbo-demo/json-rpc-demo/src/main/webapp");

        // Set the context path

        context.setContextPath("/");

        context.setParentLoaderPriority(true);

        server.setHandler(context);

        server.start();

        server.join();

    }

}

The web.xml configuration file used here is as follows:

<?xml version="1.0" encoding="UTF-8"?>

<web-app

        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

        xmlns="http://xmlns.jcp.org/xml/ns/javaee"

        xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"

        version="3.1">

    <servlet>

        <servlet-name>RpcServlet</servlet-name>

        <servlet-class>com.demo.RpcServlet</servlet-class>

    </servlet>

    <servlet-mapping>

        <servlet-name>RpcServlet</servlet-name>

        <url-pattern>/rpc</url-pattern>

    </servlet-mapping>

</web-app>

After completing the server-side code, now let’s continue to write the JSON-RPC client. In JsonRpcClient, a JsonRpcHttpClient is created, and the client sends requests to the server through the JsonRpcHttpClient:

public class JsonRpcClient {

    private static JsonRpcHttpClient rpcHttpClient;

    public static void main(String[] args) throws Throwable {

        // Create JsonRpcHttpClient

        rpcHttpClient = new JsonRpcHttpClient(new URL("http://127.0.0.1:9999/rpc"));

        JsonRpcClient jsonRpcClient = new JsonRpcClient();

        jsonRpcClient.deleteAll(); // Call the deleteAll() method to delete all Users

        // Call the createUser() method to create a User

        System.out.println(jsonRpcClient.createUser(1, "testName", 30));

        // Call the getUser(), getUserName(), and getUserId() methods to query

        System.out.println(jsonRpcClient.getUser(1));

        System.out.println(jsonRpcClient.getUserName(1));

        System.out.println(jsonRpcClient.getUserId("testName"));

    }

    public void deleteAll() throws Throwable {

        // Call the deleteAll() method on the server

        rpcHttpClient.invoke("deleteAll", null);

    }

    public User createUser(int userId, String name, int age) throws Throwable {

        Object[] params = new Object[]{userId, name, age};

        // Call the createUser() method on the server

        return rpcHttpClient.invoke("createUser", params, User.class);

    }

    public User getUser(int userId) throws Throwable {

        Integer[] params = new Integer[]{userId};

        // Call the getUser() method on the server

        return rpcHttpClient.invoke("getUser", params, User.class);

    }

    public String getUserName(int userId) throws Throwable {

        Integer[] params = new Integer[]{userId};

        // Call the getUserName() method on the server

        return rpcHttpClient.invoke("getUserName", params, String.class);

    }
public int getUserId(String name) throws Throwable {

    String[] params = new String[]{name};

    // Call the getUserId() method on the server

    return rpcHttpClient.invoke("getUserId", params, Integer.class);

}

}

// Output:

// User{userId=1, name='testName', age=30}

// User{userId=1, name='testName', age=30}

// testName

// 1

AbstractProxyProtocol #

In the export() method of AbstractProxyProtocol, it first checks the exporterMap cache based on the URL. If the query fails, it calls ProxyFactory.getProxy() method to wrap the Invoker into a proxy class of the business interface. Then, it starts the underlying ProxyProtocolServer by calling the doExport() method implemented by the subclass and initializes the serverMap collection. The specific implementation is as follows:

public <T> Exporter<T> export(final Invoker<T> invoker) throws RpcException {

    // First query the exporterMap collection

    final String uri = serviceKey(invoker.getUrl());

    Exporter<T> exporter = (Exporter<T>) exporterMap.get(uri);

    if (exporter != null) {

        if (Objects.equals(exporter.getInvoker().getUrl(), invoker.getUrl())) {

            return exporter;

        }

    }

    // Use ProxyFactory to create a proxy class and wrap the Invoker into a proxy class of the business interface

    final Runnable runnable = doExport(proxyFactory.getProxy(invoker, true), invoker.getInterface(), invoker.getUrl());

    // The Runnable returned by the doExport() method is a callback that will destroy the underlying Server and will be called in the unexport() method

    exporter = new AbstractExporter<T>(invoker) {

        public void unexport() {

            super.unexport();

            exporterMap.remove(uri);

            if (runnable != null) {

                runnable.run();

            }

        }

    };

    exporterMap.put(uri, exporter);

    return exporter;

}

In the doExport() method of the HttpProtocol, similar to the implementation of the DubboProtocol introduced earlier, it also starts a RemotingServer. To adapt to various HTTP servers, such as Tomcat, Jetty, etc., Dubbo abstracts an HttpServer interface at the Transporter layer.

Drawing 1.png

Location of the dubbo-remoting-http module

The entry point of the dubbo-remoting-http module is the HttpBinder interface, which is annotated with @SPI and is an extension interface with three extension implementations. The default implementation is JettyHttpBinder, as shown in the following diagram:

Drawing 2.png

Inheritance diagram of JettyHttpBinder

The bind() method in the HttpBinder interface is annotated with @Adaptive and selects the corresponding HttpBinder extension implementation based on the server parameter in the URL. Different HttpBinder implementations return the corresponding HttpServer implementation. The inheritance diagram of the HttpServer is shown in the following diagram:

Drawing 3.png

Inheritance diagram of HttpServer

Here, we take JettyHttpServer as an example to briefly introduce the implementation of HttpServer. In JettyHttpServer, the Jetty Server is initialized, and the thread pool and request handler used by the Jetty Server are configured:

public JettyHttpServer(URL url, final HttpHandler handler) {

    // Initialize the url and handler fields in AbstractHttpServer

    super(url, handler); 

    this.url = url;

    DispatcherServlet.addHttpHandler( // Add HttpHandler

    url.getParameter(Constants.BIND_PORT_KEY, url.getPort()), handler);

    // Create thread pool

    int threads = url.getParameter(THREADS_KEY, DEFAULT_THREADS);

    QueuedThreadPool threadPool = new QueuedThreadPool();

    threadPool.setDaemon(true);

    threadPool.setMaxThreads(threads);

    threadPool.setMinThreads(threads);

    // Create Jetty Server

    server = new Server(threadPool);

    // Create ServerConnector and specify the bound ip and port

    ServerConnector connector = new ServerConnector(server);

    String bindIp = url.getParameter(Constants.BIND_IP_KEY, url.getHost());

    if (!url.isAnyHost() && NetUtils.isValidLocalHost(bindIp)) {

        connector.setHost(bindIp);

    }

    connector.setPort(url.getParameter(Constants.BIND_PORT_KEY, url.getPort()));

    server.addConnector(connector);

    // Create ServletHandler and associate it with Jetty Server, DispatcherServlet handles all requests

    ServletHandler servletHandler = new ServletHandler();

    ServletHolder servletHolder = servletHandler.addServletWithMapping(DispatcherServlet.class, "/*");

    servletHolder.setInitOrder(2);

    // Create ServletContextHandler and associate it with Jetty Server

    ServletContextHandler context = new ServletContextHandler(server, "/", ServletContextHandler.SESSIONS);

    context.setServletHandler(servletHandler);

    ServletManager.getInstance().addServletContext(url.getParameter(Constants.BIND_PORT_KEY, url.getPort()), context.getServletContext());

    server.start();

}

We can see that all requests received by JettyHttpServer will be delegated to DispatcherServlet, which is an HttpServlet implementation. The service() method of DispatcherServlet delegates the request to the HttpHandler corresponding to the port:

protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // Query the HttpHandler object corresponding to the port from the HANDLERS collection

    HttpHandler handler = HANDLERS.get(request.getLocalPort());

    if (handler == null) { // No HttpHandler implementation for the port

        response.sendError(HttpServletResponse.SC_NOT_FOUND, "Service not found.");

    } else { // Delegate the request to the HttpHandler object

        handler.handle(request, response);

    }

}
            handler.handle(request, response);
    
        }
    
    }
    

After understanding the abstraction of HttpServer in Dubbo and the core of JettyHttpServer, let's continue analyzing the doExport() method in HttpProtocol.

In the doExport() method of HttpProtocol, a HttpServer object is created using HttpBinder and put into the serverMap to handle HTTP requests. The initialization of HttpServer and the HttpHandler used to handle requests are implemented as inner classes in HttpProtocol. Similar HttpHandler implementation can be found in other RPC protocol implementations based on the HTTP protocol, as shown in the following diagram:

![Drawing 4.png](../images/CgqCHl-hFGCARUTkAABNZnY-dJg331.png)

In the handle() method of the HttpHandler in the InternalHandler class in HttpProtocol, the request is delegated to the JsonRpcServer object recorded in the skeletonMap collection for processing:
    
```java
    public void handle(HttpServletRequest request, HttpServletResponse response) throws ServletException {

        String uri = request.getRequestURI();

        JsonRpcServer skeleton = skeletonMap.get(uri);

        if (cors) { ... // handle cross-origin issue }

        if (request.getMethod().equalsIgnoreCase("OPTIONS")) {

            response.setStatus(200); // handle OPTIONS request

        } else if (request.getMethod().equalsIgnoreCase("POST")) {

            // only handle POST request

            RpcContext.getContext().setRemoteAddress(request.getRemoteAddr(), request.getRemotePort());

            skeleton.handle(request.getInputStream(), response.getOutputStream());

        } else { // other types of method requests, such as GET request, return 500 directly

            response.setStatus(500);

        }

    }

The JsonRpcServer object in the skeletonMap collection is initialized together with the HttpServer object in the doExport() method. Finally, let’s take a look at the implementation of the doExport() method in HttpProtocol:

    protected <T> Runnable doExport(final T impl, Class<T> type, URL url) throws RpcException {

        String addr = getAddr(url);

        // check serverMap cache first

        ProtocolServer protocolServer = serverMap.get(addr);

        if (protocolServer == null) { // cache check failed

            // create HttpServer, note that the InternalHandler implementation is passed in

            RemotingServer remotingServer = httpBinder.bind(url, new InternalHandler(url.getParameter("cors", false)));

            serverMap.put(addr, new ProxyProtocolServer(remotingServer));

        }

        // create JsonRpcServer object and record the mapping between URL and JsonRpcServer in the skeletonMap collection

        final String path = url.getAbsolutePath();

        final String genericPath = path + "/" + GENERIC_KEY;

        JsonRpcServer skeleton = new JsonRpcServer(impl, type);

        JsonRpcServer genericServer = new JsonRpcServer(impl, GenericService.class);

        skeletonMap.put(path, skeleton);

        skeletonMap.put(genericPath, genericServer);

        return () -> { // return a Runnable callback executed in the unexport() method in Exporter

            skeletonMap.remove(path);

            skeletonMap.remove(genericPath);

        };

    }

After introducing the implementation of service export in HttpProtocol, let’s take a look at the implementation of service reference related methods in HttpProtocol, namely the protocolBindingRefer() method. This method first creates a proxy for the business interface using the doRefer() method. It integrates with the jsonrpc4j library and Spring, and creates a JsonRpcHttpClient object in its afterPropertiesSet() method:

    public void afterPropertiesSet() {

        ... ... // omitting the ObjectMapper and other objects

        try {

            // create JsonRpcHttpClient for sending json-rpc requests

            jsonRpcHttpClient = new JsonRpcHttpClient(objectMapper, new URL(getServiceUrl()), extraHttpHeaders);

            jsonRpcHttpClient.setRequestListener(requestListener);

            jsonRpcHttpClient.setSslContext(sslContext);

            jsonRpcHttpClient.setHostNameVerifier(hostNameVerifier);

        } catch (MalformedURLException mue) {

            throw new RuntimeException(mue);

        }

    }

Now let’s look at the implementation of the doRefer() method:

    protected <T> T doRefer(final Class<T> serviceType, URL url) throws RpcException {

        final String generic = url.getParameter(GENERIC_KEY);

        final boolean isGeneric = ProtocolUtils.isGeneric(generic) || serviceType.equals(GenericService.class);

        JsonProxyFactoryBean jsonProxyFactoryBean = new JsonProxyFactoryBean();

        ... // omitting other initialization logic

        jsonProxyFactoryBean.afterPropertiesSet();

        // return a proxy object of type serviceType

        return (T) jsonProxyFactoryBean.getObject(); 

    }

In the protocolBindingRefer() method of AbstractProxyProtocol, the proxy object returned by the doRefer() method is converted to an Invoker object using the ProxyFactory.getInvoker() method, and it is recorded in the Invokers collection, as shown in the following code:

    protected <T> Invoker<T> protocolBindingRefer(final Class<T> type, final URL url) throws RpcException {

        final Invoker<T> target = proxyFactory.getInvoker(doRefer(type, url), type, url);

        Invoker<T> invoker = new AbstractInvoker<T>(type, url) {

            @Override

            protected Result doInvoke(Invocation invocation) throws Throwable {

                Result result = target.invoke(invocation);

                // omit exception handling logic

                return result;

            }

        };

        invokers.add(invoker); // add Invoker to the invokers collection

        return invoker;

    }

Summary #

In this lesson, we have focused on how to achieve cross-language invocation in Dubbo using the “HTTP protocol + JSON-RPC” approach. First, we introduced the basic format of requests and responses in JSON-RPC, as well as the basic usage of the jsonrpc4j library. Then, we also detailed the core classes in Dubbo, such as AbstractProxyProtocol and HttpProtocol, and analyzed the implementation of the “HTTP protocol + JSON-RPC” approach in Dubbo.