Dubbo远程通信源码解析

前言

前面小编两篇文章重点讲了dubbo协议以及registry协议,协议主要作用就是服务暴露引用,当我们的服务以及暴露和引用并且放到注册中心上去了,那么接下来就要发起远程的通讯。今天小编带来的就是远程通讯。进入正题

远程通信概念

远程通信是客户端通过网络传输至服务端,并接收服务端的结果。其需要解决问题包括、连接的建立、数据流的编解码、IO线程的协议等。
假如咱们需要实现一个客户端与服务端的远程通信,那具体应该怎么做,有哪些子方案呢,小编这边罗列了一些方案,仅供参考。

Dubbo远程通信源码解析_第1张图片

  • IO模型:涉及到网络通信,必然少不了IO。BIO大家比较熟知即阻塞IO,NIO是同步非阻塞IO,AIO是异步的非阻塞IO。
  • 连接方式:短连接如HTTP协议,长连接如TCP协议,长连接的话需要保活机制,如心跳保活,还有断开重连机制。
  • 线程调度:这里前面讲dubbo时就有IO线程业务线程以及线程池调度。
  • 数据交换:包含了报文的设计,编解码,粘包拆包以及序列化。

那dubbo是如何解决远程通讯的?
Dubbo使用netty框架解决了大部分工作(IO模型,连接方式,线程调度中的IO线程以及数据交换中的粘包拆包),业务以及线程池调度、报文设计、编解码、序列化是Dubbo自己实现的。

Dubbo通信过程图:
Dubbo远程通信源码解析_第2张图片

  • 这边通过NettyTransporter这个数据传输组件构建出NettyClient和NettyServer.
  • NettyServer绑定相关端口号开启服务
  • NettyClient则是建立连接,调用send方法进行数据通信(具体由Channel来实现)。注意这里是异步操作。
  • 当Channel管道传输过来数据时,NettyServer使用ChannelHandler来接受数据,做一些操作。同时也可以向管道中发送数据。
  • NettyClient同样用ChannelHandler接受服务端写过来的数据。

小编使用代码给大家做个示例来解释上面的过程图。

public class NettyTransporterTest {
    private final static String URL_TEXT = "dubbo://127.0.0.1:20880";

    @Test
    public void openServer() throws Exception {
        NettyServer nettyServer = new NettyServer(URL.valueOf(URL_TEXT), new ChannelHandlerAdapter() {
            @Override
            public void received(Channel channel, Object message) throws RemotingException {
                System.out.println("接受到数据:" + message);
                if(Objects.nonNull(message)){
                    channel.send("我接受到你的数据了");
                }
            }
        });
        System.in.read();
    }

    @Test
    public void openClient() throws Exception {
        NettyClient nettyClient = new NettyClient(URL.valueOf(URL_TEXT), new ChannelHandlerAdapter() {
            @Override
            public void received(Channel channel, Object message) throws RemotingException {
                System.out.println("接受到数据:" + message);
            }
        });
        nettyClient.send("hello world");
        //因为发送消息是异步的会立马结束,这边不让他结束
        System.in.read();
    }
}

两边的测试结果

//NettyServer端
接受到数据:hello world
//NettyClient端
接受到数据:我接受到你的数据了

注意
这边传输的时候使用string类型没问题,但如果是java对象的时候则编解码。需要配置dubbo的编解码器。

基本了解完通信方案后紧接着小编带大家了解一下dubbo的线程协作模型。

线程之间的协作模型

之前小编在Dubbo调用及容错机制详解中讲到过一些线程,如调用线程,IO线程,业务线程。这边小编通过debug将上面示例代码的客户端与服务端的线程截图一一说明。
服务端:
Dubbo远程通信源码解析_第3张图片
客户端:(业务线程为缓存线程,默认60s)
Dubbo远程通信源码解析_第4张图片
线程协作过程
Dubbo远程通信源码解析_第5张图片

