13、Dubbo源码系列-Dubbo网络协议与编解码

357 阅读7分钟

“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第6篇文章,点击查看活动详情

海上月是天上月,眼前人是心上人。中秋假期大家过得怎么样,我是拍了剩下的婚纱照,看了看Netty相关知识,又去和木工对了下装修细节,余下精力,想了想Dubbo还剩下协议这部分还没和大家分享,今天就带着大家一起看下Dubbo网络协议设计与编解码实现。

一、概述

众所周知,在TCP协议中,每层协议都有自己的协议报文格式,OSI七层网络协议如下图:

image.png (图片资源来源于网络)

Dubbo作为基于TCP协议的应用,其协议设计也是参考了TCP协议,由header和body两部分组成,其中header格式如下图: image.png 如上图所示,header一共包含了16字节数据:

  • 前两个字节为魔数,类似Java字节码的0xCAFEBABE,只不过这里分别是0xda与0xbb。
  • 第三个字节为请求类型与序列化id。
  • 第四个字节只有在响应报文里才涉及,为响应的status,成功为20。
  • 后面八个字节为请求ID。
  • 最后的四字节是body的大小。

二、温故知新

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

    }

前文讲解Dubbo服务端处理请求的过程中曾提到过,服务端通过NettyServer的doOpen方法启动Netty服务,流程如下:

  • 启动了一个boss线程,多个worker线程(优先获取URL里的配置,默认为(Runtime.getRuntime().availableProcessors() + 1,最大值为32)。
  • 指定用来编解码的decoder与encoder。
  • 启动心跳检测IdleStateHandler。
  • 指定用来处理Netty请求的handler,nettyServerHandler。
  • 绑定端口。

以上和本文相关的则是编解码器的实现,下面就让我们看看Dubboo编解码器是如何工作的

三、编解码器详解

查看adapter.getEncoder()实现如下:

    private final ChannelHandler encoder = new InternalEncoder();
    public ChannelHandler getEncoder() {
        return encoder;
    }

可以看到,最终获取到的是InternalEncoder实例,查看InternalEncoder实现如下:

    private final Codec2 codec;
    
    private class InternalEncoder extends MessageToByteEncoder {
        @Override
        protected void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) throws Exception {
            ... ... 
            codec.encode(channel, buffer, msg);
        }
    }

这里最终是通过codec.encode方法进行的编码,查看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
    }
}

这里的Codec2也是SPI扩展点,默认实现为DubboCodec,查看其实现如下:

public class DubboCodec extends ExchangeCodec {
    ... ...
}

可以看到DubboCodec其实是ExchangeCodec的子类,ExchangeCodec类的encode和decode方法则是今天分析的重点。

3.1. ExchangeCodec-encode

    @Override
    public void encode(Channel channel, ChannelBuffer buffer, Object msg) throws IOException {
        if (msg instanceof Request) {
            encodeRequest(channel, buffer, (Request) msg);
        } else if (msg instanceof Response) {
            encodeResponse(channel, buffer, (Response) msg);
        } else {
            super.encode(channel, buffer, msg);
        }
    }

