我的架构梦:(二十九)Dubbo源码分析之网络通信原理剖析

Dubbo源码分析之网络通信原理剖析

    • 一、数据包结构详解
    • 二、数据协议ExchangeCodec详解
    • 三、处理粘包和拆包问题

这一篇我们主要来讲Dubbo在网络中如何进行通信的。由于请求都是基于TCP的,那么Dubbo中是如何处理粘包和拆包的问题。

dubbo协议采用固定长度的消息头(16字节)和不定长度的消息体来进行数据传输,消息头定义了底层
框架(netty)在IO线程处理时需要的信息,协议的报文格式如下:

一、数据包结构详解

1、协议详情

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

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

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

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

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

  • 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 (换行符) 
...

2、优点

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

3、可以改进的点

  • 类似于 http 请求,通过 header 就可以确定要访问的资源,而 Dubbo 需要涉及到用特定序列化 协议才可以将服务名、方法、方法签名解析出来,并且这些资源定位符是 string 类型或者 string 数组,很容易转成 bytes,因此可以组装到 header 中。类似于 http2 的 header 压缩,对于 rpc 调用的资源也可以协商出来一个int来标识,从而提升性能,如果在 header 上组装资源定位符的 话,该功能则更易实现。
  • 通过 req/res 是否是请求后,可以精细定制协议,去掉一些不需要的标识和添加一些特定的标识。 比如 twoWay 标识可以严格定制,去掉冗余标识。还有超时时间是作为 Dubbo 的
    attachment 进行传输的,理论上应该放到请求协议的header中,因为超时是网络请求中必不可 少的。提到 attachment ,通过实现可以看到 attachment 中有一些是跟协议 content 中已有 的字段是重复的,比如 path 和 version 等字段,这些会增大协议尺寸。
  • Dubbo 会将服务名
    com.alibaba.middleware.hsf.guide.api.param.ModifyOrderPriceParam ,转换为Lcom/alibaba/middleware/hsf/guide/api/param/ModifyOrderPriceParam;,理论上是不必
    要的,最后追加一个 ; 即可。
  • Dubbo 协议没有预留扩展字段,没法新增标识,扩展性不太好,比如新增 响应上下文 的功能,只 有改协议版本号的方式,但是这样要求客户端和服务端的版本都进行升级,对于分布式场景很不友 好。

二、数据协议ExchangeCodec详解

这里我们来看 ExchangeCodec 类,这个也是Dubbo在进行数据传输中的数据协议类。

1、我们先来看看他的常量定义。

// 消息头的长度
protected static final int HEADER_LENGTH = 16;
// 标示为0-15位
protected static final short MAGIC = -9541;
protected static final byte MAGIC_HIGH = Bytes.short2bytes((short)-9541)[0];
protected static final byte MAGIC_LOW = Bytes.short2bytes((short)-9541)[1];
// 消息头中的内容
protected static final byte FLAG_REQUEST = -128;
protected static final byte FLAG_TWOWAY = 64;
protected static final byte FLAG_EVENT = 32;
protected static final int SERIALIZATION_MASK = 31;

2、这个类中 encodedecode 分别用于将数据发送到 ByteBuffer 中,还有就是将其反向的转换为对象。encode中的Request就是我们之前所讲的Request对象。

public void encode(Channel channel, ChannelBuffer buffer, Object msg) throws IOException {
	// 处理请求对象
    if (msg instanceof Request) {
        this.encodeRequest(channel, buffer, (Request)msg);
    } else if (msg instanceof Response) {
    	// 处理响应
        this.encodeResponse(channel, buffer, (Response)msg);
    } else {
    	// 其他的交给上级处理,用于telnet模式
        super.encode(channel, buffer, msg);
    }

}

3、查看 encodeRequest 方法。这里也验证了我们之前所讲的header内容。

