Dubbo’s TelnetCodec source code analysis

Function Overview

  • TelnetCodec is used to implement the codec function specified by telnet on the terminal.

Functional analysis

Core class TelnetCodec analysis

Analysis of main member variables

private static final byte[] UP = new byte[] {<!-- -->27, 91, 65}; //Up command
 
private static final byte[] DOWN = new byte[] {<!-- -->27, 91, 66}; //Downward command

private static final List<?> ENTER = Arrays.asList( //Newline command (refer to ASCII code comparison table)
        new byte[] {<!-- -->'\r', '\\
'} /* Windows Enter */,
        new byte[] {<!-- -->'\\
'} /* Linux Enter */);

private static final List<?> EXIT = Arrays.asList( //Exit the corresponding byte array, which is a two-dimensional array
        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 */);

Main Member Method Analysis

Get character encoding

private static Charset getCharset(Channel channel) {<!-- -->
    if (channel != null) {<!-- -->
        Object attribute = channel.getAttribute(CHARSET_KEY); //Get the configured character set name
        if (attribute instanceof String) {<!-- --> //Determine whether it is a String type or a Charset type
            try {<!-- -->
                return Charset.forName((String) attribute); //Try to get the character encoding of the specified string
            } catch (Throwable t) {<!-- -->
                logger.warn(t.getMessage(), t);
            }
        } else if (attribute instanceof Charset) {<!-- -->
            return (Charset) attribute;
        }
        URL url = channel.getUrl(); //remote 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); //Get the default character encoding "UTF-8"
    } catch (Throwable t) {<!-- -->
        logger.warn(t.getMessage(), t);
    }
    return Charset.defaultCharset();
}
  • The logic of getting the character set
    • Obtained from the property value set by the channel Channel.
    • If not, get it from the URL of the channel.
    • If not, the default character set is used (the default character set is UTF-8).

Response content encoding

public void encode(Channel channel, ChannelBuffer buffer, Object message) throws IOException {<!-- --> //Encode the response content
    if (message instanceof String) {<!-- --> //String type processing
        if (isClientSide(channel)) {<!-- -->
            message = message + "\r\\
"; //The content input by the client is spliced with line breaks
        }
        byte[] msgData = ((String) message).getBytes(getCharset(channel).name()); //If it is a string, get the byte array directly according to the character set
        buffer.writeBytes(msgData);
    } else {<!-- --> //Object type processing is handled by the parent class
        super.encode(channel, buffer, message);
    }
}

Request content decoding

protected Object decode(Channel channel, ChannelBuffer buffer, int readable, byte[] message) throws IOException {<!-- --> //Process many special characters, such as newline characters and backspace characters
    if (isClientSide(channel)) {<!-- --> //If it is the client, directly convert the byte array into a string
        return toString(message, getCharset(channel)); //Get the character set and convert the character array to a string
    }
    checkPayload(channel, readable);
    if (message == null || message.length == 0) {<!-- --> //When the message content is empty, no subsequent processing will be performed
        return DecodeResult.NEED_MORE_INPUT;
    }

    if (message[message.length - 1] == '\b') {<!-- --> // Windows backspace echo (the value of '\b' is 8)
        try {<!-- -->
            boolean doublechar = message.length >= 3 & amp; & amp; message[message.length - 3] < 0; // double byte char (judgment logic: the length of the message is greater than 3, and the value of the third to last element is less than 0 )
            channel.send(new String(doublechar ? new byte[] {<!-- -->32, 32, 8, 8} : new byte[] {<!-- -->32, 8}, getCharset(channel ).name())); //The character corresponding to 32 is a space
        } catch (RemotingException e) {<!-- -->
            throw new IOException(StringUtils.toString(e));
        }
        return DecodeResult.NEED_MORE_INPUT; //More characters need to be entered
    }

    for (Object command : EXIT) {<!-- -->
        if (isEquals(message, (byte[]) command)) {<!-- --> //Determine whether the "exit command" is included, and if so, close the channel
            if (logger.isInfoEnabled()) {<!-- -->
                logger.info(new Exception("Close channel " + channel + " on exit command: " + Arrays.toString((byte[]) command)));
            }
            channel.close(); //When executing the exit command, the channel channel will be closed
            return null;
        }
    }

    boolean up = endsWith(message, UP);
    boolean down = endsWith(message, DOWN);
    if (up || down) {<!-- --> //Up and down key processing: processing of historical records
        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); //Get the history index
        Integer old = index;
        if (index == null) {<!-- -->
            index = history.size() - 1; //If no index is set, take the last item in the list
        } else {<!-- -->
            if (up) {<!-- --> //Perform upward operation
                index = index - 1;
                if (index < 0) {<!-- -->
                    index = history.size() - 1; //If the index is less than 0, poll to the last one
                }
            } else {<!-- --> //Perform downward operation
                index = index + 1;
                if (index > history.size() - 1) {<!-- -->//If it is greater than the last one, poll to the first one
                    index = 0;
                }
            }
        }
        if (old == null || !old.equals(index)) {<!-- --> //Indicates: old is not empty or old and index are not equal
            channel.setAttribute(HISTORY_INDEX_KEY, index);
            String value = history.get(index);
            if (old != null & amp; & amp; old >= 0 & amp; & amp; 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 it is the terminator, determine whether it is equal to the terminator.
            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)) {<!-- -->//If it is a newline character, determine whether it ends with a newline character
            enter = (byte[]) command; //Save the newline character
            break;
        }
    }
    if (enter == null) {<!-- --> //There needs to be a newline at the end, if not, we will not proceed.
        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); //After use, remove the HISTORY_INDEX_KEY history record
    if (CollectionUtils.isNotEmpty(history) & amp; & amp; index != null & amp; & amp; index >= 0 & amp; & amp; 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); //After the command is executed normally, the channel's history command list will be written
        }
        if (history.isEmpty()) {<!-- -->
            history.addLast(result); //Write the history command list
        } else if (!result.equals(history.getLast())) {<!-- -->
            history.remove(result);
            history.addLast(result);
            if (history.size() > 10) {<!-- -->
                history.removeFirst();
            }
        }
    }
    return result;
}
  • The up and down keys are intended to support historical commands, but different platforms have different support. For example, Mac will attach the byte array of UP to [27,91,65,13,10] and add the newline character. As a result, if it does not end with UP, it will become invalid.

