Dubbo原始碼解析之服務呼叫過程

Java勸退師發表於2019-04-08
  1. 簡介 在前面的文章中,我們分析了 Dubbo SPI、服務匯出與引入、以及叢集容錯方面的程式碼。經過前文的鋪墊,本篇文章我們終於可以分析服務呼叫過程了。Dubbo 服務呼叫過程比較複雜,包含眾多步驟,比如傳送請求、編解碼、服務降級、過濾器鏈處理、序列化、執行緒派發以及響應請求等步驟。限於篇幅原因,本篇文章無法對所有的步驟一一進行分析。本篇文章將會重點分析請求的傳送與接收、編解碼、執行緒派發以及響應的傳送與接收等過程,至於服務降級、過濾器鏈和序列化大家自行進行分析,也可以將其當成一個黑盒,暫時忽略也沒關係。介紹完本篇文章要分析的內容,接下來我們進入正題吧。

2. 原始碼分析

在進行原始碼分析之前,我們先來通過一張圖瞭解 Dubbo 服務呼叫過程。

Dubbo原始碼解析之服務呼叫過程

首先服務消費者通過代理物件 Proxy 發起遠端呼叫,接著通過網路客戶端 Client 將編碼後的請求傳送給服務提供方的網路層上,也就是 Server。Server 在收到請求後,首先要做的事情是對資料包進行解碼。然後將解碼後的請求傳送至分發器 Dispatcher,再由分發器將請求派發到指定的執行緒池上,最後由執行緒池呼叫具體的服務。這就是一個遠端呼叫請求的傳送與接收過程。至於響應的傳送與接收過程,這張圖中沒有表現出來。對於這兩個過程,我們也會進行詳細分析。

2.1 服務呼叫方式

Dubbo 支援同步和非同步兩種呼叫方式,其中非同步呼叫還可細分為“有返回值”的非同步呼叫和“無返回值”的非同步呼叫。所謂“無返回值”非同步呼叫是指服務消費方只管呼叫,但不關心呼叫結果,此時 Dubbo 會直接返回一個空的 RpcResult。若要使用非同步特性,需要服務消費方手動進行配置。預設情況下,Dubbo 使用同步呼叫方式。

本節以及其他章節將會使用 Dubbo 官方提供的 Demo 分析整個呼叫過程,下面我們從 DemoService 介面的代理類開始進行分析。Dubbo 預設使用 Javassist 框架為服務介面生成動態代理類,因此我們需要先將代理類進行反編譯才能看到原始碼。這裡使用阿里開源 Java 應用診斷工具 Arthas 反編譯代理類,結果如下:

/**
 * Arthas 反編譯步驟:
 * 1. 啟動 Arthas
 *    java -jar arthas-boot.jar
 *
 * 2. 輸入編號選擇程式
 *    Arthas 啟動後,會列印 Java 應用程式列表,如下:
 *    [1]: 11232 org.jetbrains.jps.cmdline.Launcher
 *    [2]: 22370 org.jetbrains.jps.cmdline.Launcher
 *    [3]: 22371 com.alibaba.dubbo.demo.consumer.Consumer
 *    [4]: 22362 com.alibaba.dubbo.demo.provider.Provider
 *    [5]: 2074 org.apache.zookeeper.server.quorum.QuorumPeerMain
 * 這裡輸入編號 3,讓 Arthas 關聯到啟動類為 com.....Consumer 的 Java 程式上
 *
 * 3. 由於 Demo 專案中只有一個服務介面,因此此介面的代理類類名為 proxy0,此時使用 sc 命令搜尋這個類名。
 *    $ sc *.proxy0
 *    com.alibaba.dubbo.common.bytecode.proxy0
 *
 * 4. 使用 jad 命令反編譯 com.alibaba.dubbo.common.bytecode.proxy0
 *    $ jad com.alibaba.dubbo.common.bytecode.proxy0
 *
 * 更多使用方法請參考 Arthas 官方文件:
 *   https://alibaba.github.io/arthas/quick-start.html
 */
public class proxy0 implements ClassGenerator.DC, EchoService, DemoService {
    // 方法陣列
    public static Method[] methods;
    private InvocationHandler handler;

    public proxy0(InvocationHandler invocationHandler) {
        this.handler = invocationHandler;
    }

    public proxy0() {
    }

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

    /** 回聲測試方法 */
    public Object $echo(Object object) {
        Object[] arrobject = new Object[]{object};
        Object object2 = this.handler.invoke(this, methods[1], arrobject);
        return object2;
    }
}
複製程式碼

如上,代理類的邏輯比較簡單。首先將執行時引數儲存到陣列中,然後呼叫 InvocationHandler 介面實現類的 invoke 方法,得到呼叫結果,最後將結果轉型並返回給呼叫方。關於代理類的邏輯就說這麼多,繼續向下分析。

public class InvokerInvocationHandler implements InvocationHandler {

    private final Invoker<?> invoker;

    public InvokerInvocationHandler(Invoker<?> handler) {
        this.invoker = handler;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        Class<?>[] parameterTypes = method.getParameterTypes();
        
        // 攔截定義在 Object 類中的方法(未被子類重寫),比如 wait/notify
        if (method.getDeclaringClass() == Object.class) {
            return method.invoke(invoker, args);
        }
        
        // 如果 toString、hashCode 和 equals 等方法被子類重寫了,這裡也直接呼叫
        if ("toString".equals(methodName) && parameterTypes.length == 0) {
            return invoker.toString();
        }
        if ("hashCode".equals(methodName) && parameterTypes.length == 0) {
            return invoker.hashCode();
        }
        if ("equals".equals(methodName) && parameterTypes.length == 1) {
            return invoker.equals(args[0]);
        }
        
        // 將 method 和 args 封裝到 RpcInvocation 中,並執行後續的呼叫
        return invoker.invoke(new RpcInvocation(method, args)).recreate();
    }
}
複製程式碼

InvokerInvocationHandler 中的 invoker 成員變數型別為 MockClusterInvoker,MockClusterInvoker 內部封裝了服務降級邏輯。下面簡單看一下:

public class MockClusterInvoker<T> implements Invoker<T> {
    
    private final Invoker<T> invoker;
    
    public Result invoke(Invocation invocation) throws RpcException {
        Result result = null;

        // 獲取 mock 配置值
        String value = directory.getUrl().getMethodParameter(invocation.getMethodName(), Constants.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 {
                // 呼叫其他 Invoker 物件的 invoke 方法
                result = this.invoker.invoke(invocation);
            } catch (RpcException e) {
                if (e.isBiz()) {
                    throw e;
                } else {
                    // 呼叫失敗,執行 mock 邏輯
                    result = doMockInvoke(invocation, e);
                }
            }
        }
        return result;
    }
    
    // 省略其他方法
}
複製程式碼

服務降級不是本文重點,因此這裡就不分析 doMockInvoke 方法了。考慮到前文已經詳細分析過 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 中看一下。

public class DubboInvoker<T> extends AbstractInvoker<T> {
    
    private final ExchangeClient[] clients;
    
