Dubbo Provider 生产者启动流程 / 通讯协议分析

Dubbo Provider 生产者启动流程分析

这一小节,我们简单分析下Dubbo 生产者中的启动流程。主要分析Dubbo启动时服务暴露的流程和细节。

该源码分析 基于 Dubbo2.7.15

Springboot集成流程

我们在集成Dubbo时,只需要在Springboot的启动类中添加以上注解,并指定目录即可!如下:

package org.example.provider;

import org.apache.dubbo.config.spring.context.annotation.DubboComponentScan;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;

@SpringBootApplication
@DubboComponentScan(basePackages = {"org.example.provider"})
public class ProviderApp {

    public static void main(String[] args) {
        /**
         * 以非web项目启动
         */
        new SpringApplicationBuilder(ProviderApp.class)
                .web(WebApplicationType.NONE)
                .run(args);
    }
}

接下来一步步分析其背后的原理。点开其中的源码

package org.apache.dubbo.config.spring.context.annotation;

import org.apache.dubbo.config.annotation.Reference;
import org.apache.dubbo.config.annotation.Service;

import org.springframework.context.annotation.Import;

import java.lang.annotation.Annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @see Service
 * @see Reference
 * @since 2.5.7
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
//这是一个非常关键的属性,可以简单的理解为注解背后真正实现的业务逻辑
@Import(DubboComponentScanRegistrar.class)
public @interface DubboComponentScan {

    /**
     * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation
     * declarations e.g.: {@code @DubboComponentScan("org.my.pkg")} instead of
     * {@code @DubboComponentScan(basePackages="org.my.pkg")}.
     *
     * @return the base packages to scan
     */
    String[] value() default {};

    /**
     * Base packages to scan for annotated @Service classes. {@link #value()} is an
     * alias for (and mutually exclusive with) this attribute.
     * <p>
     * Use {@link #basePackageClasses()} for a type-safe alternative to String-based
     * package names.
     *
     * @return the base packages to scan
     */
    String[] basePackages() default {};

    /**
     * Type-safe alternative to {@link #basePackages()} for specifying the packages to
     * scan for annotated @Service classes. The package of each class specified will be
     * scanned.
     *
     * @return classes from the base packages to scan
     */
    Class<?>[] basePackageClasses() default {};

}

接下来,在点击 DubboComponentScanRegistrar 的源码
在这里插入图片描述

如上图 packagesToScan 代表 Dubbo 框架需要扫描的路径, 跟进去 registerServiceClassPostProcessor 方法 (上图中的registerCommonBeans方法留个悬念 暂且不管),

/**
 * 该方法表示springboot 在启动是动态注册 ServiceClassPostProcessor 这个类的Bean实例
 * ServiceClassPostProcessor 该类非常重要
 */
private void registerServiceClassPostProcessor(Set<String> packagesToScan, BeanDefinitionRegistry registry) {

  			BeanDefinitionBuilder builder = rootBeanDefinition(ServiceClassPostProcessor.class);
        builder.addConstructorArgValue(packagesToScan);
        builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
        AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();
        BeanDefinitionReaderUtils.registerWithGeneratedName(beanDefinition, registry);

}

接下来 继续跟进 ServiceClassPostProcessor 类, 该类继承 BeanDefinitionRegistryPostProcessor 接口(该接口是Springboot 框架预留的钩子接口,只要实现该接口,Springboot 在启动时就会调用内部的逻辑), 因此直接查看postProcessBeanDefinitionRegistry方法即可。

@Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {

        // @since 2.7.5
        // 划红线1 此处注册了springboot 的监听器,等待springboot内部流程处理完成后,自动执行 DubboBootstrapApplicationListener 监听器,这一点与之前2.6.* 版本的处理流程稍有不同
        registerInfrastructureBean(registry, DubboBootstrapApplicationListener.BEAN_NAME, DubboBootstrapApplicationListener.class);

        Set<String> resolvedPackagesToScan = resolvePackagesToScan(packagesToScan);

        if (!CollectionUtils.isEmpty(resolvedPackagesToScan)) {
            // 划红线2 注册所有使用 @DubboService 注解的 service类,把他们纳入springboot 框架管理的内部,让他们跟使用普通Bean一样,可以通过 @Autowired 直接在项目内部使用
            registerServiceBeans(resolvedPackagesToScan, registry);
        } else {
            if (logger.isWarnEnabled()) {
                logger.warn("packagesToScan is empty , ServiceBean registry will be ignored!");
            }
        }

    }

由此可以得出结论 :