protected void encodeRequest(Channel channel, ChannelBuffer buffer, Request req) throws IOException {
	// 请求的序列化类型
    Serialization serialization = this.getSerialization(channel);
    // 写入header信息
    byte[] header = new byte[16];
    // 模数0-15位
    Bytes.short2bytes((short)-9541, header);
    // 标记为请求
    header[2] = (byte)(-128 | serialization.getContentTypeId());
    // 是否是单向还是双向的(异步)
    if (req.isTwoWay()) {
        header[2] = (byte)(header[2] | 64);
    }
	// 是否为事件(心跳)
    if (req.isEvent()) {
        header[2] = (byte)(header[2] | 32);
    }
	// 写入当前的请求ID
    Bytes.long2bytes(req.getId(), header, 4);
    // 保存当前写入的位置,将其写入的位置往后面偏移,保留出写入内容大小的位置,先进行写入body 内容
    int savedWriteIndex = buffer.writerIndex();
    buffer.writerIndex(savedWriteIndex + 16);
    ChannelBufferOutputStream bos = new ChannelBufferOutputStream(buffer);
    ObjectOutput out = serialization.serialize(channel.getUrl(), bos);
    // 按照数据内容的不同,来写入不同的内容
    if (req.isEvent()) {
        this.encodeEventData(channel, out, req.getData());
    } else {
        this.encodeRequestData(channel, out, req.getData(), req.getVersion());
    }

    out.flushBuffer();
    if (out instanceof Cleanable) {
        ((Cleanable)out).cleanup();
    }

    bos.flush();
    bos.close();
    // 记录body中写入的长度
    int len = bos.writtenBytes();
    checkPayload(channel, (long)len);
    // 将其写入到header中的位置中
    Bytes.int2bytes(len, header, 12);
    // 发送到buffer中
    buffer.writerIndex(savedWriteIndex);
    buffer.writeBytes(header);
    buffer.writerIndex(savedWriteIndex + 16 + len);
}

4、真正的 encodeRequestData 在子类 DubboCodec

protected void encodeRequestData(Channel channel, ObjectOutput out, Object data, String version) throws IOException {
    RpcInvocation inv = (RpcInvocation)data;
    // 写入版本
    out.writeUTF(version);
    // 接口全名称
    out.writeUTF(inv.getAttachment("path"));
    // 接口版本号
    out.writeUTF(inv.getAttachment("version"));
    // 写入方法名称
    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(CallbackServiceCodec.encodeInvocationArgument(channel, inv, i));
        }
    }
	// 写入所有的附加信息
    out.writeAttachments(inv.getObjectAttachments());
}

5、下面我们再来看看 encodeResponse 方法实现。一样的,这里可以看到和写入request相似。

protected void encodeResponse(Channel channel, ChannelBuffer buffer, Response res) throws IOException {
    int savedWriteIndex = buffer.writerIndex();

    try {
        Serialization serialization = this.getSerialization(channel);
        // 和之前的参数一致
        byte[] header = new byte[16];
        Bytes.short2bytes((short)-9541, header);
        header[2] = serialization.getContentTypeId();
        if (res.isHeartbeat()) {
            header[2] = (byte)(header[2] | 32);
        }
		// 写入状态码
        byte status = res.getStatus();
        header[3] = status;
        // 写入内容
        Bytes.long2bytes(res.getId(), header, 4);
        // 和Request一样的内容写入方式,先写入内容,再写入长度
        buffer.writerIndex(savedWriteIndex + 16);
        ChannelBufferOutputStream bos = new ChannelBufferOutputStream(buffer);
        ObjectOutput out = serialization.serialize(channel.getUrl(), bos);
        if (status == 20) {
            if (res.isHeartbeat()) {
                this.encodeEventData(channel, out, res.getResult());
            } else {
                this.encodeResponseData(channel, out, res.getResult(), res.getVersion());
            }
        } else {
        	// 这里不太一样的地方在于,如果错误的时候,则直接将错误信息写入,不需要再交由序列化
            out.writeUTF(res.getErrorMessage());
        }

        out.flushBuffer();
        if (out instanceof Cleanable) {
            ((Cleanable)out).cleanup();
        }

        bos.flush();
        bos.close();
        // 一样的写入模式
        int len = bos.writtenBytes();
        checkPayload(channel, (long)len);
        Bytes.int2bytes(len, header, 12);
        buffer.writerIndex(savedWriteIndex);
        buffer.writeBytes(header); // write header.
        buffer.writerIndex(savedWriteIndex + 16 + len);
    } catch (Throwable var13) {
        Throwable t = var13;
        // 写入出现异常
        buffer.writerIndex(savedWriteIndex);
        // send error message to Consumer, otherwise, Consumer will wait till timeout.
        if (!res.isEvent() && res.getStatus() != 50) {
            Response r = new Response(res.getId(), res.getVersion());
            r.setStatus((byte)50);
            // 如果是超过内容长度则重新设置内容大小并写入
            if (var13 instanceof ExceedPayloadLimitException) {
                logger.warn(var13.getMessage(), var13);

                try {
                    r.setErrorMessage(t.getMessage());
                    channel.send(r);
                    return;
                } catch (RemotingException var12) {
                    logger.warn("Failed to send bad_response info back: " + var13.getMessage() + ", cause: " + var12.getMessage(), var12);
                }
            } else {
                logger.warn("Fail to encode response: " + res + ", send bad_response info instead, cause: " + var13.getMessage(), var13);

                try {
                    r.setErrorMessage("Failed to send response: " + res + ", cause: " + StringUtils.toString(t));
                    channel.send(r);
                    return;
                } catch (RemotingException var11) {
                    logger.warn("Failed to send bad_response info back: " + res + ", cause: " + var11.getMessage(), var11);
                }
            }
        }

        if (var13 instanceof IOException) {
            throw (IOException)var13;
        } else if (var13 instanceof RuntimeException) {
            throw (RuntimeException)var13;
        } else if (var13 instanceof Error) {
            throw (Error)var13;
        } else {
            throw new RuntimeException(var13.getMessage(), var13);
        }
    }
}

