Dubbo源碼分析之QoS服務(二)

一、概述

上一篇文章介紹了Dubbo的QoS服務的基本作用和配置使用

https://blog.csdn.net/qq_33513250/article/details/102978132

接下來我們進行源碼分析它的功能是如何實現的。

二、QosProtocolWrapper

dubbo服務提供者ServiceBean的初始化過程中,會調用Protocol接口的export方法,RerenceBean的初始化過程中,會調用Protocol接口的refer方法。Dubbo的ExtensionLoader類會對Protocol進行自動包裝,其中一個包裝類爲QosProtocolWrapper,具體過程請參見我的其他章節內容。

QosProtocolWrapper對DubboProtocol進行包裝,當invoker的url協議是registry時,export和refer方法都會調用startQosServer(url)開啓QoS服務,銷燬時會調用stopServer方法關閉服務。

public class QosProtocolWrapper implements Protocol {

    private final Logger logger = LoggerFactory.getLogger(QosProtocolWrapper.class);

    private static AtomicBoolean hasStarted = new AtomicBoolean(false);

    private Protocol protocol;

    public QosProtocolWrapper(Protocol protocol) {
        if (protocol == null) {
            throw new IllegalArgumentException("protocol == null");
        }
        this.protocol = protocol;
    }

    @Override
    public int getDefaultPort() {
        return protocol.getDefaultPort();
    }

    @Override
    public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
        if (REGISTRY_PROTOCOL.equals(invoker.getUrl().getProtocol())) {
            startQosServer(invoker.getUrl());
            return protocol.export(invoker);
        }
        return protocol.export(invoker);
    }

    @Override
    public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
        if (REGISTRY_PROTOCOL.equals(url.getProtocol())) {
            startQosServer(url);
            return protocol.refer(type, url);
        }
        return protocol.refer(type, url);
    }

    @Override
    public void destroy() {
        protocol.destroy();
        stopServer();
    }

    /*package*/ void stopServer() {
        if (hasStarted.compareAndSet(true, false)) {
            Server server = Server.getInstance();
            server.stop();
        }
    }
}

 我們來看startQosServer代碼,獲取qos.port端口號,默認爲2222,獲取參數qos.accept.foreign.ip是否接受其他遠程ip訪問,默認爲false。

然後調用Server.getInstance獲取Qos的服務實例,並調用start方法啓動服務。

 private void startQosServer(URL url) {
        try {
            if (!hasStarted.compareAndSet(false, true)) {
                return;
            }

            boolean qosEnable = url.getParameter(QOS_ENABLE, true);
            if (!qosEnable) {
                logger.info("qos won't be started because it is disabled. " +
                        "Please check dubbo.application.qos.enable is configured either in system property, " +
                        "dubbo.properties or XML/spring-boot configuration.");
                return;
            }

            int port = url.getParameter(QOS_PORT, QosConstants.DEFAULT_PORT);
            boolean acceptForeignIp = Boolean.parseBoolean(url.getParameter(ACCEPT_FOREIGN_IP, "false"));
            Server server = Server.getInstance();
            server.setPort(port);
            server.setAcceptForeignIp(acceptForeignIp);
            server.start();

        } catch (Throwable throwable) {
            logger.warn("Fail to start qos server: ", throwable);
        }
    }

 三、qosServer

 下面是qos.Server類開啓服務,綁定端口的主要方法,先通過started變量判斷是否已經啓動,避免重複啓動。然後使用netty技術啓動ServerBootstrap,設置一個主線程池boos,名稱爲qos-boos,使用DefaultThreadFactory創建線程,並設置爲守護線程。設置一個子線程池worker,名稱爲qos-worker,也使用DefaultThreadFactory創建線程,並設置爲守護線程。

 配置serverBootstrap相關參數,通道服務channel設置爲NioServerSocketChannel,客戶端tcp無延遲連接,
 SO_REUSEADDR爲true表示允許監聽服務器捆綁其端口,即使以前建立的將該端口用作他們的本地端口的連接仍存在。
