前言
Dubbo3 推出了下一代RPC通信协议——Triple,译为第三代的。
Triple 完全兼容 gRPC 协议,运行在 HTTP2 协议之上。Dubbo 框架提供了 Triple 协议的多种语言实现,可以帮助你构建浏览器、gRPC 兼容的 HTTP API 接口。
已经有了 dubbo 协议,为啥还要推出 Triple 协议???
Triple 协议的推出,旨在解决 Dubbo2 私有协议带来的互通性问题。说白了,Dubbo 协议虽然性能好,但是大家不认,带来的问题就是:
- 跨语言限制:对于有多语言诉求的公司来说不够友好,需要额外的工作量
- 缺乏标准化:Dubbo 协议没有被广泛认可和标准化,无法享受到标准化协议的优势
- 穿透性差:网络环境中的防火墙、网关、代理服务器等不能识别 Dubbo 协议
Triple 协议就是要解决这些问题的。
服务启动
Triple 协议的服务暴露方法是TripleProtocol#export()
。
Dubbo3 开始支持在单端口上发布多协议的不同服务,所以创建的都是NettyPortUnificationServer
,它的doOpen()
会开启服务。
新连接建立时,Dubbo 会通过一个协议检测的组件ProtocolDetector
来检测客户端使用的协议,然后根据具体协议再调用WireProtocol#configServerProtocolHandler()
针对性的配置ChannelPipeline。
以下是 Triple 协议的 ChannelPipeline 配置:
@Override
public void configServerProtocolHandler(URL url, ChannelOperator operator) {
Configuration config = ConfigurationUtils.getGlobalConfiguration(url.getOrDefaultApplicationModel());
final List<HeaderFilter> headFilters;
if (filtersLoader != null) {
headFilters = filtersLoader.getActivateExtension(url, HEADER_FILTER_KEY);
} else {
headFilters = Collections.emptyList();
}
final Http2FrameCodec codec = Http2FrameCodecBuilder.forServer()
.gracefulShutdownTimeoutMillis(10000)
.initialSettings(new Http2Settings().headerTableSize(
config.getInt(H2_SETTINGS_HEADER_TABLE_SIZE_KEY, DEFAULT_SETTING_HEADER_LIST_SIZE))
.maxConcurrentStreams(
config.getInt(H2_SETTINGS_MAX_CONCURRENT_STREAMS_KEY, Integer.MAX_VALUE))
.initialWindowSize(
config.getInt(H2_SETTINGS_INITIAL_WINDOW_SIZE_KEY, DEFAULT_WINDOW_INIT_SIZE))
.maxFrameSize(config.getInt(H2_SETTINGS_MAX_FRAME_SIZE_KEY, DEFAULT_MAX_FRAME_SIZE))
.maxHeaderListSize(config.getInt(H2_SETTINGS_MAX_HEADER_LIST_SIZE_KEY,
DEFAULT_MAX_HEADER_LIST_SIZE)))
.frameLogger(SERVER_LOGGER)
.build();
final Http2MultiplexHandler handler = new Http2MultiplexHandler(
new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) {
final ChannelPipeline p = ch.pipeline();
p.addLast(new TripleCommandOutBoundHandler());
p.addLast(new TripleHttp2FrameServerHandler(frameworkModel, lookupExecutor(url),
headFilters));
}
});
List<ChannelHandler> handlers = new ArrayList<>();
handlers.add(new ChannelHandlerPretender(codec));// Http2 Frame 编解码器
handlers.add(new ChannelHandlerPretender(new TripleServerConnectionHandler()));// 处理 PING GOAWAY
handlers.add(new ChannelHandlerPretender(handler));// 请求处理器
handlers.add(new ChannelHandlerPretender(new TripleTailHandler()));// 避免内存泄漏
operator.configChannelHandler(handlers);
}
- 头部的 Http2FrameCodec 是HTTP2 Frame的编解码器
- Http2MultiplexHandler 用来支持HTTP2的多路复用
- TripleCommandOutBoundHandler 用来往外写Triple响应的数据
- TripleHttp2FrameServerHandler 用来处理服务端headers和data帧
- TripleServerConnectionHandler 处理PING、GOAWAY帧
- TripleTailHandler 防止内存泄漏
流程
Dubbo3 Triple 协议服务端处理请求的源码关键节点如下:
Http2FrameCodec#decode() HTTP2 Frame解码
TripleHttp2FrameServerHandler#onHeadersRead() 读取HEADERS
ServerTransportObserver#processHeader() 处理HEADERS
Invoker PathResolver#resolve() 查找Invoker
RpcInvocation AbstractServerCall#buildInvocation() 构建Invocation
TripleHttp2FrameServerHandler#onDataRead() 读取DATA
TriDecoder#deframe() 解帧 DATA Payload
UnPack#unpack() 解包 反序列化 可能是PB 或 Wrapper
RpcInvocation#setArguments() 解包后设置请求参数
AbstractServerCallListener#invoke() 执行本地调用
UnaryServerCallListener#onReturn() 返回结果
AbstractServerCall#sendHeader() 先发送HEADERS Frame
Pack#pack() 消息打包 序列化
DataQueueCommand#createGrpcCommand() 创建Command
WriteQueue#enqueue() 入队等待
TripleCommandOutBoundHandler#write() 往外写
DataQueueCommand#doSend() 发送HTTP2 DATA Frame
接收请求
ChannelPipeline 头部的Http2FrameCodec
首先会将字节序列解码为 HTTP2 帧,然后 TripleHttp2FrameServerHandler 就可以读取到这些帧了,而不用去关心底层细节。
服务端业务只关心Headers
和Data
帧,前者包含请求的元数据,例如服务名、方法名、Group、Version等信息;后者包含实际的请求参数。
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof Http2HeadersFrame) {
onHeadersRead(ctx, (Http2HeadersFrame) msg);
} else if (msg instanceof Http2DataFrame) {
onDataRead(ctx, (Http2DataFrame) msg);
} else if (msg instanceof ReferenceCounted) {
ReferenceCountUtil.release(msg);
}
}
对于一个 RPC 请求而言,会至少有一个Headers
帧和若干个Data
帧,接受到的帧必须要保证被顺序处理,Dubbo 使用一个叫SerializingExecutor
的线程池解决了这个问题。
ServerTransportObserver#processHeader()
处理Headers
帧,代码太长不贴了,主要是从 HTTP2 Headers 里面解析出必要的元数据并校验:
:method
:必须是POST:path
:服务路径,包含服务名、方法名content-type
:请求数据类型,必须是application/grpc
开头- 根据
:path
查找 Invoker grpc-encoding
:使用的压缩算法- 根据是否有本地存根Stub来判断使用哪种服务调用方式
- 构建 RpcInvocation
Headers
帧处理完,Dubbo 就知道客户端要调用哪个服务的哪个方法了,此时 Invoker 已经找到,RpcInvocation 也已经构建好了,但是实际的参数arguments
还没有,因为它在Data
帧里面。
Data
帧会交给一个叫 Triple 解帧器的类处理,方法是TriDecoder#deframe()
:
public void deframe(ByteBuf data) {
if (closing || closed) {
return;
}
// 可能包含多个DATA帧 会合并到CompositeByteBuf
accumulate.addComponent(true, data);
// 解帧 传送给后续监听器处理
deliver();
}
gRPC的消息主体由两部分组成:5字节的Header和变长的Payload。
- 1字节的Type:标记消息是否压缩,压缩算法在
grpc-encoding
指定 - 4字节Length:记录Payload的长度
- 变长的Payload
知道这个,再看 Dubbo 解帧的代码就很简单了:
private void processHeader() {
// 1字节的Type
int type = accumulate.readUnsignedByte();
if ((type & RESERVED_MASK) != 0) {
throw new RpcException("gRPC frame header malformed: reserved bits not zero");
}
// 压缩标记
compressedFlag = (type & COMPRESSED_FLAG_MASK) != 0;
// Payload长度
requiredLength = accumulate.readInt();
state = GrpcDecodeState.PAYLOAD;
}
private void processBody() {
// 根据是否压缩来获取Payload原始数据
byte[] stream = compressedFlag ? getCompressedBody() : getUncompressedBody();
// 触发监听器 消息解包反序列化 设置arguments
listener.onRawMessage(stream);
state = GrpcDecodeState.HEADER;
requiredLength = HEADER_LENGTH;
}
到这里,RpcInvocation就完整了。
如果请求数据发送完毕,会在Frame的Header部分标记流结束,即endStream=true
。流结束意味着请求数据全部发送完毕,服务端可以执行本地服务调用了。
private void doOnData(ByteBuf data, boolean endStream) {
if (deframer == null) {
return;
}
// 解Data帧
deframer.deframe(data);
// 如果流结束 发起本地调用
if (endStream) {
deframer.close();
}
}
流结束会触发ServerCall.Listener#onComplete()
,以 UNARY 调用为例,它会发起本地调用,然后把结果发送给客户端。
public void invoke() {
RpcContext.restoreCancellationContext(cancellationContext);
InetSocketAddress remoteAddress = (InetSocketAddress) invocation.getAttributes()
.remove(AbstractServerCall.REMOTE_ADDRESS_KEY);
RpcContext.getServerContext().setRemoteAddress(remoteAddress);
String remoteApp = (String) invocation.getAttributes()
.remove(TripleHeaderEnum.CONSUMER_APP_NAME_KEY);
if (null != remoteApp) {
RpcContext.getServerContext().setRemoteApplicationName(remoteApp);
invocation.setAttachmentIfAbsent(REMOTE_APPLICATION_KEY, remoteApp);
}
final long stInMillis = System.currentTimeMillis();
try {
// 本地服务调用
final Result response = invoker.invoke(invocation);
response.whenCompleteWithContext((r, t) -> {
// 完成后
responseObserver.setResponseAttachments(response.getObjectAttachments());
if (t != null) {
responseObserver.onError(t);
return;
}
if (response.hasException()) {
responseObserver.onError(response.getException());
return;
}
final long cost = System.currentTimeMillis() - stInMillis;
if (responseObserver.isTimeout(cost)) {
LOGGER.error(PROTOCOL_TIMEOUT_SERVER, "", "", String.format(
"Invoke timeout at server side, ignored to send response. service=%s method=%s cost=%s",
invocation.getTargetServiceUniqueName(),
invocation.getMethodName(),
cost));
responseObserver.onCompleted(TriRpcStatus.DEADLINE_EXCEEDED);
return;
}
// 返回结果
onReturn(r.getValue());
});
} catch (Exception e) {
responseObserver.onError(e);
} finally {
RpcContext.removeCancellationContext();
RpcContext.removeContext();
}
}
发送结果
UNARY
代表一元调用,它是最常见的RPC调用方式,客户端会阻塞等待服务端返回结果,就和调用本地方法一样。
对应 Dubbo 类是UnaryServerCallListener
,当本地方法执行完毕后,它会把结果发送给客户端,方法是onReturn()
。
public void onReturn(Object value) {
// 对于UNARY来说,只有一个结果 随后就是完成
responseObserver.onNext(value);
responseObserver.onCompleted();
}
结果会由AbstractServerCall#doSendMessage()
发送给客户端:
- 先发送 Headers Frame
- 结果消息打包,也就是序列化成字节序列
- 打包后的字节序列构建成HTTP2 DATA Frame发送给客户端
public final void sendMessage(Object message) {
if (closed) {
throw new IllegalStateException("Stream has already canceled");
}
// is already in executor
doSendMessage(message);
}
private void doSendMessage(Object message) {
if (closed) {
return;
}
if (!headerSent) {
// 先发送HTTP2 Headers Frame
sendHeader();
}
final byte[] data;
try {
// 打包 即序列化
data = packableMethod.packResponse(message);
} catch (Exception e) {
close(TriRpcStatus.INTERNAL.withDescription("Serialize response failed")
.withCause(e), null);
LOGGER.error(PROTOCOL_FAILED_SERIALIZE_TRIPLE,"","",String.format("Serialize triple response failed, service=%s method=%s",
serviceName, methodName), e);
return;
}
if (data == null) {
close(TriRpcStatus.INTERNAL.withDescription("Missing response"), null);
return;
}
Future<?> future;
// 判断是否要压缩 然后发送
if (compressor != null) {
int compressedFlag =
Identity.MESSAGE_ENCODING.equals(compressor.getMessageEncoding()) ? 0 : 1;
final byte[] compressed = compressor.compress(data);
future = stream.sendMessage(compressed, compressedFlag);
} else {
future = stream.sendMessage(data, 0);
}
future.addListener(f -> {
if (!f.isSuccess()) {
cancelDual(TriRpcStatus.CANCELLED
.withDescription("Send message failed")
.withCause(f.cause()));
}
});
}
这里的发送,实际上是将序列化后的消息构建成一个DataQueueCommand
对象然后入队,因为受限于HTTP2 流控,消息并不能立马发送出去。
writeQueue.enqueue(DataQueueCommand.createGrpcCommand(message, false, compressFlag));
Dubbo 会在数据可写时将DataQueueCommand
发送出去,此时会触发TripleCommandOutBoundHandler#write()
public class TripleCommandOutBoundHandler extends ChannelOutboundHandlerAdapter {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
if (msg instanceof QueuedCommand) {
QueuedCommand command = (QueuedCommand) msg;
// 发送命令
command.send(ctx, promise);
} else {
super.write(ctx, msg, promise);
}
}
}
最终构建 DefaultHttp2DataFrame 发送出去,ChannelPipeline 头部的 Http2FrameCodec 会将 Frame 解码为字节序列发送给对方。
public void doSend(ChannelHandlerContext ctx, ChannelPromise promise) {
if (data == null) {
ctx.write(new DefaultHttp2DataFrame(endStream), promise);
} else {
ByteBuf buf = ctx.alloc().buffer();
buf.writeByte(compressFlag);
buf.writeInt(data.length);
buf.writeBytes(data);
ctx.write(new DefaultHttp2DataFrame(buf, endStream), promise);
}
}