    protected Result doInvoke(final Invocation invocation) throws Throwable {
        RpcInvocation inv = (RpcInvocation) invocation;
        final String methodName = RpcUtils.getMethodName(invocation);
        // 設定 path 和 version 到 attachment 中
        inv.setAttachment(Constants.PATH_KEY, getUrl().getPath());
        inv.setAttachment(Constants.VERSION_KEY, version);

        ExchangeClient currentClient;
        if (clients.length == 1) {
            // 從 clients 陣列中獲取 ExchangeClient
            currentClient = clients[0];
        } else {
            currentClient = clients[index.getAndIncrement() % clients.length];
        }
        try {
            // 獲取非同步配置
            boolean isAsync = RpcUtils.isAsync(getUrl(), invocation);
            // isOneway 為 true,表示“單向”通訊
            boolean isOneway = RpcUtils.isOneway(getUrl(), invocation);
            int timeout = getUrl().getMethodParameter(methodName, Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT);

            // 非同步無返回值
            if (isOneway) {
                boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, false);
                // 傳送請求
                currentClient.send(inv, isSent);
                // 設定上下文中的 future 欄位為 null
                RpcContext.getContext().setFuture(null);
                // 返回一個空的 RpcResult
                return new RpcResult();
            } 

            // 非同步有返回值
            else if (isAsync) {
                // 傳送請求,並得到一個 ResponseFuture 例項
                ResponseFuture future = currentClient.request(inv, timeout);
                // 設定 future 到上下文中
                RpcContext.getContext().setFuture(new FutureAdapter<Object>(future));
                // 暫時返回一個空結果
                return new RpcResult();
            } 

            // 同步呼叫
            else {
                RpcContext.getContext().setFuture(null);
                // 傳送請求,得到一個 ResponseFuture 例項,並呼叫該例項的 get 方法進行等待
                return (Result) currentClient.request(inv, timeout).get();
            }
        } catch (TimeoutException e) {
            throw new RpcException(..., "Invoke remote method timeout....");
        } catch (RemotingException e) {
            throw new RpcException(..., "Failed to invoke remote method: ...");
        }
    }
    
    // 省略其他方法
}
複製程式碼

上面的程式碼包含了 Dubbo 對同步和非同步呼叫的處理邏輯,搞懂了上面的程式碼,會對 Dubbo 的同步和非同步呼叫方式有更深入的瞭解。Dubbo 實現同步和非同步呼叫比較關鍵的一點就在於由誰呼叫 ResponseFuture 的 get 方法。同步呼叫模式下,由框架自身呼叫 ResponseFuture 的 get 方法。非同步呼叫模式下,則由使用者呼叫該方法。ResponseFuture 是一個介面,下面我們來看一下它的預設實現類 DefaultFuture 的原始碼。

public class DefaultFuture implements ResponseFuture {
    
    private static final Map<Long, Channel> CHANNELS = 
        new ConcurrentHashMap<Long, Channel>();

    private static final Map<Long, DefaultFuture> FUTURES = 
        new ConcurrentHashMap<Long, DefaultFuture>();
    
    private final long id;
    private final Channel channel;
    private final Request request;
    private final int timeout;
    private final Lock lock = new ReentrantLock();
    private final Condition done = lock.newCondition();
    private volatile Response response;
    
    public DefaultFuture(Channel channel, Request request, int timeout) {
        this.channel = channel;
        this.request = request;
        
        // 獲取請求 id,這個 id 很重要,後面還會見到
        this.id = request.getId();
        this.timeout = timeout > 0 ? timeout : channel.getUrl().getPositiveParameter(Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT);
        // 儲存 <requestId, DefaultFuture> 對映關係到 FUTURES 中
        FUTURES.put(id, this);
        CHANNELS.put(id, channel);
    }
    
    @Override
    public Object get() throws RemotingException {
        return get(timeout);
    }

    @Override
    public Object get(int timeout) throws RemotingException {
        if (timeout <= 0) {
            timeout = Constants.DEFAULT_TIMEOUT;
        }
        
        // 檢測服務提供方是否成功返回了呼叫結果
        if (!isDone()) {
            long start = System.currentTimeMillis();
            lock.lock();
            try {
                // 迴圈檢測服務提供方是否成功返回了呼叫結果
                while (!isDone()) {
                    // 如果呼叫結果尚未返回,這裡等待一段時間
                    done.await(timeout, TimeUnit.MILLISECONDS);
                    // 如果呼叫結果成功返回,或等待超時,此時跳出 while 迴圈,執行後續的邏輯
                    if (isDone() || System.currentTimeMillis() - start > timeout) {
                        break;
                    }
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
            }
            
            // 如果呼叫結果仍未返回,則丟擲超時異常
            if (!isDone()) {
                throw new TimeoutException(sent > 0, channel, getTimeoutMessage(false));
            }
        }
        
        // 返回撥用結果
        return returnFromResponse();
    }
    
    @Override
    public boolean isDone() {
        // 通過檢測 response 欄位為空與否,判斷是否收到了呼叫結果
        return response != null;
    }
    
    private Object returnFromResponse() throws RemotingException {
        Response res = response;
        if (res == null) {
            throw new IllegalStateException("response cannot be null");
        }
        
        // 如果呼叫結果的狀態為 Response.OK,則表示呼叫過程正常,服務提供方成功返回了呼叫結果
        if (res.getStatus() == Response.OK) {
            return res.getResult();
        }
        
        // 丟擲異常
        if (res.getStatus() == Response.CLIENT_TIMEOUT || res.getStatus() == Response.SERVER_TIMEOUT) {
            throw new TimeoutException(res.getStatus() == Response.SERVER_TIMEOUT, channel, res.getErrorMessage());
        }
        throw new RemotingException(channel, res.getErrorMessage());
    }
    
    // 省略其他方法
}
複製程式碼

如上,當服務消費者還未接收到呼叫結果時,使用者執行緒呼叫 get 方法會被阻塞住。同步呼叫模式下,框架獲得 DefaultFuture 物件後,會立即呼叫 get 方法進行等待。而非同步模式下則是將該物件封裝到 FutureAdapter 例項中,並將 FutureAdapter 例項設定到 RpcContext 中,供使用者使用。FutureAdapter 是一個介面卡,用於將 Dubbo 中的 ResponseFuture 與 JDK 中的 Future 進行適配。這樣當使用者執行緒呼叫 Future 的 get 方法時,經過 FutureAdapter 適配,最終會呼叫 ResponseFuture 實現類物件的 get 方法,也就是 DefaultFuture 的 get 方法。

到這裡關於 Dubbo 幾種呼叫方式的程式碼邏輯就分析完了,下面來分析請求資料的傳送與接收,以及響應資料的傳送與接收過程。

2.2 服務消費方傳送請求

2.2.1 傳送請求

本節我們來看一下同步呼叫模式下,服務消費方是如何傳送呼叫請求的。在深入分析原始碼前,我們先來看一張圖。

這張圖展示了服務消費方傳送請求過程的部分呼叫棧,略為複雜。從上圖可以看出,經過多次呼叫後,才將請求資料送至 Netty NioClientSocketChannel。這樣做的原因是通過 Exchange 層為框架引入 Request 和 Response 語義,這一點會在接下來的原始碼分析過程中會看到。其他的就不多說了,下面開始進行分析。首先分析 ReferenceCountExchangeClient 的原始碼。

final class ReferenceCountExchangeClient implements ExchangeClient {

    private final URL url;
    private final AtomicInteger referenceCount = new AtomicInteger(0);

