前言
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 的映射关系,对方拿到以后再转换回来就好了。