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服务暴露流程
见明识义,这里不正是服务暴露的方法命名嘛, 看到这里有一种见到庐山真面目的开心与激动。
接着往下一步步跟踪,它的调用流程应该是:
-
org.apache.dubbo.config.bootstrap.DubboBootstrap#exportServices
-
org.apache.dubbo.config.bootstrap.DubboBootstrap#exportService
-
org.apache.dubbo.config.ServiceConfig#export
-
org.apache.dubbo.config.ServiceConfig#doExport
-
org.apache.dubbo.config.ServiceConfig#doExportUrls
-
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个功能
- 将自己的服务地址暴露给注册中心,这样消费者才能从注册中心获取到RPC调用的远程信息,如IP、端口等,即服务注册
- 服务提供者 应该提供远程地址以提供给消费者调用
OK,当你理解了上面的流程之后,那么之前的划红线的两个接口的作用就一目了然了。
org.apache.dubbo.registry.integration.RegistryProtocol 该类提供服务注册功能,想指定注册中心注册他的元数据
org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol 该类提供远程调用功能,让消费者通过调用远程接口获取信息
那么问题又来了,对于服务提供者这两步的流程的先后顺序如何,到底谁先谁后,我们不妨猜测一下,理论上它的逻辑应该是:
- 先暴露本地端口,提供远程服务 即先执行 DubboProtocol
- 再将本地信息的元数据 注册到注册中心去
那么究竟是不是我们想象的那样呢,继续跟踪源码。在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 内部的调用流程依次为:
-
org.apache.dubbo.remoting.exchange.Exchangers#bind(org.apache.dubbo.common.URL, org.apache.dubbo.remoting.exchange.ExchangeHandler)
-
org.apache.dubbo.remoting.exchange.support.header.HeaderExchanger#bind
-
org.apache.dubbo.remoting.Transporters#bind(org.apache.dubbo.common.URL, org.apache.dubbo.remoting.ChannelHandler…)
-
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)内部调用流程为:
-
org.apache.dubbo.remoting.transport.AbstractServer#AbstractServer
-
org.apache.dubbo.remoting.transport.AbstractServer#doOpen
-
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通信中会存在粘/拆包的现像,因此需要在上层应用层面对消息进行区分处理,通常而言采用以下四种方式:
-
消息长度固定,累计读取长度总和为固定长度LEN后,认为读取一个完成的消息。
-
将回车换行符作为消息结束符,例如FTP协议
-
将特殊的分隔符作为消息的结束标志,其实回车换行符就是一种特殊的消息结束符
-
在消息头中定义长度字段来标识消息的总长度
很明显,由于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
*/