Dubbo3单端口发布多协议服务

前言

Dubbo3开始支持在单个端口上监听多个协议的不同服务。 比如使用Triple协议启动端口复用后,可以在相同的端口上为服务增加 Dubbo协议支持,以及Qos协议支持。

这些协议的识别都是由一个统一的端口复用服务器进行处理的,可以用于服务的协议迁移,并且可以节约端口以及相关的资源,减少运维的复杂性。

NettyPortUnificationServer

开启端口复用后,Dubbo服务启动时默认会启动NettyPortUnificationServer,它在doOpen()方法里会启动ServerBootstrap,对于新连接的ChannelPipeline只添加了一个NettyPortUnificationServerHandler,用于协议检测。

java 复制代码
protected void initChannel(SocketChannel ch) throws Exception {
    final ChannelPipeline p = ch.pipeline();
    final NettyPortUnificationServerHandler puHandler;
    // 端口统一处理器 多协议服务复用单个端口
    puHandler = new NettyPortUnificationServerHandler(getUrl(), sslContext, true, getProtocols(),
        NettyPortUnificationServer.this, NettyPortUnificationServer.this.dubboChannels,
        getSupportedUrls(), getSupportedHandlers());
    p.addLast("negotiation-protocol", puHandler);
}

因为一个端口要对外提供多协议服务,在连接没有建立前,服务端也不知道客户端到底使用哪种协议,此时也无法过多的配置ChannelPipeline,只能先添加一个协议检测的Handler,后续确认具体协议后,再针对性的配置ChannelPipeline,并且把当前协议检测的Handler移除。

NettyPortUnificationServerHandler

该处理器的主要职责是:检测客户端使用的协议、针对性的配置ChannelPipeline、移除协议检测Handler。

它继承自ByteToMessageDecoder,具备解码器的能力,它会在decode()检测协议。

绝大多数协议都会在报文的前几个字节带上一个魔数、或者发送一个魔法字符串,作为协议的标识。例如Dubbo协议的魔数是0xdabb,HTTP2协议会发送一个魔法字符串PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n

Dubbo正是基于此来实现协议检测的。

java 复制代码
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)
    throws Exception {
    NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler);
    if (in.readableBytes() < 2) {
        return;
    }
    if (isSsl(in)) {
        enableSsl(ctx);
    } else {
        for (final WireProtocol protocol : protocols) {
            in.markReaderIndex();
            ChannelBuffer buf = new NettyBackedChannelBuffer(in);
            /**
             * 协议检测 通过魔法前缀
             *   dubbo > 0xdabb
             *   triple > PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n
             */
            final ProtocolDetector.Result result = protocol.detector().detect(buf);
            in.resetReaderIndex();
            switch (result) {
                case UNRECOGNIZED:
                    continue;
                case RECOGNIZED:
                    String protocolName = url.getOrDefaultFrameworkModel().getExtensionLoader(WireProtocol.class)
                        .getExtensionName(protocol);
                    ChannelHandler localHandler = this.handlerMapper.getOrDefault(protocolName, handler);
                    URL localURL = this.urlMapper.getOrDefault(protocolName, url);
                    channel.setUrl(localURL);
                    NettyConfigOperator operator = new NettyConfigOperator(channel, localHandler);
                    protocol.configServerProtocolHandler(url, operator);
                    ctx.pipeline().remove(this);// 协议一旦确认 当前Handler就没用了
                case NEED_MORE_DATA:
                    return;
                default:
                    return;
            }
        }
        byte[] preface = new byte[in.readableBytes()];
        in.readBytes(preface);
        Set<String> supported = url.getApplicationModel()
            .getExtensionLoader(WireProtocol.class)
            .getSupportedExtensions();
        LOGGER.error(INTERNAL_ERROR, "unknown error in remoting module", "", String.format("Can not recognize protocol from downstream=%s . "
                + "preface=%s protocols=%s", ctx.channel().remoteAddress(),
            Bytes.bytes2hex(preface),
            supported));
        in.clear();
        ctx.close();
    }
}

ProtocolDetector

协议检测的规则交给了org.apache.dubbo.remoting.api.ProtocolDetector接口,以Dubbo协议为例,规则是检测前2个字节是否是0xdabb

