跳转至

22 协议编解码:接口调用的数据是如何发到网络中的?

你好,我是何辉。今天我们深入研究Dubbo源码的最后一篇,协议编解码。

你已经学过了 Dubbo 框架的十大模块(01讲),上一讲也掌握消费方调用的整体框架流程,基本把 Dubbo 框架纵向走了一遍,但依然在单进程中转悠。而 Dubbo 的本质就是网络通信,要想把数据发到网络送往提供方,默认用 Netty 网络通信框架完成的。

那发起接口调用时,你创建出来的一个普普通通的请求对象,到底是如何被编解码后发到网络中的呢?如果你平常不怎么接触底层,这个问题想回答好确实有点难。不过不用担心,我们也研究了不少源码,按套路,根据一些蛛丝马迹顺着研究,解答这个问题其实就是一层窗户纸的事。

什么是帧格式?

想回答清楚今天的问题,我们从一个概念开始,帧格式。

所谓的“帧格式”,就是指根据不同协议规定的数据传输的格式。举个常见的 TCP/IP 模型,分别有应用层、传输层、网络层、数据链路层,以及物理层,每一层都有着对应的数据帧格式。

我们以数据封装的过程为例。

图片

比如,应用层的数据包+TCP头部就是传输层的帧格式,传输层的数据包+IP头部就是网络层的帧格式,网络层的数据包+MAC头部就是数据链路层的帧格式,最后,会把数据链路层的整个数据包,变成比特流,通过工作在物理层的网卡、网线或光纤发送出去。

这是我们从 TCP/IP 模型层面认识帧格式,那对于这样的数据传输格式,在客户端向服务端发送数据的实际交互过程中,又是如何体现的呢?

我们看一个日常开发的交互案例。

图片

client 把数据发往 server,这种交互形式估计你是再熟悉不过了。我们模拟一下数据的收发过程。

1. 固定长度

现在我们有 100 字节的数据,需要通过 client 发送出去,可是 client 的发送缓冲区太小了,缓冲区每次只能容纳 50 字节大小。所以,client 利用 socket 进行了 2 次 write 操作,才把 100 字节发送出去。

但是对于收数据的 server 来说,什么时候才认为数据接收齐全了呢?

图片

server 其实是很疑惑的,接收 client 第一次发来的数据后,到底该不该收 client 第二次发来的数据呢?若是不收的话,会不会造成少收数据?若是收,到底收多少数据才算完整呢?

为了解决种种疑惑,client 和 server 进行了双方的约定,按照固定长度直接进行收发。

在这个场景里约定 client 发起的一次短链接请求,server 收满 100 个字节就可以结束了。如果 client 发了 120 个字节,server 已经收下 100 个字节后,再继续收到 20 个字节时,发现 client 结束会话了,那么,server 会认为这20字节没有满 100 个字节,就干脆丢弃不要了。

2. 分隔符

看来长度的约定很重要,但是,这个长度到底该定义多少合适呢?

若短了,会增加通信的次数,增加双方的通信压力;若长了,比如定义为 1000 个字节,但是大多数情况下数据都是 1000 ~ 1100 个长度,岂不是 client 每次都要多发 900 ~ 1000 个没有意义的内容给到 server,不然 server 就会抛弃掉 client 第二次发送的数据。

图片

所以 client 又和 server 进行了约定,约定按照特殊字符切割。比如 client 给 server 发了一堆的数据,server 先一股脑全接收,当 server 碰到特殊字符时就切割开,然后继续接收新的一个数据包。

这也不失为一种办法,总比数据被丢弃好得多。

3. 定长+变长

可是试行了一段时间后,client 还是觉得有问题,因为有些数据内容如果含有分隔符,也被切割了。如果能有一种特殊的前缀标识就好了,识别到这种标识,就读多少数据,貌似更可行。

图片

