Писање софтвера са функционалношћу Виндовс клијент-сервер услужних програма, део 02

Настављајући текућу серију чланака посвећених прилагођеним имплементацијама услужних програма Виндовс конзоле, не можемо а да не дотакнемо ТФТП (Тривиал Филе Трансфер Протоцол) – једноставан протокол за пренос датотека.

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

Нећу копирати и налепити референтне информације, везе до којих се традиционално могу наћи на крају чланка, само ћу рећи да је у својој сржи ТФТП поједностављена варијација ФТП протокола, у којој је подешавање контроле приступа је уклоњен, а заправо овде нема ничега осим команди за пријем и пренос датотеке. Међутим, да би наша имплементација била мало елегантнија и прилагођена тренутним принципима писања кода, синтакса је мало измењена – то не мења принципе рада, али интерфејс, ИМХО, постаје мало логичнији и комбинује позитивне аспекте ФТП-а и ТФТП-а.

Конкретно, када се покрене, клијент захтева ИП адресу сервера и порт на којем је отворен прилагођени ТФТП (због некомпатибилности са стандардним протоколом, сматрао сам да је прикладно оставити кориснику могућност избора порта), након чега је долази до повезивања, услед чега клијент може послати једну од команди - гет или пут, да прими или пошаље датотеку серверу. Све датотеке се шаљу у бинарном режиму да би се поједноставила логика.

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

  • ТФТПЦлиент
  • ТФТПСервер
  • ТФТПЦлиентТестер
  • ТФТПСерверТестер

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

ТФТПЦлиент

Задатак ове класе је да се повеже са удаљеним сервером користећи његов ИП и број порта, прочита команду из улазног тока (у овом случају тастатуре), рашчлани је, пренесе на сервер и, у зависности од тога да ли сте треба да пошаљете или примите датотеку, пренесете је или добијете.

Код за покретање клијента да се повеже са сервером и чека команду из улазног тока изгледа овако. Бројне глобалне променљиве које се овде користе су описане ван чланка, у пуном тексту програма. Због њихове тривијалности, не наводим их да не бих преоптеретио чланак.

 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 бајта, који се накнадно конвертују у један број. Ово није баш Јава приступ, прилично је сличан за СИ, али решава његов проблем.

Тада је све тривијално – добијамо познати број бајтова из сокета и уписујемо их у датотеку, након чега приказујемо поруку о успеху.

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

Ако је команда која није гет или пут унета у прозор клијента, функција сховЕррорМессаге ће бити позвана, што указује да је унос био нетачан. Због тривијалности, нећу га цитирати. Нешто занимљивија је функција пријема и раздвајања улазног низа. У њега прослеђујемо скенер од којег очекујемо да добијемо ред одвојен са два размака и који садржи команду, адресу извора и адресу одредишта.

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

Селектор је функција која одређује радње програма у зависности од унетог низа. Овде све није баш лепо и трик који се користи није најбољи са принудним излазом ван кодног блока, али главни разлог за то је одсуство неких ствари у Јави, попут делегата у Ц#, показивача функција из Ц++ или на најмање страшни и ужасни гото, који вам омогућавају да ово лепо примените. Ако знате како да учините код мало елегантнијим, поздрављам критике у коментарима. Чини ми се да је овде потребан речник Стринг-делегате, али нема делегата...

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

ТФТПСервер

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

За почетак се користи метода рун, која прима порт као улаз и обрађује улазне податке из сокета у вечној петљи.

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

Метод пут, који обавија метод вритеТоФилеФромСоцкет који отвара ток писања у датотеку и уписује све улазне бајтове из сокета, приказује поруку која указује на успешан завршетак преноса када се уписивање заврши.

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

Метод гет преузима серверску датотеку. Као што је већ поменуто у одељку о клијентској страни програма, да бисте успешно пренели датотеку, морате да знате њену величину, ускладиштену у дугом целом броју, тако да сам је поделио на низ од 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());
        }
    };

Метод гетАндПарсеИнпут је исти као код клијента, једина разлика је у томе што чита податке из сокета, а не са тастатуре. Код је у спремишту, баш као и селектор.
У овом случају, иницијализација се ставља у посебан блок кода, јер у оквиру ове имплементације, након што је трансфер завршен, ресурси се ослобађају и поново заузимају – поново да би се обезбедила заштита од цурења меморије.

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

Да резимирамо:

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

Референце:

Репозиторијум изворног кода
Укратко о ТФТП-у
Иста ствар, али на руском

Извор: ввв.хабр.цом

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