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

Kontynuując trwającą serię artykułów poświęconych niestandardowym implementacjom narzędzi konsoli Windows, nie możemy nie wspomnieć o TFTP (Trivial File Transfer Protocol) - prostym protokole przesyłania plików.

Tak jak ostatnim razem, omówmy pokrótce teorię, zobaczmy kod realizujący funkcjonalność podobną do wymaganej i przeanalizujmy ją. Więcej szczegółów - pod rozcięciem

Nie będę kopiował-wklejał informacji referencyjnych, do których linki tradycyjnie znajdują się na końcu artykułu, powiem jedynie, że w swej istocie TFTP jest uproszczoną odmianą protokołu FTP, w którym ustawienie kontroli dostępu ma został usunięty i tak naprawdę nie ma tu nic poza poleceniami odbierania i przesyłania pliku. Aby jednak uczynić naszą implementację nieco bardziej elegancką i dostosowaną do obecnych zasad pisania kodu, składnia została nieco zmieniona - nie zmienia to zasady działania, ale interfejs IMHO staje się trochę bardziej logiczny i łączy w sobie pozytywne aspekty FTP i TFTP.

W szczególności po uruchomieniu klient żąda adresu IP serwera i portu, na którym otwarty jest niestandardowy protokół TFTP (ze względu na niezgodność ze standardowym protokołem uznałem za stosowne pozostawić użytkownikowi możliwość wyboru portu), po czym następuje następuje połączenie, w wyniku którego klient może wysłać jedno z poleceń - get lub put, aby odebrać lub wysłać plik na serwer. Wszystkie pliki są wysyłane w trybie binarnym, aby uprościć logikę.

Do implementacji protokołu tradycyjnie używałem 4 klas:

  • Klient TFTPC
  • Serwer TFTP
  • Tester klienta TFTPC
  • Tester serwera TFTP

Z uwagi na to, że klasy testowe istnieją jedynie do debugowania głównych, nie będę ich analizował, ale kod będzie w repozytorium, link do niego znajdziesz na końcu artykułu. Teraz spójrzmy na główne klasy.

Klient TFTPC

Zadaniem tej klasy jest połączenie się ze zdalnym serwerem poprzez jego adres IP i numer portu, odczytanie polecenia ze strumienia wejściowego (w tym przypadku z klawiatury), przeanalizowanie go, przesłanie na serwer i w zależności od tego, czy musisz wysłać lub odebrać plik, przesłać go lub pobrać.

Kod uruchamiający klienta w celu połączenia się z serwerem i oczekiwania na polecenie ze strumienia wejściowego wygląda następująco. Szereg zmiennych globalnych, które tu zastosowano, opisano poza artykułem, w pełnym tekście programu. Ze względu na ich banalność nie przytaczam ich, aby nie przeciążać artykułu.

 public void run(String ip, int port)
    {
        this.ip = ip;
        this.port = port;
        try {
            inicialization();
            Scanner keyboard = new Scanner(System.in);
            while (isRunning) {
                getAndParseInput(keyboard);
                sendCommand();
                selector();
                }
            }
        catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }

Przyjrzyjmy się metodom wywoływanym w tym bloku kodu:

Tutaj plik jest wysyłany - za pomocą skanera przedstawiamy zawartość pliku jako tablicę bajtów, które po kolei wpisujemy do gniazda, po czym zamykamy i ponownie otwieramy (nie jest to rozwiązanie najbardziej oczywiste, ale gwarantuje zwolnienie zasobów), po czym wyświetlamy komunikat o udanym transferze.

private  void put(String sourcePath, String destPath)
    {

        File src = new File(sourcePath);
        try {

            InputStream scanner = new FileInputStream(src);
            byte[] bytes = scanner.readAllBytes();
            for (byte b : bytes)
                sout.write(b);
            sout.close();
            inicialization();
            System.out.println("nDonen");
            }

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

Ten fragment kodu opisuje pobieranie danych z serwera. Wszystko znowu jest banalne, interesuje nas tylko pierwszy blok kodu. Aby dokładnie zrozumieć, ile bajtów należy odczytać z gniazda, trzeba wiedzieć, ile waży przesyłany plik. Rozmiar pliku na serwerze jest reprezentowany jako długa liczba całkowita, dlatego akceptowane są tutaj 4 bajty, które następnie są konwertowane na jedną liczbę. To nie jest podejście bardzo Java, jest raczej podobne do SI, ale rozwiązuje jego problem.

Wtedy wszystko jest banalne – otrzymujemy z gniazda znaną liczbę bajtów i zapisujemy je do pliku, po czym wyświetlamy komunikat o powodzeniu.

   private void get(String sourcePath, String destPath){
        long sizeOfFile = 0;
        try {


            byte[] sizeBytes = new byte[Long.SIZE];
           for (int i =0; i< Long.SIZE/Byte.SIZE; i++)
           {
               sizeBytes[i] = (byte)sin.read();
               sizeOfFile*=256;
               sizeOfFile+=sizeBytes[i];
           }

           FileOutputStream writer = new FileOutputStream(new File(destPath));
           for (int i =0; i < sizeOfFile; i++)
           {
               writer.write(sin.read());
           }
           writer.close();
           System.out.println("nDONEn");
       }
       catch (Exception e){
            System.out.println(e.getMessage());
       }
    }

Jeżeli w oknie klienta została wpisana inna komenda niż get lub put, zostanie wywołana funkcja showErrorMessage, wskazująca, że ​​wprowadzone dane były nieprawidłowe. Ze względu na banalność nie będę go cytować. Nieco bardziej interesująca jest funkcja odbierania i dzielenia ciągu wejściowego. Podajemy do niego skaner, z którego spodziewamy się otrzymać linię oddzieloną dwiema spacjami, zawierającą polecenie, adres źródłowy i adres docelowy.

    private void getAndParseInput(Scanner scanner)
    {
        try {

            input = scanner.nextLine().split(" ");
            typeOfCommand = input[0];
            sourcePath = input[1];
            destPath = input[2];
        }
        catch (Exception e) {
            System.out.println("Bad input");
        }
    }

Wysyłanie polecenia – przesyła wprowadzone polecenie ze skanera do gniazda i wymusza jego wysłanie

    private void sendCommand()
    {
        try {

            for (String str : input) {
                for (char ch : str.toCharArray()) {
                    sout.write(ch);
                }
                sout.write(' ');
            }
            sout.write('n');
        }
        catch (Exception e) {
            System.out.print(e.getMessage());
        }
    }

Selektor to funkcja określająca działanie programu w zależności od wprowadzonego ciągu znaków. Wszystko tutaj nie jest zbyt piękne i zastosowany trik nie jest najlepszy z wymuszonym wyjściem poza blok kodu, ale głównym powodem tego jest brak w Javie niektórych rzeczy, takich jak delegacje w C#, wskaźniki funkcji z C++ lub w przynajmniej okropne i okropne goto, które pozwalają ci to pięknie wdrożyć. Jeśli wiesz, jak uczynić kod nieco bardziej eleganckim, z radością przyjmę krytykę w komentarzach. Wydaje mi się, że potrzebny jest tutaj słownik delegatów typu String, ale nie ma delegata ...

    private void selector()
    {
        do{
            if (typeOfCommand.equals("get")){
                get(sourcePath, destPath);
                break;
            }
            if (typeOfCommand.equals("put")){
                put(sourcePath, destPath);
                break;
            }
            showErrorMessage();
        }
        while (false);
    }
}