在Dubbo 2.7.15版本,生产者的启动流程,Dubbo接口的实现类Bean的注册跟服务的暴露是分开的,应该是先注册bean,然后在springboot内部流程处理完成后,Dubbo框架通过实现Listener接口暴露服务。

继续打开 DubboBootstrapApplicationListener 类的源码

    @Override
    public void onApplicationContextEvent(ApplicationContextEvent event) {
        if (DubboBootstrapStartStopListenerSpringAdapter.applicationContext == null) {
            DubboBootstrapStartStopListenerSpringAdapter.applicationContext = event.getApplicationContext();
        }
      	
        if (event instanceof ContextRefreshedEvent) {
            //监听启动事件,执行dubbo 启动流程
            onContextRefreshedEvent((ContextRefreshedEvent) event);
        } else if (event instanceof ContextClosedEvent) {
           //监听关闭时间 执行dubbo 关闭流程
            onContextClosedEvent((ContextClosedEvent) event);
        }
    }

    private void onContextRefreshedEvent(ContextRefreshedEvent event) {
        //start 方法开始执行dubbo框架内部真正的启动流程了
        dubboBootstrap.start();
    }

		private void onContextClosedEvent(ContextClosedEvent event) {
        DubboShutdownHook.getDubboShutdownHook().run();
    }

接下来我们开始进入Dubbo 服务暴露的真正流程

Dubbo服务暴露流程
在这里插入图片描述

见明识义,这里不正是服务暴露的方法命名嘛, 看到这里有一种见到庐山真面目的开心与激动。

接着往下一步步跟踪,它的调用流程应该是:

  1. org.apache.dubbo.config.bootstrap.DubboBootstrap#exportServices

  2. org.apache.dubbo.config.bootstrap.DubboBootstrap#exportService

  3. org.apache.dubbo.config.ServiceConfig#export

  4. org.apache.dubbo.config.ServiceConfig#doExport

  5. org.apache.dubbo.config.ServiceConfig#doExportUrls

  6. org.apache.dubbo.config.ServiceConfig#doExportUrlsFor1Protocol

    private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List registryURLs, int protocolConfigNum) {
    String name = protocolConfig.getName();
    if (StringUtils.isEmpty(name)) {
    name = DUBBO;
    }

         Map<String, String> map = new HashMap<String, String>();
         map.put(SIDE_KEY, PROVIDER_SIDE);
    
         /**
          * 省略中间的代码
          */
        
         String scope = url.getParameter(SCOPE_KEY);
         // don't export when none is configured
         if (!SCOPE_NONE.equalsIgnoreCase(scope)) {
    
             // export to local if the config is not remote (export to remote only when config is remote)
             if (!SCOPE_REMOTE.equalsIgnoreCase(scope)) {
                 exportLocal(url);
             }
             // export to remote if the config is not local (export to local only when config is local)
             if (!SCOPE_LOCAL.equalsIgnoreCase(scope)) {
                 if (CollectionUtils.isNotEmpty(registryURLs)) {
                     for (URL registryURL : registryURLs) {
                         if (SERVICE_REGISTRY_PROTOCOL.equals(registryURL.getProtocol())) {
                             url = url.addParameterIfAbsent(SERVICE_NAME_MAPPING_KEY, "true");
                         }
    
                         //if protocol is only injvm ,not register
                         if (LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) {
                             continue;
                         }
                         url = url.addParameterIfAbsent(DYNAMIC_KEY, registryURL.getParameter(DYNAMIC_KEY));
                         URL monitorUrl = ConfigValidationUtils.loadMonitor(this, registryURL);
                         if (monitorUrl != null) {
                             url = url.addParameterAndEncoded(MONITOR_KEY, monitorUrl.toFullString());
                         }
                         if (logger.isInfoEnabled()) {
                             if (url.getParameter(REGISTER_KEY, true)) {
                                 logger.info("Register dubbo service " + interfaceClass.getName() + " url " + url + " to registry " +
                                         registryURL);
                             } else {
                                 logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url);
                             }
                         }
    
                         // For providers, this is used to enable custom proxy to generate invoker
                         String proxy = url.getParameter(PROXY_KEY);
                         if (StringUtils.isNotEmpty(proxy)) {
                             registryURL = registryURL.addParameter(PROXY_KEY, proxy);
                         }
    
                         Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass,
                                 registryURL.addParameterAndEncoded(EXPORT_KEY, url.toFullString()));
                         DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
    
                       /**
     									   * 画红线 这里才是服务暴露的最终的地方
     									   * 根据配置文件中指定的协议 进行暴露(dubbo 默认支持多种协议,一般使用默认的ZK作为注册中心)
     									   * 这里将dubbo服务 注册到注册中心 
     									   */
                         Exporter<?> exporter = PROTOCOL.export(wrapperInvoker);
                         exporters.add(exporter);
                     }
                 } else {
                     if (logger.isInfoEnabled()) {
                         logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url);
                     }
                     Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, url);
                     DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
     									
                     Exporter<?> exporter = PROTOCOL.export(wrapperInvoker);
                     exporters.add(exporter);
                 }
    
                 MetadataUtils.publishServiceDefinition(url);
             }
         }
         this.urls.add(url);
     }
    

