Softueri i shkrimit me funksionalitetin e shërbimeve të klient-server Windows, pjesa 02

Duke vazhduar serinë e vazhdueshme të artikujve kushtuar zbatimeve me porosi të shërbimeve të konsolës së Windows, nuk mund të mos prekim TFTP (Trivial File Transfer Protocol) - një protokoll i thjeshtë i transferimit të skedarëve.

Si herën e fundit, le të kalojmë shkurtimisht teorinë, të shohim kodin që zbaton funksionalitet të ngjashëm me atë të kërkuar dhe ta analizojmë atë. Më shumë detaje - nën prerje

Unë nuk do të kopjoj-ngjis informacionin e referencës, lidhjet me të cilat tradicionalisht mund të gjenden në fund të artikullit, do të them vetëm se në thelb, TFTP është një variant i thjeshtuar i protokollit FTP, në të cilin cilësimi i kontrollit të aksesit ka është hequr, dhe në fakt nuk ka asgjë këtu përveç komandave për marrjen dhe transferimin e një skedari. Sidoqoftë, për ta bërë zbatimin tonë pak më elegant dhe të përshtatur me parimet aktuale të shkrimit të kodit, sintaksa është ndryshuar pak - kjo nuk ndryshon parimet e funksionimit, por ndërfaqja, IMHO, bëhet pak më logjike dhe kombinon aspektet pozitive të FTP dhe TFTP.

