dubbo2.js 是 公里網 貢獻給 dubbo 社區的一款 nodejs dubbo 客戶端,它提供了 nodejs 對原生 dubbo 協議的支持,使得 nodejs 和 java 這兩種異構語言的 rpc 調用變得便捷,高效。
原創出處 https://www.cnkirito.moe/dubbojs-in-qianmi/ 「老徐」javascript
微服務架構已成爲目前互聯網架構的趨勢,關於微服務的討論,幾乎佔據了各類技術大會的絕大多數版面。國內使用最多的服務治理框架非阿里開源的 dubbo 莫屬,公里網也選擇了 dubbo 做爲微服務治理框架。另外一方面,和大多數互聯網公司同樣,公里的開發語言是多樣的,大多數後端業務由 java 支撐,而每一個業務線有各自開發語言的選擇權,便出現了 nodejs,python,go 多語言調用的問題。
跨語言調用是一個很大的話題,也是一個頗有挑戰的技術活,目前業界常常被說起的解決方案有以下幾種,不妨拿出來老生常談一番:php
當咱們再聊跨語言調用時咱們在聊什麼?縱觀上述幾個較爲通用,成熟的解決方案,能夠得出結論:解決跨語言調用的思路無非是兩種:前端
使用 agent 完成協議的適配
若是一個新型的團隊面臨技術選型,我認爲上述的方案均可以歸入參考,可考慮到遺留系統的兼容性問題java
springmvc的美好時代node
在沒有實現真正的跨語言調用以前,想要實現「跨語言」大多數方案是使用 http 協議作一層轉換,最多見的手段莫過於藉助 springmvc 提供的 controller/restController,間接調用 dubbo provider。這種方案的優點和劣勢顯而易見python
事實上,大多數服務治理框架都支持多種協議,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 協議爲主,考慮到新舊協議的適配,在不影響原有服務的基礎上,咱們配置了雙協議。git
<dubbo:protocol name="dubbo" port="20880" /> <dubbo:protocol name="jsonrpc" port="8080" />
dubbo 協議主要支持 java 間的相互調用,適配老接口;json-rpc 協議主要支持異構語言的調用。
github
微服務框架所謂的協議(protocol)能夠簡單理解爲:報文格式和序列化方案。服務治理框架通常都提供了衆多的協議配置項供使用者選擇,除去上述兩種通用協議,還存在一些定製化的協議,如 dubbo 框架的默認協議:dubbo 協議以及 motan 框架提供的跨語言協議:motan2。spring
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 序列化是一種較弱的序列化方案)。typescript
仔細揣摩下能夠發現這麼作和雙協議的配置區別並非大,只不過這裏的 agent 是隱式存在的,與主服務共生。明顯的區別在於 agent 方案中異構語言並不直接交互。
dubbo 協議設計之初只考慮到了常規的 rpc 調用場景,它並非爲跨語言而設計,但跨語言支持歷來不是隻有支持、不支持兩種選擇,而是要按難易程度來劃分。是的,dubbo 協議的跨語言調用可能並很差作,但並不是沒法實現。公里網便實現了這一點,nodejs 構建的前端業務是異構語言的主戰場,最終實現了 dubbo2.js,打通了 nodejs 和原生 dubbo 協議。做爲本文第二部分的核心內容,重點介紹下咱們使用 dubbo2.js 幹了什麼事。
- magic:相似java字節碼文件裏的魔數,用來判斷是否是 dubbo 協議的數據包。魔數是常量 0xdabb
- flag:標誌位, 一共8個地址位。低四位用來表示消息體數據用的序列化工具的類型(默認 hessian),高四位中,第一位爲 1 表示是 >* request 請求,第二位爲 1 表示雙向傳輸(即有返回 response),第三位爲 1 表示是心跳 ping 事件。
- status:狀態位, 設置請求響應狀態,dubbo 定義了一些響應的類型。具體類型見com.alibaba.dubbo.remoting.exchange.Response
- invoke id:消息 id, long 類型。每個請求的惟一識別 id(因爲採用異步通信的方式,用來把請求 request 和返回的 response 對應上)
- body length:消息體 body 長度, int 類型,即記錄 Body Content 有多少個字節
- body content:請求參數,響應參數的抽象序列化以後存儲於此。
協議報文最終都會變成字節,使用 tcp 傳輸,任何語言只要支持網絡模塊,有相似 Socket 之類的封裝,那麼通訊就不成問題。那,跨語言難在哪兒?以其餘語言調用 java 來講,主要有兩個難點:
- 異構語言如何表示 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 服務是一件多麼輕鬆的事。
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 協議報文,完成方法調用,並獲取響應的全流程。