Написання програмного забезпечення з функціоналом клієнт-серверних утиліт 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.

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

 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

Додати коментар або відгук