Serwer TFTP

Funkcjonalność serwera różni się zasadniczo od funkcjonalności klienta tylko tym, że polecenia przychodzą do niego nie z klawiatury, ale z gniazda. Niektóre metody są w zasadzie takie same, więc nie będę ich cytować, dotknę tylko różnic.

Na początek używana jest metoda run, która jako wejście otrzymuje port i przetwarza dane wejściowe z gniazda w wiecznej pętli.

    public void run(int port) {
            this.port = port;
            incialization();
            while (true) {
                getAndParseInput();
                selector();
            }
    }

Metoda put, która otacza metodę writeToFileFromSocket otwierającą strumień zapisu do pliku i zapisując wszystkie bajty wejściowe z gniazda, po zakończeniu zapisu wyświetla komunikat wskazujący pomyślne zakończenie przesyłania.

    private  void put(String source, String dest){
            writeToFileFromSocket();
            System.out.print("nDonen");
    };
    private void writeToFileFromSocket()
    {
        try {
            FileOutputStream writer = new FileOutputStream(new File(destPath));
            byte[] bytes = sin.readAllBytes();
            for (byte b : bytes) {
                writer.write(b);
            }
            writer.close();
        }
        catch (Exception e){
            System.out.println(e.getMessage());
        }
    }

Metoda get pobiera plik serwera. Jak już wspomniano w sekcji po stronie klienta programu, aby pomyślnie przesłać plik, musisz znać jego rozmiar zapisany w długiej liczbie całkowitej, więc dzielę go na tablicę 4 bajtów, przesyłam bajt po bajcie do gniazda, a następnie po otrzymaniu i złożeniu ich na kliencie w liczbę z powrotem przesyłam wszystkie bajty tworzące plik, odczytane ze strumienia wejściowego z pliku.


 private  void get(String source, String dest){
        File sending = new File(source);
        try {
            FileInputStream readFromFile = new FileInputStream(sending);
            byte[] arr = readFromFile.readAllBytes();
            byte[] bytes = ByteBuffer.allocate(Long.SIZE / Byte.SIZE).putLong(sending.length()).array();
            for (int i = 0; i<Long.SIZE / Byte.SIZE; i++)
                sout.write(bytes[i]);
            sout.flush();
            for (byte b : arr)
                sout.write(b);
        }
        catch (Exception e){
            System.out.println(e.getMessage());
        }
    };

Metoda getAndParseInput jest taka sama jak w kliencie, z tą tylko różnicą, że odczytuje dane z gniazda, a nie z klawiatury. Kod znajduje się w repozytorium, podobnie jak selektor.
W tym przypadku inicjacja jest umieszczana w oddzielnym bloku kodu, ponieważ w ramach tej implementacji po zakończeniu transferu zasoby są zwalniane i ponownie zajęte – ponownie w celu zapewnienia ochrony przed wyciekami pamięci.

    private void  incialization()
    {
        try {
            serverSocket = new ServerSocket(port);
            socket = serverSocket.accept();
            sin = socket.getInputStream();
            sout = socket.getOutputStream();
        }
        catch (Exception e) {
            System.out.print(e.getMessage());
        }
    }

Podsumowując:

Właśnie napisaliśmy własną wariację na temat prostego protokołu przesyłania danych i odkryliśmy, jak powinien on działać. W zasadzie nie odkryłem tu Ameryki i nie napisałem zbyt wielu nowych rzeczy, ale podobnych artykułów na temat Habré nie było, a w ramach pisania serii artykułów o narzędziach cmd nie sposób było tego nie poruszyć.

Linki:

Repozytorium kodu źródłowego
Krótko o TFTP
To samo, ale po rosyjsku

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

Dodaj komentarz