上半部分为客户端,下半部分为服务端

  1. 调用线程即nettyClient发起调用,发送消息,然后写入管道。client.send()方法
  2. 服务端使用Selector将数据从管道里面取出数据,提交到业务线程池
  3. 业务线程池进行业务的处理(服务端线程池默认是固定有限的),然后将处理完的数据写入管道
  4. 与服务端读取数据一样,之后交由客户端的业务线程池(客户端默认为缓存无限的),之后处理完结果返回结果。

虽然小编寥寥数语就差不多讲完了他的一个协作流程,但是里面细节还是很多的,尤其这边会涉及到netty的知识面,下面小编带大家稍作调试,希望小编能说清楚

线程协作源码调试

客户端

小编从调用端开始解析,只挑重点代码(看注释)
首先是new NettyClient(final URL url, final ChannelHandler handler),NettyClient的构造方法。重要的是doOpen和doConnect方法

doOpen方法主要做初始化工作,封装所需要的一系列处理器和参数,doConnect主要是netty的连接(这边主要是为netty工作小编略过了)

@Override
    protected void doOpen() throws Throwable {
    	//处理消息的处理器
        final NettyClientHandler nettyClientHandler = new NettyClientHandler(getUrl(), this);
        //都是netty相关内容 后面小编写netty文章的时候详细说明
        bootstrap = new Bootstrap();
        bootstrap.group(NIO_EVENT_LOOP_GROUP)
                .option(ChannelOption.SO_KEEPALIVE, true)
                .option(ChannelOption.TCP_NODELAY, true)
                .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
                //.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, getTimeout())
                .channel(socketChannelClass());

        bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, Math.max(3000, getConnectTimeout()));
        bootstrap.handler(new ChannelInitializer<SocketChannel>() {

            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                int heartbeatInterval = UrlUtils.getHeartbeat(getUrl());

                if (getUrl().getParameter(SSL_ENABLED_KEY, false)) {
                    ch.pipeline().addLast("negotiation", SslHandlerInitializer.sslClientHandler(getUrl(), nettyClientHandler));
                }

                NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(), NettyClient.this);
                //添加编码解码器,心跳,以及用来处理消息的handler
                ch.pipeline()//.addLast("logging",new LoggingHandler(LogLevel.INFO))//for debug
                        .addLast("decoder", adapter.getDecoder())
                        .addLast("encoder", adapter.getEncoder())
                        .addLast("client-idle-handler", new IdleStateHandler(heartbeatInterval, 0, 0, MILLISECONDS))
                        .addLast("handler", nettyClientHandler);

                String socksProxyHost = ConfigUtils.getProperty(SOCKS_PROXY_HOST);
                if(socksProxyHost != null) {
                    int socksProxyPort = Integer.parseInt(ConfigUtils.getProperty(SOCKS_PROXY_PORT, DEFAULT_SOCKS_PROXY_PORT));
                    Socks5ProxyHandler socks5ProxyHandler = new Socks5ProxyHandler(new InetSocketAddress(socksProxyHost, socksProxyPort));
                    ch.pipeline().addFirst(socks5ProxyHandler);
                }
            }
        });
    }

NettyClientHandler数据结构
Dubbo远程通信源码解析_第6张图片
接着是client调用send方法
最终调用的是org.apache.dubbo.remoting.transport.netty4.NettyChannel#send

public void send(Object message, boolean sent) throws RemotingException {
        // whether the channel is closed 主要判断管道是否被关闭
        super.send(message, sent);

        boolean success = true;
        int timeout = 0;
        try {
        	//最终调用是channel管道的writeAndFlush方法 这里会调用pipeline的writeAndFlush写到队列里面
        	//之后就交给服务端处理 
            ChannelFuture future = channel.writeAndFlush(message);
            if (sent) {
                // wait timeout ms
                timeout = getUrl().getPositiveParameter(TIMEOUT_KEY, DEFAULT_TIMEOUT);
                success = future.await(timeout);
            }
            Throwable cause = future.cause();
            if (cause != null) {
                throw cause;
            }
        } catch (Throwable e) {
            removeChannelIfDisconnected(channel);
            throw new RemotingException(this, "Failed to send message " + PayloadDropper.getRequestWithoutData(message) + " to " + getRemoteAddress() + ", cause: " + e.getMessage(), e);
        }
        if (!success) {
            throw new RemotingException(this, "Failed to send message " + PayloadDropper.getRequestWithoutData(message) + " to " + getRemoteAddress()
                    + "in timeout(" + timeout + "ms) limit");
        }
    }

