Dubbo——Dubbo中的URL统一资源模型与Dubbo协议

一、URL简介

在互联网领域,每个信息资源都有统一的且在网上唯一的地址,该地址就叫 URL(Uniform Resource Locator,统一资源定位符),它是互联网的统一资源定位标志,也就是指网络地址。

URL 本质上就是一个特殊格式的字符串。一个标准的 URL 格式可以包含如下的几个部分:

protocol://username:password@host:port/path?key=value&key=value
  • protocol:URL 的协议。我们常见的就是 HTTP 协议和 HTTPS 协议,当然,还有其他协议,如 FTP 协议、SMTP 协议等。

  • username/password:用户名/密码。 HTTP Basic Authentication 中多会使用在 URL 的协议之后直接携带用户名和密码的方式。

  • host/port:主机/端口。在实践中一般会使用域名,而不是使用具体的 host 和 port。

  • path:请求的路径。

  • parameters:参数键值对。一般在 GET 请求中会将参数放到 URL 中,POST 请求会将参数放到请求体中。

URL 是整个 Dubbo 中非常基础,也是非常核心的一个组件,阅读源码的过程中你会发现很多方法都是以 URL 作为参数的,在方法内部解析传入的 URL 得到有用的参数,所以有人将 URL 称为Dubbo 的配置总线。

Provider 将自身的信息封装成 URL 注册到 ZooKeeper 中,从而暴露自己的服务, Consumer 也是通过 URL 来确定自己订阅了哪些 Provider 的。

由此可见,URL 之于 Dubbo 是非常重要的,所以说“抓住 URL,就理解了半个 Dubbo”。那本文我们就来介绍 URL 在 Dubbo 中的应用,以及 URL 作为 Dubbo 统一契约的重要性,最后我们再通过示例说明 URL 在 Dubbo 中的具体应用。

二、Dubbo 中的 URL

Dubbo 中任意的一个实现都可以抽象为一个 URL,Dubbo 使用 URL 来统一描述了所有对象和配置信息,并贯穿在整个 Dubbo 框架之中。这里我们来看 Dubbo 中一个典型 URL 的示例,如下:

dubbo://172.17.32.91:20880/org.apache.dubbo.demo.DemoService?anyhost=true&application=dubbo-demo-api-provider&dubbo=2.0.2&interface=org.apache.dubbo.demo.DemoService&methods=sayHello,sayHelloAsync&pid=32508&release=&side=provider&timestamp=1593253404714dubbo://172.17.32.91:20880/org.apache.dubbo.demo.DemoService?anyhost=true&application=dubbo-demo-api-provider&dubbo=2.0.2&interface=org.apache.dubbo.demo.DemoService&methods=sayHello,sayHelloAsync&pid=32508&release=&side=provider&timestamp=1593253404714

这个 Demo Provider 注册到 ZooKeeper 上的 URL 信息,简单解析一下这个 URL 的各个部分:

  • protocol:dubbo 协议。

  • username/password:没有用户名和密码。

  • host/port:172.17.32.91:20880。

  • path:org.apache.dubbo.demo.DemoService。

  • parameters:参数键值对,这里是问号后面的参数。

下面是 URL 的构造方法,你可以看到其核心字段与前文分析的 URL 基本一致:

public URL(String protocol, 
            String username, 
            String password, 
            String host, 
            int port, 
            String path, 
            Map<String, String> parameters, 
            Map<String, Map<String, String>> methodParameters) { 
    if (StringUtils.isEmpty(username) 
            && StringUtils.isNotEmpty(password)) { 
        throw new IllegalArgumentException("Invalid url"); 
    } 
    this.protocol = protocol; 
    this.username = username; 
    this.password = password; 
    this.host = host; 
    this.port = Math.max(port, 0); 
    this.address = getAddress(this.host, this.port); 
    while (path != null && path.startsWith("/")) { 
        path = path.substring(1); 
    } 
    this.path = path; 
    if (parameters == null) { 
        parameters = new HashMap<>(); 
    } else { 
        parameters = new HashMap<>(parameters); 
    } 
    this.parameters = Collections.unmodifiableMap(parameters); 
    this.methodParameters = Collections.unmodifiableMap(methodParameters); 
}

另外,在 dubbo-common 包中还提供了 URL 的辅助类:

  • URLBuilder, 辅助构造 URL;
  • URLStrParser, 将字符串解析成 URL 对象。

大致样子如下:

dubbo://192.168.1.6:20880/moe.cnkirito.sample.HelloService?timeout=3000
描述一个 dubbo 协议的服务

zookeeper://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=demo-consumer&dubbo=2.0.2&interface=org.apache.dubbo.registry.RegistryService&pid=1214&qos.port=33333&timestamp=1545721981946
描述一个 zookeeper 注册中心

consumer://30.5.120.217/org.apache.dubbo.demo.DemoService?application=demo-consumer&category=consumers&check=false&dubbo=2.0.2&interface=org.apache.dubbo.demo.DemoService&methods=sayHello&pid=1209&qos.port=33333&side=consumer&timestamp=1545721827784
描述一个消费者