Auxiliary class AbstractCodec analysis

Main Member Method Analysis

Check payload size

protected static void checkPayload(Channel channel, long size) throws IOException {<!-- --> //Check the payload size (data size of the request body)
    int payload = Constants.DEFAULT_PAYLOAD; //The default payload size is 8 * 1024 * 1024, which is 8M;
    if (channel != null & amp; & amp; channel.getUrl() != null) {<!-- -->
        payload = channel.getUrl().getParameter(Constants.PAYLOAD_KEY, Constants.DEFAULT_PAYLOAD);
    }
    if (payload > 0 & amp; & amp; size > payload) {<!-- --> //If the load is exceeded, an exception will be thrown
        ExceedPayloadLimitException e = new ExceedPayloadLimitException(
                "Data length too large: " + size + ", max payload: " + payload + ", channel: " + channel);
        logger.error(e);
        throw e;
    }
}

Determine whether it is a client

protected boolean isClientSide(Channel channel) {<!-- --> //Determine whether it is a client
    String side = (String) channel.getAttribute(SIDE_KEY);
    if (CLIENT_SIDE.equals(side)) {<!-- --> //Determine the cache value of the attribute SIDE_KEY in the channel
        return true;
    } else if (SERVER_SIDE.equals(side)) {<!-- -->
        return false;
    } else {<!-- -->
        InetSocketAddress address = channel.getRemoteAddress();
        URL url = channel.getUrl(); //Remote url, that is, the url of the server
        boolean isClient = url.getPort() == address.getPort()
                 & amp; & amp; NetUtils.filterLocalHost(url.getIp()).equals(
                NetUtils.filterLocalHost(address.getAddress()
                        .getHostAddress())); //Comparison logic: When the remote address in the channel is compared with the url address, if the remote is the server, then the current one is the client, and vice versa (it can also be judged by localAddress)
        channel.setAttribute(SIDE_KEY, isClient ? CLIENT_SIDE
            : SERVER_SIDE);
        return isClient;
    }
}

Analysis of auxiliary class TelnetHandlerAdapter

Main Member Method Analysis

Instruction call

public String telnet(Channel channel, String message) throws RemotingException {<!-- --> //Telnet command processing
    String prompt = channel.getUrl().getParameterAndDecoded(Constants.PROMPT_KEY, Constants.DEFAULT_PROMPT);
    boolean noprompt = message.contains("--no-prompt");
    message = message.replace("--no-prompt", ""); //telnet prompt key (if configured, dubbo> will not be displayed)
    StringBuilder buf = new StringBuilder();
    message = message.trim();
    String command;
    if (message.length() > 0) {<!-- --> //When message is not an empty string, parse the instruction and execution content (enter the Enter key to receive an empty string)
        int i = message.indexOf(' '); //When receiving instructions, the received message does not contain the prompt, such as dubbo> ls, the received message is ls
        if (i > 0) {<!-- --> //Split commands and parameters
            command = message.substring(0, i).trim();
            message = message.substring(i + 1).trim();
        } else {<!-- -->
            command = message;
            message = "";
        }
    } else {<!-- -->
        command = "";
    }
    if (command.length() > 0) {<!-- --> //When telnet enters Enter, the content written to the channel is an empty string, and the logic here will not be entered.
        if (extensionLoader.hasExtension(command)) {<!-- --> //Use the command name as the extension of SPI
            if (commandEnabled(channel.getUrl(), command)) {<!-- -->
                try {<!-- -->
                    String result = extensionLoader.getExtension(command).telnet(channel, message); //Get the instance of the specified command and write the result to the 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\\
"); //\r carriage return character, \\
 line feed character, end the command with carriage return character and line feed character
    }
    if (StringUtils.isNotEmpty(prompt) & amp; & amp; !noprompt) {<!-- --> //Prompt key processing: If the content of the prompt key is not empty and not disabled, the corresponding display will be made
        buf.append(prompt);
    }
    return buf.toString(); //Response to the telnet client
}

Q&A

  • The TelnetHandler processing interface has multiple implementation classes, that is, there are multiple telnet commands, such as ListTelnetHandler list instructions and InvokeTelnetHandler call instructions. So how are different instructions distributed?

    • Answer: Parse the input string message by implementing the class TelnetHandlerAdapter, obtain the instruction name and execution content, and then use the instruction name as the extension of SPI to obtain the corresponding instance according to the SPI mechanism.
  • Can I set a custom prompt for the prompt key in telnet? How to set it?

    • Answer: You can customize the prompt and set it through dubbo:parameter/parameter configuration.

Summary

  • The Telnet protocol is a member of the TCP/IP protocol suite and is the standard protocol for Internet remote login services. The purpose of the Telnet protocol is to provide a relatively universal, bidirectional, octet-oriented communication method that allows interface terminal devices and terminal-oriented processes to interact with each other through a standard process. Using the Telnet protocol can turn the computer used by the local user into a terminal of the remote host system