Λογισμικό γραφής με τη λειτουργικότητα των βοηθητικών προγραμμάτων πελάτη-διακομιστή των 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());
        }
    }

Ας δούμε τις μεθόδους που ονομάζονται σε αυτό το μπλοκ κώδικα:

Εδώ το αρχείο αποστέλλεται - χρησιμοποιώντας έναν σαρωτή, παρουσιάζουμε τα περιεχόμενα του αρχείου ως μια σειρά από byte, τα οποία γράφουμε ένα προς ένα στην υποδοχή, μετά την οποία το κλείνουμε και το ανοίγουμε ξανά (όχι η πιο προφανής λύση, αλλά εγγυάται την αποδέσμευση των πόρων), μετά την οποία εμφανίζουμε ένα μήνυμα σχετικά με την επιτυχή μεταφορά.

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

Αυτό το τμήμα κώδικα περιγράφει την ανάκτηση δεδομένων από τον διακομιστή. Όλα είναι και πάλι ασήμαντα, μόνο το πρώτο μπλοκ κώδικα έχει ενδιαφέρον. Για να κατανοήσετε ακριβώς πόσα byte πρέπει να διαβαστούν από την υποδοχή, πρέπει να γνωρίζετε πόσο ζυγίζει το μεταφερόμενο αρχείο. Το μέγεθος του αρχείου στον διακομιστή αντιπροσωπεύεται ως ένας μεγάλος ακέραιος αριθμός, επομένως 4 byte γίνονται δεκτά εδώ, τα οποία στη συνέχεια μετατρέπονται σε έναν αριθμό. Αυτή δεν είναι μια πολύ Java προσέγγιση, είναι πιο παρόμοια για το SI, αλλά λύνει το πρόβλημά του.

Τότε όλα είναι ασήμαντα - λαμβάνουμε έναν γνωστό αριθμό byte από την υποδοχή και τα γράφουμε σε ένα αρχείο, μετά από το οποίο εμφανίζουμε ένα μήνυμα επιτυχίας.

   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++ ή στο τουλάχιστον το τρομερό και τρομερό goto, που σας επιτρέπει να το εφαρμόσετε όμορφα. Αν ξέρετε πώς να κάνετε τον κώδικα λίγο πιο κομψό, καλωσορίζω την κριτική στα σχόλια. Μου φαίνεται ότι χρειάζεται ένα λεξικό String-delegate εδώ, αλλά δεν υπάρχει εκπρόσωπος...

    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 που ανοίγει μια ροή εγγραφής σε ένα αρχείο και γράφει όλα τα byte εισόδου από την υποδοχή, εμφανίζει ένα μήνυμα που υποδεικνύει την επιτυχή ολοκλήρωση της μεταφοράς όταν ολοκληρωθεί η εγγραφή.

    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 byte, τα μεταφέρω byte-byte στην υποδοχή και, στη συνέχεια, αφού τα έχω λάβει και τα συναρμολογήσω στον υπολογιστή-πελάτη σε έναν αριθμό πίσω, μεταφέρω όλα τα byte που αποτελούν το αρχείο, τα οποία διαβάζονται από τη ροή εισόδου από το αρχείο.


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

Να συνοψίσουμε:

Μόλις γράψαμε τη δική μας παραλλαγή σε ένα απλό πρωτόκολλο μεταφοράς δεδομένων και καταλάβαμε πώς πρέπει να λειτουργεί. Κατ 'αρχήν, δεν ανακάλυψα την Αμερική εδώ και δεν έγραψα πολλά νέα πράγματα, αλλά δεν υπήρχαν παρόμοια άρθρα στο Habré και ως μέρος της συγγραφής μιας σειράς άρθρων σχετικά με τα βοηθητικά προγράμματα cmd ήταν αδύνατο να μην το αγγίξω.

Βιβλιογραφικές αναφορές:

Αποθετήριο πηγαίου κώδικα
Εν συντομία για το TFTP
Το ίδιο, αλλά στα ρωσικά

Πηγή: www.habr.com

Προσθέστε ένα σχόλιο