前言
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 获取到
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;
}
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)}
}
实际的请求参数会放在 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;
}
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));}
}
尾巴
Dubbo3 的 Triple 协议会把隐式参数 attachments 通过 HTTP2 头部传输,受限于 HTTP2 协议本身,所以 attachments 只能传输 String、Number、Boolean 和 byte[],其中 byte[] 会先经过 Base64 编码再传输。
又因为 HTTP2 Headers key 是不区分大小写的,但 attachments key 是区分大小写的,所以 Dubbo 还要先对 attachments 做转换处理,先统一把 key 转换成小写,再写入一个转换前后 key 的映射关系,对方拿到以后再转换回来就好了。