dubbo服務者原始碼分期

wangbiao007發表於2020-04-05

 

開局一張圖,內容全靠編
總的概括
è¿éåå¾çæè¿°
先說一下dubbo的服務端初始化過程
1.在serviceConfig裡面組裝配置引數;
2.獲取到對外提供服務的介面,實現類以及註冊url交給ProcxyFactory生成本地代理invoker;當消費者請求過來的時候,最後都是交給invoker去執行,然後invoker通過反射呼叫真正的例項;
3.生成完本地代理Invoker後,在DubboProtocol中對invoker進行暴露,先將invoker包裝生成exporter,再將export作為value,服務介面和埠號組合成key存入到DubboProtocol的map中,這個可以看成是本地註冊。
4.在DubboProtocol中生存開啟netty通訊生存ExchangeServer,ExchangeServer是負責底層通訊的,將ip加埠號作為key,ExchangeServer為value存入到map中。ExchangeServer負責相同的ip埠號服務通訊。
5.通過invoker裡面的url資訊獲取獲取註冊中心,將服務者資訊註冊到註冊中心,此節點儲存了服務提供方ip、埠、group、介面名稱、版本、應用名稱,這樣可以讓消費者從註冊中心獲取到服務者的資訊。
6.為了感知註冊中心的一些配置變化,提供者會監聽註冊中心路徑/dubbo/${interfaceClass}/configurators的節點,監聽該節點在註冊中心的一些配置資訊變更。zookeeper註冊中心通過zookeeper框架的監聽回撥介面進行監聽(redis註冊中心通過訂閱命令(subscribe)監聽),伺服器快取註冊中心的配置,當配置發生變更時,服務會重新整理本地快取,程式碼在ZookeeperRegistry的doSubscribe方法。
 

原始碼分析
下面是dubbo服務端的配置檔案

<?xml version="1.0" encoding="UTF-8"?>  
<beans xmlns="http://www.springframework.org/schema/beans"  
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"  
    xsi:schemaLocation="http://www.springframework.org/schema/beans  
        http://www.springframework.org/schema/beans/spring-beans.xsd  
        http://code.alibabatech.com/schema/dubbo  
        http://code.alibabatech.com/schema/dubbo/dubbo.xsd ">   
    <!-- 具體的實現bean -->  
    <bean id="testService" class="com.ts.services.impl.TestServiceImpl" />  
    <!-- 提供方應用資訊,用於計算依賴關係 -->  
    <dubbo:application name="provider"  />    
    <!-- 使用zookeeper註冊中心暴露服務地址 -->  
    <dubbo:registry address="zookeeper://127.0.0.1:2181" />
    <!-- 用dubbo協議在20880埠暴露服務 -->  
    <dubbo:protocol name="dubbo" port="29014" />  
    <!-- 宣告需要暴露的服務介面 -->  
    <dubbo:service interface="com.ts.service.TestService" ref="testService" timeout="300"/>
</beans> 

spring會對dubbo:service標籤進行解析生成serviceBean,serviceBean實現了ApplicationListener,重寫了onApplicationEvent方法,當所有bean重新整理完畢後,會呼叫onApplicationEvent方法,在onApplicationEvent方法中會呼叫ServiceConfig的export方法
ServiceConfig主要做了三件事
1.組裝和檢查配置檔案裡面配置的註冊資訊,介面資訊和協議資訊
2.讓proxyFactory對要暴露的服務生成Invoker
3.將Invoker交給proctol暴露,開啟netty通訊,將invoker資訊註冊到註冊中心方便消費者呼叫,並監聽註冊中心服務者的配置,
當服務者的配置改變後能動態重新整理本地快取

if (! Constants.SCOPE_NONE.toString().equalsIgnoreCase(scope)) {

            //配置不是remote的情況下做本地暴露 (配置為remote,則表示只暴露遠端服務)
            if (!Constants.SCOPE_REMOTE.toString().equalsIgnoreCase(scope)) {
                exportLocal(url);
            }
            //如果配置不是local則暴露為遠端服務.(配置為local,則表示只暴露遠端服務)
            if (! Constants.SCOPE_LOCAL.toString().equalsIgnoreCase(scope) ){
                if (logger.isInfoEnabled()) {
                    logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url);
                }
                //當配置的有註冊中心的時候
                if (registryURLs != null && registryURLs.size() > 0
                        && url.getParameter("register", true)) {
                    for (URL registryURL : registryURLs) {
                        url = url.addParameterIfAbsent("dynamic", registryURL.getParameter("dynamic"));
                        URL monitorUrl = loadMonitor(registryURL);
                        if (monitorUrl != null) {
                            url = url.addParameterAndEncoded(Constants.MONITOR_KEY, monitorUrl.toFullString());
                        }
                        if (logger.isInfoEnabled()) {
                            logger.info("Register dubbo service " + interfaceClass.getName() + " url " + url + " to registry " + registryURL);
                        }
                        //生成本地代理
                        Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));
                        //進行服務暴露和註冊
                        Exporter<?> exporter = protocol.export(invoker);
                        exporters.add(exporter);
                    }
                } else {
                    //沒有註冊中心,直接連線消費者端
                    Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url);

                    Exporter<?> exporter = protocol.export(invoker);
                    exporters.add(exporter);
                }
            }
        }