上面写完并且发送完消息后,服务端处理,之后返回到客户端,中间一些步骤先不管,直接从客户端IO线程到达客户端的业务线程。那么中间是如何到达AllChannelHandler,那我们看一下调用的栈堆信息:
Dubbo远程通信源码解析_第7张图片

org.apache.dubbo.remoting.transport.dispatcher.all.AllChannelHandler#received

public void received(Channel channel, Object message) throws RemotingException {
		//返回一个缓存线程池中的一个
        ExecutorService executor = getPreferredExecutorService(message);
        try {
        	//缓存线程池提交任务然后就到业务线程池了,之后就交给我们业务处理器处理了,即示例代码中的打印数据那个。
            executor.execute(new ChannelEventRunnable(channel, handler, ChannelState.RECEIVED, message));
        } catch (Throwable t) {
        	if(message instanceof Request && t instanceof RejectedExecutionException){
                sendFeedback(channel, (Request) message, t);
                return;
        	}
            throw new ExecutionException(message, channel, getClass() + " error when process received event .", t);
        }
    }
    
public ExecutorService getPreferredExecutorService(Object msg) {
        if (msg instanceof Response) {
            Response response = (Response) msg;
            DefaultFuture responseFuture = DefaultFuture.getFuture(response.getId());
            // a typical scenario is the response returned after timeout, the timeout response may has completed the future
            if (responseFuture == null) {
                return getSharedExecutorService();
            } else {
                ExecutorService executor = responseFuture.getExecutor();
                if (executor == null || executor.isShutdown()) {
                    executor = getSharedExecutorService();
                }
                return executor;
            }
        } else {
            return getSharedExecutorService();
        }
    }
服务端

服务端其实很多跟客户端类似。但是服务端不是那么好调试第一是因为需要客户端发消息,而客户端和服务端的一些逻辑调用的类是在一起的,容易混淆,第二发消息的时候接受到断点调试直接是从服务端的IO线程跳到业务线程,只能通过堆栈信息来看。即同样在AllChannelHandler打断点调试。小编这边稍微说明一下,首先也是new NettyServer(URL url, ChannelHandler handler)这个构造方法。同样有doOpen方法

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();

        bootstrap.group(bossGroup, workerGroup)
                .channel(NettyEventLoopFactory.serverSocketChannelClass())
                .option(ChannelOption.SO_REUSEADDR, Boolean.TRUE)
                .childOption(ChannelOption.TCP_NODELAY, Boolean.TRUE)
                .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));
                        }
                        //同样与客户端一样,只不过处理业务逻辑换成了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();

    }

这边小编也贴出NettyServerHandler结构,与NettyClientHandler类似,小编不做赘述。
Dubbo远程通信源码解析_第8张图片
Dubbo远程通信源码解析_第9张图片
小结
就此dubbo是如何使用netty远程通信就完毕了,这边的话服务端或客户端读取的话,其实是一个死循环去管道里面读取数据。

上面服务端AllChannelHandler的堆栈信息中小编截到编解码的处理,那接下来讲一下编解码的机制以及他在哪个线程做了处理和发生的时机。

编解码机制

编码解码无非有四部分,即客户端发送request的时候进行编码,服务端接收到request则解码,服务端处理完业务返回response则需要编码,客户端拿到response进行解码。其编码过程如下图:
Dubbo远程通信源码解析_第10张图片

1、客户端Request编码

那编解码操作中哪个线程中执行的,大家可以启动一个dubbo服务,在客户端使用debug模式进行调试。具体编解码工具类为org.apache.dubbo.rpc.protocol.dubbo.DubboCodec,他主要是用来编码我们的请求报文体,他继承了ExchangeCodec类,父类的话获得序列化对象然后主要写了请求头。然后调用子类的方法进行报文体的编码。编码完成后写入流中。
这里编码request的是在IO线程里面操作的,如果大家打断点就会明白,线程名为NettyClientWorker。
具体源码如下(小编只贴一个客户端request编码)
org.apache.dubbo.remoting.exchange.code.ExchangeCodec

