【Dubbo】编码解码

编解码含义

  • 编码(Encode)
    编码它将对象序列化为字节数组,用于网络传输、数据持久化或者其他用途。
  • 解码(Decode)
    解码把从网络、磁盘等读取的字节数组还原成原始对象(通常是原始对象的拷贝),以方便后续的业务逻辑操作。

粘包 拆包

tcp 为什么会出现粘包 拆包的问题?

TCP报文有个比较大的特点,就是它传输的时候,会先把应用层的数据项拆开成字节,然后按照自己的传输需要,选择合适数量的字节进行传输。什么叫"自己的传输需要"?首先TCP包有最大长度限制,那么太大的数据项肯定是要拆开的。其次因为TCP以及下层协议会附加一些协议头信息,如果数据项太小,那么可能报文大部分都是没有价值的头信息,这样传输是很不划算的。
TCP/IP协议与buffer

粘包拆包

解决方案
  1. 消息的定长,例如定1000个字节
  2. 就是在包尾增加回车或空格等特殊字符作为切割,典型的FTP协议
  3. 将消息分为消息头消息体,并记录数据长度。例如 dubbo
  • netty自带的编解码器
    public ChannelPipeline getPipeline() throws Exception {
        ChannelPipeline channelPipeline = Channels.pipeline();
        channelPipeline.addLast("decoder", new StringDecoder());
        channelPipeline.addLast("encoder", new StringEncoder());
        channelPipeline.addLast("handler", new ServerLogicHandler());
        return channelPipeline;
    }
  • dubbo自己的编解码协议
    出现粘包拆包的核心是需要将TCP层的字节数据包切分业务层的Request、Response数据包。所以dubbo使用了魔法数和数据长度作为分割符,将Buffer中的数据隔离出来了生成Request、Response这样的一个个业务层的对象。


    image.png

编解码核心

  • 编解码流程


    编解码流程
  • 注册编解码器

#com.alibaba.dubbo.remoting.transport.netty.NettyServer#doOpen
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
            public ChannelPipeline getPipeline() {
                NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec() ,getUrl(), NettyServer.this);
                ChannelPipeline pipeline = Channels.pipeline();
                /*int idleTimeout = getIdleTimeout();
                if (idleTimeout > 10000) {
                    pipeline.addLast("timer", new IdleStateHandler(timer, idleTimeout / 1000, 0, 0));
                }*/
                //解码
                pipeline.addLast("decoder", adapter.getDecoder());
                //编码
                pipeline.addLast("encoder", adapter.getEncoder());
                //逻辑处理类
                pipeline.addLast("handler", nettyHandler);
                return pipeline;
            }
        });
#com.alibaba.dubbo.remoting.exchange.codec.ExchangeCodec#encode
    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);
        }
    }

    public Object decode(Channel channel, ChannelBuffer buffer) throws IOException {
        int readable = buffer.readableBytes();
        //读取header,可能可读数据没有完全传输过来
        byte[] header = new byte[Math.min(readable, HEADER_LENGTH)];
        buffer.readBytes(header);
        return decode(channel, buffer, readable, header);
    }

Request编码

----------1------consumer请求编码----------------------
-->NettyCodecAdapter.InternalEncoder.encode
  -->DubboCountCodec.encode
    -->ExchangeCodec.encode
      -->ExchangeCodec.encodeRequest
        -->DubboCodec.encodeRequestData

consumer发起Request是在调用代理对象的方法后会触发DubboInvoker.doInvoke使用netty发送Invocktion数据。编码器将一些Request标志位按照编码规则处理成字节数组,将Invocktion序列化并计算出数据长度存放在data部分,将处理好的字节数组都写入可扩容的Buffer完成编码

#com.alibaba.dubbo.rpc.protocol.dubbo.DubboInvoker#doInvoke
RpcInvocation inv = (RpcInvocation) invocation;
ResponseFuture future = currentClient.request(inv, timeout) ;
                //取得结果的future放在上下文中