並且設置客戶端ChannelInitializer,在channel的最後通道添加QosProcessHandler處理器。

最後綁定端口並啓動服務,異步監聽。

 public void start() throws Throwable {
        if (!started.compareAndSet(false, true)) {
            return;
        }
        boss = new NioEventLoopGroup(1, new DefaultThreadFactory("qos-boss", true));
        worker = new NioEventLoopGroup(0, new DefaultThreadFactory("qos-worker", true));
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap.group(boss, worker);
        serverBootstrap.channel(NioServerSocketChannel.class);
        serverBootstrap.childOption(ChannelOption.TCP_NODELAY, true);
        serverBootstrap.childOption(ChannelOption.SO_REUSEADDR, true);
        serverBootstrap.childHandler(new ChannelInitializer<Channel>() {

            @Override
            protected void initChannel(Channel ch) throws Exception {
                ch.pipeline().addLast(new QosProcessHandler(welcome, acceptForeignIp));
            }
        });
        try {
            serverBootstrap.bind(port).sync();
            logger.info("qos-server bind localhost:" + port);
        } catch (Throwable throwable) {
            logger.error("qos-server can not bind localhost:" + port, throwable);
            throw throwable;
        }
    }

QosProcessHandler處理器繼承了netty的ByteToMessageDecoder類。重寫channelActive方法,是channel剛連接上就觸發調用。方法內部通過ChannelHandlerContext獲取到線程執行器executor,調用schedule方法執行異步任務,此異步任務爲向客戶端寫welcome字符串。

decode是每次client發送請求是調用的方法,添加了獲取client輸入字符並解析的主要邏輯。先通過輸入的第一個字符判斷協議,如果是‘G’或‘p’則爲http請求,則取消向用戶發送welcome字符,並添加HttpServerCodec,HttpObjectAggregator處理器以及HttpProcessHandler處理器,如果不是http協議,則進行utf-8的編碼及反編碼,以及TelnetProcessHandler處理器。

public class QosProcessHandler extends ByteToMessageDecoder {

    private ScheduledFuture<?> welcomeFuture;

    private String welcome;
    // true means to accept foreign IP
    private boolean acceptForeignIp;

    public static final String prompt = "dubbo>";

    public QosProcessHandler(String welcome, boolean acceptForeignIp) {
        this.welcome = welcome;
        this.acceptForeignIp = acceptForeignIp;
    }

    @Override
    public void channelActive(final ChannelHandlerContext ctx) throws Exception {
        welcomeFuture = ctx.executor().schedule(new Runnable() {

            @Override
            public void run() {
                if (welcome != null) {
                    ctx.write(Unpooled.wrappedBuffer(welcome.getBytes()));
                    ctx.writeAndFlush(Unpooled.wrappedBuffer(prompt.getBytes()));
                }
            }

        }, 500, TimeUnit.MILLISECONDS);
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        if (in.readableBytes() < 1) {
            return;
        }

        // read one byte to guess protocol
        final int magic = in.getByte(in.readerIndex());

        ChannelPipeline p = ctx.pipeline();
        p.addLast(new LocalHostPermitHandler(acceptForeignIp));
        if (isHttp(magic)) {
            // no welcome output for http protocol
            if (welcomeFuture != null && welcomeFuture.isCancellable()) {
                welcomeFuture.cancel(false);
            }
            p.addLast(new HttpServerCodec());
            p.addLast(new HttpObjectAggregator(1048576));
            p.addLast(new HttpProcessHandler());
            p.remove(this);
        } else {
            p.addLast(new LineBasedFrameDecoder(2048));
            p.addLast(new StringDecoder(CharsetUtil.UTF_8));
            p.addLast(new StringEncoder(CharsetUtil.UTF_8));
            p.addLast(new IdleStateHandler(0, 0, 5 * 60));
            p.addLast(new TelnetProcessHandler());
            p.remove(this);
        }
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            ExecutorUtil.cancelScheduledFuture(welcomeFuture);
            ctx.close();
        }
    }

    // G for GET, and P for POST
    private static boolean isHttp(int magic) {
        return magic == 'G' || magic == 'P';
    }
}