Në veçanti, kur niset, klienti kërkon adresën IP të serverit dhe portin në të cilin është hapur TFTP me porosi (për shkak të papajtueshmërisë me protokollin standard, e konsiderova të përshtatshme t'i lija përdoruesit mundësinë për të zgjedhur një port), pas së cilës një ndodh lidhja, si rezultat i së cilës klienti mund të dërgojë një nga komandat - marrë ose vendos, për të marrë ose dërguar një skedar në server. Të gjithë skedarët dërgohen në modalitetin binar për të thjeshtuar logjikën.

Për të zbatuar protokollin, unë tradicionalisht përdora 4 klasa:

  • TFTPClient
  • TFTPSserver
  • TFTPClientTester
  • TFTPServerTester

Për shkak të faktit se klasat e testimit ekzistojnë vetëm për korrigjimin e atyre kryesore, unë nuk do t'i analizoj ato, por kodi do të jetë në depo; një lidhje me të mund të gjendet në fund të artikullit. Tani do të shikoj klasat kryesore.

TFTPClient

Detyra e kësaj klase është të lidhet me një server të largët me anë të numrit të tij IP dhe portit, të lexojë një komandë nga rrjedha hyrëse (në këtë rast, tastiera), ta analizojë atë, ta transferojë atë në server dhe, në varësi të faktit nëse ju duhet të dërgoni ose merrni një skedar, ta transferoni ose ta merrni.

Kodi për nisjen e klientit për t'u lidhur me serverin dhe për të pritur një komandë nga rrjedha e hyrjes duket si ky. Një numër i variablave globale që përdoren këtu përshkruhen jashtë artikullit, në tekstin e plotë të programit. Për shkak të parëndësisë së tyre, nuk i citoj për të mos e mbingarkuar artikullin.

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

Le të shqyrtojmë metodat e thirrura në këtë bllok kodi:

Këtu skedari dërgohet - duke përdorur një skaner, ne paraqesim përmbajtjen e skedarit si një grup bajtësh, të cilat i shkruajmë një nga një në prizë, pas së cilës e mbyllim dhe e hapim përsëri (jo zgjidhja më e dukshme, por garanton lirimin e burimeve), pas së cilës shfaqim një mesazh për transferimin e suksesshëm.

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

Ky fragment kodi përshkruan marrjen e të dhënave nga serveri. Gjithçka është përsëri e parëndësishme, vetëm blloku i parë i kodit është me interes. Për të kuptuar saktësisht se sa bajt duhet të lexohen nga foleja, duhet të dini se sa peshon skedari i transferuar. Madhësia e skedarit në server përfaqësohet si një numër i plotë i gjatë, kështu që këtu pranohen 4 bajt, të cilët më pas konvertohen në një numër. Kjo nuk është një qasje shumë Java, është mjaft e ngjashme për SI, por zgjidh problemin e saj.

Atëherë gjithçka është e parëndësishme - marrim një numër të njohur bajtësh nga priza dhe i shkruajmë në një skedar, pas së cilës shfaqim një mesazh suksesi.

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

Nëse në dritaren e klientit është futur një komandë përveç get ose put, funksioni showErrorMessage do të thirret, duke treguar se hyrja ishte e gabuar. Për shkak të parëndësisë, nuk do ta citoj. Disi më interesant është funksioni i marrjes dhe ndarjes së vargut të hyrjes. Ne e kalojmë skanerin në të, nga i cili presim të marrim një linjë të ndarë nga dy hapësira dhe që përmban komandën, adresën e burimit dhe adresën e destinacionit.

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

Dërgimi i një komande—transmeton komandën e futur nga skaneri në prizë dhe e detyron atë të dërgohet

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

Një përzgjedhës është një funksion që përcakton veprimet e programit në varësi të vargut të futur. Gjithçka këtu nuk është shumë e bukur dhe truku i përdorur nuk është më i miri me dalje të detyruar jashtë bllokut të kodit, por arsyeja kryesore për këtë është mungesa në Java e disa gjërave, si delegatët në C#, treguesit e funksionit nga C++, ose në të paktën goto e tmerrshme dhe e tmerrshme, të cilat ju lejojnë ta zbatoni këtë bukur. Nëse dini ta bëni kodin pak më elegant, mirëpres kritikat në komente. Më duket se këtu duhet një fjalor varg-delegat, por nuk ka delegat...

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

TFTPSserver

Funksionaliteti i serverit ndryshon nga funksionaliteti i klientit, në përgjithësi, vetëm në atë që komandat nuk vijnë tek ai nga tastiera, por nga priza. Disa nga metodat janë përgjithësisht të njëjta, kështu që unë nuk do t'i citoj ato, do të prek vetëm dallimet.

Për të filluar, përdoret metoda e ekzekutimit, e cila merr një port si hyrje dhe përpunon të dhënat hyrëse nga priza në një lak të përjetshëm.

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

Metoda put, e cila mbështjell metodën writeToFileFromSocket që hap një rrymë shkrimi në një skedar dhe shkruan të gjitha bajtet e hyrjes nga foleja, shfaq një mesazh që tregon përfundimin me sukses të transferimit kur të përfundojë shkrimi.

    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 e marrjes merr skedarin e serverit. Siç u përmend tashmë në seksionin në anën e klientit të programit, për të transferuar me sukses një skedar, duhet të dini madhësinë e tij, të ruajtur në një numër të plotë të gjatë, kështu që unë e ndaj në një grup prej 4 bajtesh, i transferoj ato byte-by-byte në prizë, dhe më pas, pasi i kam marrë dhe i kam mbledhur ato në klient në një numër mbrapa, i transferoj të gjithë bajtet që përbëjnë skedarin, të lexuar nga rrjedha hyrëse nga skedari.


 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 është e njëjtë si tek klienti, i vetmi ndryshim është se lexon të dhënat nga foleja dhe jo nga tastiera. Kodi është në depo, ashtu si zgjedhësi.
Në këtë rast, inicializimi vendoset në një bllok të veçantë kodi, sepse brenda këtij implementimi, pas përfundimit të transferimit, burimet lëshohen dhe rikthehen përsëri - përsëri për të siguruar mbrojtje kundër rrjedhjeve të kujtesës.

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

Për të përmbledhur:

Ne sapo kemi shkruar variantin tonë në një protokoll të thjeshtë të transferimit të të dhënave dhe kemi kuptuar se si duhet të funksionojë. Në parim, nuk e zbulova Amerikën këtu dhe nuk shkrova shumë gjëra të reja, por nuk kishte artikuj të ngjashëm në Habré, dhe si pjesë e shkrimit të një serie artikujsh rreth shërbimeve cmd, ishte e pamundur të mos e prekja.

referencat:

Depoja e kodit burimor
Shkurtimisht rreth TFTP
E njëjta gjë, por në rusisht

Burimi: www.habr.com

Shto një koment