    public ReferenceCountExchangeClient(ExchangeClient client, ConcurrentMap<String, LazyConnectExchangeClient> ghostClientMap) {
        this.client = client;
        // 引用計數自增
        referenceCount.incrementAndGet();
        this.url = client.getUrl();
        
        // ...
    }

    @Override
    public ResponseFuture request(Object request) throws RemotingException {
        // 直接呼叫被裝飾物件的同簽名方法
        return client.request(request);
    }

    @Override
    public ResponseFuture request(Object request, int timeout) throws RemotingException {
        // 直接呼叫被裝飾物件的同簽名方法
        return client.request(request, timeout);
    }

    /** 引用計數自增,該方法由外部呼叫 */
    public void incrementAndGetCount() {
        // referenceCount 自增
        referenceCount.incrementAndGet();
    }
    
        @Override
    public void close(int timeout) {
        // referenceCount 自減
        if (referenceCount.decrementAndGet() <= 0) {
            if (timeout == 0) {
                client.close();
            } else {
                client.close(timeout);
            }
            client = replaceWithLazyClient();
        }
    }
    
    // 省略部分方法
}
複製程式碼

ReferenceCountExchangeClient 內部定義了一個引用計數變數 referenceCount,每當該物件被引用一次 referenceCount 都會進行自增。每當 close 方法被呼叫時,referenceCount 進行自減。ReferenceCountExchangeClient 內部僅實現了一個引用計數的功能,其他方法並無複雜邏輯,均是直接呼叫被裝飾物件的相關方法。所以這裡就不多說了,繼續向下分析,這次是 HeaderExchangeClient。

public class HeaderExchangeClient implements ExchangeClient {

    private static final ScheduledThreadPoolExecutor scheduled = new ScheduledThreadPoolExecutor(2, new NamedThreadFactory("dubbo-remoting-client-heartbeat", true));
    private final Client client;
    private final ExchangeChannel channel;
    private ScheduledFuture<?> heartbeatTimer;
    private int heartbeat;
    private int heartbeatTimeout;

    public HeaderExchangeClient(Client client, boolean needHeartbeat) {
        if (client == null) {
            throw new IllegalArgumentException("client == null");
        }
        this.client = client;
        
        // 建立 HeaderExchangeChannel 物件
        this.channel = new HeaderExchangeChannel(client);
        
        // 以下程式碼均與心跳檢測邏輯有關
        String dubbo = client.getUrl().getParameter(Constants.DUBBO_VERSION_KEY);
        this.heartbeat = client.getUrl().getParameter(Constants.HEARTBEAT_KEY, dubbo != null && dubbo.startsWith("1.0.") ? Constants.DEFAULT_HEARTBEAT : 0);
        this.heartbeatTimeout = client.getUrl().getParameter(Constants.HEARTBEAT_TIMEOUT_KEY, heartbeat * 3);
        if (heartbeatTimeout < heartbeat * 2) {
            throw new IllegalStateException("heartbeatTimeout < heartbeatInterval * 2");
        }
        if (needHeartbeat) {
            // 開啟心跳檢測定時器
            startHeartbeatTimer();
        }
    }

    @Override
    public ResponseFuture request(Object request) throws RemotingException {
        // 直接 HeaderExchangeChannel 物件的同簽名方法
        return channel.request(request);
    }

    @Override
    public ResponseFuture request(Object request, int timeout) throws RemotingException {
        // 直接 HeaderExchangeChannel 物件的同簽名方法
        return channel.request(request, timeout);
    }

    @Override
    public void close() {
        doClose();
        channel.close();
    }
    
    private void doClose() {
        // 停止心跳檢測定時器
        stopHeartbeatTimer();
    }

    private void startHeartbeatTimer() {
        stopHeartbeatTimer();
        if (heartbeat > 0) {
            heartbeatTimer = scheduled.scheduleWithFixedDelay(
                    new HeartBeatTask(new HeartBeatTask.ChannelProvider() {
                        @Override
                        public Collection<Channel> getChannels() {
                            return Collections.<Channel>singletonList(HeaderExchangeClient.this);
                        }
                    }, heartbeat, heartbeatTimeout),
                    heartbeat, heartbeat, TimeUnit.MILLISECONDS);
        }
    }

    private void stopHeartbeatTimer() {
        if (heartbeatTimer != null && !heartbeatTimer.isCancelled()) {
            try {
                heartbeatTimer.cancel(true);
                scheduled.purge();
            } catch (Throwable e) {
                if (logger.isWarnEnabled()) {
                    logger.warn(e.getMessage(), e);
                }
            }
        }
        heartbeatTimer = null;
    }
    
    // 省略部分方法
}
複製程式碼

HeaderExchangeClient 中很多方法只有一行程式碼,即呼叫 HeaderExchangeChannel 物件的同簽名方法。那 HeaderExchangeClient 有什麼用處呢?答案是封裝了一些關於心跳檢測的邏輯。心跳檢測並非本文所關注的點,因此就不多說了,繼續向下看。

final class HeaderExchangeChannel implements ExchangeChannel {
    
    private final Channel channel;
    
    HeaderExchangeChannel(Channel channel) {
        if (channel == null) {
            throw new IllegalArgumentException("channel == null");
        }
        
        // 這裡的 channel 指向的是 NettyClient
        this.channel = channel;
    }
    
    @Override
    public ResponseFuture request(Object request) throws RemotingException {
        return request(request, channel.getUrl().getPositiveParameter(Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT));
    }

    @Override
    public ResponseFuture request(Object request, int timeout) throws RemotingException {
        if (closed) {
            throw new RemotingException(..., "Failed to send request ...);
        }
        // 建立 Request 物件
        Request req = new Request();
        req.setVersion(Version.getProtocolVersion());
        // 設定雙向通訊標誌為 true
        req.setTwoWay(true);
        // 這裡的 request 變數型別為 RpcInvocation
        req.setData(request);
                                        
        // 建立 DefaultFuture 物件
        DefaultFuture future = new DefaultFuture(channel, req, timeout);
        try {
            // 呼叫 NettyClient 的 send 方法傳送請求
            channel.send(req);
        } catch (RemotingException e) {
            future.cancel();
            throw e;
        }
        // 返回 DefaultFuture 物件
        return future;
    }
}
複製程式碼

到這裡大家終於看到了 Request 語義了,上面的方法首先定義了一個 Request 物件,然後再將該物件傳給 NettyClient 的 send 方法,進行後續的呼叫。需要說明的是,NettyClient 中並未實現 send 方法,該方法繼承自父類 AbstractPeer,下面直接分析 AbstractPeer 的程式碼。

public abstract class AbstractPeer implements Endpoint, ChannelHandler {
    
    @Override
    public void send(Object message) throws RemotingException {
        // 該方法由 AbstractClient 類實現
        send(message, url.getParameter(Constants.SENT_KEY, false));
    }
    
    // 省略其他方法
}
複製程式碼
public abstract class AbstractClient extends AbstractEndpoint implements Client {
    
    @Override
    public void send(Object message, boolean sent) throws RemotingException {
        if (send_reconnect && !isConnected()) {
            connect();
        }
        
        // 獲取 Channel,getChannel 是一個抽象方法,具體由子類實現
        Channel channel = getChannel();
        if (channel == null || !channel.isConnected()) {
            throw new RemotingException(this, "message can not send ...");
        }
        
        // 繼續向下呼叫
        channel.send(message, sent);
    }
    
