dubbo2.js 是 千米网 贡献给 dubbo 社区的一款 nodejs dubbo 客户端,它提供了 nodejs 对原生 dubbo 协议的支持,使得 nodejs 和 java 这两种异构语言的 rpc 调用变得便捷,高效。
微服务架构已成为目前互联网架构的趋势,关于微服务的讨论,几乎占据了各种技术大会的绝大多数版面。国内使用最多的服务治理框架非阿里开源的 dubbo 莫属,千米网也选择了 dubbo 作为微服务治理框架。另一方面,和大多数互联网公司一样,千米的开发语言是多样的,大多数后端业务由 java 支撑,而每个业务线有各自开发语言的选择权,便出现了 nodejs,python,go 多语言调用的问题。
跨语言调用是一个很大的话题,也是一个很有挑战的技术活,目前业界经常被提及的解决方案有如下几种,不妨拿出来老生常谈一番:
当我们再聊跨语言调用时我们在聊什么?纵观上述几个较为通用,成熟的解决方案,可以得出结论:解决跨语言调用的思路无非是两种:
如果一个新型的团队面临技术选型,我认为上述的方案都可以纳入参考,可考虑到遗留系统的兼容性问题
这也关键的选型因素。我们做出的第一个尝试,便是在 RPC 协议上下功夫。
在没有实现真正的跨语言调用之前,想要实现“跨语言”大多数方案是使用 http 协议做一层转换,最常见的手段莫过于借助 springmvc 提供的 controller/restController,间接调用 dubbo provider。这种方案的优势和劣势显而易见
事实上,大多数服务治理框架都支持多种协议,dubbo 框架除默认的 dubbo 协议之外,还有当当网扩展的 rest 协议和千米网扩展的 json-rpc 协议可供选择。这两者都是通用的跨语言协议。
rest 协议为满足 JAX-RS 2.0 标准规范,在开发过程中引入了 @Path,@POST,@GET 等注解,习惯于编写传统 rpc 接口的人可能不太习惯 rest 风格的 rpc 接口。一方面这样会影响开发体验,另一方面,独树一帜的接口风格使得它与其他协议不太兼容,旧接口的共生和迁移都无法实现。如果没有遗留系统,rest 协议无疑是跨语言方案最简易的实现,绝大多数语言支持 rest 协议。
和 rest 协议类似,json-rpc 的实现也是文本序列化&http 协议。dubbox 在 restful 接口上已经做出了尝试,但是 rest 架构和 dubbo 原有的 rpc 架构是有区别的,rest 架构需要对资源(Resources)进行定义, 需要用到 http 协议的基本操作 GET、POST、PUT、DELETE。在我们看来,restful 更合适互联网系统之间的调用,而 rpc 更适合一个系统内的调用。使用 json-rpc 协议使得旧接口得以兼顾,开发习惯仍旧保留,同时获得了跨语言的能力。
千米网在早期实践中采用了 json-rpc 作为 dubbo 的跨语言协议实现,并开源了基于 json-rpc 协议下的 python 客户端 dubbo-client-py 和 node 客户端 dubbo-node-client ,使用 python 和 nodejs 的小伙伴可以借助于它们直接调用 dubbo-provider-java 提供的 rpc 服务。系统中大多数 java 服务之间的互相调用还是以 dubbo 协议为主,考虑到新旧协议的适配,在不影响原有服务的基础上,我们配置了双协议。
<dubbo:protocol name="dubbo" port="20880" /> <dubbo:protocol name="jsonrpc" port="8080" />
dubbo 协议主要支持 java 间的相互调用,适配老接口;json-rpc 协议主要支持异构语言的调用。
微服务框架所谓的协议(protocol)可以简单理解为:报文格式和序列化方案。服务治理框架一般都提供了众多的协议配置项供使用者选择,除去上述两种通用协议,还存在一些定制化的协议,如 dubbo 框架的默认协议:dubbo 协议以及 motan 框架提供的跨语言协议:motan2。
motan2 协议被设计用来满足跨语言的需求主要体现在两个细节中—MetaData 和 motan-go。在最初的 motan 协议中,协议报文仅由 Header+Body 组成,这样导致 path,param,group 等存储在 Body 中的数据需要反序列得到,这对异构语言来说是很不友好的,所以在 motan2 中修改了协议的组成;weibo 开源了 motan-go , motan-php , motan-openresty ,并借助于 motan-go 充当了 agent 这一翻译官的角色,使用 simple 序列化方案来序列化协议报文的 Body 部分(simple 序列化是一种较弱的序列化方案)。
仔细揣摩下可以发现这么做和双协议的配置区别并不是大,只不过这里的 agent 是隐式存在的,与主服务共生。明显的区别在于 agent 方案中异构语言并不直接交互。
dubbo 协议设计之初只考虑到了常规的 rpc 调用场景,它并不是为跨语言而设计,但跨语言支持从来不是只有支持、不支持两种选择,而是要按难易程度来划分。是的,dubbo 协议的跨语言调用可能并不好做,但并非无法实现。千米网便实现了这一点,nodejs 构建的前端业务是异构语言的主战场,最终实现了 dubbo2.js,打通了 nodejs 和原生 dubbo 协议。作为本文第二部分的核心内容,重点介绍下我们使用 dubbo2.js 干了什么事。
dubbo协议报文消息头详解:
com.alibaba.dubbo.remoting.exchange.Response
协议报文最终都会变成字节,使用 tcp 传输,任何语言只要支持网络模块,有类似 Socket 之类的封装,那么通信就不成问题。那,跨语言难在哪儿?以其他语言调用 java 来说,主要有两个难点:
上面我们分析出了两个难点,dubbo2.js 解决这两个问题的关键依赖于两个类库: js-to-java , hessian.js 。js-to-java 使得 nodejs 具备 java 对象的表达能力,而 hessian.js 提供了序列化能力。借助于 nodejs 的 socket ,妇科一套 dubbo 协议的报文格式,最终便实现了 nodejs 对 java-dubbo-provider 的调用。
为了让对 dubbo2.js 感兴趣的读者有一个直观的体验,本节呈现一个快速入门示例,让你体会到使用 dubbo2.js 调用 dubbo 服务是一件多么轻松的事。
后端 dubbo 服务使用 java 来提供,这服务大多数的业务场景。首先定义服务接口:
public interface DemoProvider { String sayHello(String name); String echo() ; void test(); UserResponse getUserInfo(UserRequest request); }
其次,实现服务:
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; } }
暴露服务:
<?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"> <!-- 提供方应用信息,用于计算依赖关系 --> <dubbo:application name="demo-provider"/> <dubbo:registry protocol="zookeeper" address="localhost:2181"/> <!-- 用dubbo协议在20880端口暴露服务 --> <dubbo:protocol name="dubbo" port="20880"/> <!-- 和本地bean一样实现服务 --> <bean id="demoProvider" class="com.alibaba.dubbo.demo.provider.DemoProviderImpl"/> <!-- 声明需要暴露的服务接口 --> <dubbo:service interface="com.alibaba.dubbo.demo.DemoProvider" ref="demoProvider" version="1.0.0"/> </beans>
我们完成了服务端的所有配置,启动启动类即可在本地注册一个 dubbo 服务。
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(); } }
安装 dubbo2.js:
npm install dubbo2.js --save
配置 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', }), ]; }, }, });
使用 typescript 可以带来更好的开发体验。
编写调用类 main.ts:
import {demoService} from './dubboConfig' demoService.sayHello('kirito').then(({res,err})=>{ console.log(res) });
Debug 模式启动 nodejs 客户端:
DEBUG=dubbo* ts-node main.ts
查看运行结果:
Hello kirito, response form provider: 172.19.6.151:20880
congratulation!
本文中的示例代码,提供在此处, https://github.com/lexburner/Dubbojs-Learning 。如果你对 dubbo 协议不慎了解,想要理解它的工作原理,项目中提供了一个子 moudle — java-socket-consumer,使用面向过程的思路实现了 java-socket-consumer,完成了原生 socket 发送 dubbo 协议报文,完成方法调用,并获取响应的全流程。