This article is more than one year old. Older articles may contain outdated content. Check that the information in the page has not become incorrect since its publication.
dubbo2.js is a Node.js Dubbo client contributed by Qianmi to the Dubbo community. It provides support for the native Dubbo protocol in Node.js, making RPC calls between Node.js and Java, two heterogeneous languages, convenient and efficient.
Microservice architecture has become the trend in today’s internet architecture, and discussions about microservices occupy most of the various technical conferences. The most widely used service governance framework in China is undoubtedly Dubbo, an open-source project from Alibaba. Qianmi has also chosen Dubbo as its microservice governance framework. On the other hand, like most internet companies, Qianmi has a diverse range of development languages, with most backend services supported by Java, while each business line has the freedom to choose its own development language, leading to issues of calling Node.js, Python, and Go in a multi-language environment. Cross-language invocation is a broad and challenging topic, and several solutions frequently mentioned in the industry are as follows:
What do we talk about when we discuss cross-language invocation? Through the above common and mature solutions, we can conclude that the approaches to address cross-language invocation are primarily two-fold:
For a new team facing technical decisions, I believe the above solutions can be considered, taking into account the compatibility issues with legacy systems.
This is also a critical factor in selecting solutions. Our first attempt focuses on the RPC protocol.
Before achieving true cross-language calls, most solutions trying to implement “cross-language” used HTTP protocols for a layer of conversion, with the most common being the use of SpringMVC’s controller/restController to indirectly call the Dubbo provider. The advantages and disadvantages of this approach are evident:
In fact, most service governance frameworks support various protocols. Besides the default Dubbo protocol, the Dubbo framework includes the REST protocol extended by Dangdang and the JSON-RPC protocol extended by Qianmi. Both are universal cross-language protocols.
The REST protocol satisfies the JAX-RS 2.0 standards, introducing annotations like @Path, @POST, and @GET. Those accustomed to writing traditional RPC interfaces may find the REST-style RPC interfaces less familiar. This can affect the development experience and also creates incompatibility with other protocols, making coexistence and migration of legacy interfaces challenging. If there are no legacy systems, the REST protocol is undoubtedly the easiest implementation for cross-language solutions, as most languages support REST.
Similar to the REST protocol, JSON-RPC is also implemented through text serialization and HTTP protocols. Dubbox has made attempts with RESTful interfaces, but the REST architecture differs from the original RPC architecture of Dubbo. The REST architecture requires defining resources and utilizing basic HTTP operations GET, POST, PUT, and DELETE. We believe RESTful is more suitable for calls between internet systems, while RPC is better for calls within a system. Using JSON-RPC protocol allows for the coexistence of legacy interfaces, preserving development habits while gaining cross-language capability.
In early practice, Qianmi opted for JSON-RPC as the cross-language protocol implementation for Dubbo and open-sourced Python client dubbo-client-py and Node client dubbo-node-client, enabling users of Python and Node.js to directly call the RPC services provided by dubbo-provider-java. Most cross-calls among Java services within the system still rely on the Dubbo protocol. Considering the adaptation of new and old protocols, we configured dual protocols without impacting the existing services.
<dubbo:protocol name="dubbo" port="20880" />
<dubbo:protocol name="jsonrpc" port="8080" />
The Dubbo protocol mainly supports inter-calls between Java services, adapting to old interfaces; the JSON-RPC protocol primarily supports calls from heterogeneous languages.
The so-called protocol in microservice frameworks can be understood simply as the message format and serialization scheme. Service governance frameworks generally provide a variety of protocol configurations for users to choose from. Besides the two universal protocols mentioned above, there are also some customized protocols like the default Dubbo protocol from the Dubbo framework and the cross-language protocol provided by the Motan framework: Motan2.
Motan2 protocol was designed to meet cross-language needs manifested in two aspects—MetaData and Motan-Go. In the initial Motan protocol, the protocol message consisted only of Header and Body, which required deserialization of data like path, param, and group stored in the Body, unfriendly to heterogeneous languages, so the composition of the protocol was modified in Motan2; Weibo open-sourced motan-go, motan-php, and motan-openresty and used Motan-Go as an agent to act as a translator, applying a simple serialization scheme to serialize the Body of the protocol message (simple serialization is a weaker serialization scheme).
A careful comparison shows that this is not much different from the dual protocol configuration; the only difference is the implicit existence of the agent, coexisting with the main service. The obvious distinction lies in the fact that in the agent scheme, heterogeneous languages do not interact directly.
The Dubbo protocol was initially designed only for conventional RPC calling scenarios and was not specifically designed for cross-language use. However, cross-language support is not merely a binary choice of support or not support, but is categorized by ease of implementation.
Yes, making cross-language calls using the Dubbo protocol may not be easy, but it is feasible. Qianmi achieved this with the frontend services developed in Node.js becoming the main battlefield for heterogeneous languages, ultimately bringing to life dubbo2.js, bridging Node.js with the native Dubbo protocol. As the core of this article’s second part, we will highlight the tasks accomplished using dubbo2.js.
Detailed explanation of the Dubbo protocol message header:
com.alibaba.dubbo.remoting.exchange.Response
.Ultimately, protocol messages are transformed into bytes for TCP transmission. Any language that supports network modules and provides Socket-like encapsulation can achieve communication. So, where does the cross-language difficulty lie? In calling Java from other languages, the main challenges are:
From the analysis above, we have identified two main challenges. The key to solving these problems with dubbo2.js relies on two libraries: js-to-java and hessian.js. js-to-java enables Node.js to express Java objects, while hessian.js provides serialization capability. By utilizing Node.js sockets to replicate the message format of the Dubbo protocol, Node.js is able to call Java-Dubbo-Provider.
To provide an intuitive experience for readers interested in dubbo2.js, this section presents a quick-start example, showcasing how easy it is to call Dubbo services using dubbo2.js.
The backend Dubbo service is provided using Java, serving most business scenarios. First, define the service interface:
public interface DemoProvider {
String sayHello(String name);
String echo() ;
void test();
UserResponse getUserInfo(UserRequest request);
}
Next, implement the service:
public class DemoProviderImpl implements DemoProvider {
public String sayHello(String name) {
System.out.println("[" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "] Hello " + name + ", request from consumer: " + RpcContext.getContext().getRemoteAddress());
return "Hello " + name + ", response form provider: " + RpcContext.getContext().getLocalAddress();
}
@Override
public String echo() {
System.out.println("receive....");
return "pang";
}
@Override
public void test() {
System.out.println("test");
}
@Override
public UserResponse getUserInfo(UserRequest request) {
System.out.println(request);
UserResponse response = new UserResponse();
response.setStatus("ok");
Map<String, String> map = new HashMap<String, String>();
map.put("id", "1");
map.put("name", "test");
response.setInfo(map);
return response;
}
}
Expose the service:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<!-- Provider application information, used for dependency calculation -->
<dubbo:application name="demo-provider"/>
<dubbo:registry protocol="zookeeper" address="localhost:2181"/>
<!-- Expose the service on port 20880 using the dubbo protocol -->
<dubbo:protocol name="dubbo" port="20880"/>
<!-- Implement the service like a local bean -->
<bean id="demoProvider" class="com.alibaba.dubbo.demo.provider.DemoProviderImpl"/>
<!-- Declare the service interface that needs to be exposed -->
<dubbo:service interface="com.alibaba.dubbo.demo.DemoProvider" ref="demoProvider" version="1.0.0"/>
</beans>
We have completed all configurations on the server-side, and we can start the main class to register a Dubbo service locally.
public class Provider {
public static void main(String[] args) throws Exception {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[]{"META-INF/spring/dubbo-demo-provider.xml"});
context.start();
System.in.read();
}
}
Install dubbo2.js:
npm install dubbo2.js --save
Configure dubboConfig.ts:
import { Dubbo, java, TDubboCallResult } from 'dubbo2.js'
const dubbo = new Dubbo({
application: {name: 'demo-provider'},
register: 'localhost:2181',
dubboVersion: '2.0.0',
interfaces: [
'com.alibaba.dubbo.demo.DemoProvider',
],
});
interface IDemoService {
sayHello(name: string): TDubboCallResult<string>;
}
export const demoService = dubbo.proxyService<IDemoService>({
dubboInterface: 'com.alibaba.dubbo.demo.DemoProvider',
version: '1.0.0',
methods: {
sayHello(name: string) {
return [java.String(name)];
},
echo() {},
test() {},
getUserInfo() {
return [
java.combine('com.alibaba.dubbo.demo.UserRequest', {
id: 1,
name: 'nodejs',
email: 'node@qianmi.com',
}),
];
},
},
});
Using TypeScript can provide a better development experience.
Write the calling class main.ts:
import {demoService} from './dubboConfig'
demoService.sayHello('kirito').then(({res,err})=>{
console.log(res)
});
Start the Node.js client in Debug mode:
DEBUG=dubbo* ts-node main.ts
Check the results:
Hello kirito, response form provider: 172.19.6.151:20880
Congratulations!
The example code in this article is provided here: https://github.com/dubbo/dubbo2.js. If you are not very familiar with the Dubbo protocol and want to understand how it works, the project provides a sub-module—java-socket-consumer, which implements a procedure-oriented approach to send Dubbo protocol messages with native sockets, completing the entire process of method invocation and response reception.