三、Dubbo中有关URL的服务

1、解析服务

  • 1)、Spring在遇到dubbo名称空间时,会回调DubboNamespaceHandler。这个类也是Dubbo基于spring扩展点编写的解析xml文件的类。

  • 2)、解析的xml标签使用DubboBeanDefinitionParser将其转化为bean对象。

  • 3)、服务提供方在ServiceConfig.export()初始化时将bean对象转化为URL格式,所有Bean属性转换成URL参数。这时候的URL就会传给协议扩展点。根据URL中protocol的值通过扩展点自适应机制进行不同协议的服务暴露或引用。

  • 4)、而服务消费方则是ReferenceConfig.export()方法。

2、直接暴露服务端口

  • 1)、在没有注册中心时,ServiceConfig解析出的URL格式为:dubbo://service-host/com.foo.FooService?version=1.0.0
  • 2)、基于扩展点自适应机制。通过URL的dubbo://协议头识别,这时候就调用DubboProtocol中的export方法进行暴露服务端口。

3、向注册中心暴露服务端口

传入的 URL 中包含了 Provider 的地址(172.18.112.15:20880)、暴露的接口(com.yibo.service.HelloService)等信息, toUrlPath() 方法会根据传入的 URL 参数确定在 ZooKeeper 上创建的节点路径,还会通过 URL 中的 dynamic 参数值确定创建的 ZNode 是临时节点还是持久节点。

4、直接引用服务

  • 1)、在没有注册中心,ReferenceConfig解析出的URL格式就为dubbo://service-host/com.foo.FooService?version=1.0.0
  • 2)、基于扩展点自适应机制,通过 URL 的dubbo://协议头识别,直接调用DubboProtocol的refer方法,返回提供者引用。

5、从注册中心引用服务

Consumer 启动后会向注册中心进行订阅操作,并监听自己关注的 Provider。那 Consumer 是如何告诉注册中心自己关注哪些 Provider 呢?

我们来看 ZookeeperRegistry 这个实现类,它是由上面的 ZookeeperRegistryFactory 工厂类创建的 Registry 接口实现,其中的 doSubscribe() 方法是订阅操作的核心实现,在this.zkClient.create(path, false)打一个断点,并 Debug 启动 Demo 中 Consumer,会得到下图所示的内容:

我们看到传入的 URL 参数如下:

consumer://192.168.163.1/com.yibo.service.HelloService?application=apache-dubbo-consumer&category=providers,configurators,routers&check=false&cluster=failover&dubbo=2.0.2&file=e:/dubbo-server&id=org.apache.dubbo.config.RegistryConfig#0&init=false&interface=com.yibo.service.HelloService&metadata-type=remote&methods=sayHello&mock=com.yibo.service.HelloServiceMock&owner=yibo&pid=11192&qos.enable=false&release=2.7.9&retries=2&side=consumer&sticky=false&timeout=1&timestamp=1616181085576

其中 Protocol 为 consumer ,表示是 Consumer 的订阅协议,其中的 category 参数表示要订阅的分类,这里要订阅 providers、configurators 以及 routers 三个分类;interface 参数表示订阅哪个服务接口,这里要订阅的是暴露 com.yibo.service.HelloService 实现的 Provider。

通过 URL 中的上述参数,ZookeeperRegistry 会在 toCategoriesPath() 方法中将其整理成一个 ZooKeeper 路径,然后调用 zkClient 在其上添加监听。

5、URL 在 SPI 中的应用

Dubbo SPI 中有一个依赖 URL 的重要场景——适配器方法,是被 @Adaptive 注解标注的, URL 一个很重要的作用就是与 @Adaptive 注解一起选择合适的扩展实现类。

例如,在 dubbo-registry-api 模块中我们可以看到 RegistryFactory 这个接口,其中的 getRegistry() 方法上有 @Adaptive({"protocol"}) 注解,说明这是一个适配器方法,Dubbo 在运行时会为其动态生成相应的 “$Adaptive” 类型,如下所示:

@SPI("dubbo")
public interface RegistryFactory {

    @Adaptive({"protocol"})
    Registry getRegistry(URL url);
}
public class RegistryFactory$Adaptive
              implements RegistryFactory { 
    public Registry getRegistry(org.apache.dubbo.common.URL arg0) { 
        if (arg0 == null) throw new IllegalArgumentException("..."); 
        org.apache.dubbo.common.URL url = arg0; 
        // 尝试获取URL的Protocol,如果Protocol为空,则使用默认值"dubbo" 
        String extName = (url.getProtocol() == null ? "dubbo" : 
             url.getProtocol()); 
        if (extName == null) 
            throw new IllegalStateException("..."); 
        // 根据扩展名选择相应的扩展实现,Dubbo SPI的核心原理在后面深入分析 
        RegistryFactory extension = (RegistryFactory) ExtensionLoader 
          .getExtensionLoader(RegistryFactory.class) 
                .getExtension(extName); 
        return extension.getRegistry(arg0); 
    } 
}