    protected abstract Channel getChannel();
    
    // 省略其他方法
}
複製程式碼

預設情況下,Dubbo 使用 Netty 作為底層的通訊框架,因此下面我們到 NettyClient 類中看一下 getChannel 方法的實現邏輯。

public class NettyClient extends AbstractClient {
    
    // 這裡的 Channel 全限定名稱為 org.jboss.netty.channel.Channel
    private volatile Channel channel;

    @Override
    protected com.alibaba.dubbo.remoting.Channel getChannel() {
        Channel c = channel;
        if (c == null || !c.isConnected())
            return null;
        // 獲取一個 NettyChannel 型別物件
        return NettyChannel.getOrAddChannel(c, getUrl(), this);
    }
}
複製程式碼
final class NettyChannel extends AbstractChannel {

    private static final ConcurrentMap<org.jboss.netty.channel.Channel, NettyChannel> channelMap = 
        new ConcurrentHashMap<org.jboss.netty.channel.Channel, NettyChannel>();

    private final org.jboss.netty.channel.Channel channel;
    
    /** 私有構造方法 */
    private NettyChannel(org.jboss.netty.channel.Channel channel, URL url, ChannelHandler handler) {
        super(url, handler);
        if (channel == null) {
            throw new IllegalArgumentException("netty channel == null;");
        }
        this.channel = channel;
    }

    static NettyChannel getOrAddChannel(org.jboss.netty.channel.Channel ch, URL url, ChannelHandler handler) {
        if (ch == null) {
            return null;
        }
        
        // 嘗試從集合中獲取 NettyChannel 例項
        NettyChannel ret = channelMap.get(ch);
        if (ret == null) {
            // 如果 ret = null,則建立一個新的 NettyChannel 例項
            NettyChannel nc = new NettyChannel(ch, url, handler);
            if (ch.isConnected()) {
                // 將 <Channel, NettyChannel> 鍵值對存入 channelMap 集合中
                ret = channelMap.putIfAbsent(ch, nc);
            }
            if (ret == null) {
                ret = nc;
            }
        }
        return ret;
    }
}
複製程式碼

獲取到 NettyChannel 例項後,即可進行後續的呼叫。下面看一下 NettyChannel 的 send 方法。

public void send(Object message, boolean sent) throws RemotingException {
    super.send(message, sent);

    boolean success = true;
    int timeout = 0;
    try {
        // 傳送訊息(包含請求和響應訊息)
        ChannelFuture future = channel.write(message);
        
        // sent 的值源於 <dubbo:method sent="true/false" /> 中 sent 的配置值,有兩種配置值:
        //   1. true: 等待訊息發出,訊息傳送失敗將丟擲異常
        //   2. false: 不等待訊息發出,將訊息放入 IO 佇列,即刻返回
        // 預設情況下 sent = false;
        if (sent) {
            timeout = getUrl().getPositiveParameter(Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT);
            // 等待訊息發出,若在規定時間沒能發出,success 會被置為 false
            success = future.await(timeout);
        }
        Throwable cause = future.getCause();
        if (cause != null) {
            throw cause;
        }
    } catch (Throwable e) {
        throw new RemotingException(this, "Failed to send message ...");
    }

    // 若 success 為 false,這裡丟擲異常
    if (!success) {
        throw new RemotingException(this, "Failed to send message ...");
    }
}
複製程式碼

經歷多次呼叫,到這裡請求資料的傳送過程就結束了,過程漫長。為了便於大家閱讀程式碼,這裡以 DemoService 為例,將 sayHello 方法的整個呼叫路徑貼出來。

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)
複製程式碼

在 Netty 中,出站資料在發出之前還需要進行編碼操作,接下來我們來分析一下請求資料的編碼邏輯。

2.2.2 請求編碼

在分析請求編碼邏輯之前,我們先來看一下 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 訊息體長度 執行時計算 瞭解了 Dubbo 資料包格式,接下來我們就可以探索編碼過程了。這次我們開門見山,直接分析編碼邏輯所在類。如下:

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());
    }
}
複製程式碼

至此,關於服務消費方傳送請求的過程就分析完了,接下來我們來看一下服務提供方是如何接收請求的。

2.3 服務提供方接收請求

前面說過,預設情況下 Dubbo 使用 Netty 作為底層的通訊框架。Netty 檢測到有資料入站後,首先會通過解碼器對資料進行解碼,並將解碼後的資料傳遞給下一個入站處理器的指定方法。所以在進行後續的分析之前,我們先來看一下資料解碼過程。

2.3.1 請求解碼

這裡直接分析請求資料的解碼邏輯,忽略中間過程,如下:

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、以及呼叫引數解析出來。下面我們來看一下 DecodeableRpcInvocation 的 decode 方法邏輯。

public class DecodeableRpcInvocation extends RpcInvocation implements Codec, Decodeable {
    
	@Override
    public Object decode(Channel channel, InputStream input) throws IOException {
        ObjectInput in = CodecSupport.getSerialization(channel.getUrl(), serializationType)
                .deserialize(channel.getUrl(), input);

        // 通過反序列化得到 dubbo version,並儲存到 attachments 變數中
        String dubboVersion = in.readUTF();
        request.setVersion(dubboVersion);
        setAttachment(Constants.DUBBO_VERSION_KEY, dubboVersion);

        // 通過反序列化得到 path,version,並儲存到 attachments 變數中
        setAttachment(Constants.PATH_KEY, in.readUTF());
        setAttachment(Constants.VERSION_KEY, in.readUTF());

        // 通過反序列化得到呼叫方法名
        setMethodName(in.readUTF());
        try {
            Object[] args;
            Class<?>[] pts;
            // 通過反序列化得到引數型別字串,比如 Ljava/lang/String;
            String desc = in.readUTF();
            if (desc.length() == 0) {
                pts = DubboCodec.EMPTY_CLASS_ARRAY;
                args = DubboCodec.EMPTY_OBJECT_ARRAY;
            } else {
                // 將 desc 解析為引數型別陣列
                pts = ReflectUtils.desc2classArray(desc);
                args = new Object[pts.length];
                for (int i = 0; i < args.length; i++) {
                    try {
                        // 解析執行時引數
                        args[i] = in.readObject(pts[i]);
                    } catch (Exception e) {
                        if (log.isWarnEnabled()) {
                            log.warn("Decode argument failed: " + e.getMessage(), e);
                        }
                    }
                }
            }
            
            // 設定引數型別陣列
            setParameterTypes(pts);

            // 通過反序列化得到原 attachment 的內容
            Map<String, String> map = (Map<String, String>) in.readObject(Map.class);
            if (map != null && map.size() > 0) {
                Map<String, String> attachment = getAttachments();
                if (attachment == null) {
                    attachment = new HashMap<String, String>();
                }
                // 將 map 與當前物件中的 attachment 集合進行融合
                attachment.putAll(map);
                setAttachments(attachment);
            }
            
            // 對 callback 型別的引數進行處理
            for (int i = 0; i < args.length; i++) {
                args[i] = decodeInvocationArgument(channel, this, pts, i, args[i]);
            }

            // 設定引數列表
            setArguments(args);

        } catch (ClassNotFoundException e) {
            throw new IOException(StringUtils.toString("Read invocation data failed.", e));
        } finally {
            if (in instanceof Cleanable) {
                ((Cleanable) in).cleanup();
            }
        }
        return this;
    }
}
複製程式碼