核心逻辑如上,分别为对请求信息进行编码,对响应信息进行编码,对其他信息进行编码。

  • encodeRequest实现
    protected void encodeRequest(Channel channel, ChannelBuffer buffer, Request req) throws IOException {
        // 获取序列化方式,Dubbo默认序列化方式为hession
        Serialization serialization = getSerialization(channel);
        // 创建Dubbo协议头数组,HEADER_LENGTH长度为16
        byte[] header = new byte[HEADER_LENGTH];
        // 把魔数0xdabb写入协议头
        Bytes.short2bytes(MAGIC, header);

        // 设置请求类型与序列化类型
        header[2] = (byte) (FLAG_REQUEST | serialization.getContentTypeId());

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

        // 设置请求id
        Bytes.long2bytes(req.getId(), header, 4);

        // 使用获取到的序列化方式对数据部分进行序列化
        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);

        // 将协议头写入缓存
        buffer.writerIndex(savedWriteIndex);
        buffer.writeBytes(header); // write header.
        buffer.writerIndex(savedWriteIndex + HEADER_LENGTH + len);
    }
  • encodeResponse encodeResponse实现与encodeRequest不同之点在于协议头的第四个字节要写入响应类型,其他则几乎一致。
    protected void encodeResponse(Channel channel, ChannelBuffer buffer, Response res) throws IOException {
        int savedWriteIndex = buffer.writerIndex();
        try {
            ... ...
            // 设置响应的状态码,正常响应status=20
            byte status = res.getStatus();
            header[3] = status;

            // 对响应的消息体进行序列化
            ObjectOutput out = serialization.serialize(channel.getUrl(), bos);
            // encode response data or error message.
            if (status == Response.OK) {
                if (res.isHeartbeat()) {
                    encodeEventData(channel, out, res.getResult());
                } else {
                    encodeResponseData(channel, out, res.getResult(), res.getVersion());
                }
            } else {
                out.writeUTF(res.getErrorMessage());
            }
            ... ... 
        } catch (Throwable t) {

        }
    }

3.2. ExchangeCodec-decode

查看adapter.getDecoder()实现,获取到结果为InternalDecoder实例,查看其实现如下:

    private class InternalDecoder extends SimpleChannelUpstreamHandler {
        private org.apache.dubbo.remoting.buffer.ChannelBuffer buffer =
                org.apache.dubbo.remoting.buffer.ChannelBuffers.EMPTY_BUFFER;

        @Override
        public void messageReceived(ChannelHandlerContext ctx, MessageEvent event) 
            NettyChannel channel = NettyChannel.getOrAddChannel(ctx.getChannel(), url, handler);
            Object msg;
            int saveReaderIndex;
            ... ...
            try {
                // decode object.
                do {
                    saveReaderIndex = message.readerIndex();
                    try {
                        // 解码操作
                        msg = codec.decode(channel, message);
                    } catch (IOException e) {
                        buffer = org.apache.dubbo.remoting.buffer.ChannelBuffers.EMPTY_BUFFER;
                        throw e;
                    }
                    // 碰到了半包问题,重置缓存下标
                    // 这里要注意,当检测到半包问题后,指针已经偏移了,因此要重置
                    if (msg == Codec2.DecodeResult.NEED_MORE_INPUT) {
                        message.readerIndex(saveReaderIndex);
                        break;
                    } else {
                        // 读到了一个完整消息,将消息继续传递,交给下一个handler进行处理
                        if (msg != null) {
                            Channels.fireMessageReceived(ctx, msg, event.getRemoteAddress());
                        }
                    }
                } while (message.readable());
            } finally {

            }
        }

    }