于是 client 又和 server 进行了约定,约定数据格式由两部分组成,报文头、报文体。报文头是固定长度,里面有特殊的前缀标识、报文体的总长度;而报文体,长度是可变的,有多少数据就有多少长度。

好,我们从固定长度,到分隔符,到定长+变长,梳理了三种 client 与 server 的约定传输数据方式,你也应该发现了,其实这就是约定数据如何传输的协议格式,约定是怎样的格式,那就按照怎样的格式进行收发处理。而这种约定数据包格式的方式,就是我们俗称的“数据帧格式”

那 Dubbo 框架把对象编解码后变成了什么样的帧格式呢?

Dubbo 帧格式

问题来了,偌大的 Dubbo 源码工程,我们该怎么从源码层面找到底层是如何编解码的呢?

其实之前我们已经见过了,“源码框架”中的 Serialize 模块属于哪个层次?“调用流程”中将对象进行序列化的代码入口在哪里?

没错,想必你也想到了,我们可以顺着 NettyCodecAdapter 这个类的 encode 方法看源码,应该就可以找到了。

实践是检验真理的唯一标准,我们打个断点去 Debug 调试一下。启动 ZooKeeper 和提供方,然后 Debug 启动消费方,一会就来到了 encode 方法:

图片

我们单步调试进去,先经过了 DubboCountCodec 类,然后经过了 ExchangeCodec 类,结果发现了一段惊喜的代码。

///////////////////////////////////////////////////                  
// org.apache.dubbo.remoting.exchange.codec.ExchangeCodec#encode
// 交换信息编解码操作,主要是将 Request、Response 对象按照 Dubbo 协议格式编码为字节流
///////////////////////////////////////////////////
@Override
public void encode(Channel channel, ChannelBuffer buffer, Object msg) throws IOException {
    // 如果是需要发送给提供方的对象,那么就调用 encodeRequest 进行编码
    if (msg instanceof Request) {
        encodeRequest(channel, buffer, (Request) msg);
    } 
    // 如果是响应给提供方的对象,那么就调用 encodeResponse 进行编码
    else if (msg instanceof Response) {
        encodeResponse(channel, buffer, (Response) msg);
    } 
    // 如果是其他类型对象的话,那么就调用父类的通用 encode 方法进行编码操作
    else {
        super.encode(channel, buffer, msg);
    }
}

从这段代码中,确实发现了一段非常有亮点的代码,逻辑也不多,就 3 条分支逻辑,一条是针对请求对象进行编码的分支,一条是针对响应对象进行编码的分支,最后一条是调用了父类的 encode 方法进行编码的分支。

虽然分支逻辑不多,但是我们从方法名上已经发现了端倪,这不就是即将要对 Request 和 Response 对象进行编码操作么?继续前进,我们进入 encodeRequest 方法看个究竟。

///////////////////////////////////////////////////                  
// 1、org.apache.dubbo.remoting.exchange.codec.ExchangeCodec#encodeRequest
// 2、将 Request 对象按照 Dubbo 协议格式编码为字节流
// 3、重点关注我在代码中描述的 ①②③④⑤⑥ 几个关键位置
///////////////////////////////////////////////////
// header length.
protected static final int HEADER_LENGTH = 16;
// magic header.
protected static final short MAGIC = (short) 0xdabb;
protected static final byte MAGIC_HIGH = Bytes.short2bytes(MAGIC)[0];
protected static final byte MAGIC_LOW = Bytes.short2bytes(MAGIC)[1];
// message flag.
protected static final byte FLAG_REQUEST = (byte) 0x80;
protected static final byte FLAG_TWOWAY = (byte) 0x40;
protected static final byte FLAG_EVENT = (byte) 0x20;
protected static final int SERIALIZATION_MASK = 0x1f;

