Írószoftver a Windows kliens-szerver segédprogramjaival, 02. rész

Folytatva a folyamatban lévő cikksorozatot, amely a Windows konzol segédprogramjainak egyedi implementációiról szól, nem tehetjük meg a TFTP-t (Trivial File Transfer Protocol) – egy egyszerű fájlátviteli protokollt.

Mint legutóbb, most is nézzük meg röviden az elméletet, nézzük meg a szükségeshez hasonló funkcionalitást megvalósító kódot, és elemezzük azt. További részletek - a vágás alatt

Nem fogok másolni-beilleszteni hivatkozási információkat, amelyek linkjei hagyományosan a cikk végén találhatók, csak annyit mondok, hogy lényegében a TFTP az FTP protokoll egy egyszerűsített változata, amelyben a hozzáférés-vezérlési beállítás el lett távolítva, és valójában itt nincs más, mint a fájl fogadására és átvitelére szolgáló parancsok. Annak érdekében azonban, hogy implementációnkat egy kicsit elegánsabbá és a jelenlegi kódírási elvekhez igazodva tegyük, a szintaxist némileg módosítottuk - ez a működési elveken nem változtat, viszont az interfész, az IMHO, kicsit logikusabbá válik, ill. ötvözi az FTP és a TFTP pozitív aspektusait.

A kliens különösen indításkor lekéri a szerver IP címét és azt a portot, amelyen az egyéni TFTP nyitva van (a szabványos protokollal való összeférhetetlenség miatt célszerűnek tartottam meghagyni a felhasználónak a port kiválasztásának lehetőségét), ami után a kapcsolat jön létre, melynek eredményeként a kliens elküldheti az egyik parancsot - get or put, hogy fogadjon vagy küldjön egy fájlt a szervernek. Az összes 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

Tekintettel arra, hogy a tesztelő osztályok csak a főbbek hibakeresésére léteznek, nem fogom elemezni őket, de a kód a repository-ban lesz, egy linket a cikk végén találsz. Most megnézem a főbb osztályokat.

TFTPClient

Ennek az osztálynak az a feladata, hogy csatlakozzon egy távoli szerverhez annak ip-je és portszáma alapján, beolvassa a parancsot a bemeneti adatfolyamból (jelen esetben a billentyűzetről), elemzi, átviszi a szerverre, és attól függően, hogy fájlt kell küldeni vagy fogadni, át kell vinni vagy meg kell szerezni.

A kliens elindításához szükséges kód a szerverhez való csatlakozáshoz és a bemeneti adatfolyamból érkező parancs megvárásához így néz ki. Számos itt használt globális változó leírása a cikken kívül, a program teljes szövegében található. Trivialitásuk miatt nem idézem őket, nehogy túlterheljem 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 az ebben a kódblokkban nevezett metódusokat:

Itt elküldésre kerül a fájl - egy szkenner segítségével a fájl tartalmát egy bájttömbként mutatjuk be, amit egyenként írunk a foglalatba, majd bezárjuk és újra megnyitjuk (nem a legkézenfekvőbb megoldás, de garantálja az erőforrások felszabadítását), amely után üzenetet jelenítünk meg a sikeres átvitelről.

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 az adatok lekérését írja le a szerverről. Minden megint triviális, csak az első kódblokk az érdekes. Annak megértéséhez, hogy pontosan hány bájtot kell kiolvasni a socketből, tudnia kell, mennyi az átvitt fájl súlya. A kiszolgálón lévő fájl mérete hosszú egész számként van ábrázolva, így itt 4 bájt fogadható el, amelyeket utólag egy számmá alakítanak át. Ez nem túl Java megközelítés, inkább hasonló az SI-hez, de megoldja a problémáját.

Ezután minden triviális - egy ismert számú bájtot kapunk a socketből, és kiírjuk egy fájlba, ami után megjelenítünk egy sikerüzenetet.

   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 get vagy put parancstól eltérő parancsot adtunk meg az ügyfélablakban, a showErrorMessage függvény meghívódik, jelezve, hogy a bevitel helytelen volt. A trivialitás miatt nem idézem. Valamivel érdekesebb a bemeneti karakterlánc fogadásának és felosztásának funkciója. Ebbe adjuk át a szkennert, amelytől egy két szóközzel elválasztott sort kapunk, amely tartalmazza a parancsot, a forráscímet és a célcímet.

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

Parancs küldése – továbbítja a beírt parancsot a lapolvasóból a foglalatba, és kényszeríti az elküldésre

    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 kiválasztó egy olyan funkció, amely a beírt karakterlánctól függően határozza meg a program műveleteit. Itt minden nem túl szép, és a használt trükk nem a legjobb a kódblokkon kívüli kényszerített kilépéssel, de ennek fő oka az, hogy Java-ban hiányoznak olyan dolgok, mint a delegáltak a C#-ban, a funkciómutatók a C++-ból vagy a legalábbis a szörnyű és szörnyű goto, amely lehetővé teszi, hogy ezt gyönyörűen megvalósítsa. Ha tudja, hogyan lehet egy kicsit elegánsabbá tenni a kódot, szívesen fogadom a kritikákat a megjegyzésekben. Nekem úgy tűnik, hogy szükség van ide egy String-delegate 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 nagyjából annyiban tér el a kliens funkcionalitásától, hogy a parancsok nem a billentyűzetről, hanem a socketről érkeznek hozzá. A módszerek egy része általában megegyezik, ezért nem idézem őket, csak a különbségekre térek ki.

Kezdésként a run metódust használjuk, amely egy portot kap bemenetként, és örök hurokban dolgozza fel a socket bemeneti adatait.

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

A put metódus, amely becsomagolja a writeToFileFromSocket metódust, amely megnyit egy írási adatfolyamot egy fájlba, és kiírja az összes bemeneti bájtot a socketből, üzenetet jelenít meg, amely jelzi az átvitel sikeres befejezését, amikor az írás befejeződik.

    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 metódus lekéri a kiszolgálófájlt. Ahogy a program kliens oldali részében már említettük, egy fájl sikeres átviteléhez ismerni kell a méretét, hosszú egész számban tárolva, ezért 4 bájtos tömbre bontom, bájtonként átmásolom. a socketbe, majd miután megkaptam és összeraktam őket a kliensen egy számba vissza, átviszem a fájlt alkotó összes bájtot, a bemeneti folyamból kiolvasva a fájlból.


 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 metódus ugyanaz, mint a kliensben, az egyetlen különbség az, hogy az adatokat a socketből olvassa be, nem pedig a billentyűzetről. A kód a tárolóban van, akárcsak a választó.
Ebben az esetben az inicializálás egy külön kódblokkba kerül, mert ezen a megvalósításon belül az átvitel befejezése után az erőforrások felszabadulnak, és újra lefoglalják – ismét a memóriaszivárgás elleni védelem érdekében.

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

Összefoglalni:

Éppen most írtuk meg a saját változatunkat egy egyszerű adatátviteli protokollra, és kitaláltuk, hogyan kell működnie. Amerikát elvileg nem itt fedeztem fel, és nem írtam sok újat, de Habréról nem voltak hasonló cikkek, és a cmd segédprogramokról szóló cikksorozat részeként nem lehetett nem nyúlni hozzá.

referenciák:

Forráskód tárház
Röviden a TFTP-ről
Ugyanaz, csak oroszul

Forrás: will.com

Hozzászólás