Dubbo 协议接口

由上面的分析可知,dubbo服务是通过协议暴露的,那我们来瞧一瞧协议的庐山真面目

package org.apache.dubbo.rpc;

import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.Adaptive;
import org.apache.dubbo.common.extension.SPI;

import java.util.Collections;
import java.util.List;

@SPI("dubbo")
public interface Protocol {

    /**
     * 获取默认的端口
     */
    int getDefaultPort();

    /**
     * 协议暴露接口
     */
    @Adaptive
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;

    /**
     * 协议引用接口
     */
    @Adaptive
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;

    /**
     * 摧毁接口
     */
    void destroy();

    /**
     * 获取协议所有服务器信息
     */
    default List<ProtocolServer> getServers() {
        return Collections.emptyList();
    }

}

该接口是Dubbo框架中核心接口之一,内部定义协议的暴露、引用、以及服务的服务器信息。

其中服务暴露、引用对应于开发中的 @DubboService @DubboReference。

如下图,协议已经默认提供了多种实现,如Redis、Hession、Grpc… 我们首先记住以下两个划线的核心实现类。
在这里插入图片描述

在介绍上面两个类的作用之前,回顾下Dubbo的框架(如下图),作为服务的提供者,首先需要实现2个功能
在这里插入图片描述

  1. 将自己的服务地址暴露给注册中心,这样消费者才能从注册中心获取到RPC调用的远程信息,如IP、端口等,即服务注册
  2. 服务提供者 应该提供远程地址以提供给消费者调用

OK,当你理解了上面的流程之后,那么之前的划红线的两个接口的作用就一目了然了。

org.apache.dubbo.registry.integration.RegistryProtocol 该类提供服务注册功能,想指定注册中心注册他的元数据

org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol 该类提供远程调用功能,让消费者通过调用远程接口获取信息

那么问题又来了,对于服务提供者这两步的流程的先后顺序如何,到底谁先谁后,我们不妨猜测一下,理论上它的逻辑应该是:

  1. 先暴露本地端口,提供远程服务 即先执行 DubboProtocol
  2. 再将本地信息的元数据 注册到注册中心去

那么究竟是不是我们想象的那样呢,继续跟踪源码。在ServiceConfig类的518行打上断点
在这里插入图片描述

如上图 这里显示的是这里的协议信息为注册协议,那么先跟一下

org.apache.dubbo.registry.integration.RegistryProtocol#export 方法

@Override
    public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
        URL registryUrl = getRegistryUrl(originInvoker);
        // url to export locally
       //  根据url将本地provider暴露出去
        URL providerUrl = getProviderUrl(originInvoker);

        // Subscribe the override data
        // FIXME When the provider subscribes, it will affect the scene : a certain JVM exposes the service and call
        //  the same service. Because the subscribed is cached key with the name of the service, it causes the
        //  subscription information to cover.
        final URL overrideSubscribeUrl = getSubscribedOverrideUrl(providerUrl);
        final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
        overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);

        providerUrl = overrideUrlWithConfig(providerUrl, overrideSubscribeListener);
        // export invoker
        // 画红线 1. 暴露本地端口 
        final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);

        // url to registry
        final Registry registry = getRegistry(originInvoker);
        final URL registeredProviderUrl = getUrlToRegistry(providerUrl, registryUrl);

        // decide if we need to delay publish
        boolean register = providerUrl.getParameter(REGISTER_KEY, true);
        if (register) {
            // 画红线  2. 默认想注册中心注册服务元数据
            registry.register(registeredProviderUrl);
        }

        // register stated url on provider model
        registerStatedUrl(registryUrl, registeredProviderUrl, register);


        exporter.setRegisterUrl(registeredProviderUrl);
        exporter.setSubscribeUrl(overrideSubscribeUrl);

        // Deprecated! Subscribe to override rules in 2.6.x or before.
        registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);

        notifyExport(exporter);
        //Ensure that a new exporter instance is returned every time export
        return new DestroyableExporter<>(exporter);
    }