// 将 Request 对象按照 Dubbo 协议格式编码为字节流
protected void encodeRequest(Channel channel, ChannelBuffer buffer, Request req) throws IOException {
    Serialization serialization = getSerialization(channel, req);
    // header.
    // ① 针对 header 字节数组赋值一个 0xdabb 魔术值
    byte[] header = new byte[HEADER_LENGTH];
    // set magic number.
    Bytes.short2bytes(MAGIC, header);

    // set request and serialization flag.
    // ② 设置序列化方式,序列化方式从 channel 的 url 取出 serialization 对应的参数值,
    // 默认是 hessian2 方式,
    // 而 org.apache.dubbo.common.serialize.hessian2.Hessian2Serialization#getContentTypeId 的值为 2
    // 
    // serialization 属性值,在源码中有如下这些值:
    // byte HESSIAN2_SERIALIZATION_ID = 2;
    // byte JAVA_SERIALIZATION_ID = 3;
    // byte COMPACTED_JAVA_SERIALIZATION_ID = 4;
    // byte FASTJSON_SERIALIZATION_ID = 6;
    // byte NATIVE_JAVA_SERIALIZATION_ID = 7;
    // byte KRYO_SERIALIZATION_ID = 8;
    // byte FST_SERIALIZATION_ID = 9;
    // byte NATIVE_HESSIAN_SERIALIZATION_ID = 10;
    // byte PROTOSTUFF_SERIALIZATION_ID = 12;
    // byte AVRO_SERIALIZATION_ID = 11;
    // byte GSON_SERIALIZATION_ID = 16;
    // byte PROTOBUF_JSON_SERIALIZATION_ID = 21;
    // byte PROTOBUF_SERIALIZATION_ID = 22;
    // byte KRYO_SERIALIZATION2_ID = 25;
    // byte MSGPACK_SERIALIZATION_ID = 27;    
    header[2] = (byte) (FLAG_REQUEST | serialization.getContentTypeId());

    // ③ 设置请求类型,根据 mTwoWay、mEvent 来决定是怎样的请求类型
    if (req.isTwoWay()) {
        header[2] |= FLAG_TWOWAY;
    }
    if (req.isEvent()) {
        header[2] |= FLAG_EVENT;
    }

    // ④ 设置请求唯一ID;
    // 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);

    // ⑤ 构建一个输出流,根据 mEvent 的值来将 mData 进行序列化转为字节数组
    if (req.isHeartbeat()) {
        // heartbeat request data is always null
        bos.write(CodecSupport.getNullBytesOf(serialization));
    } else {
        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);
}

对这个 encodeRequest 方法仔细研读后,恍然大悟,原来 Dubbo 协议帧格式如此简单,正好就是我们协议约定中“定长 + 变长”的实现版本。

光从代码上不好看出“定长 + 变长”的可视化效果,这里我也根据源码的编写逻辑,总结了下代码所表达的数据帧格式。

图片

图中写的 0 ~ 7、8 ~ 15 这些数字表示的是一些 bit 位对应位置,也就是,各个方块占据了多少 bit,除以 8 得到的结果,就是占据了多少 byte(字节)。接下来,我们挨个认识一下每个小方块是什么含义:

  • ​magic high:魔术高位,占用 8 bit,也就是 1 byte。该值固定为 0xda,是一种标识符。
  • magic low:魔术低位,占用 8 bit,也就是 1 byte。该值固定为 0xbb,也是一种标识符。

魔术低位和魔术高位合并起来就是 0xdabb,代表着 dubbo 数据协议报文的开始。如果从通信 socket 收到的报文不是以 0xdabb 开始的,可以认为是非法报文。

  • request flag and serialization id:请求类型和序列化方式,占用 8 bit,也就是 1 byte。前面 4 bit 是请求类型,后面 4 bit 是序列化方式,合起来用 1 个 byte 来表示。
  • response status:响应码,占用 8 bit,也是 1 byte。因为已经明确是响应码了,所以一个请求发送出去的时候,不用填充这个值,响应回来的时候,这里就有值了。