6、在encode中我们再来看看真正encode的内容。 encodeResponseData 同样位于 DubboCodec 中。

protected void encodeResponseData(Channel channel, ObjectOutput out, Object data, String version) throws IOException {
    Result result = (Result)data;
    // 是否支持返回attachment参数
    boolean attach = Version.isSupportResponseAttachment(version);
    Throwable th = result.getException();
    if (th == null) {
    	// 如果没有异常信息,则直接写入内容
        Object ret = result.getValue();
        if (ret == null) {
            out.writeByte((byte)(attach ? 5 : 2));
        } else {
            out.writeByte((byte)(attach ? 4 : 1));
            out.writeObject(ret);
        }
    } else {
    	// 否则的话则将异常信息序列化
        out.writeByte((byte)(attach ? 3 : 0));
        out.writeThrowable(th);
    }
	// 支持写入attachment,则写入
    if (attach) {
    	// returns current version of Response to consumer side.
        result.getObjectAttachments().put("dubbo", Version.getProtocolVersion());
        out.writeAttachments(result.getObjectAttachments());
    }

}

7、解码 decode

public Object decode(Channel channel, ChannelBuffer buffer) throws IOException {
	// 可读字节数
    int readable = buffer.readableBytes();
    // 选取可读字节数 和 HEADER_LENGTH 中小的
    byte[] header = new byte[Math.min(readable, 16)];
    buffer.readBytes(header);
    return this.decode(channel, buffer, readable, header);
}