1.生成本地代理Invoker 

Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url);

上面是通過proxyFactory對ref,也就是介面的實現類生成代理invoker,proxyFactory預設的是JavassistProxyFactory

public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
        // TODO Wrapper類不能正確處理帶$的類名
        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 {
                return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
            }
        };
    }

可看到getInvoker方法返回的是一個重寫了AbstractProxyInvoker doInvoker方法的例項,在doInvoker方法裡呼叫了wapper.invoker方法,這裡wapper.invoker方法運用javassist動態編碼技術形成新的代理類。記住這裡生成的invoker就是對服務端要暴露的服務的一種代理,這樣是為了解耦dubbo和要暴露的服務,讓所有客戶端的請求先交給Invoker處理,invoker再根據客戶端的請求資訊利用反射呼叫真正的服務。
 

2.對代理進行暴露

 Exporter<?> exporter = protocol.export(invoker);

這裡的protocol是通過spi機制動態生成的,它會返回ProtocolListenerWrapper->ListenerExporterWrapper->RegistryProtocol物件鏈,對於registry協議,兩個Wrapper都不會做任何處理,會直接調到RegistryProtocol.export方法。

public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
        //export invoker,進行暴露,也就是開啟了netty通訊
        final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker);
        //registry provider   獲取註冊中心
        final Registry registry = getRegistry(originInvoker);
        final URL registedProviderUrl = getRegistedProviderUrl(originInvoker);
        //註冊服務者節點
        registry.register(registedProviderUrl);
        // 訂閱override資料
        // FIXME 提供者訂閱時,會影響同一JVM即暴露服務,又引用同一服務的的場景,因為subscribed以服務名為快取的key,導致訂閱資訊覆蓋。
        final URL overrideSubscribeUrl = getSubscribedOverrideUrl(registedProviderUrl);
        final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl);
        overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
        //監聽服務者在註冊中心的配置,當配置改變的時候能重新整理本地快取
        registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
        //保證每次export都返回一個新的exporter例項
        return new Exporter<T>() {
            public Invoker<T> getInvoker() {
                return exporter.getInvoker();
            }
            public void unexport() {
            	try {
            		exporter.unexport();
            	} catch (Throwable t) {
                	logger.warn(t.getMessage(), t);
                }
                try {
                	registry.unregister(registedProviderUrl);
                } catch (Throwable t) {
                	logger.warn(t.getMessage(), t);
                }
                try {
                	overrideListeners.remove(overrideSubscribeUrl);
                	registry.unsubscribe(overrideSubscribeUrl, overrideSubscribeListener);
                } catch (Throwable t) {
                	logger.warn(t.getMessage(), t);
                }
            }
        };
    }

doLocalExport方法

private <T> ExporterChangeableWrapper<T>  doLocalExport(final Invoker<T> originInvoker){
        String key = getCacheKey(originInvoker);
        //exporter代理,建立返回的exporter與protocol export出的exporter的對應關係,在override時可以進行關係修改.
        ExporterChangeableWrapper<T> exporter = (ExporterChangeableWrapper<T>) bounds.get(key);
        if (exporter == null) {
            synchronized (bounds) {
                exporter = (ExporterChangeableWrapper<T>) bounds.get(key);
                if (exporter == null) {
                    //宣告一個invoker的代理
                    final Invoker<?> invokerDelegete = new InvokerDelegete<T>(originInvoker, getProviderUrl(originInvoker));
                    //利用dubboProtocol對invoker進行暴露
                    exporter = new ExporterChangeableWrapper<T>((Exporter<T>)protocol.export(invokerDelegete), originInvoker);
                    bounds.put(key, exporter);
                }
            }
        }
        return (ExporterChangeableWrapper<T>) exporter;
    }

進入dubboproctol的export方法