但是这个响应码,并不是那些真实业务数据功能的响应码,而是 Dubbo 通信层面的错误码,比如通信响应成功码、消费方超时码、服务方超时码、请求格式错误码等等,都是一些 Dubbo 框架自己易于通信识别错误的码,并非那些真正上层业务功能的错误码。

  • request id:请求唯一ID,占用 64 bit,也就是 8 byte。标识请求的唯一性,用来证明你收到的响应,就是你曾经发出去的请求返回来的数据。
  • ​body length:报文体长度,占用 32 bit,也就是 4 byte。体现真正的业务报文数据到底有多长。

因为真实的业务数据有大有小,如果报文里不告知业务数据的长度,服务方就不知道要读取多长的字节,所以,就需要知道业务报文数据到底有多长。当客户端发送数据时,把要发送的业务数据报文计算一下长度后,放到这个位置,服务方看到该长度后,就会读取指定长度的字节,读完就结束,也就收到了一个完整的报文数据。

  • body content:报文体数据,占用的 bit 未知,占用的 byte 字节个数也未知。这里是我们真正业务数据的内容,至于真正的业务数据的长度有多长,完全由报文体长度决定。

了解了 Dubbo 的协议帧格式,是不是并没有那么难,我们只需要按照上面的帧格式一步步构建出这样的字节数组,然后利用 socket,write 出去就可以了。服务方在接收数据的时候,也是一样,严格按照报文格式进行解析,不是 oxdabb 开头的就直接丢弃,是的话就继续往后读取,按照数据帧格式,直到读完整个报文为止。

好,到这里,我们详读了 encodeRequest 源码,反推出了 Dubbo 的协议帧格式,另外一个 encodeResponse 方法想必对你来说是小菜一碟了,课后你可以自己研究一下。

协议编解码的应用

Dubbo 协议的数据帧格式,是一个非常典型的协议格式案例,你以后在实际开发中,如果需要自定义数据传输报文格式,也可以参照 Dubbo 的协议数据格式,稍加改造一番,然后自行通过代码编码和解码就可以了。

除了 Dubbo 协议有这样的协议编解码的操作,其实市面上还有许许多多的应用案例,我们看 3 个常见的。

第一,HTTP 协议,是由请求行 + 请求头 + 请求体构成的,比如我们熟知的 Tomcat 容器就有这样一个 Http11Processor 的类,按照 HTTP 协议规范,来解析 HTTP 请求报文。

第二,RESP 协议,Redis 通信使用的协议,通过首字节的字符,来区分不同数据类型的序列化协议,甚至你都可以直接手写 Socket,把一段事先准备好的 RESP 报文调用 write 方法,就可以对 Redis 进行操作了。

第三,WebSocket 协议,是 HTML5 的一种新型协议,实现了浏览器与服务器的全双工通信,本质还是基于 HTTP 协议的基础之上,借助 HTTP 协议来完成握手工作的。

总结

今天,我们从已经学过的知识点中挑出一个从未深究的细节,一个接口调用时“所创建的请求对象”是如何编码发到网络中的?

针对这个问题,我们梳理了帧格式的概念,借助 client 向 server 发送数据的简单模型案例,引出了 client 与 server 约定了三种类型的协议格式,分别是固定长度、分隔符、定长+变长。

然后回忆所学的知识点,找到了 Dubbo 框架进行编码的入口,通过 Debug 模式步步深入,阅读 encodeRequest 方法后,挖掘出了把对象按照 Dubbo 协议进行编码的核心代码。

图片

类似协议编解码的常见应用案例比如HTTP 协议、RESP 协议、WebSocket 协议等。

思考题

留个作业给你,我们已经分析了 encodeRequest 方法,你尝试自己研究一下 encodeResponse 方法的逻辑,总结一下与 encodeRequest 的异同。

期待看到你的思考,如果觉得今天的内容对你有帮助,也欢迎分享给身边的朋友一起讨论。我们下一讲见。

21 思考题参考