protected void encodeRequest(Channel channel, ChannelBuffer buffer, Request req) throws IOException {
		//获取Hessian2Serialization,序列化对象
        Serialization serialization = getSerialization(channel);
        // header.封装请求头
        byte[] header = new byte[HEADER_LENGTH];
        // set magic number. 设置魔数
        Bytes.short2bytes(MAGIC, header);

        // set request and serialization flag. 设置request 和 serialization 的标记
        header[2] = (byte) (FLAG_REQUEST | serialization.getContentTypeId());

        if (req.isTwoWay()) {
            header[2] |= FLAG_TWOWAY;
        }
        if (req.isEvent()) {
            header[2] |= FLAG_EVENT;
        }

        // set request id.
        Bytes.long2bytes(req.getId(), header, 4);

        // encode request data.
        int savedWriteIndex = buffer.writerIndex();
        buffer.writerIndex(savedWriteIndex + HEADER_LENGTH);
        ChannelBufferOutputStream bos = new ChannelBufferOutputStream(buffer);
        ObjectOutput out = serialization.serialize(channel.getUrl(), bos);
        if (req.isEvent()) {
            encodeEventData(channel, out, req.getData());
        } else {
        	//编码请求体
            encodeRequestData(channel, out, req.getData(), req.getVersion());
        }
        out.flushBuffer();
        if (out instanceof Cleanable) {
            ((Cleanable) out).cleanup();
        }
        bos.flush();
        bos.close();
        int len = bos.writtenBytes();
        checkPayload(channel, len);
        Bytes.int2bytes(len, header, 12);

        // write 写入流
        buffer.writerIndex(savedWriteIndex);
        buffer.writeBytes(header); // write header.
        buffer.writerIndex(savedWriteIndex + HEADER_LENGTH + len);
    }

encodeRequestData(channel, out, req.getData(), req.getVersion());这代码为org.apache.dubbo.rpc.protocol.dubbo.DubboCodec

@Override
    protected void encodeRequestData(Channel channel, ObjectOutput out, Object data, String version) throws IOException {
        RpcInvocation inv = (RpcInvocation) data;
		//写入版本号
        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());
    }

报文格式以及报文格式说明在Dubbo远程传输协议详解,有详细讲解,这边小编贴个图,大家了解一下。
Dubbo远程通信源码解析_第11张图片
解释完了报文头之后,接着我们看一下报文体,即Body Content,报文体分为request的和response的(感觉是一种赘述)。
Dubbo远程通信源码解析_第12张图片
其实大家看到源码的时候也就一目了然了。
客户端request编码流程图:
Dubbo远程通信源码解析_第13张图片

2、服务端Request解码

代码同样在DubboCodec#decode和ExchangeCodec#decodeBody中,这边小编就不贴源代码了。这边的话记得服务端debug,客户端正常启动来调试。先告诉大家一个结论,然后使用流程图说明。
在解码的时候读取header信息实际在IO线程中调试的话线程名为NettyServerWorker,但是在requestBody解码在业务线程中

Dubbo远程通信源码解析_第14张图片

3、服务端Response编码

这里编码response的是在IO线程里面操作的,如果大家打断点就会明白,线程名为NettyServerWorker。
Dubbo远程通信源码解析_第15张图片

4、客户端Response解码

Response解码在业务线程中操作
Dubbo远程通信源码解析_第16张图片

总结

今天远程通讯的知识点不好消化,讲到线程协作的知识点,用到了很多netty的知识,小伙伴得自行去学习一下,当然小编后续也会继续学习,有缘的话写一下netty的学习总结。编解码机制的话小编觉得总体还可以,只不过调试代码的时候有点累,所以这边不贴源代码了,需要自己调试。再接再厉加油!

你可能感兴趣的:(#,Dubbo篇,java,dubbo,网络通信)