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);
}
相关推荐
F-2H1 小时前
C语言:指针4(常量指针和指针常量及动态内存分配)
java·linux·c语言·开发语言·前端·c++
苹果酱05671 小时前
「Mysql优化大师一」mysql服务性能剖析工具
java·vue.js·spring boot·mysql·课程设计
_oP_i2 小时前
Pinpoint 是一个开源的分布式追踪系统
java·分布式·开源
mmsx2 小时前
android sqlite 数据库简单封装示例(java)
android·java·数据库
武子康3 小时前
大数据-258 离线数仓 - Griffin架构 配置安装 Livy 架构设计 解压配置 Hadoop Hive
java·大数据·数据仓库·hive·hadoop·架构
豪宇刘4 小时前
MyBatis的面试题以及详细解答二
java·servlet·tomcat
秋恬意4 小时前
Mybatis能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别
java·数据库·mybatis
刘大辉在路上4 小时前
突发!!!GitLab停止为中国大陆、港澳地区提供服务,60天内需迁移账号否则将被删除
git·后端·gitlab·版本管理·源代码管理
FF在路上5 小时前
Knife4j调试实体类传参扁平化模式修改:default-flat-param-object: true
java·开发语言
真的很上进5 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html