接着分析 doLocalExport 方法,你会发现最终会执行到

org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol#export

    @Override
    public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
        URL url = invoker.getUrl();

        // export service.
        String key = serviceKey(url);
        DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap);
        exporterMap.addExportMap(key, exporter);

        //export an stub service for dispatching event
        Boolean isStubSupportEvent = url.getParameter(STUB_EVENT_KEY, DEFAULT_STUB_EVENT);
        Boolean isCallbackservice = url.getParameter(IS_CALLBACK_SERVICE, false);
        if (isStubSupportEvent && !isCallbackservice) {
            String stubServiceMethods = url.getParameter(STUB_EVENT_METHODS_KEY);
            if (stubServiceMethods == null || stubServiceMethods.length() == 0) {
                if (logger.isWarnEnabled()) {
                    logger.warn(new IllegalStateException("consumer [" + url.getParameter(INTERFACE_KEY) +
                            "], has set stubproxy support event ,but no stub methods founded."));
                }

            }
        }

        //打开服务
        openServer(url);
        optimizeSerialization(url);

        return exporter;
    }

    private void openServer(URL url) {
        // find server.
        String key = url.getAddress();
        //client can export a service which's only for server to invoke
        boolean isServer = url.getParameter(IS_SERVER_KEY, true);
        if (isServer) {
            ProtocolServer server = serverMap.get(key);
            // 之前的服务未暴露 则执行 createServer方法
            if (server == null) {
                synchronized (this) {
                    server = serverMap.get(key);
                    if (server == null) {
                        serverMap.put(key, createServer(url));
                    }
                }
            } else {
                // server supports reset, use together with override
                server.reset(url);
            }
        }
    }

