dubbo 源码 v2.7 分析:通信过程及序列化协议

简介: 前面我们介绍了dubbo的核心机制,今天将开始分析远程调用流程。毕竟,作为一个rpc框架,远程调用是理论的核心内容。通过对dubbo相关实现的探究,深入了解rpc原理及可能的问题。

一 摘要

   前面我们介绍了dubbo的核心机制,今天将开始分析远程调用流程。毕竟,作为一个rpc框架,远程调用是理论的核心内容。通过对dubbo相关实现的探究,深入了解rpc原理及可能的问题。

二 rpc通信核心问题

2.1 rpc核心问题

1、远程调用,使用什么传输协议

2、远程传输数据,不能直接原格式传输,只能通过流数据传输,序列化和反序列化协议选择?

3、发起请求方式?

4、请求处理方式?同步/异步,长连接/短链接...

除此之外,还有超时时间、异常处理、缓存、负载均衡策略、注册中心、网关等等相关问题,都需要解决。我们先关注前面四个。

2.2 rpc框架构成

1)服务提供者,运行在服务器端,提供服务接口定义与服务实现类。

2)服务中心,运行在服务器端,负责将本地服务发布成远程服务,管理远程服务,提供给服务消费者使用。

3)服务消费者,运行在客户端,通过远程代理对象调用远程服务。

三 序列化和反序列化

3.1 相关概念

3.1.1 定义

先说一个老生常谈的问题,什么是序列化和反序列化?简单来说,定义如下:

序列化(Serialization):将数据结构或对象转换成二进制串的过程;

反序列化(Deserialization):将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程。

3.1.2 数据结构、对象与二进制串

不同的开发语言中,数据结构、对象和二进制串的表示方式略有不同。(内容来自美团技术团队的文章,序列化和反序列化

1)数据结构和对象:

对于类似Java这种完全面向对象的语言,开发者操作的都是对象(Object),来自于类的实例化。在Java语言中最接近数据结构的概念,就是POJO(Plain Old Java Object)或者Java Bean--那些只有setter/getter方法的类。而在C++这种半面向对象的语言中,数据结构和struct对应,对象和class对应。

2)二进制串:

序列化所生成的二进制串指的是存储在内存中的一块数据。C++语言具有内存操作符,所以二进制串的概念容易理解,例如,C++语言的字符串可以直接被传输层使用,因为其本质上就是以’\0’结尾的存储在内存中的二进制串。在Java语言里面,二进制串的概念容易和String混淆。实际上String 是Java的一等公民,是一种特殊对象(Object)。对于跨语言间的通讯,序列化后的数据当然不能是某种语言的特殊数据类型。二进制串在Java里面所指的是byte[],byte是Java的8中原生数据类型之一(Primitive data types)。

3.2 为什么要序列化?

   这是与远程通信的过程相关的,网络传输的数据必须是二进制数据,但通常调用方请求的入参有可能是对象(各种面向对象语言),而对象是不能直接在网络中传输的,所以需要把它转成可传输的二进制串,并且要求转换算法是可逆的,这个过程也就是“序列化”过程。这样,服务提供方就可以正确地从输入的二进制数据中分割出不同的请求参数,同时根据请求类型和序列化类型,把二进制的消息体逆向还原成请求对象,这个过程也就是“反序列化”。

   回顾一下rpc请求过程:

   另外,作为一个框架,通用性是必须考虑的要素之一;而且,作为跨机器的请求,服务提供方和调用方也很可能使用的不是一种编程语言,那么保证两端的数据结构能够相互理解就是必须解决的一个问题,这也是序列化和反序列化的意义所在。

四 dubbo的通信过程

以dubbo官方提供的服务方和客户端demo作为生产和消费两端,对应的请求过程如下图所示:

可见,dubbo采用的是Netty(TCP)协议作为默认的通信协议;通过Netty的NIO来进行数据传输,并使用了它的响应机制。

五 Dubbo通信分析