上一期留了个作业,研究下消费方进行泛化调用时会经历哪些流程,泛化调用的底层是如何实现的。

1.泛化调用流程

想要解答第一个问题,我们得首先有拥有一套消费方泛化调用的代码,这里我已经给你准备好了。

///////////////////////////////////////////////////                  
// org.apache.dubbo.remoting.transport.netty4.NettyCodecAdapter.InternalEncoder#encode
// 将对象按照一定的序列化方式发送出去
///////////////////////////////////////////////////
@Component
public class GenericDemoService {
    public String invokeDemo() {
        return stringStyleRpcInvoke();
    }

    public static String stringStyleRpcInvoke() {
        ReferenceConfig<GenericService> referenceConfig =
                buildReferenceConfig("com.hmilyylimh.cloud.facade.demo.DemoFacade");
        referenceConfig.setGeneric("true");
        // 获取泛化引用对象
        GenericService genericService = referenceConfig.get();
        // 返回调用
        Object rpcResult = genericService.$invoke(
                "sayHello",
                new String[]{"java.lang.String"},
                new Object[]{"Generic Demo"});
        // 返回结果
        return (String) rpcResult;
    }

    private static ReferenceConfig<GenericService> buildReferenceConfig(String interfaceName) {
        DubboBootstrap dubboBootstrap = DubboBootstrap.getInstance();
        // 设置应用服务名称
        ApplicationConfig applicationConfig = new ApplicationConfig();
        applicationConfig.setName(dubboBootstrap.getApplicationModel().getApplicationName());
        // 设置注册中心的地址
        String address = dubboBootstrap.getConfigManager().getRegistries().iterator().next().getAddress();
        RegistryConfig registryConfig = new RegistryConfig(address);
        ReferenceConfig<GenericService> referenceConfig = new ReferenceConfig<>();
        referenceConfig.setApplication(applicationConfig);
        referenceConfig.setRegistry(registryConfig);
        referenceConfig.setInterface(interfaceName);
        return referenceConfig;
    }
}

代码其实非常简单,通过创建 ReferenceConfig 得到泛化引用对象,也就是泛化服务对象,然后调用泛化服务对象的 $invoke 方法,实现远程调用,并最终泛化调用的结果。

有了消费方的泛化调用代码后,学过了“调用流程”巧妙打断点的方式,想必你已经想到了,可以直接在 NettyClient 的 send 方法打个断点,然后启动提供方,Debug 启动消费方,静候断点的到来。

图片

我们观察左侧的调用堆栈,很明显就找到了泛化实现类的过滤器,再细看其他环节的调用,可以发现,泛化调用和普通调用的区别就在于,泛化调用会经过一个泛化实现类过滤器(GenericImplFilter),其他环节的流程基本上没啥异样。

2.泛化调用内幕

既然要弄懂泛化调用的核心内幕逻辑,那就自然少不了去拜读一下源码的细节。