上面的方法通過反序列化將諸如 path、version、呼叫方法名、引數列表等資訊依次解析出來,並設定到相應的欄位中,最終得到一個具有完整呼叫資訊的 DecodeableRpcInvocation 物件。

到這裡,請求資料解碼的過程就分析完了。此時我們得到了一個 Request 物件,這個物件會被傳送到下一個入站處理器中,我們繼續往下看。

2.3.2 呼叫服務

解碼器將資料包解析成 Request 物件後,NettyHandler 的 messageReceived 方法緊接著會收到這個物件,並將這個物件繼續向下傳遞。這期間該物件會被依次傳遞給 NettyServer、MultiMessageHandler、HeartbeatHandler 以及 AllChannelHandler。最後由 AllChannelHandler 將該物件封裝到 Runnable 實現類物件中,並將 Runnable 放入執行緒池中執行後續的呼叫邏輯。整個呼叫棧如下:

NettyHandler#messageReceived(ChannelHandlerContext, MessageEvent) —> AbstractPeer#received(Channel, Object) —> MultiMessageHandler#received(Channel, Object) —> HeartbeatHandler#received(Channel, Object) —> AllChannelHandler#received(Channel, Object) —> ExecutorService#execute(Runnable) // 由執行緒池執行後續的呼叫邏輯 考慮到篇幅,以及很多中間呼叫的邏輯並非十分重要,所以這裡就不對呼叫棧中的每個方法都進行分析了。這裡我們直接分析呼叫棧中的分析第一個和最後一個呼叫方法邏輯。如下:

@Sharable
public class NettyHandler extends SimpleChannelHandler {
    
    private final Map<String, Channel> channels = new ConcurrentHashMap<String, Channel>();

    private final URL url;

    private final ChannelHandler handler;
    
    public NettyHandler(URL url, ChannelHandler handler) {
        if (url == null) {
            throw new IllegalArgumentException("url == null");
        }
        if (handler == null) {
            throw new IllegalArgumentException("handler == null");
        }
        this.url = url;
        
        // 這裡的 handler 型別為 NettyServer
        this.handler = handler;
    }
    
	public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
        // 獲取 NettyChannel
        NettyChannel channel = NettyChannel.getOrAddChannel(ctx.getChannel(), url, handler);
        try {
            // 繼續向下呼叫
            handler.received(channel, e.getMessage());
        } finally {
            NettyChannel.removeChannelIfDisconnected(ctx.getChannel());
        }
    }
}
複製程式碼

如上,NettyHandler 中的 messageReceived 邏輯比較簡單。首先根據一些資訊獲取 NettyChannel 例項,然後將 NettyChannel 例項以及 Request 物件向下傳遞。下面再來看看 AllChannelHandler 的邏輯,在詳細分析程式碼之前,我們先來了解一下 Dubbo 中的執行緒派發模型。

2.3.2.1 執行緒派發模型

Dubbo 將底層通訊框架中接收請求的執行緒稱為 IO 執行緒。如果一些事件處理邏輯可以很快執行完,比如只在記憶體打一個標記,此時直接在 IO 執行緒上執行該段邏輯即可。但如果事件的處理邏輯比較耗時,比如該段邏輯會發起資料庫查詢或者 HTTP 請求。此時我們就不應該讓事件處理邏輯在 IO 執行緒上執行,而是應該派發到執行緒池中去執行。原因也很簡單,IO 執行緒主要用於接收請求,如果 IO 執行緒被佔滿,將導致它不能接收新的請求。

以上就是執行緒派發的背景,下面我們再來通過 Dubbo 呼叫圖,看一下執行緒派發器所處的位置。

如上圖,紅框中的 Dispatcher 就是執行緒派發器。需要說明的是,Dispatcher 真實的職責建立具有執行緒派發能力的 ChannelHandler,比如 AllChannelHandler、MessageOnlyChannelHandler 和 ExecutionChannelHandler 等,其本身並不具備執行緒派發能力。Dubbo 支援 5 種不同的執行緒派發策略,下面通過一個表格列舉一下。

策略 用途 all 所有訊息都派發到執行緒池,包括請求,響應,連線事件,斷開事件等 direct 所有訊息都不派發到執行緒池,全部在 IO 執行緒上直接執行 message 只有請求和響應訊息派發到執行緒池,其它訊息均在 IO 執行緒上執行 execution 只有請求訊息派發到執行緒池,不含響應。其它訊息均在 IO 執行緒上執行 connection 在 IO 執行緒上,將連線斷開事件放入佇列,有序逐個執行,其它訊息派發到執行緒池 預設配置下,Dubbo 使用 all 派發策略,即將所有的訊息都派發到執行緒池中。下面我們來分析一下 AllChannelHandler 的程式碼。

public class AllChannelHandler extends WrappedChannelHandler {

    public AllChannelHandler(ChannelHandler handler, URL url) {
        super(handler, url);
    }

    /** 處理連線事件 */
    @Override
    public void connected(Channel channel) throws RemotingException {
        // 獲取執行緒池
        ExecutorService cexecutor = getExecutorService();
        try {
            // 將連線事件派發到執行緒池中處理
            cexecutor.execute(new ChannelEventRunnable(channel, handler, ChannelState.CONNECTED));
        } catch (Throwable t) {
            throw new ExecutionException(..., " error when process connected event .", t);
        }
    }

    /** 處理斷開事件 */
    @Override
    public void disconnected(Channel channel) throws RemotingException {
        ExecutorService cexecutor = getExecutorService();
        try {
            cexecutor.execute(new ChannelEventRunnable(channel, handler, ChannelState.DISCONNECTED));
        } catch (Throwable t) {
            throw new ExecutionException(..., "error when process disconnected event .", t);
        }
    }

    /** 處理請求和響應訊息,這裡的 message 變數型別可能是 Request,也可能是 Response */
    @Override
    public void received(Channel channel, Object message) throws RemotingException {
        ExecutorService cexecutor = getExecutorService();
        try {
            // 將請求和響應訊息派發到執行緒池中處理
            cexecutor.execute(new ChannelEventRunnable(channel, handler, ChannelState.RECEIVED, message));
        } catch (Throwable t) {
            if(message instanceof Request && t instanceof RejectedExecutionException){
                Request request = (Request)message;
                // 如果通訊方式為雙向通訊,此時將 Server side ... threadpool is exhausted 
                // 錯誤資訊封裝到 Response 中,並返回給服務消費方。
                if(request.isTwoWay()){
                    String msg = "Server side(" + url.getIp() + "," + url.getPort() 
                        + ") threadpool is exhausted ,detail msg:" + t.getMessage();
                    Response response = new Response(request.getId(), request.getVersion());
                    response.setStatus(Response.SERVER_THREADPOOL_EXHAUSTED_ERROR);
                    response.setErrorMessage(msg);
                    // 返回包含錯誤資訊的 Response 物件
                    channel.send(response);
                    return;
                }
            }
            throw new ExecutionException(..., " error when process received event .", t);
        }
    }

