Java進階專題(二十七) 將近2萬字的Dubbo原理解析,徹底搞懂dubbo (下)

有夢想的老王發表於2021-03-29

...接上文

服務發現

服務發現流程

整體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;
   }
}

好了,到這裡代理類生成邏輯就分析完了。整個過程比較複雜,大家需要耐心看一下。

總結

  1. 從註冊中心發現引用服務:在有註冊中心,通過註冊中心發現提供者地址的情況下,ReferenceConfig 解析出的URL 格式為: registry://registryhost:/org.apache.registry.RegistryService?refer=URL.encode("conumerhost/com.foo.FooService?version=1.0.0") 。
  2. 通過URL 的registry://協議頭識別,就會呼叫RegistryProtocol#refer()方法
  3. 查詢提供者URL,如 dubbo://service-host/com.foo.FooService?version=1.0.0 ,來獲取註冊中心
  4. 建立一個RegistryDirectory 例項並設定註冊中心和協議
  5. 生成conusmer 連線,在consumer 目錄下建立節點,向註冊中心註冊
  6. 註冊完畢後,訂閱providers,configurators,routers 等節點的資料
  7. 通過URL 的 dubbo:// 協議頭識別,呼叫 DubboProtocol#refer() 方法,建立一個
    ExchangeClient 客戶端並返回DubboInvoker 例項
  8. 由於一個服務可能會部署在多臺伺服器上,這樣就會在providers 產生多個節點,這樣也就會得到多個DubboInvoker 例項,就需要RegistryProtocol 呼叫Cluster 將多個服務提供者節點偽裝成一個節點,並返回一個Invoker
  9. 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就是使用這種形式的資料傳輸格式

preview

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的核心流程原理及其原始碼,我們就分析完畢了,整體流程思路不復雜,但是細節很多,要先理解其思想,還是得多花時間再仔細擼一遍。

相關文章