我们会看到,在生成的 RegistryFactory$Adaptive 类中会自动实现 getRegistry() 方法,其中会根据 URL 的 Protocol 确定扩展名称,从而确定使用的具体扩展实现类。我们可以找到 RegistryProtocol 这个类,并在其 getRegistry() 方法中打一个断点, Debug 启动Dubbo的 Provider,得到如下图所示的内容:


这里传入的 registryUrl 值为:

zookeeper://localhost:2181/org.apache.dubbo.registry.RegistryService?application=apache-dubbo-provider&dubbo=2.0.2&export=dubbo%3A%2F%2F192.168.174.1%3A20880%2Fcom.yibo.service.HelloService%3Fanyhost%3Dtrue%26application%3Dapache-dubbo-provider%26bind.ip%3D192.168.174.1%26bind.port%3D20880%26deprecated%3Dfalse%26dubbo%3D2.0.2%26dynamic%3Dtrue%26generic%3Dfalse%26interface%3Dcom.yibo.service.HelloService%26metadata-type%3Dremote%26methods%3DsayHello%26owner%3Dyibo%26pid%3D10368%26qos.enable%3Dfalse%26release%3D2.7.9%26side%3Dprovider%26timestamp%3D1616180722645&id=org.apache.dubbo.config.RegistryConfig#0&owner=yibo&pid=10368&qos.enable=false&release=2.7.9&timestamp=1616180722643

那么在 RegistryFactory$Adaptive 中得到的扩展名称为 zookeeper,此次使用的 Registry 扩展实现类就是 ZookeeperRegistryFactory。

通过上述示例,相信你已经感觉到 URL 在 Dubbo 体系中称为“总线”或是“契约”的原因了,在后面的源码分析中,我们还将看到更多关于 URL 的实现。

四、Dubbo协议

1、协议简介

聊完了Dubbo中的URL模型就来聊聊Dubbo中的协议。协议是双方确定的交流语义,协议在双方传输数据中起到的了交换作用,没有协议就无法完成数据交换。在dubbo中就是Codec2

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

}

encode是将通信对象编码到ByteBufferWrapper,decode是将从网络上读取的ChannelBuffer解码为Object。

2、协议图解

协议详情

  • Magic - Magic High & Magic Low (16 bits):标识协议版本号,Dubbo 协议:0xdabb

  • Req/Res (1 bit):标识是请求或响应。请求: 1; 响应: 0。

  • 2 Way (1 bit):仅在 Req/Res 为1(请求)时才有用,标记是否期望从服务器返回值。如果需要来自服务器的返回值,则设置为1。

  • Event (1 bit):标识是否是事件消息,例如,心跳事件。如果这是一个事件,则设置为1。

  • Serialization ID (5 bit):标识序列化类型:比如 fastjson 的值为6。

  • Status (8 bits):仅在 Req/Res 为0(响应)时有用,用于标识响应的状态。

    • 20 - OK
    • 30 - CLIENT_TIMEOUT
    • 31 - SERVER_TIMEOUT
    • 40 - BAD_REQUEST
    • 50 - BAD_RESPONSE
    • 60 - SERVICE_NOT_FOUND
    • 70 - SERVICE_ERROR
    • 80 - SERVER_ERROR
    • 90 - CLIENT_ERROR
    • 100 - SERVER_THREADPOOL_EXHAUSTED_ERROR
  • Request ID (64 bits):标识唯一请求。类型为long。

  • Data Length (32 bits):序列化后的内容长度(可变部分),按字节计数。int类型。

  • Variable Part:被特定的序列化类型(由序列化 ID 标识)序列化后,每个部分都是一个 byte [] 或者 byte。

    • 如果是请求包 ( Req/Res = 1),则每个部分依次为:
      • Dubbo version
      • Service name
      • Service version
      • Method name
      • Method parameter types
      • Method arguments
      • Attachments
  • 如果是响应包(Req/Res = 0),则每个部分依次为:

    • 返回值类型(byte),标识从服务器端返回的值类型:
      • 返回空值:RESPONSE_NULL_VALUE 2
      • 正常响应值: RESPONSE_VALUE 1
      • 异常:RESPONSE_WITH_EXCEPTION 0
  • 返回值:从服务端返回的响应bytes

注意:对于(Variable Part)变长部分,当前版本的Dubbo 框架使用json序列化时,在每部分内容间额外增加了换行符作为分隔,请在Variable Part的每个part后额外增加换行符, 如:

Dubbo version bytes (换行符)
Service name bytes  (换行符)
...

Dubbo 协议的优缺点

优点

  • 协议设计上很紧凑,能用 1 个 bit 表示的,不会用一个 byte 来表示,比如 boolean 类型的标识。
  • 请求、响应的 header 一致,通过序列化器对 content 组装特定的内容,代码实现起来简单。

参考:
https://www.cnblogs.com/Cubemen/p/12312981.html

https://www.cnblogs.com/Cubemen/p/11409068.html

https://zhuanlan.zhihu.com/p/98562180

https://baike.baidu.com/item/SOA/2140650?fr=aladdin

https://msd.misuland.com/pd/14073785615868401

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容