• Triple协议的隐式参数传递过程


    前言

    Dubbo 框架的 RPC 调用除了可以传递正常的接口参数外,还支持隐式参数传递。
    隐式参数的传递依赖 RpcContext 对象,它持有一个 Map 对象,消费者往 Map 里写入数据,客户端在发起 RPC 调用前会构建 RpcInvocation,然后把 RpcContext 里的 Map 数据拷贝到 RpcInvocation 的 attachments 属性,最后客户端把 RpcInvocation 序列化后再传输给服务端。

    隐式参数传递的一个典型的应用场景:分布式链路追踪。给调用链生成一个 TraceID,由于 TraceID 与业务无关,放在请求参数里显然不合适,我们可以通过 attachments 进行传递。同时为了不侵入业务,我们可以实现一个 Filter 来统一处理。

    在 Dubbo 3 中,RpcContext 被拆分为四大模块(ServerContext、ClientAttachment、ServerAttachment 和 ServiceContext)。
    它们分别承担了不同的职责:

    • ServiceContext:在 Dubbo 内部使用,用于传递调用链路上的参数信息,如 invoker 对象等
    • ClientAttachment:在 Client 端使用,往 ClientAttachment 中写入的参数将被传递到 Server 端
    • ServerAttachment:在 Server 端使用,从 ServerAttachment 中读取的参数是从 Client 中传递过来的
    • ServerContext:在 Client 端和 Server 端使用,用于从 Server 端回传 Client 端使用,Server 端写入到 ServerContext 的参数在调用结束后可以在 Client 端的 ServerContext 获取到

    image.png

    Dubbo3 的 Triple 协议针对 attachments 的传输有改动。
    dubbo 协议的处理方式是:Dubbo 会把 RpcInvocation 按照格式序列化,其中就包含 attachments,服务端反序列化后就能拿到 attachments。
    Triple 协议的处理方式是:DATA Frame 只包含序列化后的请求参数,attachments 是不包含在内的。Triple 把 attachments 放到哪里去了呢???没错,在 Headers 里面。

    源码分析

    Triple 协议对应的客户端是 TripleInvoker,客户端在发起 RPC 调用前会先创建请求元数据对象 RequestMetadata,它除了没有实际的请求参数外,该有的都有了:

    public class RequestMetadata {
        public AsciiString scheme;
        public String application;
        public String service;
        public String version;
        public String group;
        public String address;
        public String acceptEncoding;
        public String timeout;
        public Compressor compressor;
        public CancellationContext cancellationContext;
        public MethodDescriptor method;
        public PackableMethod packableMethod;
        public Map<String, Object> attachments;
        public boolean convertNoLowerHeader;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    RequestMetadata 的构建依赖 RpcInvocation,很多数据都是从 RpcInvocation 拷贝过来的,attachments 就是。
    RPC 调用就是客户端给服务端发送一段请求数据,Dubbo 会调用TripleClientCall#sendMessage()发送请求数据:

    @Override
    public void sendMessage(Object message) {
        if (canceled) {
            throw new IllegalStateException("Call already canceled");
        }
        // 先发送Headers帧,再发送Data帧
        if (!headerSent) {
            headerSent = true;
            stream.sendHeader(requestMetadata.toHeaders());
        }
        final byte[] data;
        try {
            data = requestMetadata.packableMethod.packRequest(message);
            stream.sendMessage(compress, compressed, false)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    实际的请求参数会放在 DATA Frame 里,在发送 DATA Frame 前必须先发送 HEADERS Frame。
    RequestMetadata#toHeaders()会生成 DefaultHttp2Headers 对象,它是 Netty 对 HEADERS Frame 的封装。

    public DefaultHttp2Headers toHeaders() {
        DefaultHttp2Headers header = new DefaultHttp2Headers(false);
        // 设置HTTP2 伪首部 & triple内置首部
        header.scheme(scheme)
            .authority(address)
            .method(HttpMethod.POST.asciiName())
            .path("/" + service + "/" + method.getMethodName())
            .set(TripleHeaderEnum.CONTENT_TYPE_KEY.getHeader(), TripleConstant.CONTENT_PROTO)
            .set(HttpHeaderNames.TE, HttpHeaderValues.TRAILERS);
        setIfNotNull(header, TripleHeaderEnum.TIMEOUT.getHeader(), timeout);
        if (!"1.0.0".equals(version)) {
            setIfNotNull(header, TripleHeaderEnum.SERVICE_VERSION.getHeader(), version);
        }
        setIfNotNull(header, TripleHeaderEnum.SERVICE_GROUP.getHeader(), group);
        setIfNotNull(header, TripleHeaderEnum.CONSUMER_APP_NAME_KEY.getHeader(),
            application);
        setIfNotNull(header, TripleHeaderEnum.GRPC_ACCEPT_ENCODING.getHeader(),
            acceptEncoding);
        if (!Identity.MESSAGE_ENCODING.equals(compressor.getMessageEncoding())) {
            setIfNotNull(header, TripleHeaderEnum.GRPC_ENCODING.getHeader(),
                compressor.getMessageEncoding());
        }
        // 转换attachments,解决key大小写问题
        StreamUtils.convertAttachment(header, attachments, convertNoLowerHeader);
        return header;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    StreamUtils#convertAttachment()会先转换 attachments,再写入 Headers。
    为什么还要转换呢???

    因为 HTTP2 规范里 Headers key 是不区分大小写的,但是 attachments key 是区分大小写的,如果不做处理,就乱套了。

    看下 Dubbo 是怎么转换的:

    • 遍历 attachments,把 key 转换成小写
    • 判断 key 是否与 HTTP2 伪首部、Triple 内置的 key 冲突,冲突则忽略不传输
    • 校验 value 类型,只能传输 String、Number、Boolean、byte[],其中字节数组会被 Base64 编码
    • 把 key value 写入 Headers
    • 把转换后的 key 和转换前的 key 构建一个 JSON 串,写入 Headers,key = tri-header-convert,远端接收到以后,再把 key 转换回去即可
    public static void convertAttachment(DefaultHttp2Headers headers,
                                         Map<String, Object> attachments,
                                         boolean needConvertHeaderKey) {
        if (attachments == null) {
            return;
        }
        Map<String, String> needConvertKey = new HashMap<>();
        for (Map.Entry<String, Object> entry : attachments.entrySet()) {
            String key = lruHeaderMap.get(entry.getKey());
            if (key == null) {
                final String lowerCaseKey = entry.getKey().toLowerCase(Locale.ROOT);
                lruHeaderMap.put(entry.getKey(), lowerCaseKey);
                key = lowerCaseKey;
            }
            // key的命名与 HTTP2伪首部、内部key 冲突则不会传输
            if (TripleHeaderEnum.containsExcludeAttachments(key)) {
                continue;
            }
            if (needConvertHeaderKey && !key.equals(entry.getKey())) {
                needConvertKey.put(key, entry.getKey());
            }
            // 转换写入Headers 只能传 String、Number、Boolean、byte[](Base64编码)
            final Object v = entry.getValue();
            convertSingleAttachment(headers, key, v);
        }
        /**
         * 因为http头部key是忽略大小写的 统一转小写发送
         * 但是attachments key是区分大小写的
         * 这里会映射转换前后的key,远端接收到再转换一下
         */
        if (!needConvertKey.isEmpty()) {
            String needConvertJson = JsonUtils.getJson().toJson(needConvertKey);
            headers.add(TripleHeaderEnum.TRI_HEADER_CONVERT.getHeader(), TriRpcStatus.encodeMessage(needConvertJson));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    尾巴

    Dubbo3 的 Triple 协议会把隐式参数 attachments 通过 HTTP2 头部传输,受限于 HTTP2 协议本身,所以 attachments 只能传输 String、Number、Boolean 和 byte[],其中 byte[] 会先经过 Base64 编码再传输。
    又因为 HTTP2 Headers key 是不区分大小写的,但 attachments key 是区分大小写的,所以 Dubbo 还要先对 attachments 做转换处理,先统一把 key 转换成小写,再写入一个转换前后 key 的映射关系,对方拿到以后再转换回来就好了。

  • 相关阅读:
    CAN总线数据采集工具PCAN的使用教程
    制作一个简单HTML静态网页(HTML+CSS)
    某电商网站的数据库设计(6)——创建查询销售数据的视图
    PCL可视化只有顶点彩色信息的OBJ文件
    localhost和127.0.0.1都可以访问项目,但是本地的外网IP不能访问
    Kotlin协程:StateFlow的设计与使用
    从0开始学习JavaScript--JavaScript 数字与日期
    AM@定积分的基本概念和性质
    4.1.5-检查网页内容是否存在信息泄露
    绁炵粡缃戠粶浠跨湡鐮旂┒鍗氬+璁烘枃涓嬭浇
  • 原文地址:https://blog.csdn.net/qq_32099833/article/details/133888976