public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
        //dubbo://202.106.199.34:29014/com.ts.service.TestService?anyhost=true&application=provider&dubbo=2.5.3&interface=com.ts.service.TestService&methods=getName&pid=59736&side=provider&timeout=300&timestamp=1563845476832
        URL url = invoker.getUrl();
        // export service.  key的值:com.ts.service.TestService:29014
        //key其實包括介面名,埠號,以及服務的版本號,分組名
        String key = serviceKey(url);
        DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap);
        //將key和export存入map中,這樣可以根據客戶端傳送過來的invocation解析得到取出哪一個export
        exporterMap.put(key, exporter);
        
        //export an stub service for dispaching event
        Boolean isStubSupportEvent = url.getParameter(Constants.STUB_EVENT_KEY,Constants.DEFAULT_STUB_EVENT);
        Boolean isCallbackservice = url.getParameter(Constants.IS_CALLBACK_SERVICE, false);
        if (isStubSupportEvent && !isCallbackservice){
            String stubServiceMethods = url.getParameter(Constants.STUB_EVENT_METHODS_KEY);
            if (stubServiceMethods == null || stubServiceMethods.length() == 0 ){
                if (logger.isWarnEnabled()){
                    logger.warn(new IllegalStateException("consumer [" +url.getParameter(Constants.INTERFACE_KEY) +
                            "], has set stubproxy support event ,but no stub methods founded."));
                }
            } else {
                stubServiceMethodsMap.put(url.getServiceKey(), stubServiceMethods);
            }
        }

        openServer(url);
        
        return exporter;
    }

openServer方法,生成一個ExchangeServer,ExchangeServer負責底層通訊

private void openServer(URL url) {
        // find server.  202.106.199.34:29014
        String key = url.getAddress();
        //client 也可以暴露一個只有server可以呼叫的服務。
        boolean isServer = url.getParameter(Constants.IS_SERVER_KEY,true);
        if (isServer) {
            //同一個ip和埠號的服務都交給同一個ExchangeServer處理
        	ExchangeServer server = serverMap.get(key);
        	if (server == null) {
        		serverMap.put(key, createServer(url));
        	} else {
        		//server支援reset,配合override功能使用
        		server.reset(url);
        	}
        }
    }

createServer(url)方法

private ExchangeServer createServer(URL url) {
        //預設開啟server關閉時傳送readonly事件
        url = url.addParameterIfAbsent(Constants.CHANNEL_READONLYEVENT_SENT_KEY, Boolean.TRUE.toString());
        //預設開啟heartbeat
        url = url.addParameterIfAbsent(Constants.HEARTBEAT_KEY, String.valueOf(Constants.DEFAULT_HEARTBEAT));
        String str = url.getParameter(Constants.SERVER_KEY, Constants.DEFAULT_REMOTING_SERVER);

        if (str != null && str.length() > 0 && ! ExtensionLoader.getExtensionLoader(Transporter.class).hasExtension(str))
            throw new RpcException("Unsupported server type: " + str + ", url: " + url);

        url = url.addParameter(Constants.CODEC_KEY, Version.isCompatibleVersion() ? COMPATIBLE_CODEC_NAME : DubboCodec.NAME);
        ExchangeServer server;
        try {
            //開啟netty通訊
            server = Exchangers.bind(url, requestHandler);
        } catch (RemotingException e) {
            throw new RpcException("Fail to start server(url: " + url + ") " + e.getMessage(), e);
        }
        str = url.getParameter(Constants.CLIENT_KEY);
        if (str != null && str.length() > 0) {
            Set<String> supportedTypes = ExtensionLoader.getExtensionLoader(Transporter.class).getSupportedExtensions();
            if (!supportedTypes.contains(str)) {
                throw new RpcException("Unsupported client type: " + str);
            }
        }
        return server;
    }

先看看requestHandler這個DubboProcto裡面的內部類,這個類是處理接受從客戶端來的請求,並通過invocation裡面所帶的引數組長key取出對應的export,再利用export裡面的invoker處理請求
 

