一、概述
上一篇文章介紹了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();
}
}