welcome字符串:

public class DubboLogo {
    public static final String dubbo =
                    "   ___   __  __ ___   ___   ____     " + System.lineSeparator() +
                    "  / _ \\ / / / // _ ) / _ ) / __ \\  " + System.lineSeparator() +
                    " / // // /_/ // _  |/ _  |/ /_/ /    " + System.lineSeparator() +
                    "/____/ \\____//____//____/ \\____/   " + System.lineSeparator();
}

HttpProcessHandler處理http請求,繼承netty的SimpleChannelInboundHandler,msg類型爲HttpRequest。核心方法爲channelRead0處理msg,然後委派給DefaultCommandExecutor處理解碼了msg的commandContext,並將結果writeAndFlush給客戶端。

public class HttpProcessHandler extends SimpleChannelInboundHandler<HttpRequest> {

    private static final Logger log = LoggerFactory.getLogger(HttpProcessHandler.class);
    private static CommandExecutor commandExecutor = new DefaultCommandExecutor();


    @Override
    protected void channelRead0(ChannelHandlerContext ctx, HttpRequest msg) throws Exception {
        CommandContext commandContext = HttpCommandDecoder.decode(msg);
        // return 404 when fail to construct command context
        if (commandContext == null) {
            log.warn("can not found commandContext url: " + msg.getUri());
            FullHttpResponse response = http404();
            ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
        } else {
            commandContext.setRemote(ctx.channel());
            try {
                String result = commandExecutor.execute(commandContext);
                FullHttpResponse response = http200(result);
                ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
            } catch (NoSuchCommandException ex) {
                log.error("can not find commandContext: " + commandContext, ex);
                FullHttpResponse response = http404();
                ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
            } catch (Exception qosEx) {
                log.error("execute commandContext: " + commandContext + " got exception", qosEx);
                FullHttpResponse response = http500(qosEx.getMessage());
                ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
            }
        }
    }
}

TelnetProcessHandler處理telnet請求,同樣繼承netty的SimpleChannelInboundHandler,此時msg類型爲String。
核心方法爲channelRead0處理msg,然後委派給DefaultCommandExecutor處理解碼了msg的commandContext,並將結果writeAndFlush給客戶端。

public class TelnetProcessHandler extends SimpleChannelInboundHandler<String> {

    private static final Logger log = LoggerFactory.getLogger(TelnetProcessHandler.class);
    private static CommandExecutor commandExecutor = new DefaultCommandExecutor();

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {

        if (StringUtils.isBlank(msg)) {
            ctx.writeAndFlush(QosProcessHandler.prompt);
        } else {
            CommandContext commandContext = TelnetCommandDecoder.decode(msg);
            commandContext.setRemote(ctx.channel());

            try {
                String result = commandExecutor.execute(commandContext);
                if (StringUtils.isEquals(QosConstants.CLOSE, result)) {
                    ctx.writeAndFlush(getByeLabel()).addListener(ChannelFutureListener.CLOSE);
                } else {
                    ctx.writeAndFlush(result + QosConstants.BR_STR + QosProcessHandler.prompt);
                }
            } catch (NoSuchCommandException ex) {
                ctx.writeAndFlush(msg + " :no such command");
                ctx.writeAndFlush(QosConstants.BR_STR + QosProcessHandler.prompt);
                log.error("can not found command " + commandContext, ex);
            } catch (Exception ex) {
                ctx.writeAndFlush(msg + " :fail to execute commandContext by " + ex.getMessage());
                ctx.writeAndFlush(QosConstants.BR_STR + QosProcessHandler.prompt);
                log.error("execute commandContext got exception " + commandContext, ex);
            }
        }
    }

    private String getByeLabel() {
        return "BYE!\n";
    }

}