RpcContext.getContext().setFuture(new FutureAdapter<Object>(future));

#com.alibaba.dubbo.remoting.exchange.support.header.HeaderExchangeChannel#request(java.lang.Object, int)
 public ResponseFuture request(Object request, int timeout) throws RemotingException {
        if (closed) {
            throw new RemotingException(this.getLocalAddress(), null, "Failed to send request " + request + ", cause: The channel " + this + " is closed!");
        }
        // create request.
        Request req = new Request();
        req.setVersion("2.0.0");
        req.setTwoWay(true);
        req.setData(request);
        DefaultFuture future = new DefaultFuture(channel, req, timeout);
        try{
            //AbstractPeer
            channel.send(req);
        }catch (RemotingException e) {
            future.cancel();
            throw e;
        }
        return future;
    }
  • 消息头
    dubbo的消息头是一个定长的 16个字节。
    第1-2个字节:是一个魔数数字:就是一个固定的数字
    第3个字节:序列化器的id、是否是事件、是双向(有去有回) 或单向(有去无回)的标记
    第4个字节:status??? (request 没有第四个字节)
    第5-12个字节:请求id:long型8个字节。异步变同步的全局唯一ID,用来做consumer和provider的来回通信标记。
    第13-16个字节:消息体的长度,也就是消息头+请求数据的长度。
  • 消息体
    data实际上是一个Invocation,标识了需要调用的方法以及参数。而且使用的buffer是可以动态扩展的
#com.alibaba.dubbo.rpc.protocol.dubbo.DubboInvoker#doInvoke
RpcInvocation inv = (RpcInvocation) invocation;
ResponseFuture future = currentClient.request(inv, timeout) ;
  • 编码流程
#com.alibaba.dubbo.remoting.exchange.codec.ExchangeCodec#encodeRequest
 protected void encodeRequest(Channel channel, ChannelBuffer buffer, Request req) throws IOException {
        Serialization serialization = getSerialization(channel);
        // header.16字节,128位
        byte[] header = new byte[HEADER_LENGTH];
        // set magic number.16位魔法数0xdabb
        //short2bytes 将16位数存到两个byte中
        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.
        //long 64位8字节,从第5个字节
        Bytes.long2bytes(req.getId(), header, 4);
        //buffer这里写入buffer并不是直接写入到netty,是一个新建的buffer
        //写完需要netty发送
        // 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 {
            //编码写入请求数据DubboCodec
            encodeRequestData(channel, out, req.getData());
        }
        out.flushBuffer();
        bos.flush();
        bos.close();
        //获取写入数据的长度
        int len = bos.writtenBytes();
        checkPayload(channel, len);
        Bytes.int2bytes(len, header, 12);
        // write
        buffer.writerIndex(savedWriteIndex);
        //写header
        buffer.writeBytes(header); // write header.
        //设置当前写入位置
        buffer.writerIndex(savedWriteIndex + HEADER_LENGTH + len);
    }

Request解码

provider接收到consumer的Request数据之后会进入Request解码流程

----------2------provider 请求解码----------------------
--NettyCodecAdapter.InternalDecoder.messageReceived
  -->DubboCountCodec.decode
    -->ExchangeCodec.decode
      -->ExchangeCodec.decodeBody

解码流程就是接收到数据之后,先读取header长度的字节。判断开头两字节是不是magicNumber,如果不是则遍历数据的每一位来判断魔法数,交给父类来解码;是则校验header长度,数据长度,根据header的属性构造Response、Request等对象,然后读取序列化器来解析data默认使用Hessian进行序列化和反序列化