先回顾一下OSI模型,下图左侧是7层结构,右侧是对该层的解释:

   其中,传输层是因特网协议套件和OSI模型中的网络堆栈中的协议的分层体系结构中的方法的概念划分。该层的协议为应用程序提供主机到主机的通信服务。如UDP、TCP。并且提供面向连接的通信,可靠性,流量控制和多路复用等服务。

   在dubbo中,Transporter就是对传输层的实现。它对于提供了dubbo服务间通讯的支持。有了它,各个服务就可以进行网络通信了,不再是信息孤岛了。

5.1 相关代码结构

dubbo远程通信相关代码在org.apache.dubbo.remoting包下。

  • transport为网路传送层,抽象mina和netty为统一接口,以Message为中心,扩展接口为Channel、Transporter、Client、Server、Codec。
  • exchange是信息交换层,封装请求响应模式,以Request为中心,Response为中心, 扩展接口为Exchanger、ExchangeChannel、ExchangeClient、ExchangeServer。

5.2 通信协议及框架

从上图可见,dubbo支持了mina, netty(3 & 4版本), grizzly三种通信框架,默认配置常量在org.apache.dubbo.remoting.Constants中,这是一个常量接口,并未定义任何抽象方法。里面的常量包括一些关键的key(DISPATCHER_KEY,BIND_IP_KEY,BIND_PORT_KEY等等),默认的transporter、默认的远程客户端、默认的二进制协议等等。

5.3 Transport分析

5.3.1 Transporter接口

@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})
    Server 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;
}

通过这个接口定义可以知道:

1)dubbo是C/S架构

2)Transport提供了创建服务端和客户端的功能;

3)默认的Transporter是NettyTransporter,@SPI("netty")注解中标记;

4)可以通过server/client、transport来配置server/client的类型,目前支持的类型有netty、mina等

注:3)中的netty,指的是netty4(2.5.6版本以后,之前的版本是netty3),也就是对应transport.netty4包下的内容,这是因为netty4包下的NettyTransporter中,name是netty,SPI是通过name来获取对应的Transporter的:

而 netty包下标记的NAME为netty3:

5.3.2 NettyServer

1、属性和构造方法

继承自AbstractServer,且实现了Server接口:

2、构造方法和AbstractServer

构造方法的主要动作都是在AbstractServer中完成的(继承关系从子类到父类:AbstractServer->AbstractEndpoint->AbstractPeer),在这里设置了ip、端口、空闲时间等参数,并调用了doOpen()方法,这是定义在AbstractServer的抽象方法,所以需要子类实现:

public AbstractServer(URL url, ChannelHandler handler) throws RemotingException {
        super(url, handler);
        localAddress = getUrl().toInetSocketAddress();
        String bindIp = getUrl().getParameter(Constants.BIND_IP_KEY, getUrl().getHost());
        int bindPort = getUrl().getParameter(Constants.BIND_PORT_KEY, getUrl().getPort());
        if (url.getParameter(ANYHOST_KEY, false) || NetUtils.isInvalidLocalHost(bindIp)) {
            bindIp = ANYHOST_VALUE;
        }
        bindAddress = new InetSocketAddress(bindIp, bindPort);
        this.accepts = url.getParameter(ACCEPTS_KEY, DEFAULT_ACCEPTS);
        this.idleTimeout = url.getParameter(IDLE_TIMEOUT_KEY, DEFAULT_IDLE_TIMEOUT);
        try {
            doOpen();
            if (logger.isInfoEnabled()) {
                logger.info("Start " + getClass().getSimpleName() + " bind " + getBindAddress() + ", export " + getLocalAddress());
            }
        } catch (Throwable t) {
            throw new RemotingException(url.toInetSocketAddress(), null, "Failed to bind " + getClass().getSimpleName()
                    + " on " + getLocalAddress() + ", cause: " + t.getMessage(), t);
        }
        //fixme replace this with better method
        DataStore dataStore = ExtensionLoader.getExtensionLoader(DataStore.class).getDefaultExtension();
        executor = (ExecutorService) dataStore.get(Constants.EXECUTOR_SERVICE_COMPONENT_KEY, Integer.toString(url.getPort()));
    }