java 复制代码
public class DubboDetector implements ProtocolDetector {
    private final ChannelBuffer Preface = new ByteBufferBackedChannelBuffer(
        ByteBuffer.wrap(new byte[]{(byte)0xda, (byte)0xbb})
    );

    @Override
    public Result detect(ChannelBuffer in) {
        int prefaceLen = Preface.readableBytes();
        int bytesRead = min(in.readableBytes(), prefaceLen);

        if (bytesRead ==0 || !ChannelBuffers.prefixEquals(in,  Preface,  bytesRead)) {
            return Result.UNRECOGNIZED;
        }
        if (bytesRead == prefaceLen) {
            return Result.RECOGNIZED;
        }

        return Result.NEED_MORE_DATA;
    }
}

Dubbo3主推的Triple协议和grpc协议都是基于HTTP2的,所以如何检测客户端使用的是HTTP2协议呢?HTTP2规定了,客户端建立连接后,首先要发送一个魔法字符串,用于确认服务端支持HTTP2,这个魔法字符串内容是PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n

java 复制代码
public class Http2ProtocolDetector implements ProtocolDetector {
    private final ChannelBuffer clientPrefaceString = new ByteBufferBackedChannelBuffer(
        Http2CodecUtil.connectionPrefaceBuf().nioBuffer());

    @Override
    public Result detect(ChannelBuffer in) {
        int prefaceLen = clientPrefaceString.readableBytes();
        int bytesRead = min(in.readableBytes(), prefaceLen);

        // If the input so far doesn't match the preface, break the connection.
        if (bytesRead == 0 || !ChannelBuffers.prefixEquals(in, clientPrefaceString, bytesRead)) {
            return Result.UNRECOGNIZED;
        }
        if (bytesRead == prefaceLen) {
            return Result.RECOGNIZED;
        }
        return Result.NEED_MORE_DATA;
    }
}

Http2CodecUtil.connectionPrefaceBuf()内容就是魔法字符串。

java 复制代码
private static final ByteBuf CONNECTION_PREFACE =
    unreleasableBuffer(directBuffer(24)
    .writeBytes("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".getBytes(UTF_8)))
    asReadOnly();

配置Handler

协议一旦检测确认,协议检测的Handler对于子Channel来说就没用了,会从ChannelPipeline中移除。

协议确认后,会通过WireProtocol#configServerProtocolHandler()来配置ChannelPipeline。

因此已经知道客户端使用哪种协议调用服务了,所以就可以针对性的配置编解码器、业务处理等Handler了。

以HTTP2为例,核心是需要配置Http2FrameCodec对HTTP2 Frame编解码、配置Http2MultiplexHandler以支持多路复用。

java 复制代码
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);
}
相关推荐
赶飞机偏偏下雨1 分钟前
【Java笔记】消息队列
java·开发语言·笔记
豐儀麟阁贵21 分钟前
2.6 代码注释与编码规
java·开发语言
程序员三明治22 分钟前
【Mybatis从入门到入土】ResultMap映射、多表查询与缓存机制全解析
java·sql·缓存·mybatis·resultmap·缓存机制·多表查询
间彧27 分钟前
Java transient关键字详解与项目实战
后端
华仔啊28 分钟前
Java 重试机制没写对,线上很容易出问题!这份生产级方案请收好
java·后端
你不是我我30 分钟前
【Java 开发日记】什么是线程池?它的工作原理?
java·开发语言
Seven9732 分钟前
剑指offer-35、数组中的逆序对
java·leetcode
梵得儿SHI1 小时前
Java 反射机制深度解析:从运行时 “解剖” 类的底层逻辑
java·开发语言·反射·反射机制·private·类成员·反射的三大核心功能
CodeSheep1 小时前
大家有没有发现一个奇特现象:你能在一个公司工作 12 年以上,无论你多忠诚多卖力,一旦公司赚的少了,那你就成了“眼中钉肉中刺”
前端·后端·程序员
豆沙沙包?1 小时前
2025年--Lc188--931. 下降路径最小和(多维动态规划,矩阵)--Java版
java·矩阵·动态规划