private ProtocolServer createServer(URL url) {
        url = URLBuilder.from(url)
                // send readonly event when server closes, it's enabled by default
                .addParameterIfAbsent(CHANNEL_READONLYEVENT_SENT_KEY, Boolean.TRUE.toString())
                // enable heartbeat by default
                .addParameterIfAbsent(HEARTBEAT_KEY, String.valueOf(DEFAULT_HEARTBEAT))
                .addParameter(CODEC_KEY, DubboCodec.NAME)
                .build();
        String str = url.getParameter(SERVER_KEY, DEFAULT_REMOTING_SERVER);

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

        ExchangeServer server;
        try {
            // 划红线  开始绑定本地端口
            server = Exchangers.bind(url, requestHandler);
        } catch (RemotingException e) {
            throw new RpcException("Fail to start server(url: " + url + ") " + e.getMessage(), e);
        }

        str = url.getParameter(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 new DubboProtocolServer(server);
    }

Exchangers.bind 内部的调用流程依次为:

  1. org.apache.dubbo.remoting.exchange.Exchangers#bind(org.apache.dubbo.common.URL, org.apache.dubbo.remoting.exchange.ExchangeHandler)

  2. org.apache.dubbo.remoting.exchange.support.header.HeaderExchanger#bind

  3. org.apache.dubbo.remoting.Transporters#bind(org.apache.dubbo.common.URL, org.apache.dubbo.remoting.ChannelHandler…)

  4. org.apache.dubbo.remoting.Transporters#getTransporter

    public static Transporter getTransporter() {
    return ExtensionLoader.getExtensionLoader(Transporter.class).getAdaptiveExtension();
    }

从上面的方法中,我们看到另外一个核心的接口 Transporter 接口(如下图),它的默认直线为netty,由此可知dubbo底层是使用netty作为通讯框架的。他提供绑定、链接两个方法

@SPI("netty")
public interface Transporter {

    /**
     * Bind a server.
     *
     * @param url     server url
     * @param handler
     * @return server
     * @throws RemotingException
     * @see org.apache.dubbo.remoting.Transporters#bind(URL, ChannelHandler...)
     */
    @Adaptive({Constants.SERVER_KEY, Constants.TRANSPORTER_KEY})
    RemotingServer bind(URL url, ChannelHandler handler) throws RemotingException;

    /**
     * Connect to a server.
     *
     * @param url     server url
     * @param handler
     * @return client
     * @throws RemotingException
     * @see org.apache.dubbo.remoting.Transporters#connect(URL, ChannelHandler...)
     */
    @Adaptive({Constants.CLIENT_KEY, Constants.TRANSPORTER_KEY})
    Client connect(URL url, ChannelHandler handler) throws RemotingException;

}

public class NettyTransporter implements Transporter {

    public static final String NAME = "netty";

    @Override
    public RemotingServer bind(URL url, ChannelHandler handler) throws RemotingException {
        // 创建一个netty 服务器 其内部会打开一个netty端口进行通信
      	return new NettyServer(url, handler);
    }

    @Override
    public Client connect(URL url, ChannelHandler handler) throws RemotingException {
        return new NettyClient(url, handler);
    }

}

new NettyServer(url, handler)内部调用流程为:

  1. org.apache.dubbo.remoting.transport.AbstractServer#AbstractServer

  2. org.apache.dubbo.remoting.transport.AbstractServer#doOpen

  3. org.apache.dubbo.remoting.transport.netty4.NettyServer#doOpen

    @Override
    protected void doOpen() throws Throwable {
    bootstrap = new ServerBootstrap();

         bossGroup = NettyEventLoopFactory.eventLoopGroup(1, "NettyServerBoss");
         workerGroup = NettyEventLoopFactory.eventLoopGroup(
                 getUrl().getPositiveParameter(IO_THREADS_KEY, Constants.DEFAULT_IO_THREADS),
                 "NettyServerWorker");
    
         final NettyServerHandler nettyServerHandler = new NettyServerHandler(getUrl(), this);
         channels = nettyServerHandler.getChannels();
    
         boolean keepalive = getUrl().getParameter(KEEP_ALIVE_KEY, Boolean.FALSE);
    
         bootstrap.group(bossGroup, workerGroup)
                 .channel(NettyEventLoopFactory.serverSocketChannelClass())
                 .option(ChannelOption.SO_REUSEADDR, Boolean.TRUE)
                 .childOption(ChannelOption.TCP_NODELAY, Boolean.TRUE)
                 .childOption(ChannelOption.SO_KEEPALIVE, keepalive)
                 .childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
                 .childHandler(new ChannelInitializer<SocketChannel>() {
                     @Override
                     protected void initChannel(SocketChannel ch) throws Exception {
                         // FIXME: should we use getTimeout()?
                         int idleTimeout = UrlUtils.getIdleTimeout(getUrl());
                         NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(), NettyServer.this);
                         if (getUrl().getParameter(SSL_ENABLED_KEY, false)) {
                             ch.pipeline().addLast("negotiation",
                                     SslHandlerInitializer.sslServerHandler(getUrl(), nettyServerHandler));
                         }
                         ch.pipeline()
                                 .addLast("decoder", adapter.getDecoder())
                                 .addLast("encoder", adapter.getEncoder())
                                 .addLast("server-idle-handler", new IdleStateHandler(0, 0, idleTimeout, MILLISECONDS))
                                 .addLast("handler", nettyServerHandler);
                     }
                 });
         // bind
         ChannelFuture channelFuture = bootstrap.bind(getBindAddress());
         channelFuture.syncUninterruptibly();
         channel = channelFuture.channel();
    
     }
    

学习netty的小伙伴是不是有种很熟悉的感觉,这里就是通过netty 打开端口 等待消费者的调用,这里指定了netty编/解码的方式 这里也是一个很重要的细节,后续再讲。

以上的整个代码分析 没有看到任何异步调用的逻辑,那么就可以很肯定的论证之前的猜测了:

在Dubbo服务暴露的过程中先使用netty框架暴露本地端口,然后再向注册中心进行服务注册。

Dubbo 通讯协议

上面讲到了Dubbo使用netty作为底层的通讯框架,那么它的底层通讯协议应该也采用netty内部的编解码机制,对传输的消息进行处理。先来复习下netty通讯协议,然后再分析一下消费者调用底层的二进制协议。

Netty 框架的编/解码机制

由于TCP通信中会存在粘/拆包的现像,因此需要在上层应用层面对消息进行区分处理,通常而言采用以下四种方式:

  1. 消息长度固定,累计读取长度总和为固定长度LEN后,认为读取一个完成的消息。

  2. 将回车换行符作为消息结束符,例如FTP协议

  3. 将特殊的分隔符作为消息的结束标志,其实回车换行符就是一种特殊的消息结束符

  4. 在消息头中定义长度字段来标识消息的总长度

很明显,由于Dubbo RPC调用过程中需要处理参数不固定的问题,因此采用第4中方式进行消息的编解码。

Dubbo DEMO 二进制协议

先来简单的跑一下Rpc调用Demo,然后通过抓包工具追根溯源地分析Dubbo 底层编/解码方式

