功能概述
- TelnetCodec用于实现在终端执行telnet指定的编解码功能。
功能分析
核心类TelnetCodec分析
主要成员变量分析
private static final byte[] UP = new byte[] {27, 91, 65}; //向上指令
private static final byte[] DOWN = new byte[] {27, 91, 66}; //向下指令
private static final List<?> ENTER = Arrays.asList( //换行指令 (参照ASCII码对照表)
new byte[] {'\r', '\n'} /* Windows Enter */,
new byte[] {'\n'} /* Linux Enter */);
private static final List<?> EXIT = Arrays.asList( //退出对应的字节数组,是个二维数组
new byte[] {3} /* Windows Ctrl+C */,
new byte[] {-1, -12, -1, -3, 6} /* Linux Ctrl+C */,
new byte[] {-1, -19, -1, -3, 6} /* Linux Pause */);
主要成员方法分析
获取字符编码
private static Charset getCharset(Channel channel) {
if (channel != null) {
Object attribute = channel.getAttribute(CHARSET_KEY); //获取配置的字符集名称
if (attribute instanceof String) { //判断是String类型还是Charset类型
try {
return Charset.forName((String) attribute); //尝试获取指定字符串的字符编码
} catch (Throwable t) {
logger.warn(t.getMessage(), t);
}
} else if (attribute instanceof Charset) {
return (Charset) attribute;
}
URL url = channel.getUrl(); //远程url
if (url != null) {
String parameter = url.getParameter(CHARSET_KEY);
if (StringUtils.isNotEmpty(parameter)) {
try {
return Charset.forName(parameter);
} catch (Throwable t) {
logger.warn(t.getMessage(), t);
}
}
}
}
try {
return Charset.forName(DEFAULT_CHARSET); //获取默认字符编码“UTF-8”
} catch (Throwable t) {
logger.warn(t.getMessage(), t);
}
return Charset.defaultCharset();
}
- 获取字符集的逻辑
- 从通道Channel的设置的属性值获取。
- 若没有,从通道的Url中获取。
- 若还没有,则取默认的字符集(默认字符集为UTF-8)。
响应内容编码
public void encode(Channel channel, ChannelBuffer buffer, Object message) throws IOException { //对响应的内容进行编码
if (message instanceof String) { //字符串类型处理
if (isClientSide(channel)) {
message = message + "\r\n"; //客户端输入的内容拼接上换行符
}
byte[] msgData = ((String) message).getBytes(getCharset(channel).name()); //若是字符串,直接根据字符集获取字节数组
buffer.writeBytes(msgData);
} else { //对象类型处理,交由父类来处理
super.encode(channel, buffer, message);
}
}
请求内容解码
protected Object decode(Channel channel, ChannelBuffer buffer, int readable, byte[] message) throws IOException { //对许多特殊字符,如换行符、退位符进行处理
if (isClientSide(channel)) { //若是客户端,直接将字节数组转换为字符串
return toString(message, getCharset(channel)); //获取字符集,并将字符数组转换为字符串
}
checkPayload(channel, readable);
if (message == null || message.length == 0) { //消息内容为空时,不再进行后续处理
return DecodeResult.NEED_MORE_INPUT;
}
if (message[message.length - 1] == '\b') { // Windows backspace echo ('\b'的值为8)
try {
boolean doublechar = message.length >= 3 && message[message.length - 3] < 0; // double byte char (判断逻辑:消息的长度大于3,并且倒数第三个元素数值小于0)
channel.send(new String(doublechar ? new byte[] {32, 32, 8, 8} : new byte[] {32, 8}, getCharset(channel).name())); //32对应的字符为空格
} catch (RemotingException e) {
throw new IOException(StringUtils.toString(e));
}
return DecodeResult.NEED_MORE_INPUT; //需要输入更多的字符
}
for (Object command : EXIT) {
if (isEquals(message, (byte[]) command)) { //判断是否包含"退出指令",若包含则关闭channel
if (logger.isInfoEnabled()) {
logger.info(new Exception("Close channel " + channel + " on exit command: " + Arrays.toString((byte[]) command)));
}
channel.close(); //执行退出指令时,会将通道channel关闭
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);
if (CollectionUtils.isEmpty(history)) {
return DecodeResult.NEED_MORE_INPUT;
}
Integer index = (Integer) channel.getAttribute(HISTORY_INDEX_KEY); //取出历史记录索引
Integer old = index;
if (index == null) {
index = history.size() - 1; //若没设置索引,则取列表中的最后一条
} else {
if (up) { //执行向上操作
index = index - 1;
if (index < 0) {
index = history.size() - 1; //如果索引小于0,则轮询到最后一条
}
} else { //执行向下操作
index = index + 1;
if (index > history.size() - 1) {//如果所以大于最后一条,则轮询到第一条
index = 0;
}
}
}
if (old == null || !old.equals(index)) { //表示:old不为空或old与index不相等
channel.setAttribute(HISTORY_INDEX_KEY, index);
String value = history.get(index);
if (old != null && old >= 0 && old < history.size()) {
String ov = history.get(old);
StringBuilder buf = new StringBuilder();
for (int i = 0; i < ov.length(); i++) {
buf.append("\b");
}
for (int i = 0; i < ov.length(); i++) {
buf.append(" ");
}
for (int i = 0; i < ov.length(); i++) {
buf.append("\b");
}
value = buf.toString() + value;
}
try {
channel.send(value);
} 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 " + command));
}
channel.close();
return null;
}
}
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); //使用过后,将HISTORY_INDEX_KEY历史记录所以移除
if (CollectionUtils.isNotEmpty(history) && 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;
}
- 上下键本意上是对历史指令的支持,但是不同平台的支持不一样,比如Mac就会附加的UP的字节数组为[27,91,65,13,10]把换行符加上了,就导致不是以UP结尾,就失效了。
辅助类AbstractCodec分析
主要成员方法分析
检查负载大小
protected static void checkPayload(Channel channel, long size) throws IOException { //检查负载大小(请求体的数据大小)
int payload = Constants.DEFAULT_PAYLOAD; //默认负载大小为8 * 1024 * 1024,即8M;
if (channel != null && channel.getUrl() != null) {
payload = channel.getUrl().getParameter(Constants.PAYLOAD_KEY, Constants.DEFAULT_PAYLOAD);
}
if (payload > 0 && size > payload) { //超过负载,则抛出异常
ExceedPayloadLimitException e = new ExceedPayloadLimitException(
"Data length too large: " + size + ", max payload: " + payload + ", channel: " + channel);
logger.error(e);
throw e;
}
}
判断是否是客户端
protected boolean isClientSide(Channel channel) { //判断是否是客户端
String side = (String) channel.getAttribute(SIDE_KEY);
if (CLIENT_SIDE.equals(side)) { //判断通道中属性SIDE_KEY的缓存值
return true;
} else if (SERVER_SIDE.equals(side)) {
return false;
} else {
InetSocketAddress address = channel.getRemoteAddress();
URL url = channel.getUrl(); //远程url,即服务端的url
boolean isClient = url.getPort() == address.getPort()
&& NetUtils.filterLocalHost(url.getIp()).equals(
NetUtils.filterLocalHost(address.getAddress()
.getHostAddress())); //比较逻辑:当通道中的远程地址与url地址比较,如果远程是服务端,那么当前就是客户端,反之类推(也可以按localAddress来判断)
channel.setAttribute(SIDE_KEY, isClient ? CLIENT_SIDE
: SERVER_SIDE);
return isClient;
}
}
辅助类TelnetHandlerAdapter分析
主要成员方法分析
指令调用
public String telnet(Channel channel, String message) throws RemotingException { //Telnet指令处理
String prompt = channel.getUrl().getParameterAndDecoded(Constants.PROMPT_KEY, Constants.DEFAULT_PROMPT);
boolean noprompt = message.contains("--no-prompt");
message = message.replace("--no-prompt", ""); //telnet提示键(如果做了配置,将不显示dubbo>)
StringBuilder buf = new StringBuilder();
message = message.trim();
String command;
if (message.length() > 0) { //message不为空字符串时,解析指令和执行的内容(输入回车键会收到空字符串)
int i = message.indexOf(' '); //接收指令时,收到的message不包含提示符,如dubbo> ls,收到的message为ls
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) { //telnet输入回车时,写到通道的内容为空字符串,不会进入此处逻辑
if (extensionLoader.hasExtension(command)) { //将命令名作为SPI的扩展名
if (commandEnabled(channel.getUrl(), command)) {
try {
String result = extensionLoader.getExtension(command).telnet(channel, message); //获取指定命令的实例,并将结果写到channel
if (result == null) {
return null;
}
buf.append(result);
} catch (Throwable t) {
buf.append(t.getMessage());
}
} else {
buf.append("Command: ");
buf.append(command);
buf.append(" disabled");
}
} else {
buf.append("Unsupported command: ");
buf.append(command);
}
}
if (buf.length() > 0) {
buf.append("\r\n"); //\r 回车符,\n 换行符,用回车符换行符结束指令
}
if (StringUtils.isNotEmpty(prompt) && !noprompt) { //提示键处理:若提示键内容不为空且没有禁用,则作对应展示
buf.append(prompt);
}
return buf.toString(); //响应给telnet客户端的内容
}
问题点答疑
-
TelnetHandler处理接口有多个实现类,即telnet命令会有多个,如ListTelnetHandler列表指令,InvokeTelnetHandler调用指令,那么不同指令是怎么分发的?
- 解答:通过实现类TelnetHandlerAdapter将输入的字符串message进行解析,获取到指令名以及执行内容,再把指令名称作为SPI的扩展名,根据SPI机制获取到对应的实例。
-
telnet中的提示键可以设置自定义提示符吗,怎么设置?
- 解答:可以自定义提示符,通过dubbo:parameter/参数配置设置。
归纳总结
- Telnet协议是TCP/IP协议族中的一员,是Internet远程登陆服务的标准协议。Telnet协议的目的是提供一个相对通用的,双向的,面向八位字节的通信方法,允许界面终端设备和面向终端的过程能通过一个标准过程进行互相交互。应用Telnet协议能够把本地用户所使用的计算机变成远程主机系统的一个终端