    /** 處理異常資訊 */
    @Override
    public void caught(Channel channel, Throwable exception) throws RemotingException {
        ExecutorService cexecutor = getExecutorService();
        try {
            cexecutor.execute(new ChannelEventRunnable(channel, handler, ChannelState.CAUGHT, exception));
        } catch (Throwable t) {
            throw new ExecutionException(..., "error when process caught event ...");
        }
    }
}
複製程式碼

如上,請求物件會被封裝 ChannelEventRunnable 中,ChannelEventRunnable 將會是服務呼叫過程的新起點。所以接下來我們以 ChannelEventRunnable 為起點向下探索。

2.3.2.2 呼叫服務

本小節,我們從 ChannelEventRunnable 開始分析,該類的主要程式碼如下:

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 主要是包含了一些解碼邏輯。2.2.1 節分析請求解碼時說過,請求解碼可在 IO 執行緒上執行,也可線上程池中執行,這個取決於執行時配置。DecodeHandler 存在的意義就是保證請求或響應物件可線上程池中被解碼。解碼完畢後,完全解碼後的 Request 物件會繼續向後傳遞,下一站是 HeaderExchangeHandler。

public class HeaderExchangeHandler implements ChannelHandlerDelegate {

    private final ExchangeHandler handler;

    public HeaderExchangeHandler(ExchangeHandler handler) {
        if (handler == null) {
            throw new IllegalArgumentException("handler == null");
        }
        this.handler = handler;
    }

    @Override
    public void received(Channel channel, Object message) throws RemotingException {
        channel.setAttribute(KEY_READ_TIMESTAMP, System.currentTimeMillis());
        ExchangeChannel exchangeChannel = HeaderExchangeChannel.getOrAddChannel(channel);
        try {
            // 處理請求物件
            if (message instanceof Request) {
                Request request = (Request) message;
                if (request.isEvent()) {
                    // 處理事件
                    handlerEvent(channel, request);
                } 
                // 處理普通的請求
                else {
                    // 雙向通訊
                    if (request.isTwoWay()) {
                        // 向後呼叫服務,並得到呼叫結果
                        Response response = handleRequest(exchangeChannel, request);
                        // 將呼叫結果返回給服務消費端
                        channel.send(response);
                    } 
                    // 如果是單向通訊,僅向後呼叫指定服務即可,無需返回撥用結果
                    else {
                        handler.received(exchangeChannel, request.getData());
                    }
                }
            }      
            // 處理響應物件,服務消費方會執行此處邏輯,後面分析
            else if (message instanceof Response) {
                handleResponse(channel, (Response) message);
            } else if (message instanceof String) {
                // telnet 相關,忽略
            } else {
                handler.received(exchangeChannel, message);
            }
        } finally {
            HeaderExchangeChannel.removeChannelIfDisconnected(channel);
        }
    }

    Response handleRequest(ExchangeChannel channel, Request req) throws RemotingException {
        Response res = new Response(req.getId(), req.getVersion());
        // 檢測請求是否合法,不合法則返回狀態碼為 BAD_REQUEST 的響應
        if (req.isBroken()) {
            Object data = req.getData();

            String msg;
            if (data == null)
                msg = null;
            else if
                (data instanceof Throwable) msg = StringUtils.toString((Throwable) data);
            else
                msg = data.toString();
            res.setErrorMessage("Fail to decode request due to: " + msg);
            // 設定 BAD_REQUEST 狀態
            res.setStatus(Response.BAD_REQUEST);

            return res;
        }
        
        // 獲取 data 欄位值,也就是 RpcInvocation 物件
        Object msg = req.getData();
        try {
            // 繼續向下呼叫
            Object result = handler.reply(channel, msg);
            // 設定 OK 狀態碼
            res.setStatus(Response.OK);
            // 設定呼叫結果
            res.setResult(result);
        } catch (Throwable e) {
            // 若呼叫過程出現異常,則設定 SERVICE_ERROR,表示服務端異常
            res.setStatus(Response.SERVICE_ERROR);
            res.setErrorMessage(StringUtils.toString(e));
        }
        return res;
    }
}
複製程式碼

到這裡,我們看到了比較清晰的請求和響應邏輯。對於雙向通訊,HeaderExchangeHandler 首先向後進行呼叫,得到呼叫結果。然後將呼叫結果封裝到 Response 物件中,最後再將該物件返回給服務消費方。如果請求不合法,或者呼叫失敗,則將錯誤資訊封裝到 Response 物件中,並返回給服務消費方。接下來我們繼續向後分析,把剩餘的呼叫過程分析完。下面分析定義在 DubboProtocol 類中的匿名類物件邏輯,如下:

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_INVOKE))) {
                    // 回撥相關,忽略
                }
                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();
    }
    
    // 忽略其他方法
}
複製程式碼

以上邏輯用於獲取與指定服務對應的 Invoker 例項,並通過 Invoker 的 invoke 方法呼叫服務邏輯。invoke 方法定義在 AbstractProxyInvoker 中,程式碼如下。

public abstract class AbstractProxyInvoker implements Invoker {

@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) 2.4 服務提供方返回撥用結果 服務提供方呼叫指定服務後,會將呼叫結果封裝到 Response 物件中,並將該物件返回給服務消費方。服務提供方也是通過 NettyChannel 的 send 方法將 Response 物件返回,這個方法在 2.2.1 節分析過,這裡就不在重複分析了。本節我們僅需關注 Response 物件的編碼過程即可,這裡仍然省略一些中間呼叫,直接分析具體的編碼邏輯。

public class ExchangeCodec extends TelnetCodec {
	public void encode(Channel channel, ChannelBuffer buffer, Object msg) throws IOException {
        if (msg instanceof Request) {
            encodeRequest(channel, buffer, (Request) msg);
        } else if (msg instanceof Response) {
            // 對響應物件進行編碼
            encodeResponse(channel, buffer, (Response) msg);
        } else {
            super.encode(channel, buffer, msg);
        }
    }
    
    protected void encodeResponse(Channel channel, ChannelBuffer buffer, Response res) throws IOException {
        int savedWriteIndex = buffer.writerIndex();
        try {
            Serialization serialization = getSerialization(channel);
            // 建立訊息頭位元組陣列
            byte[] header = new byte[HEADER_LENGTH];
            // 設定魔數
            Bytes.short2bytes(MAGIC, header);
            // 設定序列化器編號
            header[2] = serialization.getContentTypeId();
            if (res.isHeartbeat()) header[2] |= FLAG_EVENT;
            // 獲取響應狀態
            byte status = res.getStatus();
            // 設定響應狀態
            header[3] = status;
            // 設定請求編號
            Bytes.long2bytes(res.getId(), header, 4);

            // 更新 writerIndex,為訊息頭預留 16 個位元組的空間
            buffer.writerIndex(savedWriteIndex + HEADER_LENGTH);
            ChannelBufferOutputStream bos = new ChannelBufferOutputStream(buffer);
            ObjectOutput out = serialization.serialize(channel.getUrl(), bos);
           
            if (status == Response.OK) {
                if (res.isHeartbeat()) {
                    // 對心跳響應結果進行序列化,已廢棄
                    encodeHeartbeatData(channel, out, res.getResult());
                } else {
                    // 對呼叫結果進行序列化
                    encodeResponseData(channel, out, res.getResult(), res.getVersion());
                }
            } else { 
                // 對錯誤資訊進行序列化
                out.writeUTF(res.getErrorMessage())
            };
            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);
        } catch (Throwable t) {
            // 異常處理邏輯不是很難理解,但是程式碼略多,這裡忽略了
        }
    }
}
複製程式碼
public class DubboCodec extends ExchangeCodec implements Codec2 {
    