#com.alibaba.dubbo.remoting.exchange.codec.ExchangeCodec#decode(com.alibaba.dubbo.remoting.Channel, com.alibaba.dubbo.remoting.buffer.ChannelBuffer)
public Object decode(Channel channel, ChannelBuffer buffer) throws IOException {
        int readable = buffer.readableBytes();
        //读取header
        byte[] header = new byte[Math.min(readable, HEADER_LENGTH)];
        buffer.readBytes(header);
        return decode(channel, buffer, readable, header);
    }
    
    protected Object decode(Channel channel, ChannelBuffer buffer, int readable, byte[] header) throws IOException {
        // check magic number.
        //每次解码先判断是否是magic——number
        if (readable > 0 && header[0] != MAGIC_HIGH 
                || readable > 1 && header[1] != MAGIC_LOW) {
            //数据的起始位置不是header
            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) {
                    //检查到magic
                    buffer.readerIndex(buffer.readerIndex() - header.length + i);
                    header = Bytes.copyOf(header, i);
                    break;
                }
            }
            return super.decode(channel, buffer, readable, header);
        }
        // check length.数据长度不够继续等待读取数据
        if (readable < HEADER_LENGTH) {
            return DecodeResult.NEED_MORE_INPUT;
        }

        // get data length.消息体长度
        int len = Bytes.bytes2int(header, 12);
        checkPayload(channel, len);

        int tt = len + HEADER_LENGTH;
        if( readable < tt ) {
            return DecodeResult.NEED_MORE_INPUT;
        }

        // limit input stream.
        ChannelBufferInputStream is = new ChannelBufferInputStream(buffer, len);
        try {
            //body解析
            return decodeBody(channel, is, header);
        } finally {
            if (is.available() > 0) {
                try {
                    if (logger.isWarnEnabled()) {
                        logger.warn("Skip input stream " + is.available());
                    }
                    StreamUtils.skipUnusedStream(is);
                } catch (IOException e) {
                    logger.warn(e.getMessage(), e);
                }
            }
        }

provider响应结果编码

provider调用完本地的invoker后需要返回Response,返回数据也是需要编码

#com.alibaba.dubbo.remoting.exchange.support.header.HeaderExchangeHandler#received
    public void received(Channel channel, Object message) throws RemotingException {
    if (message instanceof Request) {
                // handle request.
                Request request = (Request) message;
                if (request.isEvent()) {
                    handlerEvent(channel, request);
                } else {
                    if (request.isTwoWay()) {
                        //接收数据网络通信接收处理
                        Response response = handleRequest(exchangeChannel, request);
                        channel.send(response);
                    } else {
                        handler.received(exchangeChannel, request.getData());
                    }
                }
            } else if (message instanceof Response) {
                handleResponse(channel, (Response) message);
            }
}
----------3------provider响应结果编码----------------------
-->NettyCodecAdapter.InternalEncoder.encode
  -->DubboCountCodec.encode
    -->ExchangeCodec.encode
      -->ExchangeCodec.encodeResponse
        -->DubboCodec.encodeResponseData//先写入一个字节 这个字节可能是RESPONSE_NULL_VALUE  RESPONSE_VALUE  RESPONSE_WITH_EXCEPTION

编码协议都是一致的,和consumer请求编码有一小点不同
第3个字节:序列号组件类型,它用于和客户端约定的序列号编码号
第4个字节:它是response的结果响应码 例如 OK=20

consumer请求结果解码

----------4------consumer响应结果解码----------------------
--NettyCodecAdapter.InternalDecoder.messageReceived
  -->DubboCountCodec.decode
    -->ExchangeCodec.decode
      -->DubboCodec.decodeBody
        -->DecodeableRpcResult.decode//根据RESPONSE_NULL_VALUE  RESPONSE_VALUE  RESPONSE_WITH_EXCEPTION进行响应的处理

解码完成后是一个Response

解码之后

nettyServer的pipeline中注册三个处理器,前两个用来数据的编解码。在收到的数据已经完成解码工作成为了Request、Response之后就该进入nettyHandler的处理了

pipeline.addLast("decoder", adapter.getDecoder());
                //编码
pipeline.addLast("encoder", adapter.getEncoder());
                //逻辑处理类
