Написание программного обеспечения с функционалом клиент-серверных утилит Windows, part 01

Приветствую.

Сегодня хотелось бы разобрать процесс написания клиент-серверных приложений, выполняющих функции стандартных утилит Windows, как то Telnet, TFTP, et cetera, et cetera на чистой Javа. Понятно, что ничего нового я не привнесу — все эти утилиты уже успешно работают не один год, но, полагаю, что происходит под капотом у них знают не все.

Именно об этом и пойдет речь под катом.

В этой статье, для того, что бы ее не затягивать, помимо общей информации я напишу только о Telnet сервере, но на данный момент есть еще материал по другим утилитам — он будет в дальнейших частях цикла.

Прежде всего, следует разобраться, что же такое Telnet, для чего он нужен и с чем его едят. Не буду дословно цитировать источники(если надо — в конце статьи прикреплю ссылку на материалы по теме), скажу лишь, что Telnet обеспечивает удаленный доступ к командной строке устройства. По большому, на этом его функционал заканчивается (о обращении к серверному порту умолчал осознанно, об этом позднее). Значит, для его реализации, нам нужно принять строку на клиенте, передать ее на сервер, попытаться передать ее в командную строку, считать ответ командной строки, если он есть, передать его обратно на клиент и вывести на экран, либо же, в случае возникновения ошибки, дать пользователю понять, что что-то не так.

Для реализации вышеизложенного, соответственно, нужно 2 рабочих класса, и некоторый тестовый класс, из которого мы будем запускать сервер и через который будет работать клиент.
Соответственно, на данный момент структура приложения включает в себя:

  • TelnetClient
  • TelnetClientTester
  • TelnetServer
  • TelnetServerTester

Пробежимся по каждому из них:

TelnetClient

Все, что должен уметь делать этот класс — отправлять полученные команды и показывать полученные ответы. Кроме того, нужно уметь подключаться к произвольному (о чем говорил выше) порту удаленного устройства и отключаться от него.

Для этого были реализованы следующие функции:

Функция, принимающая в качестве аргумента адрес сокета, открывающая соединение и запускающая потоки ввода и вывода (переменные потока объявлены выше, полные исходники — в конце статьи).

 public void run(String ip, int port)
    {
        try {
            Socket socket = new Socket(ip, port);
            InputStream sin = socket.getInputStream();
            OutputStream sout = socket.getOutputStream();
            Scanner keyboard = new Scanner(System.in);
            reader = new Thread(()->read(keyboard, sout));
            writer = new Thread(()->write(sin));
            reader.start();
            writer.start();
        }
        catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }

Перегрузка этой же функции, подключающаяся к порту по умолчанию — для телнета это 23


    public void run(String ip)
    {
        run(ip, 23);
    }

Функция читает символы с клавиатуры и отправляет их на выходной сокет — что характерно, в строчном, а не символьном режиме:


    private void read(Scanner keyboard, OutputStream sout)
    {
        try {
            String input = new String();
            while (true) {
                input = keyboard.nextLine();
                for (char i : (input + " n").toCharArray())
                    sout.write(i);
            }
        }
        catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }

Функция принимает данные с сокета и выводит их на экран


    private void write(InputStream sin)
    {
        try {
            int tmp;
            while (true){
                tmp = sin.read();
                System.out.print((char)tmp);
            }
        }
        catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }

Функция останавливает прием и передачу данных


    public void stop()
    {
        reader.stop();
        writer.stop();
    }
}

TelnetServer

Этот класс должен обладать функционалом принятия команды с сокета, отправки ее на исполнение и отправки ответа от команды обратно на сокет. В программе умышленно нет проверки входных данных, потому что во-первых, и в «коробочном телнете» есть возможность форматнуть диск сервера, а во-вторых, вопрос безопасности в этой статье опущен в принципе, и именно поэтому тут нет ни слова о шифровании или SSL.

Тут всего 2 функции(одна из них перегружена), и в целом это не очень хорошая практика, однако в рамках данной задачи мне показалось уместным оставить все как есть.

 boolean isRunning = true;
    public void run(int port)    {

        (new Thread(()->{ try {
            ServerSocket ss = new ServerSocket(port); // создаем сокет сервера и привязываем его к вышеуказанному порту
            System.out.println("Port "+port+" is waiting for connections");

            Socket socket = ss.accept();
            System.out.println("Connected");
            System.out.println();

            // Берем входной и выходной потоки сокета, теперь можем получать и отсылать данные клиенту.
            InputStream sin = socket.getInputStream();
            OutputStream sout = socket.getOutputStream();

            Map<String, String> env = System.getenv();
            String wayToTemp = env.get("TEMP") + "tmp.txt";
            for (int i :("Connectednnr".toCharArray()))
                sout.write(i);
            sout.flush();

            String buffer = new String();
            while (isRunning) {

                int intReader = 0;
                while ((char) intReader != 'n') {
                    intReader = sin.read();
                    buffer += (char) intReader;
                }


                final String inputToSubThread = "cmd /c " + buffer.substring(0, buffer.length()-2) + " 2>&1";


                new Thread(()-> {
                    try {

                        Process p = Runtime.getRuntime().exec(inputToSubThread);
                        InputStream out = p.getInputStream();
                        Scanner fromProcess = new Scanner(out);
                        try {

                            while (fromProcess.hasNextLine()) {
                                String temp = fromProcess.nextLine();
                                System.out.println(temp);
                                for (char i : temp.toCharArray())
                                    sout.write(i);
                                sout.write('n');
                                sout.write('r');
                            }
                        }
                        catch (Exception e) {
                            String output = "Something gets wrong... Err code: "+ e.getStackTrace();
                            System.out.println(output);
                            for (char i : output.toCharArray())
                                sout.write(i);
                            sout.write('n');
                            sout.write('r');
                        }

                        p.getErrorStream().close();
                        p.getOutputStream().close();
                        p.getInputStream().close();
                        sout.flush();

                    }
                    catch (Exception e) {
                        System.out.println("Error: " + e.getMessage());
                    }
                }).start();
                System.out.println(buffer);
                buffer = "";

            }
        }
        catch(Exception x) {
            System.out.println(x.getMessage());
        }})).start();

    }

Программа открывает серверный порт, читает с него данные, пока не встретит символ окончания команды, передает команду в новый процесс, а вывод из процесса перенаправлен в сокет. Все просто как автомат Калашникова.

Соответственно, для этой функции существует перегрузка с портом по умолчанию:

 public void run()
    {
        run(23);
    }

Ну и соответственно, функция, останавливающая сервер — тоже все тривиально, она прерывает вечный цикл, нарушая его условие.

    public void stop()
    {
        System.out.println("Server was stopped");
        this.isRunning = false;
    }

Тестовые классы я тут приводить не буду, они есть внизу — все что они делают — проверяют работоспособность публичных методов. Все есть на гите.

Резюмируя, за пару вечеров можно понять принципы действия основных консольных утилит. Теперь, когда мы телнетимся к удаленному компу, мы понимаем, что происходит — магия исчезла)

Итак, ссылки:
Все исходники были, есть и будут есть здесь
О Телнете
Еще о Телнете

Источник: habr.com