@Test
    public void sayHi() {
        // 当前应用配置
        ApplicationConfig application = new ApplicationConfig();
        application.setName("test-consumer");
        application.setOwner("owner");

        // 连接注册中心配置
        RegistryConfig registry1 = new RegistryConfig();
        registry1.setAddress("zookeeper://localhost:2181");

        //InterfaceAPI 为dubbo接口定义
        ReferenceConfig<IHello> reference = new ReferenceConfig();
        reference.setApplication(application);
        reference.setRegistries(Arrays.asList(registry1));
        reference.setInterface(IHello.class);
        //接口定义的版本号
        reference.setVersion("1.0.0");

        IHello interfaceAPI = reference.get();
        String result = interfaceAPI.sayHi("kobe");
        System.out.println("===== : " + result);
    }		

然后打开WireShark工具 选择本地回环网关进行抓包,如下图
在这里插入图片描述

第二步 输入tcp.srcport == 20889 or tcp.dstport==20889,意思根据来源/目标端口过滤请求 (本地配置的dubbo端口为20889 需要根据自己的实际情况进行更改)
在这里插入图片描述

第三步 运行第一步的调用代码 观察Wireshark工具
在这里插入图片描述

第四步 看到上图是不是有一种很熟悉的感觉 熟悉的味道(跟参数一模一样), 点击第二步 复制里面的值,得到如下二进制代码

dabbc2000000000000000000000000c105322e302e321c6f72672e6578616d706c652e6170692e64617930312e4948656c6c6f05312e302e30057361794869124c6a6176612f6c616e672f537472696e673b046b6f62654804706174681c6f72672e6578616d706c652e6170692e64617930312e4948656c6c6f1272656d6f74652e6170706c69636174696f6e0d746573742d636f6e73756d657209696e746572666163651c6f72672e6578616d706c652e6170692e64617930312e4948656c6c6f0776657273696f6e05312e302e305a

Dubbo 协议分析

接下来我们结合源码来分析 上面的协议,首先Dubbo内部协议转换的核心接口为

package org.apache.dubbo.remoting;

import org.apache.dubbo.common.extension.Adaptive;
import org.apache.dubbo.common.extension.SPI;
import org.apache.dubbo.remoting.buffer.ChannelBuffer;

import java.io.IOException;

@SPI
public interface Codec2 {

    @Adaptive({Constants.CODEC_KEY})
    void encode(Channel channel, ChannelBuffer buffer, Object message) throws IOException;

    @Adaptive({Constants.CODEC_KEY})
    Object decode(Channel channel, ChannelBuffer buffer) throws IOException;


    enum DecodeResult {
        NEED_MORE_INPUT, SKIP_SOME_INPUT
    }	

}

Dubbo SPI 内部扩展的实现有以下几种
在这里插入图片描述

也可以在 org.apache.dubbo.remoting.transport.netty4.NettyServer#doOpen 方法内部增加断点来确认
在这里插入图片描述

注意上述new 操作其实是通过URL里面的协议 选择合适的编/解码器,这也是Dubbo SPI扩展机制的一大亮点 根据参数动态指定扩展。
在这里插入图片描述

在DubboCountCodec 内部定义了 DubboCodec 属性, 真正的逻辑处理都是调用该属性的方法进行处理。
在这里插入图片描述

DubboCodec 继承了 ExchangeCodec 类并重写了 decodeBody方法,根据方法名称可以猜到对消息进行解码。
在这里插入图片描述

查看父类ExchangeCodec 可以看到很明显的处理逻辑,根据消息类型 进行编/解码。现在我们拿出来之前的二进制协议,结合encodeRequest方法的处理逻辑,进行分析

