Szoftverírás Windows kliens-szerver segédprogram funkciókkal, 2. rész

Folytatva a Windows parancssori segédprogramok testreszabott megvalósításainak szentelt cikksorozatot, nem lehet figyelmen kívül hagyni a TFTP-t (Trivial File Transfer Protocol) — egy egyszerű fájlátviteli protokollt.

Ahogy az előző alkalommal, most is röviden áttekintjük az elméletet, megnézzük a funkciót megvalósító kódot, és elemezzük azt. Részletesebben — a cikk alján.

Nem szeretném másolni a hagyományos referenciainformációt, amelyet általában a cikk végén találhatunk, csupán annyit mondanék, hogy a TFTP alapvetően az FTP protokoll egyszerűsített változata, amelyből eltávolították a hozzáférési kontroll beállítást, és tulajdonképpen csak fájlok fogadására és küldésére szolgáló parancsok vannak benne. Azonban annak érdekében, hogy megvalósításunk egy kicsit elegánsabb és a mai kódírási elvekhez alkalmazkodó legyen, a szintaxis kissé megváltozott — a működési elveket nem befolyásolja, de a felület, IMHO, kicsit logikusabbá válik, kombinálva az FTP és TFTP pozitív tulajdonságait.

Különösen, amikor elindul, a kliens megkéri a szerver IP-címét és a portot, amelyen a testreszabott TFTP nyitva áll (a standard protokollal való inkompatibilitás miatt helyénvalónak tartottam, hogy lehetőséget adjak a felhasználónak a portválasztásra), majd létrejön a kapcsolat, amelynek eredményeként a kliens egyik parancsot — get vagy put — adhat ki a fájl fogadására vagy a szerverre történő küldésére. Minden fájl bináris módban kerül elküldésre — a logika egyszerűsítése érdekében.

A protokoll megvalósításához hagyományosan 4 osztályt használtam:

  • TFTPClient
  • TFTPServer
  • TFTPClientTester
  • TFTPServerTester

Mivel a tesztelő osztályok csak az alapvető osztályok hibakeresésére szolgálnak, azokat nem fogom elemezni, de a kód a tárolóban megtalálható, amelynek hivatkozása a cikk végén elérhető. Most azonban a fő osztályokat fogom áttekinteni.

TFTPClient

Ennek az osztálynak a feladata, hogy csatlakozzon a távoli szerverhez annak IP-címén és portszámán, beolvassa a bemeneti áramlásból (ebben az esetben a billentyűzetről) a parancsot, elemezze azt, átadja a szervernek, és a fájl küldésének vagy fogadásának szükségessége szerint vagy átadja vagy megkapja azt.

A kliens szerverhez való csatlakozásának kezdeti kódja és a bemeneti áramlásból származó parancs várakozása így néz ki. Az itt használt globális változók leírását a cikken kívül találják meg, a program teljes szövegében. Triviális voltuk miatt nem sorolom fel őket, hogy ne terheljem túl a cikket.

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

Nézzük át a kódblokkban meghívott metódusokat:

Itt a fájl küldése történik — a szkenner segítségével a fájl tartalmát bájtok tömbjeként ábrázoljuk, amelyeket sorban írunk a socketbe, majd bezárjuk és újra megnyitjuk (nem a legnyilvánvalóbb megoldás, de garantálja az erőforrások felszabadítását), majd sikeres átvitelről szóló üzenetet írunk ki a képernyőre.

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

Ez a kódrészlet a szerverről történő adatok megszerzését írja le. Újra csak a triviális, az érdekesség az első kódrészletben rejlik. Ahhoz, hogy tudjuk, hány bájtot kell olvasni a socketből, tudnunk kell, hogy mekkora a továbbított fájl. A fájl mérete a szerveren hosszú egész számként van ábrázolva, ezért itt 4 bájtot veszünk, amelyeket később egy számra konvertálunk. Ez nem igazán Java megközelítés, inkább C-re hasonlít, de a feladatát megoldja.