///////////////////////////////////////////////////                  
// org.apache.dubbo.rpc.filter.GenericImplFilter#invoke
// 泛化调用的核心 invoke 处理逻辑
///////////////////////////////////////////////////
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
    // 获取 url 上的 generic 属性值,目前得到的值是 true
    String generic = invoker.getUrl().getParameter(GENERIC_KEY);
    // calling a generic impl service
    // 1、如果是泛化调用所认可的标识(ProtocolUtils.isGeneric(generic))
    // 2、且调用下游接口的方法名不是 "$invoke"
    // 3、且调用下游接口的方法名不是 "$invokeAsync"
    // 4、且 invocation 对象的类型还必须属于 RpcInvocation 类型
    // 如果刚刚的 4 点都符合的话,那么就会进入 isCallingGenericImpl 方法逻辑
    if (isCallingGenericImpl(generic, invocation)) {
        // 将入参的 invocation 重新拷贝一份新的对象为 invocation2
        RpcInvocation invocation2 = new RpcInvocation(invocation);
        invocation2.put("GENERIC_IMPL", true);
        String methodName = invocation2.getMethodName();
        Class<?>[] parameterTypes = invocation2.getParameterTypes();
        Object[] arguments = invocation2.getArguments();
        String[] types = new String[parameterTypes.length];
        for (int i = 0; i < parameterTypes.length; i++) {
            types[i] = ReflectUtils.getName(parameterTypes[i]);
        }
        Object[] args;
        // 如果 generic = bean,则调用 JavaBeanSerializeUtil 工具类进行序列化操作
        if (ProtocolUtils.isBeanGenericSerialization(generic)) {
            args = new Object[arguments.length];
            for (int i = 0; i < arguments.length; i++) {
                args[i] = JavaBeanSerializeUtil.serialize(arguments[i], JavaBeanAccessor.METHOD);
            }
        } 
        // 如果 generic != bean 的话,则调用默认的序列化操作
        else {
            args = PojoUtils.generalize(arguments);
        }
        // 然后把一些调用所需的一些参数都全部放到这个新的 invocation2 对象中
        if (RpcUtils.isReturnTypeFuture(invocation)) {
            invocation2.setMethodName("$invokeAsync");
        } else {
            invocation2.setMethodName("$invoke");
        }
        invocation2.setParameterTypes(new Class<?>[]{String.class, String[].class, Object[].class});
        invocation2.setParameterTypesDesc("Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;");
        invocation2.setArguments(new Object[]{methodName, types, args});
        // 最终传递给下一步进行后续调用操作
        return invoker.invoke(invocation2);
    }

    // making a generic call to a normal service
    // 1、如果调用下游接口的方法名是 "$invoke" 或 "$invokeAsync"
    // 2、如果 invocation 的入参数据不为空,且入参的个数恰好是 3 个
    // 3、如果是泛化调用所认可的标识(ProtocolUtils.isGeneric(generic))
    // 如果刚刚的 3 点都符合的话,那么就会进入 isMakingGenericCall 方法逻辑
    else if (isMakingGenericCall(generic, invocation)) {
        Object[] args = (Object[]) invocation.getArguments()[2];
        // 如果 generic = nativejava
        if (ProtocolUtils.isJavaGenericSerialization(generic)) {
            for (Object arg : args) {
                if (byte[].class != arg.getClass()) {
                    error(generic, byte[].class.getName(), arg.getClass().getName());
                }
            }
        } 
        // 如果 generic = bean
        else if (ProtocolUtils.isBeanGenericSerialization(generic)) {
            for (Object arg : args) {
                if (!(arg instanceof JavaBeanDescriptor)) {
                    error(generic, JavaBeanDescriptor.class.getName(), arg.getClass().getName());
                }
            }
        }
        invocation.setAttachment(
                "generic", invoker.getUrl().getParameter("generic"));
    }
    return invoker.invoke(invocation);
}
                  
///////////////////////////////////////////////////                  
// org.apache.dubbo.rpc.filter.GenericImplFilter#isCallingGenericImpl
// 1、如果是泛化调用所认可的标识(ProtocolUtils.isGeneric(generic))
// 2、且调用下游接口的方法名不是 "$invoke"
// 3、且调用下游接口的方法名不是 "$invokeAsync"
// 4、且 invocation 对象的类型还必须属于 RpcInvocation 类型
///////////////////////////////////////////////////                  
private boolean isCallingGenericImpl(String generic, Invocation invocation) {
    return ProtocolUtils.isGeneric(generic)
            && (!"$invoke".equals(invocation.getMethodName()) && !"$invokeAsync".equals(invocation.getMethodName()))
            && invocation instanceof RpcInvocation;
}
                  