protected void encodeRequest(Channel channel, ChannelBuffer buffer, Request req) throws IOException {
        Serialization serialization = getSerialization(channel, req);
        // header 申请16个长度的数组
        byte[] header = new byte[HEADER_LENGTH];
        // set magic number.
        // 将固定消息头 MAGIC = (short) 0xdabb; 写入header中
        Bytes.short2bytes(MAGIC, header);

        // set request and serialization flag.
        // 将第三位 写入状态 | 序列化 (表示位)
        header[2] = (byte) (FLAG_REQUEST | serialization.getContentTypeId());
				//根据调用方式 设置返回标志 
        if (req.isTwoWay()) {
            header[2] |= FLAG_TWOWAY;
        }
        if (req.isEvent()) {
            header[2] |= FLAG_EVENT;
        }
				
        // set request id. 从位置4开始写 long类型的Request ID 
       // 注意位置3 为空 默认为0
        Bytes.long2bytes(req.getId(), header, 4);

        // encode request data.
        // 记录写索引
        int savedWriteIndex = buffer.writerIndex();
        //设置 buffer 的写位置
        buffer.writerIndex(savedWriteIndex + HEADER_LENGTH);
  			//对buffer 进行包装 即利用bos对象从write index 开始写数据
        ChannelBufferOutputStream bos = new ChannelBufferOutputStream(buffer);

        if (req.isHeartbeat()) {
            // heartbeat request data is always null
            bos.write(CodecSupport.getNullBytesOf(serialization));
        } else {
            ObjectOutput out = serialization.serialize(channel.getUrl(), bos);
            if (req.isEvent()) {
                encodeEventData(channel, out, req.getData());
            } else {
                //真实的写入 请求数据 
                // org.apache.dubbo.rpc.protocol.dubbo.DubboCodec#encodeRequestData(org.apache.dubbo.remoting.Channel, org.apache.dubbo.common.serialize.ObjectOutput, java.lang.Object, java.lang.String) 重载了该方法
                encodeRequestData(channel, out, req.getData(), req.getVersion());
            }
            out.flushBuffer();
            if (out instanceof Cleanable) {
                ((Cleanable) out).cleanup();
            }
        }
				//将bos写的数据 flush 到buffer
        bos.flush();
        bos.close();
        int len = bos.writtenBytes();
        checkPayload(channel, len);
  			//从偏移量12的位置开始写入header 数据包的长度 int 为4个字节
        Bytes.int2bytes(len, header, 12);

        // 重置 write index
        buffer.writerIndex(savedWriteIndex);
        buffer.writeBytes(header); // write header.
        buffer.writerIndex(savedWriteIndex + HEADER_LENGTH + len);
    }	

    @Override
    protected void encodeRequestData(Channel channel, ObjectOutput out, Object data, String version) throws IOException {
        RpcInvocation inv = (RpcInvocation) data;
				//设置版本号 writeUTF 是将字符串 逐个转换为 ascii码值
        out.writeUTF(version);
        // https://github.com/apache/dubbo/issues/6138
        //获取接口名称
        String serviceName = inv.getAttachment(INTERFACE_KEY);
        if (serviceName == null) {
            serviceName = inv.getAttachment(PATH_KEY);
        }
        //写入接口名称
        out.writeUTF(serviceName);
       //写入接口版本
        out.writeUTF(inv.getAttachment(VERSION_KEY));
				//写入方法名称
        out.writeUTF(inv.getMethodName());
        //写入参数描述
        out.writeUTF(inv.getParameterTypesDesc());
        Object[] args = inv.getArguments();
        if (args != null) {
            for (int i = 0; i < args.length; i++) {
                //写入参数值
                out.writeObject(encodeInvocationArgument(channel, inv, i));
            }
        }
        // 此处画红线,跟后面的协议分析有很大关系
        out.writeAttachments(inv.getObjectAttachments());
    }

至此,请求解析的源代码已经分析完毕,现在可以试着对之前的二进制协议进行拆解分析.

dabbc2000000000000000000000000c105322e302e321c6f72672e6578616d706c652e6170692e64617930312e4948656c6c6f05312e302e30057361794869124c6a6176612f6c616e672f537472696e673b046b6f62654804706174681c6f72672e6578616d706c652e6170692e64617930312e4948656c6c6f1272656d6f74652e6170706c69636174696f6e0d746573742d636f6e73756d657209696e746572666163651c6f72672e6578616d706c652e6170692e64617930312e4948656c6c6f0776657273696f6e05312e302e305a

/**
 * 为了代码美观 写在注释里面
dabb  -- 固定2个字节消息头
c2    -- magic number (request and serialization flag 1)
00    -- 固定为0(源代码没写该字段值)
0000000000000000     -- requestid     
000000c1             -- 消息长度 (10进制: 193)
05                   -- 版本号 长度 
322e302e32           -- 版本号 2.0.2 (32 -> 2(ASCII),32 -> 2(ASCII),2e -> .(ASCII),依次类推 )
1c                   -- 接口名长度 即 org.example.api.day01.IHello 的长度
6f72672e6578616d706c652e6170692e64617930312e4948656c6c6f -- serviceName(org.example.api.day01.IHello)
05                   -- 接口版本长度 即 1.0.0 的长度
312e302e30           -- 接口版本号   即 1.0.0
05                   -- 方法名长度 即 sayHi 的长度
7361794869           -- 方法名 即 sayHi
12                   -- 参数描述符长度 即 Ljava/lang/String; 的长度
4c6a6176612f6c616e672f537472696e673b      -- Ljava/lang/String;
04                   -- 参数长度 
6b6f6265             -- 参数数据 即 kobe

// 以下协议数据体现为attchments里面的map值 因此会带有 消息头尾标识 48 -> H (map头); 5a -> Z (mao尾),见下图
48                   -- map 头标识
04                   -- 4长度 表示map里面有4个值
70617468             -- 值 path 
1c                   -- 接口名长度 即 org.example.api.day01.IHello 的长度
6f72672e6578616d706c652e6170692e64617930312e4948656c6c6f   -- path(org.example.api.day01.IHello)
12                   -- 应用名称key长度
72656d6f74652e6170706c69636174696f6e --值 remote.application test-consumer
0d                   -- 应用名称value长度
746573742d636f6e73756d6572   --- 应用名称 test-consumer
09                   -- 接口名称key长度  
696e74657266616365   -- 接口名称key  interface
1c                   -- 接口名称value长度  
6f72672e6578616d706c652e6170692e64617930312e4948656c6c6f
07                   -- 版本名称key长度 
76657273696f6e       -- version
05                   -- 版本名称value长度 
312e302e30           -- 版本名称 1.0.0
5a                   -- map 尾标识
 */