	protected void encodeResponseData(Channel channel, ObjectOutput out, Object data, String version) throws IOException {
        Result result = (Result) data;
        // 檢測當前協議版本是否支援帶有 attachment 集合的 Response 物件
        boolean attach = Version.isSupportResponseAttachment(version);
        Throwable th = result.getException();
        
        // 異常資訊為空
        if (th == null) {
            Object ret = result.getValue();
            // 呼叫結果為空
            if (ret == null) {
                // 序列化響應型別
                out.writeByte(attach ? RESPONSE_NULL_VALUE_WITH_ATTACHMENTS : RESPONSE_NULL_VALUE);
            } 
            // 呼叫結果非空
            else {
                // 序列化響應型別
                out.writeByte(attach ? RESPONSE_VALUE_WITH_ATTACHMENTS : RESPONSE_VALUE);
                // 序列化呼叫結果
                out.writeObject(ret);
            }
        } 
        // 異常資訊非空
        else {
            // 序列化響應型別
            out.writeByte(attach ? RESPONSE_WITH_EXCEPTION_WITH_ATTACHMENTS : RESPONSE_WITH_EXCEPTION);
            // 序列化異常物件
            out.writeObject(th);
        }

        if (attach) {
            // 記錄 Dubbo 協議版本
            result.getAttachments().put(Constants.DUBBO_VERSION_KEY, Version.getProtocolVersion());
            // 序列化 attachments 集合
            out.writeObject(result.getAttachments());
        }
    }
}
複製程式碼

以上就是 Response 物件編碼的過程,和前面分析的 Request 物件編碼過程很相似。如果大家能看 Request 物件的編碼邏輯,那麼這裡的 Response 物件的編碼邏輯也不難理解,就不多說了。接下來我們再來分析雙向通訊的最後一環 —— 服務消費方接收呼叫結果。

2.5 服務消費方接收呼叫結果

服務消費方在收到響應資料後,首先要做的事情是對響應資料進行解碼,得到 Response 物件。然後再將該物件傳遞給下一個入站處理器,這個入站處理器就是 NettyHandler。接下來 NettyHandler 會將這個物件繼續向下傳遞,最後 AllChannelHandler 的 received 方法會收到這個物件,並將這個物件派發到執行緒池中。這個過程和服務提供方接收請求的過程是一樣的,因此這裡就不重複分析了。本節我們重點分析兩個方面的內容,一是響應資料的解碼過程,二是 Dubbo 如何將呼叫結果傳遞給使用者執行緒的。下面先來分析響應資料的解碼過程。

2.5.1 響應資料解碼

響應資料解碼邏輯主要的邏輯封裝在 DubboCodec 中,我們直接分析這個類的程式碼。如下:

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);
        // 檢測訊息型別,若下面的條件成立,表明訊息型別為 Response
        if ((flag & FLAG_REQUEST) == 0) {
            // 建立 Response 物件
            Response res = new Response(id);
            // 檢測事件標誌位
            if ((flag & FLAG_EVENT) != 0) {
                // 設定心跳事件
                res.setEvent(Response.HEARTBEAT_EVENT);
            }
            // 獲取響應狀態
            byte status = header[3];
            // 設定響應狀態
            res.setStatus(status);
            
            // 如果響應狀態為 OK,表明呼叫過程正常
            if (status == Response.OK) {
                try {
                    Object data;
                    if (res.isHeartbeat()) {
                        // 反序列化心跳資料,已廢棄
                        data = decodeHeartbeatData(channel, deserialize(s, channel.getUrl(), is));
                    } else if (res.isEvent()) {
                        // 反序列化事件資料
                        data = decodeEventData(channel, deserialize(s, channel.getUrl(), is));
                    } else {
                        DecodeableRpcResult result;
                        // 根據 url 引數決定是否在 IO 執行緒上執行解碼邏輯
                        if (channel.getUrl().getParameter(
                                Constants.DECODE_IN_IO_THREAD_KEY,
                                Constants.DEFAULT_DECODE_IN_IO_THREAD)) {
                            // 建立 DecodeableRpcResult 物件
                            result = new DecodeableRpcResult(channel, res, is,
                                    (Invocation) getRequestData(id), proto);
                            // 進行後續的解碼工作
                            result.decode();
                        } else {
                            // 建立 DecodeableRpcResult 物件
                            result = new DecodeableRpcResult(channel, res,
                                    new UnsafeByteArrayInputStream(readMessageData(is)),
                                    (Invocation) getRequestData(id), proto);
                        }
                        data = result;
                    }
                    
                    // 設定 DecodeableRpcResult 物件到 Response 物件中
                    res.setResult(data);
                } catch (Throwable t) {
                    // 解碼過程中出現了錯誤,此時設定 CLIENT_ERROR 狀態碼到 Response 物件中
                    res.setStatus(Response.CLIENT_ERROR);
                    res.setErrorMessage(StringUtils.toString(t));
                }
            } 
            // 響應狀態非 OK,表明呼叫過程出現了異常
            else {
                // 反序列化異常資訊,並設定到 Response 物件中
                res.setErrorMessage(deserialize(s, channel.getUrl(), is).readUTF());
            }
            return res;
        } else {
            // 對請求資料進行解碼,前面已分析過,此處忽略
        }
    }
}
複製程式碼

以上就是響應資料的解碼過程,上面邏輯看起來是不是似曾相識。對的,我們在前面章節分析過 DubboCodec 的 decodeBody 方法中關於請求資料的解碼過程,該過程和響應資料的解碼過程很相似。下面,我們繼續分析呼叫結果的反序列化過程,如下:

public class DecodeableRpcResult extends RpcResult implements Codec, Decodeable {
    
    private Invocation invocation;
	
    @Override
    public void decode() throws Exception {
        if (!hasDecoded && channel != null && inputStream != null) {
            try {
                // 執行反序列化操作
                decode(channel, inputStream);
            } catch (Throwable e) {
                // 反序列化失敗,設定 CLIENT_ERROR 狀態到 Response 物件中
                response.setStatus(Response.CLIENT_ERROR);
                // 設定異常資訊
                response.setErrorMessage(StringUtils.toString(e));
            } finally {
                hasDecoded = true;
            }
        }
    }
    