A továbbiakban minden triviális — a socketből egy ismert számú bájtot kapunk és fájlba írjuk őket, majd üzenetet írunk ki a sikerességről.

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

Ha a kliens ablakába olyan parancs került beírásra, amely eltér a get vagy put parancstól, akkor a showErrorMessage függvény kerül meghívásra, amely jelzi a bemenet helytelenségét. A triviális okok miatt ezt nem részletezem. Egy kicsit érdekesebb a bemeneti szöveg megszerzésének és feldarabolásának a függvénye. Itt egy szkenner fog belekerülni, amelytől azt várjuk, hogy egy két szóközre elválasztott sort adjon vissza, amely tartalmazza a parancsot, a forráspathoz és a célpathoz.

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

A parancs küldése – a szkennerből beírt parancs átküldése a socketre és annak kényszerített elküldése.

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

A selector – függvény, amely meghatározza a program működését a beírt szöveg alapján. Itt a dolog nem túl szép, és nem a legjobb megoldást használja a kódblokk kényszerített elhagyásával, de ennek fő oka, hogy a Java nem rendelkezik olyan dolgokkal, mint a C# delegáltak, vagy a C++-ban lévő függvénymutatók, vagy legalább a rémisztő és borzalmas goto, amelyek lehetővé tennék a szép megvalósítást. Ha tudja, hogyan lehetne a kódot egy kicsit elegánsabbá tenni – várom a kritikákat a megjegyzések között. Úgy tűnik, itt szükség lenne egy String-delegált szótárra, de nincs delegált...

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

TFTPServer

A szerver funkcionalitása alapvetően csak annyiban különbözik az ügyfél funkcionalitásától, hogy a parancsok nem a billentyűzetről érkeznek, hanem socketből. Néhány módszer egyáltalán megegyezik, ezért nem sorolom fel őket, csupán a különbségeket említem meg.

A futáshoz itt a run metódus használatos, amely egy portot kap bemenetként és folyamatosan kezeli az adatokat a socketből.

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

A put módszer, amely a writeToFileFromSocket módszer köré épül, egy írási folyamatot nyit meg egy fájlhoz, és az összes bejövő bájtot a socketen keresztül írja, majd a művelet befejezése után sikeres átviteli üzenetet küld.

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

A get módszer biztosítja a fájl letöltését a szerverről. Ahogy a program kliens oldaláról már említettem, a fájl sikeres átviteléhez ismerni kell a méretét, amely egy hosszú egész számként van tárolva, ezért ezt egy 4 bájtos tömbé bontom, bájtonként küldöm a socketen, majd miután visszanyertem és összeraktam a kliensen a számot, átadom az összes bájtot, amelyek a fájlból lettek olvasva.


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

A getAndParseInput módszer megfelel az ügyfélben található hasonlónak, azzal a különbséggel, hogy az adatokat socketből olvassa be, nem pedig a billentyűzetről. A kód a repositoryban, ahogy a selector.
Ebben az esetben a inicializálás külön blokkba került, mivel a megvalósítás keretein belül a források felszabadulnak az átadás befejezésével, és újra elérhetővé válnak, ismételten a memória szivárgás elleni védelem érdekében.

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

Összefoglalva:

Most írtuk meg a saját változatunkat az egyszerű adatátviteli protokollról, és megértettük, hogy annak hogyan kell működnie. Alapvetően nem hoztam újítást, és nem írtam semmi különöset, de – ilyen jellegű cikkek nem voltak a Hubon, és a cmd eszközökről szóló cikksorozat írása során nem lehetett ezt a témát figyelmen kívül hagyni.

Linkek:

A repository az alap kóddal
Röviden a TFTP-ről
Ugyanaz, de oroszul

Forrás: habr.com

Megbízható hosting vásárlása weboldalak számára, DDoS védelemmel, VPS VDS szerverek. 🔥 Megbízható webtárhely vásárlása DDoS védelemmel, VPS VDS szerverek | ProHoster