Софтвер за пишување со функционалност на комуналните услуги за клиент-сервер на Windows, дел 02

Продолжувајќи со тековната серија написи посветени на прилагодени имплементации на комуналните услуги на конзолата на Windows, не можеме а да не го допреме TFTP (Trivial File Transfer Protocol) - едноставен протокол за пренос на датотеки.

Како и минатиот пат, ајде накратко да ја разгледаме теоријата, да го видиме кодот што имплементира функционалност слична на потребната и да ја анализираме. Повеќе детали - под сечењето

Нема да копирам-залепувам референтни информации, врски до кои традиционално може да се најдат на крајот од статијата, само ќе кажам дека во суштина, TFTP е поедноставена варијација на протоколот FTP, во кој поставката за контрола на пристап има е отстранета, а всушност тука нема ништо освен команди за примање и пренесување датотека . Сепак, за да ја направиме нашата имплементација малку поелегантна и прилагодена на сегашните принципи на пишување код, синтаксата е малку променета - ова не ги менува принципите на работа, но интерфејсот, IMHO, станува малку пологичен и ги комбинира позитивните аспекти на FTP и TFTP.

Конкретно, кога ќе се стартува, клиентот ја бара IP адресата на серверот и портот на кој е отворен прилагодениот TFTP (поради некомпатибилност со стандардниот протокол, сметав дека е соодветно да му се остави на корисникот можност да избере порта), по што се јавува врска, како резултат на што клиентот може да испрати една од командите - get или put, да прима или испрати датотека до серверот. Сите датотеки се испраќаат во бинарен режим за да се поедностави логиката.

За имплементација на протоколот, јас традиционално користев 4 класи:

  • TFTPClient
  • TFTPS-сервер
  • TFTPClientTester
  • TFTPServerTester

Поради фактот што класите за тестирање постојат само за дебагирање на главните, нема да ги анализирам, но кодот ќе биде во складиштето, врската до неа може да се најде на крајот од статијата. Сега ќе ги разгледам главните часови.

TFTPClient

Задачата на оваа класа е да се поврзе со оддалечен сервер користејќи ја неговата IP и број на порта, да чита команда од влезниот тек (во овој случај, тастатурата), да ја анализира, да ја пренесе на серверот и, во зависност од тоа дали треба да испратите или примите датотека, да ја пренесете или да добиете.

Кодот за стартување на клиентот да се поврзе со серверот и да чека команда од влезниот поток изгледа вака. Голем број глобални променливи што се користат овде се опишани надвор од статијата, во целосниот текст на програмата. Поради нивната тривијалност, не ги цитирам за да не ја преоптоварувам статијата.

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

Ајде да ги разгледаме методите повикани во овој блок код:

Овде датотеката се испраќа - со помош на скенер, ја прикажуваме содржината на датотеката како низа од бајти, кои ги запишуваме еден по еден во штекерот, по што го затвораме и повторно го отвораме (не најочигледното решение, но гарантира ослободување на ресурсите), по што прикажуваме порака за успешен трансфер.

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

Овој фрагмент од код опишува преземање податоци од серверот. Сè е повторно тривијално, само првиот блок код е од интерес. За да разберете точно колку бајти треба да се прочитаат од штекерот, треба да знаете колку тежи пренесената датотека. Големината на датотеката на серверот е претставена како долг цел број, така што овде се прифаќаат 4 бајти, кои последователно се претвораат во еден број. Ова не е многу Java пристап, тој е прилично сличен за SI, но го решава неговиот проблем.

Тогаш сè е тривијално - добиваме познат број бајти од штекерот и ги запишуваме во датотека, по што прикажуваме успешна порака.

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

Ако во прозорецот на клиентот е внесена команда различна од get или put, ќе се повика функцијата showErrorMessage, што покажува дека внесувањето е погрешно. Поради тривијалност, нема да го цитирам. Нешто поинтересна е функцијата за примање и разделување на влезната низа. Го пренесуваме скенерот во него, од кој очекуваме да добиеме линија одвоена со две празни места и која ги содржи командата, изворната адреса и адресата на дестинацијата.

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

Испраќање команда - ја пренесува командата внесена од скенерот до штекерот и ја принудува да се испрати

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

Селекторот е функција која ги одредува дејствата на програмата во зависност од внесената низа. Сè овде не е многу убаво и трикот што се користи не е најдобриот со принуден излез надвор од кодниот блок, но главната причина за ова е отсуството во Java на некои работи, како делегати во C#, покажувачи на функции од C++ или на барем страшното и страшно гото, кое ви овозможува убаво да го спроведете ова. Ако знаете како да го направите кодот малку поелегантен, ја поздравувам критиката во коментарите. Ми се чини дека тука е потребен Стринг-делегатски речник, но нема делегат...

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

TFTPS-сервер

Функционалноста на серверот се разликува од функционалноста на клиентот, во голема мера, само по тоа што командите не доаѓаат до него од тастатурата, туку од штекерот. Некои од методите се генерално исти, затоа нема да ги наведам, ќе допрам само на разликите.

За почеток, се користи методот на извршување, кој прима порта како влез и ги обработува влезните податоци од штекерот во вечна јамка.

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

Методот put, кој го обвиткува методот writeToFileFromSocket кој отвора поток за запишување во датотека и ги запишува сите влезни бајти од штекерот, прикажува порака што укажува на успешно завршување на преносот кога ќе заврши запишувањето.

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

Методот get ја враќа датотеката на серверот. Како што веќе беше споменато во делот од страната на клиентот на програмата, за успешно префрлување датотека треба да ја знаете нејзината големина, складирана во долг цел број, па затоа ја поделив во низа од 4 бајти, ги пренесувам бајт по бајт во штекерот, а потоа, откако ги примив и ги составив на клиентот во број назад, ги префрлам сите бајти што ја сочинуваат датотеката, прочитани од влезниот поток од датотеката.


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

Методот getAndParseInput е ист како кај клиентот, единствената разлика е во тоа што ги чита податоците од сокетот наместо од тастатурата. Кодот е во складиштето, исто како селекторот.
Во овој случај, иницијализацијата се става во посебен блок код, бидејќи во рамките на оваа имплементација, по завршувањето на трансферот, ресурсите се ослободуваат и повторно се окупираат - повторно за да се обезбеди заштита од протекување на меморијата.

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

За да резимираме:

Штотуку напишавме своја варијација на едноставен протокол за пренос на податоци и сфативме како треба да работи. Во принцип, не ја открив Америка овде и не напишав многу нови работи, но немаше слични написи на Хабре, а како дел од пишувањето серија написи за комуналните услуги cmd беше невозможно да не се допре.

Референци:

Складиште на изворен код
Накратко за TFTP
Истото, но на руски

Извор: www.habr.com

Додадете коментар