private ExchangeHandler requestHandler = new ExchangeHandlerAdapter() {
        
        public Object reply(ExchangeChannel channel, Object message) throws RemotingException {
            if (message instanceof Invocation) {
                Invocation inv = (Invocation) message;
                //通過invocation獲取invoker
                Invoker<?> invoker = getInvoker(channel, inv);
                //如果是callback 需要處理高版本呼叫低版本的問題
                if (Boolean.TRUE.toString().equals(inv.getAttachments().get(IS_CALLBACK_SERVICE_INVOKE))){
                    String methodsStr = invoker.getUrl().getParameters().get("methods");
                    boolean hasMethod = false;
                    if (methodsStr == null || methodsStr.indexOf(",") == -1){
                        hasMethod = inv.getMethodName().equals(methodsStr);
                    } else {
                        String[] methods = methodsStr.split(",");
                        for (String method : methods){
                            if (inv.getMethodName().equals(method)){
                                hasMethod = true;
                                break;
                            }
                        }
                    }
                    if (!hasMethod){
                        logger.warn(new IllegalStateException("The methodName "+inv.getMethodName()+" not found in callback service interface ,invoke will be ignored. please update the api interface. url is:" + invoker.getUrl()) +" ,invocation is :"+inv );
                        return null;
                    }
                }
                RpcContext.getContext().setRemoteAddress(channel.getRemoteAddress());
                //執行具體的請求
                return invoker.invoke(inv);
            }
            throw new RemotingException(channel, "Unsupported request: " + message == null ? null : (message.getClass().getName() + ": " + message) + ", channel: consumer: " + channel.getRemoteAddress() + " --> provider: " + channel.getLocalAddress());
        }

.......
}
Invoker<?> getInvoker(Channel channel, Invocation inv) throws RemotingException{
        boolean isCallBackServiceInvoke = false;
        boolean isStubServiceInvoke = false;
        int port = channel.getLocalAddress().getPort();
        String path = inv.getAttachments().get(Constants.PATH_KEY);
        //如果是客戶端的回撥服務.
        isStubServiceInvoke = Boolean.TRUE.toString().equals(inv.getAttachments().get(Constants.STUB_EVENT_KEY));
        if (isStubServiceInvoke){
            port = channel.getRemoteAddress().getPort();
        }
        //callback
        isCallBackServiceInvoke = isClientSide(channel) && !isStubServiceInvoke;
        if(isCallBackServiceInvoke){
            path = inv.getAttachments().get(Constants.PATH_KEY)+"."+inv.getAttachments().get(Constants.CALLBACK_SERVICE_KEY);
            inv.getAttachments().put(IS_CALLBACK_SERVICE_INVOKE, Boolean.TRUE.toString());
        }
        //埠號,介面服務,版本號,分組組合成一個key
        String serviceKey = serviceKey(port, path, inv.getAttachments().get(Constants.VERSION_KEY), inv.getAttachments().get(Constants.GROUP_KEY));
        //通過key獲取到對應的export
        DubboExporter<?> exporter = (DubboExporter<?>) exporterMap.get(serviceKey);
        
        if (exporter == null)
            throw new RemotingException(channel, "Not found exported service: " + serviceKey + " in " + exporterMap.keySet() + ", may be version or group mismatch " + ", channel: consumer: " + channel.getRemoteAddress() + " --> provider: " + channel.getLocalAddress() + ", message:" + inv);

        return exporter.getInvoker();
    }

下面看一下dubbo服務端是如何開啟netty通訊的

server = Exchangers.bind(url, requestHandler);
public static ExchangeServer bind(URL url, ExchangeHandler handler) throws RemotingException {
        if (url == null) {
            throw new IllegalArgumentException("url == null");
        }
        if (handler == null) {
            throw new IllegalArgumentException("handler == null");
        }
        url = url.addParameterIfAbsent(Constants.CODEC_KEY, "exchange");
        //getExchanger(url) 獲取到HeaderExchanger   
        //url的值是:dubbo://202.106.199.34:29014/com.ts.service.TestService?anyhost=true&application=provider&channel.readonly.sent=true&codec=dubbo&dubbo=2.5.3&heartbeat=60000&interface=com.ts.service.TestService&methods=getName&pid=59736&side=provider&timeout=300&timestamp=1563845476832
        return getExchanger(url).bind(url, handler);
    }

HeaderExchanger的bind方法

public ExchangeServer bind(URL url, ExchangeHandler handler) throws RemotingException {
        return new HeaderExchangeServer(Transporters.bind(url, new DecodeHandler(new HeaderExchangeHandler(handler))));
    }

可以看到這裡是一個裝飾模式的呼叫鏈,HeaderExchangeServer--->NettyServer--->DecodeHandler--->HeaderExchangeHandler-->ExchangeHandler。

Transporters.bind(url, new DecodeHandler(new HeaderExchangeHandler(handler)))

 返回的是一個nettyservice,這個地方的呼叫鏈比想象的要複雜,只能淺嘗輒止了。

在開啟netty通訊後,下面再看服務的註冊和監聽,RegistryProcotol的export方法