可以看出兩種協議HttpProcessHandler和TelnetProcessHandler都委派給DefaultCommandExecutor處理消息。
DefaultCommandExecutor的execute方法中,先通過ExtensionLoader根據客戶端的命令名稱獲取BaseCommand實現類,
然後再調用實現類的execute方法獲取結果。

public class DefaultCommandExecutor implements CommandExecutor {
    @Override
    public String execute(CommandContext commandContext) throws NoSuchCommandException {
        BaseCommand command = null;
        try {
            command = ExtensionLoader.getExtensionLoader(BaseCommand.class).getExtension(commandContext.getCommandName());
        } catch (Throwable throwable) {
                //can't find command
        }
        if (command == null) {
            throw new NoSuchCommandException(commandContext.getCommandName());
        }
        return command.execute(commandContext, commandContext.getArgs());
}

 打開dubbo/internal目錄下的org.apache.dubbo.qos.command.BaseCommand文件官方提供的支持如下命令,用戶也可以根據自己需要實現BaseCommand接口自行擴展。

online=org.apache.dubbo.qos.command.impl.Online
help=org.apache.dubbo.qos.command.impl.Help
quit=org.apache.dubbo.qos.command.impl.Quit
ls=org.apache.dubbo.qos.command.impl.Ls
offline=org.apache.dubbo.qos.command.impl.Offline

下面我們打開Help命令,當沒有參數時,調用mainHelp方法先畫一個table,然後通過ExtensionLoader獲取獲取所有的BaseCommand實現類,反射註解@Cmd,獲取他們的名稱、描述以及示例拼接至table上,返回字符串給用戶。
 

@Cmd(name = "help", summary = "help command", example = {
        "help",
        "help online"
})
public class Help implements BaseCommand {
    @Override
    public String execute(CommandContext commandContext, String[] args) {
        if (args != null && args.length > 0) {
            return commandHelp(args[0]);
        } else {
            return mainHelp();
        }

    }


    private String commandHelp(String commandName) {

        if (!CommandHelper.hasCommand(commandName)) {
            return "no such command:" + commandName;
        }

        Class<?> clazz = CommandHelper.getCommandClass(commandName);

        final Cmd cmd = clazz.getAnnotation(Cmd.class);
        final TTable tTable = new TTable(new TTable.ColumnDefine[]{
                new TTable.ColumnDefine(TTable.Align.RIGHT),
                new TTable.ColumnDefine(80, false, TTable.Align.LEFT)
        });

        tTable.addRow("COMMAND NAME", commandName);

        if (null != cmd.example()) {
            tTable.addRow("EXAMPLE", drawExample(cmd));
        }

        return tTable.padding(1).rendering();
    }

    private String drawExample(Cmd cmd) {
        final StringBuilder drawExampleStringBuilder = new StringBuilder();
        for (String example : cmd.example()) {
            drawExampleStringBuilder.append(example).append("\n");
        }
        return drawExampleStringBuilder.toString();
    }

    /*
     * output main help
     */
    private String mainHelp() {

        final TTable tTable = new TTable(new TTable.ColumnDefine[]{
                new TTable.ColumnDefine(TTable.Align.RIGHT),
                new TTable.ColumnDefine(80, false, TTable.Align.LEFT)
        });

        final List<Class<?>> classes = CommandHelper.getAllCommandClass();

        Collections.sort(classes, new Comparator<Class<?>>() {

            @Override
            public int compare(Class<?> o1, Class<?> o2) {
                final Integer o1s = o1.getAnnotation(Cmd.class).sort();
                final Integer o2s = o2.getAnnotation(Cmd.class).sort();
                return o1s.compareTo(o2s);
            }

        });
        for (Class<?> clazz : classes) {

            if (clazz.isAnnotationPresent(Cmd.class)) {
                final Cmd cmd = clazz.getAnnotation(Cmd.class);
                tTable.addRow(cmd.name(), cmd.summary());
            }

        }

        return tTable.padding(1).rendering();
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章