pipeline.addLast("handler", nettyHandler);
#com.alibaba.dubbo.remoting.transport.netty.NettyHandler#messageReceived
    @Override
    public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
        //数据接收
        NettyChannel channel = NettyChannel.getOrAddChannel(ctx.getChannel(), url, handler);
        try {
            handler.received(channel, e.getMessage());
        } finally {
            NettyChannel.removeChannelIfDisconnected(ctx.getChannel());
        }
    }

DecodeHandler区分数据是Request还是Response,Request中的数据解析为DecodeableRpcInvocation,Response中的数据解析为DecodeableRpcResult

#com.alibaba.dubbo.remoting.transport.DecodeHandler#received
public void received(Channel channel, Object message) throws RemotingException {
        if (message instanceof Decodeable) {
            decode(message);
        }

        if (message instanceof Request) {
            //如果是consumer请求则解析data为invocation
            decode(((Request)message).getData());
        }

        if (message instanceof Response) {
            //解析请求返回的response数据
            decode( ((Response)message).getResult());
        }

        handler.received(channel, message);
    }

如果是Request则寻找本地的invoker并调用并发回返回值Response,如果是Response则处理处理之前保存的future唤醒等待的线程

#com.alibaba.dubbo.remoting.exchange.support.header.HeaderExchangeHandler#received
public void received(Channel channel, Object message) throws RemotingException {
        channel.setAttribute(KEY_READ_TIMESTAMP, System.currentTimeMillis());
        ExchangeChannel exchangeChannel = HeaderExchangeChannel.getOrAddChannel(channel);
        try {
            if (message instanceof Request) {
                // handle request.
                Request request = (Request) message;
                if (request.isEvent()) {
                    handlerEvent(channel, request);
                } else {
                    if (request.isTwoWay()) {
                        //有返回值的请求
                        Response response = handleRequest(exchangeChannel, request);
                        channel.send(response);
                    } else {
                        handler.received(exchangeChannel, request.getData());
                    }
                }
            } else if (message instanceof Response) {
                handleResponse(channel, (Response) message);
            } else if (message instanceof String) {
                if (isClientSide(channel)) {
                    Exception e = new Exception("Dubbo client can not supported string message: " + message + " in channel: " + channel + ", url: " + channel.getUrl());
                    logger.error(e.getMessage(), e);
                } else {
                    String echo = handler.telnet(channel, (String) message);
                    if (echo != null && echo.length() > 0) {
                        channel.send(echo);
                    }
                }
            } else {
                handler.received(exchangeChannel, message);
            }
        } finally {
            HeaderExchangeChannel.removeChannelIfDisconnected(channel);
        }
    }
static void handleResponse(Channel channel, Response response) throws RemotingException {
        if (response != null && !response.isHeartbeat()) {
            DefaultFuture.received(channel, response);
        }
    }

解决粘包拆包问题

出现拆包可能没有魔法数或者数据长度不够,那就跳出循环等待下一次数据;如果出现粘包会有多个魔法数每次解码一个循环多次解码。

#com.alibaba.dubbo.rpc.protocol.dubbo.DubboCountCodec
    public Object decode(Channel channel, ChannelBuffer buffer) throws IOException {
        int save = buffer.readerIndex();
        MultiMessage result = MultiMessage.create();
        do {
            Object obj = codec.decode(channel, buffer);
            //出现拆包跳出循环等待下一次数据
            if (Codec2.DecodeResult.NEED_MORE_INPUT == obj) {
                buffer.readerIndex(save);
                break;
            } else {
                //出现粘包会多次解析
                result.addMessage(obj);
                logMessageLength(obj, buffer.readerIndex() - save);
                save = buffer.readerIndex();
            }
        } while (true);
        if (result.isEmpty()) {
            return Codec2.DecodeResult.NEED_MORE_INPUT;
        }
        if (result.size() == 1) {
            return result.get(0);
        }
        return result;
    }

总结

出现粘包拆包的核心是需要将TCP层的字节数据包切分业务层的Request、Response数据包。所以dubbo使用了魔法数和数据长度作为分割符,将Buffer中的数据隔离出来了生成Request、Response这样的一个个业务层的对象。

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