///////////////////////////////////////////////////                  
// org.apache.dubbo.rpc.filter.GenericImplFilter#isMakingGenericCall
// 1、如果调用下游接口的方法名是 "$invoke" 或 "$invokeAsync"
// 2、如果 invocation 的入参数据不为空,且入参的个数恰好是 3 个
// 3、如果是泛化调用所认可的标识(ProtocolUtils.isGeneric(generic))
///////////////////////////////////////////////////                    
private boolean isMakingGenericCall(String generic, Invocation invocation) {
    return (invocation.getMethodName().equals("$invoke") || invocation.getMethodName().equals("$invokeAsync"))
            && invocation.getArguments() != null
            && invocation.getArguments().length == 3
            && ProtocolUtils.isGeneric(generic);
}
                  
///////////////////////////////////////////////////                  
// org.apache.dubbo.rpc.support.ProtocolUtils#isGeneric
// generic 的属性值可以有以下 6 种值:
// 1、true
// 2、nativejava
// 3、bean
// 4、protobuf-json
// 5、gson
// 6、raw.return
///////////////////////////////////////////////////                    
public static boolean isGeneric(String generic) {
    return StringUtils.isNotEmpty(generic)
            && ("true".equalsIgnoreCase(generic)  /* Normal generalization cal */
            || "nativejava".equalsIgnoreCase(generic) /* Streaming generalization call supporting jdk serialization */
            || "bean".equalsIgnoreCase(generic)
            || "protobuf-json".equalsIgnoreCase(generic)
            || "gson".equalsIgnoreCase(generic)
            || "raw.return".equalsIgnoreCase(generic));
}

翻看消费方泛化调用的源码,由于我们调用的是下游 sayHello 方法,但是通过泛化形式调用的话,从 invocation 中获取的 methodName 却是 $invoke,自然跳过了 isCallingGenericImpl 逻辑的判断,然后进入到 isMakingGenericCall 的分支逻辑中,只是赋值了一个属性就完事了,所以,我们可以得知设置 generic = true 进行泛化调用是最简单的泛化调用形式。

当然,我们从源码中了解了以前不知道的一些细节逻辑。

  1. 设置 generic 属性的值时,有六种设置方法,分别为 true、nativejava、bean、protobuf-json、gson、raw.return。
  2. 从 isCallingGenericImpl 分支逻辑中,我们发现进行普通的远程调用时,也可以在 @DubboReference 注解中加上 generic 属性值。

总的来说,消费方的泛化服务过滤器,也确实没有干什么非常特别的事情,关键就是将 generic 属性值传到了提供方那边,至于提供方拿到 generic 的不同属性值,会按照怎样的处理逻辑,有兴趣的话你可以自行研究 GenericFilter,这个类是专门处理提供方接收泛化请求的泛化过滤器。

精选留言(4)
  • 张三丰 👍(0) 💬(3)

    老师,这里我有个疑问,在定长+变长这个协议格式传输的时候,如果服务端第一次接收的不完整,比如120字节的消息,第一次接收100字节,第二次接收20字节,对于服务端来说,如果第二次接收20字节的时候,这个20字节的消息会像100字节的消息那样包括请求头吗? 比如请求头+100字节请求体、请求头+20字节请求体,如果包括请求头,那么第一次处理消息和第二次处理消息在逻辑上有什么不同?第一次处理消息时肯定是先解析请求头再解析请求体,第二次也是这样解析吗?服务端怎么确认第二次收到的消息是第一次消息的接续?

    2023-03-20

  • Lum 👍(0) 💬(2)

    希望老师出一章消费端发送请求后,服务端是如何接收消息并返回的章节~

    2023-03-06

  • BraveSky 👍(0) 💬(1)

    老师你好,请教个dubbo序列化问题,目前线上服务用的kryo 序列化,kryo序列化对象dto 加字段,调用方反序列化就会报错,重新部署服务才行,这个有什么好的解决方案吗?如果换序列化方式的话,如何不影响现有服务情况下,平滑切换?

    2023-02-14

  • Six Days 👍(0) 💬(0)

    本章能稍微讲下服务端如何接收处理数据包就更好了

    2024-04-07