Pisanie oprogramowania z funkcjonalnością narzędzi klient-serwer Windows, część 01

Witamy.

Dzisiaj chciałbym przyjrzeć się procesowi pisania aplikacji klient-serwer, które realizują funkcje standardowych narzędzi Windows, takich jak Telnet, TFTP i tak dalej, i tak dalej, w czystej Javie. Oczywiste jest, że nie wniosę nic nowego - wszystkie te narzędzia działają z powodzeniem od ponad roku, ale uważam, że nie wszyscy wiedzą, co dzieje się pod maską.

Dokładnie to zostanie omówione w ramach cięcia.

W tym artykule, żeby nie przeciągać, poza informacjami ogólnymi napiszę tylko o serwerze Telnet, ale w tej chwili jest też materiał o innych narzędziach - będzie w dalszych częściach serii.

Przede wszystkim musisz dowiedzieć się, czym jest Telnet, do czego jest potrzebny i do czego służy. Nie będę cytował źródeł dosłownie (jeśli zajdzie taka potrzeba, na końcu artykułu załączę link do materiałów na ten temat), powiem jedynie, że Telnet zapewnia zdalny dostęp do linii poleceń urządzenia. W zasadzie na tym kończy się jego funkcjonalność (celowo przemilczałem kwestię dostępu do portu serwera; o tym później). Oznacza to, że aby to zaimplementować, musimy zaakceptować linię na kliencie, przekazać ją do serwera, spróbować przekazać ją do linii poleceń, przeczytać odpowiedź linii poleceń, jeśli taka istnieje, przekazać ją z powrotem do klienta i wyświetl go na ekranie lub, w przypadku błędów, daj znać użytkownikowi, że coś jest nie tak.

Aby zaimplementować powyższe, odpowiednio potrzebujemy 2 klas roboczych i pewnej klasy testowej, z której uruchomimy serwer i przez którą będzie działał klient.
W związku z tym na chwilę obecną struktura aplikacji obejmuje:

  • Klient Telnet
  • Tester klienta Telnet
  • Serwer Telnet
  • Tester serwera Telnet

Przejrzyjmy każdy z nich:

Klient Telnet

Wszystko, co ta klasa powinna umieć, to wysyłać otrzymane polecenia i wyświetlać otrzymane odpowiedzi. Ponadto musisz mieć możliwość połączenia się z dowolnym (jak wspomniano powyżej) portem zdalnego urządzenia i rozłączenia się z nim.

Aby to osiągnąć zaimplementowano następujące funkcje:

Funkcja pobierająca jako argument adres gniazda, otwiera połączenie oraz uruchamia strumienie wejściowe i wyjściowe (zmienne strumieniowe deklarujemy powyżej, pełne źródła znajdują się na końcu artykułu).

 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());
        }
    }

Przeciążanie tej samej funkcji, łączenie się z portem domyślnym - dla telnetu jest to 23


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

Funkcja odczytuje znaki z klawiatury i wysyła je do gniazda wyjściowego - co jest typowe w trybie liniowym, a nie znakowym:


    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());
        }
    }

Funkcja odbiera dane z gniazda i wyświetla je na ekranie


    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());
        }
    }

Funkcja zatrzymuje odbiór i transmisję danych


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

Serwer Telnet

Klasa ta musi posiadać funkcjonalność odbierania polecenia z gniazda, wysyłania go do wykonania i wysyłania odpowiedzi z polecenia z powrotem do gniazda. Program celowo nie sprawdza wprowadzanych danych, ponieważ po pierwsze nawet w „pudełkowym telnecie” można sformatować dysk serwera, a po drugie, w tym artykule kwestia bezpieczeństwa została w zasadzie pominięta i dlatego nie ma słowo o szyfrowaniu lub SSL.

Są tylko 2 funkcje (jedna z nich jest przeciążona) i w sumie nie jest to zbyt dobra praktyka, ale na potrzeby tego zadania wydało mi się właściwe zostawić wszystko tak jak jest.

 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();

    }

Program otwiera port serwera, odczytuje z niego dane do momentu napotkania znaku końca polecenia, przekazuje polecenie do nowego procesu i przekierowuje wyjście z procesu do gniazda. Wszystko jest tak proste, jak karabin szturmowy Kałasznikowa.

W związku z tym występuje przeciążenie tej funkcji przy domyślnym porcie:

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

Cóż, odpowiednio, funkcja zatrzymująca serwer jest również trywialna, przerywa wieczną pętlę, naruszając jej stan.

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

Nie będę tutaj podawać klas testowych, są one poniżej - jedyne, co robią, to sprawdzają funkcjonalność metod publicznych. Wszystko jest na gicie.

Podsumowując, w ciągu kilku wieczorów można zrozumieć zasady działania głównych narzędzi konsoli. Teraz, kiedy łączymy się telenetem z komputerem zdalnym, rozumiemy, co się dzieje - magia zniknęła)

Zatem linki:
Wszystkie źródła były, są i będą tutaj
O Telnecie
Więcej o Telnecie

Źródło: www.habr.com

Dodaj komentarz