protected Object decode(Channel channel, ChannelBuffer buffer, int readable, byte[] header) throws IOException {
    int len;
    int i;
    // 检查魔数
    if ((readable <= 0 || header[0] == MAGIC_HIGH) && (readable <= 1 || header[1] == MAGIC_LOW)) {
    	// check length. 不完整的包 需要继续读取
        if (readable < 16) {
            return DecodeResult.NEED_MORE_INPUT;
        } else {
        	// 获取数据长度
            len = Bytes.bytes2int(header, 12);
            checkPayload(channel, (long)len);
            i = len + 16;
            // 需要继续读取
            if (readable < i) {
                return DecodeResult.NEED_MORE_INPUT;
            } else {
                ChannelBufferInputStream is = new ChannelBufferInputStream(buffer, len);

                Object var8;
                try {
                	// 解码数据
                    var8 = this.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 var15) {
                            logger.warn(var15.getMessage(), var15);
                        }
                    }

                }

                return var8;
            }
        }
    } else {
        len = header.length;
        if (header.length < readable) {
            header = Bytes.copyOf(header, readable);
            buffer.readBytes(header, len, readable - len);
        }

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

8、这时候我们再来看看解析响应中的信息处理。

protected Object decodeBody(Channel channel, InputStream is, byte[] header) throws IOException {
    byte flag = header[2];
    byte proto = (byte)(flag & 31);
    // 获取请求ID
    long id = Bytes.bytes2long(header, 4);
    // 判断是请求还是响应
    if ((flag & -128) == 0) {
    	// 说明是响应
        Response res = new Response(id);
        // 是否是event事件
        if ((flag & 32) != 0) {
            res.setEvent(true);
        }
		// 获取请求的状态码
        byte status = header[3];
        res.setStatus(status);

        try {
        	// 进行数据内容解析
            ObjectInput in = CodecSupport.deserialize(channel.getUrl(), is, proto);
            if (status == 20) {
                Object data;
                // 根据不同的类型来进行解析
                if (res.isHeartbeat()) {
                    data = this.decodeHeartbeatData(channel, in);
                } else if (res.isEvent()) {
                    data = this.decodeEventData(channel, in);
                } else {
                    data = this.decodeResponseData(channel, in, this.getRequestData(id));
                }

                res.setResult(data);
            } else {
                res.setErrorMessage(in.readUTF());
            }
        } catch (Throwable var12) {
            res.setStatus((byte)90);
            res.setErrorMessage(StringUtils.toString(var12));
        }

        return res;
    } else {
    	// 解析为请求
        Request req = new Request(id);
        req.setVersion(Version.getProtocolVersion());
        req.setTwoWay((flag & 64) != 0);
        if ((flag & 32) != 0) {
            req.setEvent(true);
        }

        try {
        	// 与响应相同,进行内容解析
            ObjectInput in = CodecSupport.deserialize(channel.getUrl(), is, proto);
            Object data;
            if (req.isHeartbeat()) {
                data = this.decodeHeartbeatData(channel, in);
            } else if (req.isEvent()) {
                data = this.decodeEventData(channel, in);
            } else {
                data = this.decodeRequestData(channel, in);
            }

            req.setData(data);
        } catch (Throwable var13) {
        	// bad request
            req.setBroken(true);
            req.setData(var13);
        }

        return req;
    }
}

三、处理粘包和拆包问题

当发生TCP拆包问题时候 这里假设之前还没有发生过任何数据交互,系统刚刚初始化好,那么这个时候在 InternalDecoder里面的buffer属性会是EMPTY_BUFFER。当发生第一次inbound数据的时候,第一次 在InternalDecoder里面接收的肯定是dubbo消息头的部分(这个由TCP协议保证),由于发生了拆包情 况,那么此时接收的inbound消息可能存在一下几种情况
1、当前inbound消息只包含dubbo协议头的一部分
2、当前inbound消息只包含dubbo的协议头
3、当前inbound消息只包含dubbo消息头和部分payload消息

通过上面的讨论,我们知道发生上面三种情况,都会触发ExchangeCodec返回NEED_MORE_INPUT,由于在DubboCountCodec对于返回NEED_MORE_INPUT会回滚读索引,所以此时的buffer里面的数据可以当作
并没有发生过读取操作,并且DubboCountCodec的decode也会返回NEED_MORE_INPUT,在
InternalDecoder对于当判断返回NEED_MORE_INPUT,也会进行读索引回滚,并且退出循环,最后会执
行finally内容,这里会判断inbound消息是否还有可读的,由于在DubboCountCodec里面进行了读索引
回滚,所以此时的buffer里面不是完整的inbound消息,等待第二次的inbound消息的到来,当第二次
inbound消息过来的时候,再次经过上面的判断。


当发生TCP粘包的时候 是tcp将一个dubbo协议栈放在一个tcp包中,那么有可能发生下面几种情况
1、当前inbound消息只包含一个dubbo协议栈
2、当前inbound消息包含一个dubbo协议栈,同时包含部分另一个或者多个dubbo协议栈内容
如果发生只包含一个协议栈,那么当前buffer通过ExchangeCodec解析协议之后,当前的buffer的 readeIndex位置应该是buffer尾部,那么在返回到InternalDecoder中message的方法readable返回 的是false,那么就会对buffer重新赋予EMPTY_BUFFER实体,而针对包含一个以上的dubbo协议栈,当然 也会解析出其中一个dubbo协议栈,但是经过ExchangeCodec解析之后,message的readIndex不在 message尾部,所以message的readable方法返回的是true。那么则会继续遍历message,读取下面的 信息。最终要么message刚好整数倍包含完整的dubbo协议栈,要不ExchangeCodec返回 NEED_MORE_INPUT,最后将未读完的数据缓存到buffer中,等待下次inbound事件,将buffer中的消息合 并到下次的inbound消息中,种类又回到了拆包的问题上。
dubbo在处理tcp的粘包和拆包时是借助InternalDecoder的buffer缓存对象来缓存不完整的dubbo协议 栈数据,等待下次inbound事件,合并进去。所以说在dubbo中解决TCP拆包和粘包的时候是通过buffer 变量来解决的。

你可能感兴趣的:(我的架构梦)