Dubbo3主推的Triple协议

前言

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() 解包 反序列化 可能是PBWrapper
      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 就可以读取到这些帧了,而不用去关心底层细节。

服务端业务只关心HeadersData帧,前者包含请求的元数据,例如服务名、方法名、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);
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
Dubbo Triple协议是Apache Dubbo框架的一个新增特性,它提供了一种新的网络传输协议。在传统的Dubbo框架中,主要使用的网络传输协议是TCP,而Dubbo Triple协议则提供了一种基于HTTP/2协议的新的传输方式。 Dubbo Triple协议具有以下特点: 1. 支持双向通信:Dubbo Triple协议实现了Dubbo的双向通信能力。这意味着服务提供方和服务消费方都可以同时发送和接收消息,实现了更加灵活的通信方式。 2. 基于HTTP/2:Dubbo Triple协议底层使用了HTTP/2协议作为传输协议。相比于传统的HTTP/1.1协议,HTTP/2协议具有更高的性能和更低的延迟,能够提供更好的网络传输效率。 3. 支持长连接:Dubbo Triple协议通过复用底层的TCP连接实现长连接,提高了连接的复用率,减少建立连接的开销,提高了通信效率。 4. 支持多种消息格式:Dubbo Triple协议支持多种常见的消息格式,包括JSON、Protobuf等,可以根据需求选择合适的消息格式进行通信。 5. 支持多种编解码方式:Dubbo Triple协议支持多种编解码方式,包括Dubbo自身的序列化方式和常见的序列化框架,如Hessian、Kryo等,提供了更灵活的编解码选择。 总的来说,Dubbo Triple协议是一种基于HTTP/2的新的网络传输协议,它通过支持双向通信、长连接以及多种消息格式和编解码方式,提供了更加高效和灵活的服务通信方式,为Dubbo框架提供了更多的选择和扩展能力。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员小潘

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值