Kirjoitusohjelmisto Windows-asiakas-palvelin-apuohjelmien toiminnallisuudella, osa 02

Jatkamme meneillään olevaa artikkelisarjaa, joka on omistettu Windows-konsoliapuohjelmien mukautetuille toteutuksille, emme voi muuta kuin koskettaa TFTP:tä (Trivial File Transfer Protocol) - yksinkertaista tiedostonsiirtoprotokollaa.

Kuten viime kerralla, käydään lyhyesti läpi teoria, katsotaan koodi, joka toteuttaa vaaditun kaltaisen toiminnallisuuden, ja analysoidaan sitä. Lisätiedot - leikkauksen alla

En kopioi-liitä viitetietoja, joihin linkit löytyvät perinteisesti artikkelin lopusta, vaan sanon vain, että TFTP on ytimessä FTP-protokollan yksinkertaistettu muunnelma, jossa pääsynhallinta-asetus on poistettu, ja itse asiassa täällä ei ole mitään muuta kuin komentoja tiedoston vastaanottamiseen ja siirtämiseen. Jotta toteutuksestamme tulisi hieman tyylikkäämpi ja mukautettu nykyisiin koodinkirjoitusperiaatteisiin, syntaksia on kuitenkin hieman muutettu - tämä ei muuta toimintaperiaatteita, mutta käyttöliittymä, IMHO, muuttuu hieman loogisemmaksi ja yhdistää FTP:n ja TFTP:n positiiviset puolet.

Erityisesti käynnistettäessä asiakas pyytää palvelimen IP-osoitetta ja porttia, jossa mukautettu TFTP on avoinna (yhteensopimattomuuden vuoksi vakioprotokollan kanssa, katsoin sopivaksi jättää käyttäjälle mahdollisuus valita portti), minkä jälkeen syntyy yhteys, jonka seurauksena asiakas voi lähettää yhden komennoista - get or put, vastaanottaa tai lähettää tiedoston palvelimelle. Kaikki tiedostot lähetetään binääritilassa logiikan yksinkertaistamiseksi.

Protokollan toteuttamiseksi käytin perinteisesti 4 luokkaa:

  • TFTPClient
  • TFTPS-palvelin
  • TFTPClientTester
  • TFTPServerTesteri

Koska testiluokat ovat olemassa vain tärkeimpien virheenkorjausta varten, en analysoi niitä, mutta koodi tulee olemaan arkistossa; linkki siihen löytyy artikkelin lopusta. Katson nyt pääluokkia.

TFTPClient

Tämän luokan tehtävänä on muodostaa yhteys etäpalvelimeen sen ip:n ja portin numeron perusteella, lukea komento syöttövirrasta (tässä tapauksessa näppäimistöltä), jäsentää se, siirtää se palvelimelle ja riippuen siitä oletko täytyy lähettää tai vastaanottaa tiedosto, siirtää se tai saada.

Koodi, jolla asiakas käynnistää yhteyden palvelimeen ja odottaa komentoa syöttövirrasta, näyttää tältä. Useita tässä käytettyjä globaaleja muuttujia kuvataan artikkelin ulkopuolella, ohjelman koko tekstissä. Niiden triviaalisuuden vuoksi en lainaa niitä, jotta en ylikuormittaisi artikkelia.

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

Käydään läpi tässä koodilohkossa kutsutut menetelmät:

Täällä tiedosto lähetetään - skannerin avulla esitämme tiedoston sisällön tavujoukona, jonka kirjoitamme yksitellen pistorasiaan, minkä jälkeen suljemme sen ja avaamme sen uudelleen (ei ilmeisin ratkaisu, mutta se takaa resurssien vapautumisen), jonka jälkeen näytämme viestin onnistuneesta siirrosta.

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

Tämä koodinpätkä kuvaa tietojen hakemista palvelimelta. Kaikki on taas triviaalia, vain ensimmäinen koodilohko kiinnostaa. Ymmärtääksesi tarkalleen kuinka monta tavua on luettava socketista, sinun on tiedettävä, kuinka paljon siirretty tiedosto painaa. Palvelimen tiedostokoko esitetään pitkänä kokonaislukuna, joten tässä hyväksytään 4 tavua, jotka muunnetaan myöhemmin yhdeksi numeroksi. Tämä ei ole kovin Java-lähestymistapa, se on melko samanlainen SI:lle, mutta se ratkaisee sen ongelman.