public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
        //export invoker
        final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker);
        //registry provider
        final Registry registry = getRegistry(originInvoker);
        final URL registedProviderUrl = getRegistedProviderUrl(originInvoker);
        //將服務提供者資訊註冊到註冊中心,方便消費者能在註冊中心訂閱到服務
        registry.register(registedProviderUrl);
        // 訂閱override資料
        // FIXME 提供者訂閱時,會影響同一JVM即暴露服務,又引用同一服務的的場景,因為subscribed以服務名為快取的key,導致訂閱資訊覆蓋。
        final URL overrideSubscribeUrl = getSubscribedOverrideUrl(registedProviderUrl);
        final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl);
        overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
        //訂閱服務者配置資訊,這樣當服務者配置資訊發生改變的時候,能重新整理本地快取,重新生成export
        registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
        //保證每次export都返回一個新的exporter例項
        return new Exporter<T>() {
            public Invoker<T> getInvoker() {
                return exporter.getInvoker();
            }
            public void unexport() {
            	try {
            		exporter.unexport();
            	} catch (Throwable t) {
                	logger.warn(t.getMessage(), t);
                }
                try {
                	registry.unregister(registedProviderUrl);
                } catch (Throwable t) {
                	logger.warn(t.getMessage(), t);
                }
                try {
                	overrideListeners.remove(overrideSubscribeUrl);
                	registry.unsubscribe(overrideSubscribeUrl, overrideSubscribeListener);
                } catch (Throwable t) {
                	logger.warn(t.getMessage(), t);
                }
            }
        };
    }



最後總結一下dubbo裡面比較重要的組建

Invoker
Invoker起到代理處理器的作用,在消費者端,Invoker負責和遠端服務端通訊
在服務者端,Invoker服務利用反射呼叫具體的服務實力處理客戶端的請求

ProxyFactory
從JavassistProxyFactory就可以看出來,在客戶端ProxyFactory會將invoker代理成一個符合客戶端要求的例項,比如,客戶端要呼叫遠端服務DemoService,那ProxyFactory就會將invoker作為一個被代理的目標生成DemoService代理物件。
在服務端,ProxyFactory會將具體的服務,例如DemoServiceImpl代理成invoker

public class JavassistProxyFactory extends AbstractProxyFactory {

    @SuppressWarnings("unchecked")
    public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) {
        return (T) Proxy.getProxy(interfaces).newInstance(new InvokerInvocationHandler(invoker));
    }

    public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
        // TODO Wrapper類不能正確處理帶$的類名
        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 {
                return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
            }
        };
    }

}

Procotol

@SPI("dubbo")
public interface Protocol {
    
    /**
     * 獲取預設埠,當使用者沒有配置埠時使用。
     * 
     * @return 預設埠
     */
    int getDefaultPort();

    /**
     * 暴露遠端服務:<br>
     * 1. 協議在接收請求時,應記錄請求來源方地址資訊:RpcContext.getContext().setRemoteAddress();<br>
     * 2. export()必須是冪等的,也就是暴露同一個URL的Invoker兩次,和暴露一次沒有區別。<br>
     * 3. export()傳入的Invoker由框架實現並傳入,協議不需要關心。<br>
     * 
     * @param <T> 服務的型別
     * @param invoker 服務的執行體
     * @return exporter 暴露服務的引用,用於取消暴露
     * @throws RpcException 當暴露服務出錯時丟擲,比如埠已佔用
     */
    @Adaptive
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;

    /**
     * 引用遠端服務:<br>
     * 1. 當使用者呼叫refer()所返回的Invoker物件的invoke()方法時,協議需相應執行同URL遠端export()傳入的Invoker物件的invoke()方法。<br>
     * 2. refer()返回的Invoker由協議實現,協議通常需要在此Invoker中傳送遠端請求。<br>
     * 3. 當url中有設定check=false時,連線失敗不能丟擲異常,並內部自動恢復。<br>
     * 
     * @param <T> 服務的型別
     * @param type 服務的型別
     * @param url 遠端服務的URL地址
     * @return invoker 服務的本地代理
     * @throws RpcException 當連線服務提供方失敗時丟擲
     */
    @Adaptive
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;

    /**
     * 釋放協議:<br>
     * 1. 取消該協議所有已經暴露和引用的服務。<br>
     * 2. 釋放協議所佔用的所有資源,比如連線和埠。<br>
     * 3. 協議在釋放後,依然能暴露和引用新的服務。<br>
     */
    void destroy();

}

procotol通常就是暴露遠端服務和引用遠端服務,但在RegistryProcotol中的程式碼,可以看到除了暴露遠端服務和引用遠端服務外還有將服務註冊到註冊中心方便訂閱,然後監聽註冊中心,當註冊中心裡面有什麼變化的時候可以重新整理本地快取。
在DubboProctol裡面負責客戶端底層通訊的是ExchangeClient,在服務端負責底層通訊的是ExchangeServer

 

 

相關文章