AbstractPeer.java,除提供构造方法外,还提供了消息发送方法,以及一些handler:

public AbstractPeer(URL url, ChannelHandler handler) {
        if (url == null) {
            throw new IllegalArgumentException("url == null");
        }
        if (handler == null) {
            throw new IllegalArgumentException("handler == null");
        }
        this.url = url;
        this.handler = handler;
    }
    @Override
    public void send(Object message) throws RemotingException {
        send(message, url.getParameter(Constants.SENT_KEY, false));
    }
    @Override
    public ChannelHandler getChannelHandler() {
        if (handler instanceof ChannelHandlerDelegate) {
            return ((ChannelHandlerDelegate) handler).getHandler();
        } else {
            return handler;
        }
    }
    /**
     * @return ChannelHandler
     */
    @Deprecated
    public ChannelHandler getHandler() {
        return getDelegateHandler();
    }
    /**
     * Return the final handler (which may have been wrapped). This method should be distinguished with getChannelHandler() method
     *
     * @return ChannelHandler
     */
    public ChannelHandler getDelegateHandler() {
        return handler;
    }

Transport初始化是通过Transporters这个工具类进行初始化的,查看源码,有以下两个bind方法,支持传参url和ChannelHandler,并且支持传入多个Handler:

public static Server bind(String url, ChannelHandler... handler) throws RemotingException {
        return bind(URL.valueOf(url), handler);
    }
    public static Server bind(URL url, ChannelHandler... handlers) throws RemotingException {
        if (url == null) {
            throw new IllegalArgumentException("url == null");
        }
        if (handlers == null || handlers.length == 0) {
            throw new IllegalArgumentException("handlers == null");
        }
        ChannelHandler handler;
        if (handlers.length == 1) {
            handler = handlers[0];
        } else {
            handler = new ChannelHandlerDispatcher(handlers);
        }
        return getTransporter().bind(url, handler);
    }

3、doOpen

大部分方法是在AbstractServer中实现,NettyServer中最重要的方法就是这个doOpen方法,从代码中可以看出,这就是一个标准流程:

@Override
    protected void doOpen() throws Throwable {
        bootstrap = new ServerBootstrap();
        bossGroup = new NioEventLoopGroup(1, new DefaultThreadFactory("NettyServerBoss", true));
        workerGroup = new NioEventLoopGroup(getUrl().getPositiveParameter(IO_THREADS_KEY, Constants.DEFAULT_IO_THREADS),
                new DefaultThreadFactory("NettyServerWorker", true));
        final NettyServerHandler nettyServerHandler = new NettyServerHandler(getUrl(), this);
        channels = nettyServerHandler.getChannels();
        bootstrap.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .childOption(ChannelOption.TCP_NODELAY, Boolean.TRUE)
                .childOption(ChannelOption.SO_REUSEADDR, Boolean.TRUE)
                .childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel ch) throws Exception {
                        // FIXME: should we use getTimeout()?
                        int idleTimeout = UrlUtils.getIdleTimeout(getUrl());
                        NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(), NettyServer.this);
                        ch.pipeline()//.addLast("logging",new LoggingHandler(LogLevel.INFO))//for debug
                                .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();
    }

六 调用序列

通过一张时序图来回顾调用链路:

有一点需要注意(From dubbo官网):

Remoting 实现是 Dubbo 协议的实现,如果你选择 RMI 协议,整个 Remoting 都不会用上,Remoting 内部再划为 Transport 传输层和 Exchange 信息交换层,Transport 层只负责单向消息传输,是对 Mina, Netty, Grizzly 的抽象,它也可以扩展 UDP 传输,而 Exchange 层是在传输层之上封装了 Request-Response 语义。