    @Override
    public Object decode(Channel channel, InputStream input) throws IOException {
        ObjectInput in = CodecSupport.getSerialization(channel.getUrl(), serializationType)
                .deserialize(channel.getUrl(), input);
        
        // 反序列化響應型別
        byte flag = in.readByte();
        switch (flag) {
            case DubboCodec.RESPONSE_NULL_VALUE:
                break;
            case DubboCodec.RESPONSE_VALUE:
                // ...
                break;
            case DubboCodec.RESPONSE_WITH_EXCEPTION:
                // ...
                break;
                
            // 返回值為空,且攜帶了 attachments 集合
            case DubboCodec.RESPONSE_NULL_VALUE_WITH_ATTACHMENTS:
                try {
                    // 反序列化 attachments 集合,並儲存起來 
                    setAttachments((Map<String, String>) in.readObject(Map.class));
                } catch (ClassNotFoundException e) {
                    throw new IOException(StringUtils.toString("Read response data failed.", e));
                }
                break;
                
            // 返回值不為空,且攜帶了 attachments 集合
            case DubboCodec.RESPONSE_VALUE_WITH_ATTACHMENTS:
                try {
                    // 獲取返回值型別
                    Type[] returnType = RpcUtils.getReturnTypes(invocation);
                    // 反序列化呼叫結果,並儲存起來
                    setValue(returnType == null || returnType.length == 0 ? in.readObject() :
                            (returnType.length == 1 ? in.readObject((Class<?>) returnType[0])
                                    : in.readObject((Class<?>) returnType[0], returnType[1])));
                    // 反序列化 attachments 集合,並儲存起來
                    setAttachments((Map<String, String>) in.readObject(Map.class));
                } catch (ClassNotFoundException e) {
                    throw new IOException(StringUtils.toString("Read response data failed.", e));
                }
                break;
                
            // 異常物件不為空,且攜帶了 attachments 集合
            case DubboCodec.RESPONSE_WITH_EXCEPTION_WITH_ATTACHMENTS:
                try {
                    // 反序列化異常物件
                    Object obj = in.readObject();
                    if (obj instanceof Throwable == false)
                        throw new IOException("Response data error, expect Throwable, but get " + obj);
                    // 設定異常物件
                    setException((Throwable) obj);
                    // 反序列化 attachments 集合,並儲存起來
                    setAttachments((Map<String, String>) in.readObject(Map.class));
                } catch (ClassNotFoundException e) {
                    throw new IOException(StringUtils.toString("Read response data failed.", e));
                }
                break;
            default:
                throw new IOException("Unknown result flag, expect '0' '1' '2', get " + flag);
        }
        if (in instanceof Cleanable) {
            ((Cleanable) in).cleanup();
        }
        return this;
    }
}
複製程式碼

本篇文章所分析的原始碼版本為 2.6.4,該版本下的 Response 支援 attachments 集合,所以上面僅對部分 case 分支進行了註釋。其他 case 分支的邏輯比被註釋分支的邏輯更為簡單,這裡就忽略了。我們所使用的測試服務介面 DemoService 包含了一個具有返回值的方法,正常呼叫下,執行緒會進入 RESPONSE_VALUE_WITH_ATTACHMENTS 分支中。然後執行緒會從 invocation 變數(大家探索一下 invocation 變數的由來)中獲取返回值型別,接著對呼叫結果進行反序列化,並將序列化後的結果儲存起來。最後對 attachments 集合進行反序列化,並存到指定欄位中。到此,關於響應資料的解碼過程就分析完了。接下來,我們再來探索一下響應物件 Response 的去向。

2.5.2 向使用者執行緒傳遞呼叫結果

響應資料解碼完成後,Dubbo 會將響應物件派發到執行緒池上。要注意的是,執行緒池中的執行緒並非使用者的呼叫執行緒,所以要想辦法將響應物件從執行緒池執行緒傳遞到使用者執行緒上。我們在 2.1 節分析過使用者執行緒在傳送完請求後的動作,即呼叫 DefaultFuture 的 get 方法等待響應物件的到來。當響應物件到來後,使用者執行緒會被喚醒,並通過呼叫編號獲取屬於自己的響應物件。下面我們來看一下整個過程對應的程式碼。

public class HeaderExchangeHandler implements ChannelHandlerDelegate {
    
    @Override
    public void received(Channel channel, Object message) throws RemotingException {
        channel.setAttribute(KEY_READ_TIMESTAMP, System.currentTimeMillis());
        ExchangeChannel exchangeChannel = HeaderExchangeChannel.getOrAddChannel(channel);
        try {
            if (message instanceof Request) {
                // 處理請求,前面已分析過,省略
            } else if (message instanceof Response) {
                // 處理響應
                handleResponse(channel, (Response) message);
            } else if (message instanceof String) {
                // telnet 相關,忽略
            } else {
                handler.received(exchangeChannel, message);
            }
        } finally {
            HeaderExchangeChannel.removeChannelIfDisconnected(channel);
        }
    }

    static void handleResponse(Channel channel, Response response) throws RemotingException {
        if (response != null && !response.isHeartbeat()) {
            // 繼續向下呼叫
            DefaultFuture.received(channel, response);
        }
    }
}

public class DefaultFuture implements ResponseFuture {  
    
    private final Lock lock = new ReentrantLock();
    private final Condition done = lock.newCondition();
    private volatile Response response;
    
	public static void received(Channel channel, Response response) {
        try {
            // 根據呼叫編號從 FUTURES 集合中查詢指定的 DefaultFuture 物件
            DefaultFuture future = FUTURES.remove(response.getId());
            if (future != null) {
                // 繼續向下呼叫
                future.doReceived(response);
            } else {
                logger.warn("The timeout response finally returned at ...");
            }
        } finally {
            CHANNELS.remove(response.getId());
        }
    }

	private void doReceived(Response res) {
        lock.lock();
        try {
            // 儲存響應物件
            response = res;
            if (done != null) {
                // 喚醒使用者執行緒
                done.signal();
            }
        } finally {
            lock.unlock();
        }
        if (callback != null) {
            invokeCallback(callback);
        }
    }
}
複製程式碼

以上邏輯是將響應物件儲存到相應的 DefaultFuture 例項中,然後再喚醒使用者執行緒,隨後使用者執行緒即可從 DefaultFuture 例項中獲取到相應結果。

本篇文章在多個地方都強調過呼叫編號很重要,但一直沒有解釋原因,這裡簡單說明一下。一般情況下,服務消費方會併發呼叫多個服務,每個使用者執行緒傳送請求後,會呼叫不同 DefaultFuture 物件的 get 方法進行等待。 一段時間後,服務消費方的執行緒池會收到多個響應物件。這個時候要考慮一個問題,如何將每個響應物件傳遞給相應的 DefaultFuture 物件,且不出錯。答案是通過呼叫編號。DefaultFuture 被建立時,會要求傳入一個 Request 物件。此時 DefaultFuture 可從 Request 物件中獲取呼叫編號,並將 <呼叫編號, DefaultFuture 物件> 對映關係存入到靜態 Map 中,即 FUTURES。執行緒池中的執行緒在收到 Response 物件後,會根據 Response 物件中的呼叫編號到 FUTURES 集合中取出相應的 DefaultFuture 物件,然後再將 Response 物件設定到 DefaultFuture 物件中。最後再喚醒使用者執行緒,這樣使用者執行緒即可從 DefaultFuture 物件中獲取呼叫結果了。整個過程大致如下圖:

3. 總結

本篇文章主要對 Dubbo 中的幾種服務呼叫方式,以及從雙向通訊的角度對整個通訊過程進行了詳細的分析。按照通訊順序,通訊過程包括服務消費方傳送請求,服務提供方接收請求,服務提供方返回響應資料,服務消費方接收響應資料等過程。理解這些過程需要大家對網路程式設計,尤其是 Netty 有一定的瞭解。限於篇幅原因,本篇文章無法將服務呼叫的所有內容都一一進行分析。對於本篇文章未講到或未詳細分析的內容,比如服務降級、過濾器鏈、以及序列化等。

歡迎大家加入Java高階架構群 378461078

相關文章