InternalDecoder继承了SimpleChannelUpstreamHandler,重写了messageReceived方法,顾名思义,NettyServer收到消息流时,触发改方法,核心步骤如下:

  • 首先调用codec.decode解码获取到对应的消息,这里的codec即为ExchangeCodec。

  • 解码的时候,涉及到了TCP半包问题的处理(不了解什么是半包问题可自行百度tcp粘包和拆包)。

  • 最后将完整的消息,继续传递给下一个Handler,最终由nettyServerHandler进行消息处理。

  • decode源码

    @Override
    public Object decode(Channel channel, ChannelBuffer buffer) throws IOException {
        // 协议头解析
        int readable = buffer.readableBytes();
        byte[] header = new byte[Math.min(readable, HEADER_LENGTH)];
        buffer.readBytes(header);
        // body解析
        return decode(channel, buffer, readable, header);
    }

    @Override
    protected Object decode(Channel channel, ChannelBuffer buffer, int readable, byte[] header) throws IOException {
        // 魔数校验
        if (readable > 0 && header[0] != MAGIC_HIGH
                || readable > 1 && header[1] != MAGIC_LOW) {
            int length = header.length;
            if (header.length < readable) {
                header = Bytes.copyOf(header, readable);
                buffer.readBytes(header, length, readable - length);
            }
            for (int i = 1; i < header.length - 1; i++) {
                if (header[i] == MAGIC_HIGH && header[i + 1] == MAGIC_LOW) {
                    buffer.readerIndex(buffer.readerIndex() - header.length + i);
                    header = Bytes.copyOf(header, i);
                    break;
                }
            }
            return super.decode(channel, buffer, readable, header);
        }
        // 是否读取到了一个完整的协议头,如果不是,则发生了半包问题
        if (readable < HEADER_LENGTH) {
            return DecodeResult.NEED_MORE_INPUT;
        }

        // 协议头最后四个字节获取body的大小
        int len = Bytes.bytes2int(header, 12);
        checkPayload(channel, len);

        int tt = len + HEADER_LENGTH;
        // 判断是否发生半包问题
        if (readable < tt) {
            return DecodeResult.NEED_MORE_INPUT;
        }

        // 解析协议数据部分
        ChannelBufferInputStream is = new ChannelBufferInputStream(buffer, len);
        
        try {
            // 进行body的解析
            return decodeBody(channel, is, header);
        } finally {
            ... ... 
        }
    }

  • decodeBody实现
    protected Object decodeBody(Channel channel, InputStream is, byte[] header) throws IOException {
        // 序列化类型
        byte flag = header[2], proto = (byte) (flag & SERIALIZATION_MASK);
        // 获取requestId
        long id = Bytes.bytes2long(header, 4);
        // 响应处理
        if ((flag & FLAG_REQUEST) == 0) {
            // decode response.
            Response res = new Response(id);
            if ((flag & FLAG_EVENT) != 0) {
                res.setEvent(true);
            }
            // 响应体的状态码
            byte status = header[3];
            res.setStatus(status);
            // 使用和客户端一致的序列化方式进行解码
            try {
                ObjectInput in = CodecSupport.deserialize(channel.getUrl(), is, proto);
                if (status == Response.OK) {
                    Object data;
                    // 心跳数据
                    if (res.isHeartbeat()) {
                        data = decodeHeartbeatData(channel, in);
                    } else if (res.isEvent()) {
                        // 事件
                        data = decodeEventData(channel, in);
                    } else {
                        // 响应信息
                        data = decodeResponseData(channel, in, getRequestData(id));
                    }
                    res.setResult(data);
                } else {
                    res.setErrorMessage(in.readUTF());
                }
            } catch (Throwable t) {
                res.setStatus(Response.CLIENT_ERROR);
                res.setErrorMessage(StringUtils.toString(t));
            }
            return res;
        } else {
            // 请求的处理
            Request req = new Request(id);
            req.setVersion(Version.getProtocolVersion());
            req.setTwoWay((flag & FLAG_TWOWAY) != 0);
            if ((flag & FLAG_EVENT) != 0) {
                req.setEvent(true);
            }
            try {
                ObjectInput in = CodecSupport.deserialize(channel.getUrl(), is, proto);
                Object data;
                if (req.isHeartbeat()) {
                    data = decodeHeartbeatData(channel, in);
                } else if (req.isEvent()) {
                    data = decodeEventData(channel, in);
                } else {
                    data = decodeRequestData(channel, in);
                }
                req.setData(data);
            } catch (Throwable t) {
                // bad request
                req.setBroken(true);
                req.setData(t);
            }
            return req;
        }
    }

四、小节

本文主要为大家介绍了下Dubbo网络协议数据格式,以及Netty编解码的实现,其中提到了Dubbo处理TCP半包问题是通过自定义header + body的方式解决。需要注意的是,当发现半包问题后,此时message的读取指针已经后移,因此需要把读指针重置。

说些题外话,Dubbo的相关设计,在我们日常的开发中,还是很高的参考意义。以笔者正在做的一个Netty应用为例,虽然网络协议最终选型未采取Dubbo的协议设计,不过对于技术方案的完整性,还是提供了不错的参考。包括我们的Netty服务的Hanlder的设计、心跳的处理、路由算法的实现,其实都从Dubbo框架中获取到了很多灵感,正所谓万变不离其宗,吃透一个成熟的开源架构设计,对我们日常的工作还是有很大的帮助的。