Sitten kaikki on triviaalia - vastaanotamme tiedossa olevan määrän tavuja socketista ja kirjoitamme ne tiedostoon, jonka jälkeen näytämme onnistumisviestin.

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

Jos asiakas-ikkunaan syötettiin jokin muu komento kuin get tai put, kutsutaan showErrorMessage-toiminto, joka osoittaa, että syöte oli virheellinen. Triviaalisuuden vuoksi en lainaa sitä. Hieman mielenkiintoisempi on toiminto vastaanottaa ja jakaa syötemerkkijono. Ohjaamme skannerin siihen, josta odotamme saavamme kahdella välilyönnillä erotetun rivin, joka sisältää komennon, lähdeosoitteen ja kohdeosoitteen.

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

Lähetetään komento — lähettää skannerista syötetyn komennon liitäntään ja pakottaa sen lähettämään

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

Valitsin on toiminto, joka määrittää ohjelman toiminnot syötetyn merkkijonon mukaan. Kaikki täällä ei ole kovin kaunista ja käytetty temppu ei ole paras tapa, jolla on pakotettu poistuminen koodilohkon ulkopuolella, mutta suurin syy tähän on joidenkin asioiden puuttuminen Javassa, kuten delegaatit C#:ssa, funktioosoittimet C++:sta tai ainakin kauhea ja kauhea goto, jonka avulla voit toteuttaa tämän kauniisti. Jos tiedät kuinka tehdä koodista hieman tyylikkäämpi, otan kritiikkiä vastaan ​​kommenteissa. Minusta näyttää siltä, ​​​​että tähän tarvitaan merkkijono-delegaate-sanakirja, mutta delegaattia ei ole...

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

TFTPS-palvelin

Palvelimen toiminnallisuus eroaa suurelta osin asiakkaan toimivuudesta vain siinä, että komennot eivät tule siihen näppäimistöltä, vaan pistorasiasta. Jotkut menetelmät ovat yleensä samoja, joten en lainaa niitä, käsittelen vain eroja.

Aluksi käytetään run-menetelmää, joka vastaanottaa portin tulona ja käsittelee syöttötiedot socketista ikuisessa silmukassa.

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

Put-metodi, joka kääriä writeToFileFromSocket-menetelmän, joka avaa kirjoitusvirran tiedostoon ja kirjoittaa kaikki syöttötavut socketista, näyttää viestin, joka ilmaisee siirron onnistuneen valmistumisen, kun kirjoitus on valmis.

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

Get-menetelmä hakee palvelintiedoston. Kuten jo mainittiin ohjelman asiakaspuolen osiossa, tiedoston siirtämiseksi onnistuneesti sinun on tiedettävä sen koko, joka on tallennettu pitkälle kokonaisluvulle, joten jaan sen 4 tavun taulukkoon, siirrän ne tavu kerrallaan. pistorasiaan, ja sitten vastaanotettuani ja koottuani ne asiakkaalla numeroksi takaisin siirrän kaikki tiedoston muodostavat tavut, jotka luetaan tiedostosta syötevirrasta.


 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-menetelmä on sama kuin asiakassovelluksessa, ainoa ero on, että se lukee tietoja socketista näppäimistön sijaan. Koodi on arkistossa, kuten valitsin.
Tässä tapauksessa alustus sijoitetaan erilliseen koodilohkoon, koska tässä toteutuksessa siirron päätyttyä resurssit vapautetaan ja ne otetaan uudelleen käyttöön - jälleen suojatakseen muistivuotoja.

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

Yhteenvetona:

Olemme juuri kirjoittaneet oman muunnelmamme yksinkertaisesta tiedonsiirtoprotokollasta ja keksineet, kuinka sen pitäisi toimia. Periaatteessa en löytänyt Amerikkaa täältä enkä kirjoittanut paljon uutta, mutta Habresta ei ollut vastaavia artikkeleita, ja osana artikkelisarjan kirjoittamista cmd-apuohjelmista oli mahdotonta olla koskematta siihen.

viitteet:

Lähdekoodivarasto
Lyhyesti TFTP:stä
Sama asia, mutta venäjäksi

Lähde: will.com

Lisää kommentti