写在前面
在dubbo之telnet使用 一文中我们介绍了如何使用dubbo 提供的telnet server功能,本文一起来看下其具体实现,对应的源码模块如下图:
该功能模块对应的顶层接口是com.alibaba.dubbo.remoting.telnet.TelnetHandler
,不同的telnet命令对应了不同的实现子类,如command包下对应的命令分别是clear
,exit
,help
,log
,status
,如下图红框对部分命令的使用:
客户端方发送的消息需要经过适配,才能执行到对应的子类,这个适配的类是com.alibaba.dubbo.remoting.telnet.support.TelnetHandlerAdapter
,而发送的消息需要经过编解码类com.alibaba.dubbo.remoting.telnet.codec.TelnetCodec
的解码,整个消息发送的过程如下图:
我们按照从左向右的顺序来看。
1:TelnetCodec
为什么要有不同的编解码器:不同协议下的数据格式是不一样的,因此解析其中我们需要的业务数据方式也是不一样的,所以想要从不同协议格式中获取业务数据,就需要不同的编解码器。
1.1:解码
源码如下:
class FakeCls {
// com.alibaba.dubbo.remoting.telnet.codec.TelnetCodec.decode(channel, buffer, readable, message)
protected Object decode(Channel channel, ChannelBuffer buffer, int readable, byte[] message) throws IOException {
// false
if (isClientSide(channel)) {
return toString(message, getCharset(channel));
}
// 检查消息体大小
checkPayload(channel, readable);
// false
if (message == null || message.length == 0) {
return DecodeResult.NEED_MORE_INPUT;
}
// 处理退格
if (message[message.length - 1] == '\b') { // Windows backspace echo
try {
// 32 空格 8 退格
boolean doublechar = message.length >= 3 && message[message.length - 3] < 0; // double byte char
channel.send(new String(doublechar ? new byte[]{32, 32, 8, 8} : new byte[]{32, 8}, getCharset(channel).name()));
} catch (RemotingException e) {
throw new IOException(StringUtils.toString(e));
}
return DecodeResult.NEED_MORE_INPUT;
}
// 退出
for (Object command : EXIT) {
if (isEquals(message, (byte[]) command)) {
if (logger.isInfoEnabled()) {
logger.info(new Exception("Close channel " + channel + " on exit command: " + Arrays.toString((byte[]) command)));
}
channel.close();
return null;
}
}
// 键盘的上下键 ↑ ↓,用于查看历史使用的指令
boolean up = endsWith(message, UP);
boolean down = endsWith(message, DOWN);
if (up || down) {
LinkedList<String> history = (LinkedList<String>) channel.getAttribute(HISTORY_LIST_KEY);
// snip code
}
// 需要更多输入
return DecodeResult.NEED_MORE_INPUT;
}
for (Object command : EXIT) {
if (isEquals(message, (byte[]) command)) {
if (logger.isInfoEnabled()) {
logger.info(new Exception("Close channel " + channel + " on exit command " + command));
}
channel.close();
return null;
}
}
// 判断是否是回车结尾,如果不是,则说明用户还没有输入完毕
/*
0 = {byte[2]@4590}
0 = 13
1 = 10
1 = {byte[1]@4591}
0 = 10
*/
byte[] enter = null;
for (Object command : ENTER) {
if (endsWith(message, (byte[]) command)) {
enter = (byte[]) command;
break;
}
}
// 不是回车,则返回需要更多输入
if (enter == null) {
return DecodeResult.NEED_MORE_INPUT;
}
LinkedList<String> history = (LinkedList<String>) channel.getAttribute(HISTORY_LIST_KEY);
Integer index = (Integer) channel.getAttribute(HISTORY_INDEX_KEY);
channel.removeAttribute(HISTORY_INDEX_KEY);
if (history != null && !history.isEmpty() && index != null && index >= 0 && index < history.size()) {
String value = history.get(index);
if (value != null) {
byte[] b1 = value.getBytes();
byte[] b2 = new byte[b1.length + message.length];
System.arraycopy(b1, 0, b2, 0, b1.length);
System.arraycopy(message, 0, b2, b1.length, message.length);
message = b2;
}
}
// 获取用户输入的内容
String result = toString(message, getCharset(channel));
// 存档到历史记录中,用于后续通过上下键↑ ↓查看历史命令记录
if (result.trim().length() > 0) {
if (history == null) {
history = new LinkedList<String>();
channel.setAttribute(HISTORY_LIST_KEY, history);
}
if (history.isEmpty()) {
history.addLast(result);
} else if (!result.equals(history.getLast())) {
history.remove(result);
history.addLast(result);
if (history.size() > 10) {
history.removeFirst();
}
}
}
// 返回用户输入的命令
return result;
}
}
2:TelnetHandler
telnet命令处理器的顶层接口,源码如下:
@SPI
public interface TelnetHandler {
String telnet(Channel channel, String message) throws RemotingException;
}
对应的dubbo spi文件如下图:
3:TelnetHandlerAdapter
该类是telnet处理器的适配器类,用来解析发送的telnet命令,然后通过dubbo SPI机制获取对应的telnet实现类,而后调用其逻辑执行具体处理,源码如下:
public class TelnetHandlerAdapter extends ChannelHandlerAdapter implements TelnetHandler {
// dubbo com.alibaba.dubbo.common.extension.ExtensionLoader
// 2022年3月26日17:17:22
private final ExtensionLoader<TelnetHandler> extensionLoader = ExtensionLoader.getExtensionLoader(TelnetHandler.class);
@Override
public String telnet(Channel channel, String message) throws RemotingException {
// 2022年3月26日17:28:47
String prompt = channel.getUrl().getParameterAndDecoded(Constants.PROMPT_KEY, Constants.DEFAULT_PROMPT);
// 2022年3月28日10:16:39
boolean noprompt = message.contains("--no-prompt");
// 替换掉--no-prompt,获取命令信息,如ls -l --no-prompt 变为 ls -l
message = message.replace("--no-prompt", "");
StringBuilder buf = new StringBuilder();
message = message.trim();
String command;
// 获取命令和命令参数 如ls -l,command="ls" message = "-l"
if (message.length() > 0) {
int i = message.indexOf(' ');
if (i > 0) {
command = message.substring(0, i).trim();
message = message.substring(i + 1).trim();
} else {
command = message;
message = "";
}
} else {
command = "";
}
if (command.length() > 0) {
// 如果存在的dubbo SPI实现类的话
if (extensionLoader.hasExtension(command)) {
// 2022年3月28日14:07:52
if (commandEnabled(channel.getUrl(), command)) {
try {
// 2022年3月28日15:08:57
String result = extensionLoader.getExtension(command).telnet(channel, message);
if (result == null) {
return null;
}
buf.append(result);
} catch (Throwable t) {
buf.append(t.getMessage());
}
// 命令不支持,返回Command: {command} disabled
} else {
buf.append("Command: ");
buf.append(command);
buf.append(" disabled");
}
// 不存在对应的Dubbo SPI扩展类,说明不支持该命令
} else {
buf.append("Unsupported command: ");
buf.append(command);
}
}
// 拼接换行到结果
if (buf.length() > 0) {
buf.append("\r\n");
}
// 拼接命令提示符到结果
// 比如返回的结果是xxx,则最终用户看到的效果是:
/*
xxx
dubbo>
*/
if (prompt != null && prompt.length() > 0 && !noprompt) {
buf.append(prompt);
}
return buf.toString();
}
}
2022年3月26日17:17:22
处关于dubbo spi机制可以参考dubbo之SPI分析 。2022年3月26日17:28:47
处是处理提示符的情况,提示符默认是dubbo>
,如下图:
也可以通过<dubbo:provider ... prompt="dongshidaddy--"/>
,这里就是将提示符修改为dongshidaddy--
,如下图:
当我们不进行任何配置时默认的提示符如下图:
2022年3月28日10:16:39
查看是否使用了--no-prompt
参数,如果是使用了该参数则在返回结果中不会显示命令提示符,如下图:
2022年3月28日14:07:52
处是判断当前命令是否支持,如果是命令不支持,会返回Command: log disabled
。具体参考3.1:TelnetHandlerAdapter
。2022年3月28日15:08:57
处是根据命令获取对应的Dubbo SPI扩展类,并调用telnet方法获取命令执行的结果,具体参考4:TelnetHandler命令实现
。
3.1:TelnetHandlerAdapter
源码如下:
// com.alibaba.dubbo.remoting.telnet.support.TelnetHandlerAdapter.commandEnabled
class FakeCls {
private boolean commandEnabled(URL url, String command) {
boolean commandEnable = false;
// 获取url上的telnet值,该值用于配置支持的命令,不配置或者是空的话代表全局支持
String supportCommands = url.getParameter(Constants.TELNET);
// 全部支持
if (StringUtils.isEmpty(supportCommands)) {
commandEnable = true;
// 部分支持,则判断当前命令是否在部分支持的命令中,如<dubbo:provider ... telnet="ls,status"/>,仅支持ls,status命令
} else {
// 逗号分隔,依次判断
String[] commands = Constants.COMMA_SPLIT_PATTERN.split(supportCommands);
for (String c : commands) {
if (command.equals(c)) {
commandEnable = true;
break;
}
}
}
return commandEnable;
}
}
4:TelnetHandler命令实现
对应的DUBBO xml配置如下图:
我们以invoke=org.apache.dubbo.rpc.protocol.dubbo.telnet.InvokeTelnetHandler
为例来看下,具体参考4.1:InvokeTelnetHandler
。
4.1:InvokeTelnetHandler
当我们使用命令invoke 命令调用提供者的服务时,就会使用InvokeTelnetHandler,如invoke XxxService.xxxMethod(1234, "abcd", {"prop" : "value"}) 或者 invoke com.xxx.XxxService.xxxMethod(1234, "abcd", {"prop" : "value"})
。
源码如下:
class FakeCls {
// com.alibaba.dubbo.rpc.protocol.dubbo.telnet.InvokeTelnetHandler.telnet
public String telnet(Channel channel, String message) {
// 发送的telnet,如invoke dongshi.daddy.service.scoperemote.ScopeRemoteService.sayHi("from telnet client")
if (message == null || message.length() == 0) {
return "Please input method name, eg: \r\ninvoke xxxMethod(1234, \"abcd\", {\"prop\" : \"value\"})\r\ninvoke XxxService.xxxMethod(1234, \"abcd\", {\"prop\" : \"value\"})\r\ninvoke com.xxx.XxxService.xxxMethod(1234, \"abcd\", {\"prop\" : \"value\"})";
}
StringBuilder buf = new StringBuilder();
// 通过telnet.service获取默认的服务
String service = (String) channel.getAttribute(ChangeTelnetHandler.SERVICE_KEY);
if (service != null && service.length() > 0) {
buf.append("Use default service " + service + ".\r\n");
}
// 获取方法参数的开始位置
int i = message.indexOf("(");
// 如果是不存在(,或者是没有以)结尾
if (i < 0 || !message.endsWith(")")) {
return "Invalid parameters, format: service.method(args)";
}
// 获取方法名称,如invoke dongshi.daddy.service.scoperemote.ScopeRemoteService.sayHi("from telnet client") -> dongshi.daddy.service.scoperemote.ScopeRemoteService.sayHi
String method = message.substring(0, i).trim();
// 获取方法参数,即(和)中间的内容
String args = message.substring(i + 1, message.length() - 1).trim();
// 获取服务名和方法名
i = method.lastIndexOf(".");
if (i >= 0) {
service = method.substring(0, i).trim();
method = method.substring(i + 1).trim();
}
List<Object> list;
try {
list = JSON.parseArray("[" + args + "]", Object.class);
} catch (Throwable t) {
return "Invalid json argument, cause: " + t.getMessage();
}
// 服务类对应的invoker
Invoker<?> invoker = null;
// 要调用的服务方法
Method invokeMethod = null;
// 通过Exporter获取目标invoker
for (Exporter<?> exporter : DubboProtocol.getDubboProtocol().getExporters()) {
if (service == null || service.length() == 0) {
invokeMethod = findMethod(exporter, method, list);
if (invokeMethod != null) {
invoker = exporter.getInvoker();
break;
}
} else {
if (service.equals(exporter.getInvoker().getInterface().getSimpleName())
|| service.equals(exporter.getInvoker().getInterface().getName())
|| service.equals(exporter.getInvoker().getUrl().getPath())) {
invokeMethod = findMethod(exporter, method, list);
invoker = exporter.getInvoker();
break;
}
}
}
if (invoker != null) {
if (invokeMethod != null) {
try {
Object[] array = PojoUtils.realize(list.toArray(), invokeMethod.getParameterTypes(), invokeMethod.getGenericParameterTypes());
RpcContext.getContext().setLocalAddress(channel.getLocalAddress()).setRemoteAddress(channel.getRemoteAddress());
long start = System.currentTimeMillis();
// 调用获取结果
Object result = invoker.invoke(new RpcInvocation(invokeMethod, array)).recreate();
// 记录程序执行耗时信息
long end = System.currentTimeMillis();
buf.append(JSON.toJSONString(result));
buf.append("\r\nelapsed: ");
buf.append(end - start);
buf.append(" ms.");
} catch (Throwable t) {
return "Failed to invoke method " + invokeMethod.getName() + ", cause: " + StringUtils.toString(t);
}
} else {
buf.append("No such method " + method + " in service " + service);
}
} else {
buf.append("No such service " + service);
}
return buf.toString();
}
}