相关文章
|
6月前
|
Dubbo Cloud Native 网络协议
【Dubbo3技术专题】「服务架构体系」第一章之Dubbo3新特性要点之RPC协议分析介绍
【Dubbo3技术专题】「服务架构体系」第一章之Dubbo3新特性要点之RPC协议分析介绍
94 1
|
6月前
|
消息中间件 存储 负载均衡
Kafka【付诸实践 01】生产者发送消息的过程描述及设计+创建生产者并发送消息(同步、异步)+自定义分区器+自定义序列化器+生产者其他属性说明(实例源码粘贴可用)【一篇学会使用Kafka生产者】
【2月更文挑战第21天】Kafka【付诸实践 01】生产者发送消息的过程描述及设计+创建生产者并发送消息(同步、异步)+自定义分区器+自定义序列化器+生产者其他属性说明(实例源码粘贴可用)【一篇学会使用Kafka生产者】
484 4
|
1月前
|
Dubbo IDE Java
dubbo学习二:下载Dubbo-Admin管理控制台,并分析在2.6.1及2.6.1以后版本的变化
这篇文章是关于如何下载和部署Dubbo管理控制台(dubbo-admin)的教程,并分析了2.6.1版本及以后版本的变化。
41 0
dubbo学习二:下载Dubbo-Admin管理控制台,并分析在2.6.1及2.6.1以后版本的变化
|
3月前
|
JSON Dubbo Java
【Dubbo协议指南】揭秘高性能服务通信,选择最佳协议的终极攻略!
【8月更文挑战第24天】在分布式服务架构中,Apache Dubbo作为一款高性能的Java RPC框架,支持多种通信协议,包括Dubbo协议、HTTP协议及Hessian协议等。Dubbo协议是默认选择,采用NIO异步通讯,适用于高要求的内部服务通信。HTTP协议通用性强,利于跨语言调用;Hessian协议则在数据传输效率上有优势。选择合适协议需综合考虑性能需求、序列化方式、网络环境及安全性等因素。通过合理配置,可实现服务性能最优化及系统可靠性提升。
57 3
|
3月前
|
C# 开发者 Windows
勇敢迈出第一步:手把手教你如何在WPF开源项目中贡献你的第一行代码,从选择项目到提交PR的全过程解析与实战技巧分享
【8月更文挑战第31天】本文指导您如何在Windows Presentation Foundation(WPF)相关的开源项目中贡献代码。无论您是初学者还是有经验的开发者,参与这类项目都能加深对WPF框架的理解并拓展职业履历。文章推荐了一些适合入门的项目如MvvmLight和MahApps.Metro,并详细介绍了从选择项目、设置开发环境到提交代码的全过程。通过具体示例,如添加按钮点击事件处理程序,帮助您迈出第一步。此外,还强调了提交Pull Request时保持专业沟通的重要性。参与开源不仅能提升技能,还能促进社区交流。
44 0
|
3月前
|
XML 存储 JSON
(十二)探索高性能通信与RPC框架基石:Json、ProtoBuf、Hessian序列化详解
如今这个分布式风靡的时代,网络通信技术,是每位技术人员必须掌握的技能,因为无论是哪种分布式技术,都离不开心跳、选举、节点感知、数据同步……等机制,而究其根本,这些技术的本质都是网络间的数据交互。正因如此,想要构建一个高性能的分布式组件/系统,不得不思考一个问题:怎么才能让数据传输的速度更快?
|
6月前
|
XML JSON 分布式计算
如何选择序列化协议:关键因素与场景分析
如何选择序列化协议:关键因素与场景分析
63 0
|
6月前
|
安全 数据处理 开发者
Boost序列化与Protobuf比较:深入分析 (Boost Serialization vs. Protobuf: An In-depth Comparison)...
Boost序列化与Protobuf比较:深入分析 (Boost Serialization vs. Protobuf: An In-depth Comparison)...
271 1
|
6月前
|
Java fastjson 数据安全/隐私保护
【Dubbo3技术专题】「云原生微服务开发实战」 一同探索和分析研究RPC服务的底层原理和实现
【Dubbo3技术专题】「云原生微服务开发实战」 一同探索和分析研究RPC服务的底层原理和实现
153 0
|
6月前
|
存储 JSON 网络协议
【计算机网络】序列化,反序列化和初识协议
【计算机网络】序列化,反序列化和初识协议