...接上文
服務發現
服務發現流程
整體duubo的服務消費原理
Dubbo 框架做服務消費也分為兩大部分 , 第一步通過持有遠端服務例項生成Invoker,這個Invoker 在客戶端是核心的遠端代理物件 。 第二步會把Invoker 通過動態代理轉換成實現使用者介面的動態代理引用 。
服務消費方引用服務的藍色初始化鏈,時序圖
原始碼分析應用
引用入口:ReferenceBean 的getObject 方法,該方法定義在Spring 的FactoryBean 介面中,ReferenceBean 實現了這個方法。
public Object getObject() throws Exception {
return get();
}
public synchronized T get() {
// 檢測 ref 是否為空,為空則通過 init 方法建立
if (ref == null) {
// init 方法主要用於處理配置,以及呼叫 createProxy 生成代理類
init();
}
return ref;
}
Dubbo 提供了豐富的配置,用於調整和優化框架行為,效能等。Dubbo 在引用或匯出服務時,首先會對這些配置進行檢查和處理,以保證配置的正確性。
private void init() {
// 建立代理類
ref = createProxy(map);
}
此方法程式碼很長,主要完成的配置載入,檢查,以及建立引用的代理物件。這裡要從createProxy 開始看起。從字面意思上來看,createProxy 似乎只是用於建立代理物件的。但實際上並非如此,該方法還會呼叫其他方法構建以及合併Invoker 例項。具體細節如下。
private T createProxy(Map<String, String> map) {
URL tmpUrl = new URL("temp", "localhost", 0, map);
...........
isDvmRefer = InjvmProtocol . getlnjvmProtocol( ) . islnjvmRefer(tmpUrl)
// 本地引用略
if (isJvmRefer) {
} else {
// 點對點呼叫略
if (url != null && url.length() > 0) {
} else {
// 載入註冊中心 url
List<URL> us = loadRegistries(false);
if (us != null && !us.isEmpty()) {
for (URL u : us) {
URL monitorUrl = loadMonitor(u);
if (monitorUrl != null) {
map.put(Constants.MONITOR_KEY,
URL.encode(monitorUrl.toFullString()));
}
// 新增 refer 引數到 url 中,並將 url 新增到 urls 中
urls.add(u.addParameterAndEncoded(Constants.REFER_KEY,
StringUtils.toQueryString(map)));
}
}
}
// 單個註冊中心或服務提供者(服務直連,下同)
if (urls.size() == 1) {
// 呼叫 RegistryProtocol 的 refer 構建 Invoker 例項
invoker = refprotocol.refer(interfaceClass, urls.get(0));
// 多個註冊中心或多個服務提供者,或者兩者混合
} else {
List<Invoker<?>> invokers = new ArrayList<Invoker<?>>();
URL registryURL = null;
// 獲取所有的 Invoker
for (URL url : urls) {
// 通過 refprotocol 呼叫 refer 構建 Invoker,refprotocol 會在執行時
// 根據 url 協議頭載入指定的 Protocol 例項,並呼叫例項的 refer 方法
invokers.add(refprotocol.refer(interfaceClass, url));
if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {
registryURL = url;
}
}
if (registryURL != null) {
// 如果註冊中心連結不為空,則將使用 AvailableCluster
URL u = registryURL.addParameter(Constants.CLUSTER_KEY,
AvailableCluster.NAME);
// 建立 StaticDirectory 例項,並由 Cluster 對多個 Invoker 進行合併
invoker = cluster.join(new StaticDirectory(u, invokers));
} else {
invoker = cluster.join(new StaticDirectory(invokers));
}
}
}
//省略無關程式碼...
// 生成代理類
return (T) proxyFactory.getProxy(invoker);
}
上面程式碼很多,不過邏輯比較清晰。
1、如果是本地呼叫,直接jvm 協議從記憶體中獲取例項
2、如果只有一個註冊中心,直接通過Protocol 自適應擴充類構建Invoker 例項介面
3、如果有多個註冊中心,此時先根據url 構建Invoker。然後再通過Cluster 合併多個Invoker,最後呼叫ProxyFactory 生成代理類
建立客戶端
在服務消費方,Invoker 用於執行遠端呼叫。Invoker 是由Protocol 實現類構建而來。Protocol 實現類有很多,這裡分析DubboProtocol
public <T> Invoker<T> refer(Class<T> serviceType, URL url) throws RpcException {
optimizeSerialization(url);
// 建立 DubboInvoker
DubboInvoker<T> invoker = new DubboInvoker<T>(serviceType, url,
getClients(url), invokers);
invokers.add(invoker);
return invoker;
}
上面方法看起來比較簡單,建立一個DubboInvoker。通過構造方法傳入遠端呼叫的client物件。預設情況下,Dubbo 使用NettyClient 進行通訊。接下來,我們簡單看一下getClients 方法的邏輯。
private ExchangeClient[] getClients(URL url) {
// 是否共享連線
boolean service_share_connect = false;
// 獲取連線數,預設為0,表示未配置
int connections = url.getParameter(Constants.CONNECTIONS_KEY, 0);
// 如果未配置 connections,則共享連線
if (connections == 0) {
service_share_connect = true;
connections = 1;
}
ExchangeClient[] clients = new ExchangeClient[connections];
for (int i = 0; i < clients.length; i++) {
if (service_share_connect) {
// 獲取共享客戶端
clients[i] = getSharedClient(url);
} else {
// 初始化新的客戶端
clients[i] = initClient(url);
}
}
return clients;
}
這裡根據connections 數量決定是獲取共享客戶端還是建立新的客戶端例項,getSharedClient 方法中也會呼叫initClient 方法,因此下面我們一起看一下這個方法。
private ExchangeClient initClient(URL url) {
// 獲取客戶端型別,預設為 netty
String str = url.getParameter(Constants.CLIENT_KEY,
url.getParameter(Constants.SERVER_KEY, Constants.DEFAULT_REMOTING_CLIENT));
//省略無關程式碼
ExchangeClient client;
try {
// 獲取 lazy 配置,並根據配置值決定建立的客戶端型別
if (url.getParameter(Constants.LAZY_CONNECT_KEY, false)) {
// 建立懶載入 ExchangeClient 例項
client = new LazyConnectExchangeClient(url, requestHandler);
} else {
// 建立普通 ExchangeClient 例項
client = Exchangers.connect(url, requestHandler);
}
} catch (RemotingException e) {
throw new RpcException("Fail to create remoting client for service...");
}
return client;
}
initClient 方法首先獲取使用者配置的客戶端型別,預設為netty。下面我們分析一下Exchangers 的connect 方法。
public static ExchangeClient connect(URL url, ExchangeHandler handler) throws
RemotingException {
// 獲取 Exchanger 例項,預設為 HeaderExchangeClient
return getExchanger(url).connect(url, handler);
}
如上,getExchanger 會通過SPI 載入HeaderExchangeClient 例項,這個方法比較簡單,大家自己看一下吧。接下來分析HeaderExchangeClient 的實現。
public ExchangeClient connect(URL url, ExchangeHandler handler) throws
RemotingException {
// 這裡包含了多個呼叫,分別如下:
// 1. 建立 HeaderExchangeHandler 物件
// 2. 建立 DecodeHandler 物件
// 3. 通過 Transporters 構建 Client 例項
// 4. 建立 HeaderExchangeClient 物件
return new HeaderExchangeClient(Transporters.connect(url, new
DecodeHandler(new HeaderExchangeHandler(handler))), true);
}
這裡的呼叫比較多,我們這裡重點看一下Transporters 的connect 方法。如下:
public static Client connect(URL url, ChannelHandler... handlers) throws
RemotingException {
if (url == null) {
throw new IllegalArgumentException("url == null");
}
ChannelHandler handler;
if (handlers == null || handlers.length == 0) {
handler = new ChannelHandlerAdapter();
} else if (handlers.length == 1) {
handler = handlers[0];
} else {
// 如果 handler 數量大於1,則建立一個 ChannelHandler 分發器
handler = new ChannelHandlerDispatcher(handlers);
}
// 獲取 Transporter 自適應擴充類,並呼叫 connect 方法生成 Client 例項
return getTransporter().connect(url, handler);
}
如上,getTransporter 方法返回的是自適應擴充類,該類會在執行時根據客戶端型別載入指定的Transporter 實現類。若使用者未配置客戶端型別,則預設載入NettyTransporter,並呼叫該類的connect 方法。如下:
public Client connect(URL url, ChannelHandler listener) throws RemotingException
{
// 建立 NettyClient 物件
return new NettyClient(url, listener);
}
註冊
這裡就已經建立好了NettyClient物件。關於DubboProtocol 的refer 方法就分析完了。接下來,繼續分析RegistryProtocol 的refer 方法邏輯。
public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
// 取 registry 引數值,並將其設定為協議頭
url = url.setProtocol(url.getParameter(Constants.REGISTRY_KEY,
Constants.DEFAULT_REGISTRY)).removeParameter(Constants.REGISTRY_KEY);
// 獲取註冊中心例項
Registry registry = registryFactory.getRegistry(url);
if (RegistryService.class.equals(type)) {
return proxyFactory.getInvoker((T) registry, type, url);
}
// 將 url 查詢字串轉為 Map
Map<String, String> qs =
StringUtils.parseQueryString(url.getParameterAndDecoded(Constants.REFER_KEY));
// 獲取 group 配置
String group = qs.get(Constants.GROUP_KEY);
if (group != null && group.length() > 0) {
if ((Constants.COMMA_SPLIT_PATTERN.split(group)).length > 1
|| "*".equals(group)) {
// 通過 SPI 載入 MergeableCluster 例項,並呼叫 doRefer 繼續執行服務引用邏輯
return doRefer(getMergeableCluster(), registry, type, url);
}
}
// 呼叫 doRefer 繼續執行服務引用邏輯
return doRefer(cluster, registry, type, url);
}
上面程式碼首先為url 設定協議頭,然後根據url 引數載入註冊中心例項。然後獲取group 配置,根據group 配置決定doRefer 第一個引數的型別。這裡的重點是doRefer 方法,如下:
private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T>
type, URL url) {
// 建立 RegistryDirectory 例項
RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url);
// 設定註冊中心和協議
directory.setRegistry(registry);
directory.setProtocol(protocol);
Map<String, String> parameters = new HashMap<String, String>
(directory.getUrl().getParameters());
// 生成服務消費者連結
URL subscribeUrl = new URL(Constants.CONSUMER_PROTOCOL,
parameters.remove(Constants.REGISTER_IP_KEY), 0, type.getName(), parameters);
// 註冊服務消費者,在 consumers 目錄下新節點
if (!Constants.ANY_VALUE.equals(url.getServiceInterface())
&& url.getParameter(Constants.REGISTER_KEY, true)) {
registry.register(subscribeUrl.addParameters(Constants.CATEGORY_KEY,
Constants.CONSUMERS_CATEGORY,
Constants.CHECK_KEY, String.valueOf(false)));
}
// 訂閱 providers、configurators、routers 等節點資料
directory.subscribe(subscribeUrl.addParameter(Constants.CATEGORY_KEY,
Constants.PROVIDERS_CATEGORY
+ "," + Constants.CONFIGURATORS_CATEGORY
+ "," + Constants.ROUTERS_CATEGORY));
// 一個註冊中心可能有多個服務提供者,因此這裡需要將多個服務提供者合併為一個
Invoker invoker = cluster.join(directory);
ProviderConsumerRegTable.registerConsumer(invoker, url, subscribeUrl,
directory);
return invoker;
}
如上,doRefer 方法建立一個RegistryDirectory 例項,然後生成服務者消費者連結,並向註冊中心進行註冊。註冊完畢後,緊接著訂閱providers、configurators、routers 等節點下的資料。完成訂閱後,RegistryDirectory 會收到這幾個節點下的子節點資訊。由於一個服務可能部署在多臺伺服器上,這樣就會在providers 產生多個節點,這個時候就需要Cluster 將多個服務節點合併為一個,並生成一個Invoker。
建立代理物件
Invoker 建立完畢後,接下來要做的事情是為服務介面生成代理物件。有了代理物件,即可進行遠端呼叫。代理物件生成的入口方法為ProxyFactory 的getProxy,接下來進行分析。
public <T> T getProxy(Invoker<T> invoker) throws RpcException {
// 呼叫過載方法
return getProxy(invoker, false);
}
public <T> T getProxy(Invoker<T> invoker, boolean generic) throws RpcException {
Class<?>[] interfaces = null;
// 獲取介面列表
String config = invoker.getUrl().getParameter("interfaces");
if (config != null && config.length() > 0) {
// 切分介面列表
String[] types = Constants.COMMA_SPLIT_PATTERN.split(config);
if (types != null && types.length > 0) {
interfaces = new Class<?>[types.length + 2];
// 設定服務介面類和 EchoService.class 到 interfaces 中
interfaces[0] = invoker.getInterface();
interfaces[1] = EchoService.class;
for (int i = 0; i < types.length; i++) {
// 載入介面類
interfaces[i + 1] = ReflectUtils.forName(types[i]);
}
}
}
if (interfaces == null) {
interfaces = new Class<?>[]{invoker.getInterface(), EchoService.class};
}
// 為 http 和 hessian 協議提供泛化呼叫支援,參考 pull request #1827
if (!invoker.getInterface().equals(GenericService.class) && generic) {
int len = interfaces.length;
Class<?>[] temp = interfaces;
// 建立新的 interfaces 陣列
interfaces = new Class<?>[len + 1];
System.arraycopy(temp, 0, interfaces, 0, len);
// 設定 GenericService.class 到陣列中
interfaces[len] = GenericService.class;
}
// 呼叫過載方法
return getProxy(invoker, interfaces);
}
public abstract <T> T getProxy(Invoker<T> invoker, Class<?>[] types);
如上,上面大段程式碼都是用來獲取interfaces 陣列的,我們繼續往下看。getProxy(Invoker, Class<?>[]) 這個方法是一個抽象方法,下面我們到JavassistProxyFactory 類中看一下該方法的實現程式碼。
public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) {
// 生成 Proxy 子類(Proxy 是抽象類)。並呼叫 Proxy 子類的 newInstance 方法建立Proxy 例項
return (T) Proxy.getProxy(interfaces).newInstance(new
InvokerInvocationHandler(invoker));
}
上面程式碼並不多,首先是通過Proxy 的getProxy 方法獲取Proxy 子類,然後建立InvokerInvocationHandler 物件,並將該物件傳給newInstance 生成Proxy 例項。InvokerInvocationHandler 實現JDK 的InvocationHandler 介面,具體的用途是攔截介面類呼叫。下面以org.apache.dubbo.demo.DemoService 這個介面為例,來看一下該介面代理類程式碼大致是怎樣的(忽略EchoService 介面)。
package org.apache.dubbo.common.bytecode;
public class proxy0 implements org.apache.dubbo.demo.DemoService {
public static java.lang.reflect.Method[] methods;
private java.lang.reflect.InvocationHandler handler;
public proxy0() {
}
public proxy0(java.lang.reflect.InvocationHandler arg0) {
handler = $1;
}
public java.lang.String sayHello(java.lang.String arg0) {
Object[] args = new Object[1];
args[0] = ($w) $1;
Object ret = handler.invoke(this, methods[0], args);
return (java.lang.String) ret;
}
}
好了,到這裡代理類生成邏輯就分析完了。整個過程比較複雜,大家需要耐心看一下。
總結
- 從註冊中心發現引用服務:在有註冊中心,通過註冊中心發現提供者地址的情況下,ReferenceConfig 解析出的URL 格式為: registry://registryhost:/org.apache.registry.RegistryService?refer=URL.encode("conumerhost/com.foo.FooService?version=1.0.0") 。
- 通過URL 的registry://協議頭識別,就會呼叫RegistryProtocol#refer()方法
- 查詢提供者URL,如 dubbo://service-host/com.foo.FooService?version=1.0.0 ,來獲取註冊中心
- 建立一個RegistryDirectory 例項並設定註冊中心和協議
- 生成conusmer 連線,在consumer 目錄下建立節點,向註冊中心註冊
- 註冊完畢後,訂閱providers,configurators,routers 等節點的資料
- 通過URL 的 dubbo:// 協議頭識別,呼叫 DubboProtocol#refer() 方法,建立一個
ExchangeClient 客戶端並返回DubboInvoker 例項 - 由於一個服務可能會部署在多臺伺服器上,這樣就會在providers 產生多個節點,這樣也就會得到多個DubboInvoker 例項,就需要RegistryProtocol 呼叫Cluster 將多個服務提供者節點偽裝成一個節點,並返回一個Invoker
- Invoker 建立完畢後,呼叫ProxyFactory 為服務介面生成代理物件,返回提供者引用
網路通訊
在之前的內容中,我們分析了消費者端服務發現與提供者端服務暴露的相關內容,同時也知道消費者端通過內建的負載均衡演算法獲取合適的呼叫invoker進行遠端呼叫。接下來我們再研究下遠端呼叫過程即網路通訊。
網路通訊位於Remoting模組:
Remoting 實現是Dubbo 協議的實現,如果你選擇RMI 協議,整個Remoting 都不會用上;
Remoting 內部再劃為 Transport 傳輸層 和 Exchange 資訊交換層 ;
Transport 層只負責單向訊息傳輸,是對Mina, Netty, Grizzly 的抽象,它也可以擴充套件UDP 傳輸;
Exchange 層是在傳輸層之上封裝了Request-Response 語義;
網路通訊的問題:
客戶端與服務端連通性問題
粘包拆包問題
非同步多執行緒資料一致問題
通訊協議
dubbo內建,dubbo協議 ,rmi協議,hessian協議,http協議,webservice協議,thrift協議,rest協議,grpc協議,memcached協議,redis協議等10種通訊協議。各個協議特點如下
dubbo協議
Dubbo 預設協議採用單一長連線和NIO 非同步通訊,適合於小資料量大併發的服務呼叫,以及服務消費者機器數遠大於服務提供者機器數的情況。
預設協議,使用基於mina 1.1.7 和hessian 3.2.1 的tbremoting 互動。
連線個數:單連線
連線方式:長連線
傳輸協議:TCP
傳輸方式:NIO 非同步傳輸
序列化:Hessian 二進位制序列化
適用範圍:傳入傳出引數資料包較小(建議小於100K),消費者比提供者個數多,單一消費者無法壓滿提供者,儘量不要用dubbo 協議傳輸大檔案或超大字串。
適用場景:常規遠端服務方法呼叫
rmi協議
RMI 協議採用JDK 標準的 java.rmi.* 實現,採用阻塞式短連線和JDK 標準序列化方式。
連線個數:多連線
連線方式:短連線
傳輸協議:TCP
傳輸方式:同步傳輸
序列化:Java 標準二進位制序列化
適用範圍:傳入傳出引數資料包大小混合,消費者與提供者個數差不多,可傳檔案。
適用場景:常規遠端服務方法呼叫,與原生RMI服務互操作
hessian協議
Hessian 協議用於整合Hessian 的服務,Hessian 底層採用Http 通訊,採用Servlet 暴露服務,
Dubbo 預設內嵌Jetty 作為伺服器實現。
Dubbo 的Hessian 協議可以和原生Hessian 服務互操作,即:提供者用Dubbo 的Hessian 協議暴露服務,消費者直接用標準Hessian 介面呼叫或者提供方用標準Hessian 暴露服務,消費方用Dubbo 的Hessian 協議呼叫。
連線個數:多連線
連線方式:短連線
傳輸協議:HTTP
傳輸方式:同步傳輸
序列化:Hessian二進位制序列化
適用範圍:傳入傳出引數資料包較大,提供者比消費者個數多,提供者壓力較大,可傳檔案。
適用場景:頁面傳輸,檔案傳輸,或與原生hessian服務互操作
http協議
基於HTTP 表單的遠端呼叫協議,採用Spring 的HttpInvoker 實現
連線個數:多連線
連線方式:短連線
傳輸協議:HTTP
傳輸方式:同步傳輸
序列化:表單序列化
適用範圍:傳入傳出引數資料包大小混合,提供者比消費者個數多,可用瀏覽器檢視,可用表單或URL傳入引數,暫不支援傳檔案。
適用場景:需同時給應用程式和瀏覽器JS 使用的服務。
webservice協議
基於WebService 的遠端呼叫協議,基於Apache CXF 實現](http://dubbo.apache.org/zh-cn/docs/us
er/references/protocol/webservice.html#fn2)。
可以和原生WebService 服務互操作,即:提供者用Dubbo 的WebService 協議暴露服務,消費者直接用標準WebService 介面呼叫,或者提供方用標準WebService 暴露服務,消費方用Dubbo 的WebService 協議呼叫。
連線個數:多連線
連線方式:短連線
傳輸協議:HTTP
傳輸方式:同步傳輸
序列化:SOAP 文字序列化(http + xml)
適用場景:系統整合,跨語言呼叫
thrift協議
當前dubbo 支援[1]的thrift 協議是對thrift 原生協議[2] 的擴充套件,在原生協議的基礎上新增了一些額外的頭資訊,比如service name,magic number 等。
rest協議
基於標準的Java REST API——JAX-RS 2.0(Java API for RESTful Web Services的簡寫)實現的REST呼叫支援
grpc協議
Dubbo 自2.7.5 版本開始支援gRPC 協議,對於計劃使用HTTP/2 通訊,或者想利用gRPC 帶來的Stream、反壓、Reactive 程式設計等能力的開發者來說, 都可以考慮啟用gRPC 協議。
為期望使用gRPC 協議的使用者帶來服務治理能力,方便接入Dubbo 體系使用者可以使用Dubbo 風格的,基於介面的程式設計風格來定義和使用遠端服務
memcached協議
基於memcached實現的RPC 協議
redis協議
基於Redis 實現的RPC 協議
序列化
序列化就是將物件轉成位元組流,用於網路傳輸,以及將位元組流轉為物件,用於在收到位元組流資料後還原成物件。序列化的優勢有很多,例如安全性更好、可跨平臺等。我們知道dubbo基於netty進行網路通訊,在NettyClient.doOpen() 方法中可以看到Netty的相關類
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
public ChannelPipeline getPipeline() {
NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(),
NettyClient.this);
ChannelPipeline pipeline = Channels.pipeline();
pipeline.addLast("decoder", adapter.getDecoder());
pipeline.addLast("encoder", adapter.getEncoder());
pipeline.addLast("handler", nettyHandler);
return pipeline;
}
});
然後去看NettyCodecAdapter 類最後進入ExchangeCodec類的encodeRequest方法,如下:
protected void encodeRequest(Channel channel, ChannelBuffer buffer, Request
req) throws IOException {
Serialization serialization = getSerialization(channel);
// header.
byte[] header = new byte[HEADER_LENGTH];
是的,就是Serialization介面,預設是Hessian2Serialization序列化介面。
Dubbo序列化支援java、compactedjava、nativejava、fastjson、dubbo、fst、hessian2、kryo,protostuff其中預設hessian2。其中java、compactedjava、nativejava屬於原生java的序列化。
dubbo序列化:阿里尚未開發成熟的高效java序列化實現,阿里不建議在生產環境使用它。
hessian2序列化:hessian是一種跨語言的高效二進位制序列化方式。但這裡實際不是原生的hessian2序列化,而是阿里修改過的,它是dubbo RPC預設啟用的序列化方式。
json序列化:目前有兩種實現,一種是採用的阿里的fastjson庫,另一種是採用dubbo中自己實現的簡單json庫,但其實現都不是特別成熟,而且json這種文字序列化效能一般不如上面兩種二進位制序列化。
java序列化:主要是採用JDK自帶的Java序列化實現,效能很不理想。
網路通訊
Dubbo中的資料格式
解決socket中資料粘包拆包問題,一般有三種方式
定長協議(資料包長度一致)
定長的協議是指協議內容的長度是固定的,比如協議byte長度是50,當從網路上讀取50個byte後,就進行decode解碼操作。定長協議在讀取或者寫入時,效率比較高,因為資料快取的大小基本都確定了,就好比陣列一樣,缺陷就是適應性不足,以RPC場景為例,很難估計出定長的長度是多少。
特殊結束符(資料尾:通過特殊的字元標識#)
相比定長協議,如果能夠定義一個特殊字元作為每個協議單元結束的標示,就能夠以變長的方式進行通訊,從而在資料傳輸和高效之間取得平衡,比如用特殊字元 \n 。特殊結束符方式的問題是過於簡單的思考了協議傳輸的過程,對於一個協議單元必須要全部讀入才能夠進行處理,除此之外必須要防止使用者傳輸的資料不能同結束符相同,否則就會出現紊亂。
變長協議(協議頭+payload模式)
這種一般是自定義協議,會以定長加不定長的部分組成,其中定長的部分需要描述不定長的內容長度。
dubbo就是使用這種形式的資料傳輸格式
Dubbo 資料包分為訊息頭和訊息體,訊息頭用於儲存一些元資訊,比如魔數(Magic),資料包型別(Request/Response),訊息體長度(Data Length)等。訊息體中用於儲存具體的呼叫訊息,比如方法名稱,引數列表等。下面簡單列舉一下訊息頭的內容。
偏移量(Bit) 欄位 取值
0 ~ 7 魔數高位 0xda00
8 ~ 15 魔數低位 0xbb
16 資料包型別 0 - Response, 1 - Request
17 呼叫方式 僅在第16位被設為1的情況下有效,0 - 單向呼叫,1 - 雙向呼叫
18 事件標 識 0 - 當前資料包是請求或響應包,1 - 當前資料包是心跳包
19 ~23 序列化器編號 2 - Hessian2Serialization
3 - JavaSerialization
4 - CompactedJavaSerialization
6 - FastJsonSerialization
7 - NativeJavaSerialization
8 - KryoSerialization
9 - FstSerialization
24 ~31 狀態 20 - OK 30 - CLIENT_TIMEOUT 31 - SERVER_TIMEOUT 40 -BAD_REQUEST 50 - BAD_RESPONSE ......
32 ~95 請求編號 共8位元組,執行時生成
96 ~127 訊息體長度 執行時計算
消費端傳送請求
/**
*proxy0#sayHello(String)
*—> InvokerInvocationHandler#invoke(Object, Method, Object[])
* —> MockClusterInvoker#invoke(Invocation)
* —> AbstractClusterInvoker#invoke(Invocation)
* —> FailoverClusterInvoker#doInvoke(Invocation, List<Invoker<T>>,LoadBalance)
* —> Filter#invoke(Invoker, Invocation) // 包含多個 Filter 呼叫
* —> ListenerInvokerWrapper#invoke(Invocation)
* —> AbstractInvoker#invoke(Invocation)
* —> DubboInvoker#doInvoke(Invocation)
* —> ReferenceCountExchangeClient#request(Object, int)
* —> HeaderExchangeClient#request(Object, int)
* —> HeaderExchangeChannel#request(Object, int)
* —> AbstractPeer#send(Object)
* —> AbstractClient#send(Object, boolean)
* —> NettyChannel#send(Object, boolean)
* —> NioClientSocketChannel#write(Object)
*/
dubbo消費方,自動生成程式碼物件如下
public class proxy0 implements ClassGenerator.DC, EchoService, DemoService {
private InvocationHandler handler;
public String sayHello(String string) {
// 將引數儲存到 Object 陣列中
Object[] arrobject = new Object[]{string};
// 呼叫 InvocationHandler 實現類的 invoke 方法得到呼叫結果
Object object = this.handler.invoke(this, methods[0], arrobject);
// 返回撥用結果
return (String)object;
}
}
InvokerInvocationHandler 中的invoker 成員變數型別為MockClusterInvoker,MockClusterInvoker內部封裝了服務降級邏輯。下面簡單看一下:
public Result invoke(Invocation invocation) throws RpcException {
Result result = null;
// 獲取 mock 配置值
String value =
directory.getUrl().getMethodParameter(invocation.getMethodName(), MOCK_KEY,
Boolean.FALSE.toString()).trim();
if (value.length() == 0 || value.equalsIgnoreCase("false")) {
// 無 mock 邏輯,直接呼叫其他 Invoker 物件的 invoke 方法,
// 比如 FailoverClusterInvoker
result = this.invoker.invoke(invocation);
} else if (value.startsWith("force")) {
// force:xxx 直接執行 mock 邏輯,不發起遠端呼叫
result = doMockInvoke(invocation, null);
} else {
// fail:xxx 表示消費方對呼叫服務失敗後,再執行 mock 邏輯,不丟擲異常
try {
result = this.invoker.invoke(invocation);
} catch (RpcException e) {
// 呼叫失敗,執行 mock 邏輯
result = doMockInvoke(invocation, e);
}
}
return result;
}
考慮到前文已經詳細分析過FailoverClusterInvoker,因此本節略過FailoverClusterInvoker,直接分析DubboInvoker。
public abstract class AbstractInvoker<T> implements Invoker<T> {
public Result invoke(Invocation inv) throws RpcException {
if (destroyed.get()) {
throw new RpcException("Rpc invoker for service ...");
}
RpcInvocation invocation = (RpcInvocation) inv;
// 設定 Invoker
invocation.setInvoker(this);
if (attachment != null && attachment.size() > 0) {
// 設定 attachment
invocation.addAttachmentsIfAbsent(attachment);
}
Map<String, String> contextAttachments =
RpcContext.getContext().getAttachments();
if (contextAttachments != null && contextAttachments.size() != 0) {
// 新增 contextAttachments 到 RpcInvocation#attachment 變數中
invocation.addAttachments(contextAttachments);
}
if (getUrl().getMethodParameter(invocation.getMethodName(),
Constants.ASYNC_KEY, false)) {
// 設定非同步資訊到 RpcInvocation#attachment 中
invocation.setAttachment(Constants.ASYNC_KEY,
Boolean.TRUE.toString());
}
RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
try {
// 抽象方法,由子類實現
return doInvoke(invocation);
} catch (InvocationTargetException e) {
// ...
} catch (RpcException e) {
// ...
} catch (Throwable e) {
return new RpcResult(e);
}
}
protected abstract Result doInvoke(Invocation invocation) throws Throwable;
// 省略其他方法
}
上面的程式碼來自AbstractInvoker 類,其中大部分程式碼用於新增資訊到RpcInvocation#attachment 變數中,新增完畢後,呼叫doInvoke 執行後續的呼叫。doInvoke 是一個抽象方法,需要由子類實現,下面到DubboInvoker 中看一下。
@Override
protected Result doInvoke(final Invocation invocation) throws Throwable {
RpcInvocation inv = (RpcInvocation) invocation;
final String methodName = RpcUtils.getMethodName(invocation);
//將目標方法以及版本號作為引數放入到Invocation中
inv.setAttachment(PATH_KEY, getUrl().getPath());
inv.setAttachment(VERSION_KEY, version);
//獲得客戶端連線
ExchangeClient currentClient; //初始化invoker的時候,構建的一個遠端通訊連線
if (clients.length == 1) { //預設
currentClient = clients[0];
} else {
//通過取模獲得其中一個連線
currentClient = clients[index.getAndIncrement() % clients.length];
}
try {
//表示當前的方法是否存在返回值
boolean isOneway = RpcUtils.isOneway(getUrl(), invocation);
int timeout = getUrl().getMethodParameter(methodName, TIMEOUT_KEY,
DEFAULT_TIMEOUT);
//isOneway 為 true,表示“單向”通訊
if (isOneway) {//非同步無返回值
boolean isSent = getUrl().getMethodParameter(methodName,
Constants.SENT_KEY, false);
currentClient.send(inv, isSent);
RpcContext.getContext().setFuture(null);
return AsyncRpcResult.newDefaultAsyncResult(invocation);
} else { //存在返回值
//是否採用非同步
AsyncRpcResult asyncRpcResult = new AsyncRpcResult(inv);
CompletableFuture<Object> responseFuture =
currentClient.request(inv, timeout);
responseFuture.whenComplete((obj, t) -> {
if (t != null) {
asyncRpcResult.completeExceptionally(t);
} else {
asyncRpcResult.complete((AppResponse) obj);
}
});
RpcContext.getContext().setFuture(new
FutureAdapter(asyncRpcResult));
return asyncRpcResult;
}
}
//省略無關程式碼
}
最終進入到HeaderExchangeChannel#request方法,拼裝Request並將請求傳送出去
public CompletableFuture<Object> request(Object request, int timeout) throws
RemotingException {
if (closed) {
throw new RemotingException(this.getLocalAddress(), null, "Failed
tosend request " + request + ", cause: The channel " + this + " is closed!");
}
// 建立請求物件
Request req = new Request();
req.setVersion(Version.getProtocolVersion());
req.setTwoWay(true);
req.setData(request);
DefaultFuture future = DefaultFuture.newFuture(channel, req, timeout);
try {
//NettyClient
channel.send(req);
} catch (RemotingException e) {
future.cancel();
throw e;
}
return future;
}
請求編碼如何做的?
在netty啟動時,我們設定了編解碼器,其中通過ExchangeCodec完成編解碼工作如下:
public class ExchangeCodec extends TelnetCodec {
// 訊息頭長度
protected static final int HEADER_LENGTH = 16;
// 魔數內容
protected static final short MAGIC = (short) 0xdabb;
protected static final byte MAGIC_HIGH = Bytes.short2bytes(MAGIC)[0];
protected static final byte MAGIC_LOW = Bytes.short2bytes(MAGIC)[1];
protected static final byte FLAG_REQUEST = (byte) 0x80;
protected static final byte FLAG_TWOWAY = (byte) 0x40;
protected static final byte FLAG_EVENT = (byte) 0x20;
protected static final int SERIALIZATION_MASK = 0x1f;
private static final Logger logger =
LoggerFactory.getLogger(ExchangeCodec.class);
public Short getMagicCode() {
return MAGIC;
}
@Override
public void encode(Channel channel, ChannelBuffer buffer, Object msg) throws
IOException {
if (msg instanceof Request) {
// 對 Request 物件進行編碼
encodeRequest(channel, buffer, (Request) msg);
} else if (msg instanceof Response) {
// 對 Response 物件進行編碼,後面分析
encodeResponse(channel, buffer, (Response) msg);
} else {
super.encode(channel, buffer, msg);
}
}
protected void encodeRequest(Channel channel, ChannelBuffer buffer, Request
req) throws IOException {
Serialization serialization = getSerialization(channel);
// 建立訊息頭位元組陣列,長度為 16
byte[] header = new byte[HEADER_LENGTH];
// 設定魔數
Bytes.short2bytes(MAGIC, header);
// 設定資料包型別(Request/Response)和序列化器編號
header[2] = (byte) (FLAG_REQUEST | serialization.getContentTypeId());
// 設定通訊方式(單向/雙向)
if (req.isTwoWay()) {
header[2] |= FLAG_TWOWAY;
}
// 設定事件標識
if (req.isEvent()) { header[2] |= FLAG_EVENT;
}
// 設定請求編號,8個位元組,從第4個位元組開始設定
Bytes.long2bytes(req.getId(), header, 4);
// 獲取 buffer 當前的寫位置
int savedWriteIndex = buffer.writerIndex();
// 更新 writerIndex,為訊息頭預留 16 個位元組的空間
buffer.writerIndex(savedWriteIndex + HEADER_LENGTH);
ChannelBufferOutputStream bos = new ChannelBufferOutputStream(buffer);
// 建立序列化器,比如 Hessian2ObjectOutput
ObjectOutput out = serialization.serialize(channel.getUrl(), bos);
if (req.isEvent()) {
// 對事件資料進行序列化操作
encodeEventData(channel, out, req.getData());
} else {
// 對請求資料進行序列化操作
encodeRequestData(channel, out, req.getData(), req.getVersion());
}
out.flushBuffer();
if (out instanceof Cleanable) {
((Cleanable) out).cleanup();
}
bos.flush();
bos.close();
// 獲取寫入的位元組數,也就是訊息體長度
int len = bos.writtenBytes();
checkPayload(channel, len);
// 將訊息體長度寫入到訊息頭中
Bytes.int2bytes(len, header, 12);
// 將 buffer 指標移動到 savedWriteIndex,為寫訊息頭做準備
buffer.writerIndex(savedWriteIndex);
// 從 savedWriteIndex 下標處寫入訊息頭
buffer.writeBytes(header);
// 設定新的 writerIndex,writerIndex = 原寫下標 + 訊息頭長度 + 訊息體長度
buffer.writerIndex(savedWriteIndex + HEADER_LENGTH + len);
}
// 省略其他方法
}
以上就是請求物件的編碼過程,該過程首先會通過位運算將訊息頭寫入到header 陣列中。然後對Request 物件的data 欄位執行序列化操作,序列化後的資料最終會儲存到ChannelBuffer 中。序列化操作執行完後,可得到資料序列化後的長度len,緊接著將len 寫入到header 指定位置處。最後再將訊息頭位元組陣列header 寫入到ChannelBuffer 中,整個編碼過程就結束了。本節的最後,我們再來看一下Request 物件的data 欄位序列化過程,也就是encodeRequestData 方法的邏輯,如下:
public class DubboCodec extends ExchangeCodec implements Codec2 {
protected void encodeRequestData(Channel channel, ObjectOutput out, Object
data, String version) throws IOException {
RpcInvocation inv = (RpcInvocation) data;
// 依次序列化 dubbo version、path、version
out.writeUTF(version);
out.writeUTF(inv.getAttachment(Constants.PATH_KEY));
out.writeUTF(inv.getAttachment(Constants.VERSION_KEY));
// 序列化呼叫方法名
out.writeUTF(inv.getMethodName());
// 將引數型別轉換為字串,並進行序列化
out.writeUTF(ReflectUtils.getDesc(inv.getParameterTypes()));
Object[] args = inv.getArguments();
if (args != null)
for (int i = 0; i < args.length; i++) {
// 對執行時引數進行序列化
out.writeObject(encodeInvocationArgument(channel, inv, i));
}
// 序列化 attachments
out.writeObject(inv.getAttachments());
}
}
至此,關於服務消費方傳送請求的過程就分析完了,接下來我們來看一下服務提供方是如何接收請求的。
提供方接受請求
請求如何解碼?
這裡直接分析請求資料的解碼邏輯,忽略中間過程,如下:
public class ExchangeCodec extends TelnetCodec {
@Override
public Object decode(Channel channel, ChannelBuffer buffer) throws
IOException {
int readable = buffer.readableBytes();
// 建立訊息頭位元組陣列
byte[] header = new byte[Math.min(readable, HEADER_LENGTH)];
// 讀取訊息頭資料
buffer.readBytes(header);
// 呼叫過載方法進行後續解碼工作
return decode(channel, buffer, readable, header);
}
@Override
protected Object decode(Channel channel, ChannelBuffer buffer, int readable,
byte[] header) throws IOException {
// 檢查魔數是否相等
if (readable > 0 && header[0] != MAGIC_HIGH
|| readable > 1 && header[1] != MAGIC_LOW) {
int length = header.length;
if (header.length < readable) {
header = Bytes.copyOf(header, readable);
buffer.readBytes(header, length, readable - length);
}
for (int i = 1; i < header.length - 1; i++) {
if (header[i] == MAGIC_HIGH && header[i + 1] == MAGIC_LOW) {
buffer.readerIndex(buffer.readerIndex() - header.length +
i);
header = Bytes.copyOf(header, i);
break;
}
}
// 通過 telnet 命令列傳送的資料包不包含訊息頭,所以這裡
// 呼叫 TelnetCodec 的 decode 方法對資料包進行解碼
return super.decode(channel, buffer, readable, header);
}
// 檢測可讀資料量是否少於訊息頭長度,若小於則立即返回
DecodeResult.NEED_MORE_INPUT
if (readable < HEADER_LENGTH) {
return DecodeResult.NEED_MORE_INPUT;
}
// 從訊息頭中獲取訊息體長度
int len = Bytes.bytes2int(header, 12);
// 檢測訊息體長度是否超出限制,超出則丟擲異常
checkPayload(channel, len);
int tt = len + HEADER_LENGTH;
// 檢測可讀的位元組數是否小於實際的位元組數
if (readable < tt) {
return DecodeResult.NEED_MORE_INPUT;
}
ChannelBufferInputStream is = new ChannelBufferInputStream(buffer, len);
try {
// 繼續進行解碼工作
return decodeBody(channel, is, header);
} finally {
if (is.available() > 0) {
try {
StreamUtils.skipUnusedStream(is);
} catch (IOException e) {
logger.warn(e.getMessage(), e);
}
}
}
}
}
上面方法通過檢測訊息頭中的魔數是否與規定的魔數相等,提前攔截掉非常規資料包,比如通過telnet命令列發出的資料包。接著再對訊息體長度,以及可讀位元組數進行檢測。最後呼叫decodeBody 方法進行後續的解碼工作,ExchangeCodec 中實現了decodeBody 方法,但因其子類DubboCodec 覆寫了該方法,所以在執行時DubboCodec 中的decodeBody 方法會被呼叫。下面我們來看一下該方法的程式碼。
public class DubboCodec extends ExchangeCodec implements Codec2 {
@Override
protected Object decodeBody(Channel channel, InputStream is, byte[] header)
throws IOException {
// 獲取訊息頭中的第三個位元組,並通過邏輯與運算得到序列化器編號byte flag = header[2], proto = (byte) (flag & SERIALIZATION_MASK);
Serialization s = CodecSupport.getSerialization(channel.getUrl(),
proto);
// 獲取呼叫編號
long id = Bytes.bytes2long(header, 4);
// 通過邏輯與運算得到呼叫型別,0 - Response,1 - Request
if ((flag & FLAG_REQUEST) == 0) {
// 對響應結果進行解碼,得到 Response 物件。這個非本節內容,後面再分析
// ...
} else {
// 建立 Request 物件
Request req = new Request(id);
req.setVersion(Version.getProtocolVersion());
// 通過邏輯與運算得到通訊方式,並設定到 Request 物件中
req.setTwoWay((flag & FLAG_TWOWAY) != 0);
// 通過位運算檢測資料包是否為事件型別
if ((flag & FLAG_EVENT) != 0) {
// 設定心跳事件到 Request 物件中
req.setEvent(Request.HEARTBEAT_EVENT);
}
try {
Object data;
if (req.isHeartbeat()) {
// 對心跳包進行解碼,該方法已被標註為廢棄
data = decodeHeartbeatData(channel, deserialize(s,
channel.getUrl(), is));
} else if (req.isEvent()) {
// 對事件資料進行解碼
data = decodeEventData(channel, deserialize(s,
channel.getUrl(), is));
} else {
DecodeableRpcInvocation inv;
// 根據 url 引數判斷是否在 IO 執行緒上對訊息體進行解碼
if (channel.getUrl().getParameter(
Constants.DECODE_IN_IO_THREAD_KEY,
Constants.DEFAULT_DECODE_IN_IO_THREAD)) {
inv = new DecodeableRpcInvocation(channel, req, is,
proto);
// 在當前執行緒,也就是 IO 執行緒上進行後續的解碼工作。此工作完成後,
可將
// 呼叫方法名、attachment、以及呼叫引數解析出來
inv.decode();
} else {
// 僅建立 DecodeableRpcInvocation 物件,但不在當前執行緒上執行解
碼邏輯
inv = new DecodeableRpcInvocation(channel, req,
new
UnsafeByteArrayInputStream(readMessageData(is)), proto);
}
data = inv;
}
// 設定 data 到 Request 物件中
req.setData(data);
} catch (Throwable t) {
// 若解碼過程中出現異常,則將 broken 欄位設為 true,
// 並將異常物件設定到 Reqeust 物件中
req.setBroken(true);
req.setData(t);
}
return req;
}
}
}
如上,decodeBody 對部分欄位進行了解碼,並將解碼得到的欄位封裝到Request 中。隨後會呼叫DecodeableRpcInvocation 的decode 方法進行後續的解碼工作。此工作完成後,可將呼叫方法名、attachment、以及呼叫引數解析出來。
呼叫服務
解碼器將資料包解析成Request 物件後,NettyHandler 的messageReceived 方法緊接著會收到這個物件,並將這個物件繼續向下傳遞。整個呼叫棧如下:
NettyServerHandler#channelRead(ChannelHandlerContext, MessageEvent)
—> AbstractPeer#received(Channel, Object)
—> MultiMessageHandler#received(Channel, Object)
—> HeartbeatHandler#received(Channel, Object)
—> AllChannelHandler#received(Channel, Object)
—> ExecutorService#execute(Runnable) // 由執行緒池執行後續的呼叫邏輯
這裡我們直接分析呼叫棧中的分析第一個和最後一個呼叫方法邏輯。如下:
考慮到篇幅,以及很多中間呼叫的邏輯並非十分重要,所以這裡就不對呼叫棧中的每個方法都進行分析了。這裡我們直接分析最後一個呼叫方法邏輯。如下:
public class ChannelEventRunnable implements Runnable {
private final ChannelHandler handler;
private final Channel channel;
private final ChannelState state;
private final Throwable exception;
private final Object message;
@Override
public void run() {
// 檢測通道狀態,對於請求或響應訊息,此時 state = RECEIVED
if (state == ChannelState.RECEIVED) {
try {
// 將 channel 和 message 傳給 ChannelHandler 物件,進行後續的呼叫
handler.received(channel, message);
} catch (Exception e) {
logger.warn("... operation error, channel is ... message is
...");
}
}
// 其他訊息型別通過 switch 進行處理
else {
switch (state) {
case CONNECTED:
try {
handler.connected(channel);
} catch (Exception e) {
logger.warn("... operation error, channel is ...");
}
break;
case DISCONNECTED:
// ...
case SENT:
// ...
case CAUGHT:
// ...
default:
logger.warn("unknown state: " + state + ", message is " +
message);
}
}
}
}
如上,請求和響應訊息出現頻率明顯比其他型別訊息高,所以這裡對該型別的訊息進行了針對性判斷ChannelEventRunnable 僅是一箇中轉站,它的run 方法中並不包含具體的呼叫邏輯,僅用於將引數傳給其他ChannelHandler 物件進行處理,該物件型別為DecodeHandler
public class DecodeHandler extends AbstractChannelHandlerDelegate {
public DecodeHandler(ChannelHandler handler) {
super(handler);
}
@Override
public void received(Channel channel, Object message) throws
RemotingException {
if (message instanceof Decodeable) {
// 對 Decodeable 介面實現類物件進行解碼
decode(message);
}
if (message instanceof Request) {
// 對 Request 的 data 欄位進行解碼
decode(((Request) message).getData());
}
if (message instanceof Response) {
// 對 Request 的 result 欄位進行解碼
decode(((Response) message).getResult());
}
// 執行後續邏輯
handler.received(channel, message);
}
private void decode(Object message) {
// Decodeable 介面目前有兩個實現類,
// 分別為 DecodeableRpcInvocation 和 DecodeableRpcResult
if (message != null && message instanceof Decodeable) {
try {
// 執行解碼邏輯
((Decodeable) message).decode();
} catch (Throwable e) {
if (log.isWarnEnabled()) {
log.warn("Call Decodeable.decode failed: " + e.getMessage(),
e);
}
}
}
}
}
DecodeHandler 主要是包含了一些解碼邏輯,完全解碼後的Request 物件會繼續向後傳遞
public class DubboProtocol extends AbstractProtocol {
public static final String NAME = "dubbo";
private ExchangeHandler requestHandler = new ExchangeHandlerAdapter() {
@Override
public Object reply(ExchangeChannel channel, Object message) throws
RemotingException {
if (message instanceof Invocation) {
Invocation inv = (Invocation) message;
// 獲取 Invoker 例項
Invoker<?> invoker = getInvoker(channel, inv);
if
(Boolean.TRUE.toString().equals(inv.getAttachments().get(IS_CALLBACK_SERVICE_INV
OKE))) {
// 回撥相關,忽略
}
RpcContext.getContext().setRemoteAddress(channel.getRemoteAddress());
// 通過 Invoker 呼叫具體的服務
return invoker.invoke(inv);
}
throw new RemotingException(channel, "Unsupported request: ...");
}
// 忽略其他方法
}
Invoker<?> getInvoker(Channel channel, Invocation inv) throws
RemotingException {
// 忽略回撥和本地存根相關邏輯
// ...
int port = channel.getLocalAddress().getPort();
// 計算 service key,格式為 groupName/serviceName:serviceVersion:port。比
如:
// dubbo/com.alibaba.dubbo.demo.DemoService:1.0.0:20880
String serviceKey = serviceKey(port, path,
inv.getAttachments().get(Constants.VERSION_KEY),
inv.getAttachments().get(Constants.GROUP_KEY));
// 從 exporterMap 查詢與 serviceKey 相對應的 DubboExporter 物件,
// 服務匯出過程中會將 <serviceKey, DubboExporter> 對映關係儲存到 exporterMap
集合中
DubboExporter<?> exporter = (DubboExporter<?>)
exporterMap.get(serviceKey);
if (exporter == null)
throw new RemotingException(channel, "Not found exported service
...");
// 獲取 Invoker 物件,並返回
return exporter.getInvoker();
}
// 忽略其他方法
}
在之前課程中介紹過,服務全部暴露完成之後儲存到exporterMap中。這裡就是通過serviceKey獲取exporter之後獲取Invoker,並通過Invoker 的invoke 方法呼叫服務邏輯
public abstract class AbstractProxyInvoker<T> implements Invoker<T> {
@Override
public Result invoke(Invocation invocation) throws RpcException {
try {
// 呼叫 doInvoke 執行後續的呼叫,並將呼叫結果封裝到 RpcResult 中,並
return new RpcResult(doInvoke(proxy, invocation.getMethodName(),
invocation.getParameterTypes(), invocation.getArguments()));
} catch (InvocationTargetException e) {
return new RpcResult(e.getTargetException());
} catch (Throwable e) {
throw new RpcException("Failed to invoke remote proxy method ...");
}
}
protected abstract Object doInvoke(T proxy, String methodName, Class<?>[]
parameterTypes, Object[] arguments) throws Throwable;
}
如上,doInvoke 是一個抽象方法,這個需要由具體的Invoker 例項實現。Invoker 例項是在執行時通過JavassistProxyFactory 建立的,建立邏輯如下:
public class JavassistProxyFactory extends AbstractProxyFactory {
// 省略其他方法
@Override
public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
final Wrapper wrapper =
Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ?
proxy.getClass() : type);
// 建立匿名類物件
return new AbstractProxyInvoker<T>(proxy, type, url) {
@Override
protected Object doInvoke(T proxy, String methodName,
Class<?>[] parameterTypes,
Object[] arguments) throws Throwable {
// 呼叫 invokeMethod 方法進行後續的呼叫
return wrapper.invokeMethod(proxy, methodName, parameterTypes,
arguments);
}
};
}
}
Wrapper 是一個抽象類,其中invokeMethod 是一個抽象方法。Dubbo 會在執行時通過Javassist 框架為Wrapper 生成實現類,並實現invokeMethod 方法,該方法最終會根據呼叫資訊呼叫具體的服務。以DemoServiceImpl 為例,Javassist 為其生成的代理類如下。
/** Wrapper0 是在執行時生成的,大家可使用 Arthas 進行反編譯 */
public class Wrapper0 extends Wrapper implements ClassGenerator.DC {
public static String[] pns;
public static Map pts;
public static String[] mns;
public static String[] dmns;
public static Class[] mts0;
// 省略其他方法
public Object invokeMethod(Object object, String string, Class[] arrclass,
Object[] arrobject) throws InvocationTargetException {
DemoService demoService;
try {
// 型別轉換
demoService = (DemoService)object;
}
catch (Throwable throwable) {
throw new IllegalArgumentException(throwable);
}
try {
// 根據方法名呼叫指定的方法
if ("sayHello".equals(string) && arrclass.length == 1) {
return demoService.sayHello((String)arrobject[0]);
}
}
catch (Throwable throwable) {
throw new InvocationTargetException(throwable);
}
throw new NoSuchMethodException(new StringBuffer().append("Not found
method \"").append(string).append("\" in class
com.alibaba.dubbo.demo.DemoService.").toString());
}
}
到這裡,整個服務呼叫過程就分析完了。最後把呼叫過程貼出來,如下:
ChannelEventRunnable#run()
—> DecodeHandler#received(Channel, Object)
—> HeaderExchangeHandler#received(Channel, Object)
—> HeaderExchangeHandler#handleRequest(ExchangeChannel, Request)
—> DubboProtocol.requestHandler#reply(ExchangeChannel, Object)
—> Filter#invoke(Invoker, Invocation)
—> AbstractProxyInvoker#invoke(Invocation)
—> Wrapper0#invokeMethod(Object, String, Class[], Object[])
—> DemoServiceImpl#sayHello(String)
提供方返回撥用結果
服務提供方呼叫指定服務後,會將呼叫結果封裝到Response 物件中,並將該物件返回給服務消費方。服務提供方也是通過NettyChannel 的send 方法將Response 物件返回,這裡就不在重複分析了。
消費方接收呼叫結果
服務消費方在收到響應資料後,首先要做的事情是對響應資料進行解碼,得到Response 物件。然後再將該物件傳遞給下一個入站處理器,這個入站處理器就是NettyHandler。接下來NettyHandler 會將這個物件繼續向下傳遞,最後AllChannelHandler 的received 方法會收到這個物件,並將這個物件派發到執行緒池中。這個過程和服務提供方接收請求的過程是一樣的,因此這裡就不重複分析了
小結
至此整個dubbo的核心流程原理及其原始碼,我們就分析完畢了,整體流程思路不復雜,但是細節很多,要先理解其思想,還是得多花時間再仔細擼一遍。