在这里插入图片描述

对map进行序列化的源代码如下,可以很清楚的看到在map头尾写上了固定的标识

/**
 * com.alibaba.com.caucho.hessian.io.MapSerializer#writeObject
 */
@Override
    public void writeObject(Object obj, AbstractHessianOutput out)
            throws IOException {
        if (out.addRef(obj))
            return;

        Map map = (Map) obj;

        Class cl = obj.getClass();

        //根据判断 写入头标示位
        if (cl.equals(HashMap.class)
                || !_isSendJavaType
                || !(obj instanceof java.io.Serializable))
            out.writeMapBegin(null);
        else
            out.writeMapBegin(obj.getClass().getName());

        Iterator iter = map.entrySet().iterator();
        while (iter.hasNext()) {
            Map.Entry entry = (Map.Entry) iter.next();

            out.writeObject(entry.getKey());
            out.writeObject(entry.getValue());
        }
        //写入尾标示位 这两个方法为抽象方法 实现类为 com.alibaba.com.caucho.hessian.io.Hessian2Output
        out.writeMapEnd();
    }

@Override
    public void writeMapBegin(String type)
            throws IOException {
        if (SIZE < _offset + 32)
            flush();

        if (type != null) {
            _buffer[_offset++] = BC_MAP;

            writeType(type);
        } else
          // BC_MAP_UNTYPED = H (转换成16进制: 48 )
            _buffer[_offset++] = BC_MAP_UNTYPED;
    }

    /**
     * Writes the tail of the map to the stream.
     */
    @Override
    public void writeMapEnd()
            throws IOException {
        if (SIZE < _offset + 32)
            flush();
				// BC_END = Z (转换成16进制: 5a )
        _buffer[_offset++] = (byte) BC_END;
    }

跨语言调用

至此,dubbo协议的请求协议已经分析完毕。由于Dubbo底层是利用TCP协议进行通信,那是否可以通过其他语言直接发送TCP请求进行远程调用呢(纯粹好玩 无实际作用),。真正跨语言级别的通讯协议推荐使用 ProtoBuf,不会使用这么粗暴的方式进行调用

package main

import (
	"encoding/hex"
	"fmt"
	"net"
)

func main() {
	conn, err := net.Dial("tcp", "localhost:20889")
	if err != nil {
		fmt.Println("Error dialing", err.Error())
		return
	}
	defer conn.Close()

	args := "dabbc2000000000000000000000000c105322e302e321c6f72672e6578616d706c652e6170692e64617930312e4948656c6c6f05312e302e30057361794869124c6a6176612f6c616e672f537472696e673b046b6f62654804706174681c6f72672e6578616d706c652e6170692e64617930312e4948656c6c6f1272656d6f74652e6170706c69636174696f6e0d746573742d636f6e73756d657209696e746572666163651c6f72672e6578616d706c652e6170692e64617930312e4948656c6c6f0776657273696f6e05312e302e305a"
	bytes, _ := hex.DecodeString(args)
	conn.Write(bytes)
	buf := make([]byte, 1024)
	read, err := conn.Read(buf)
	if err != nil {
		fmt.Println("Error Read", err.Error())
		return
	}
	if read > 0 {
		fmt.Println(hex.EncodeToString(buf[0:read]))
	}
}
/**
 * 输出如下 由于没有对返回协议进行解析,直接显示二进制数据
 * dabb021400000000000000000000001d940d736179486920746f206b6f62654805647562626f05322e302e325a
 */
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值