Windows istemci-sunucu yardımcı programlarının işlevselliğine sahip yazılım yazma, bölüm 02

Windows konsol yardımcı programlarının özel uygulamalarına ayrılmış devam eden makale serisine devam ederken, basit bir dosya aktarım protokolü olan TFTP'ye (Önemsiz Dosya Aktarım Protokolü) değinmeden edemiyoruz.

Geçen sefer olduğu gibi teoriye kısaca göz atalım, istenilen işlevselliğe benzer işlevleri uygulayan kodu görelim ve analiz edelim. Daha fazla ayrıntı - kesimin altında

Bağlantıları geleneksel olarak makalenin sonunda bulunabilen referans bilgilerini kopyalayıp yapıştırmayacağım, sadece özünde TFTP'nin, erişim kontrolü ayarının bulunduğu FTP protokolünün basitleştirilmiş bir varyasyonu olduğunu söyleyeceğim. kaldırıldı ve aslında burada bir dosyayı alma ve aktarma komutları dışında hiçbir şey yok. Ancak, uygulamamızı biraz daha zarif hale getirmek ve mevcut kod yazma ilkelerine uygun hale getirmek için sözdizimi biraz değiştirildi - bu, çalışma ilkelerini değiştirmiyor ancak IMHO arayüzü biraz daha mantıklı hale geliyor ve FTP ve TFTP'nin olumlu yönlerini birleştirir.

Özellikle, istemci başlatıldığında sunucunun IP adresini ve özel TFTP'nin açık olduğu bağlantı noktasını ister (standart protokolle uyumsuzluk nedeniyle kullanıcıya bir bağlantı noktası seçme olanağı bırakmanın uygun olduğunu düşündüm), ardından istemcinin sunucuya bir dosya almak veya göndermek için komutlardan birini - al veya koy - gönderebilmesinin bir sonucu olarak bağlantı oluşur. Mantığı basitleştirmek için tüm dosyalar ikili modda gönderilir.

Protokolü uygulamak için geleneksel olarak 4 sınıf kullandım:

  • TFTPCistemcisi
  • TFTP Sunucusu
  • TFTPClientTest Cihazı
  • TTFTP Sunucu Test Cihazı

Test sınıflarının yalnızca ana sınıflarda hata ayıklamak için mevcut olması nedeniyle bunları analiz etmeyeceğim, ancak kod depoda olacak; makalenin sonunda ona bir bağlantı bulunabilir. Şimdi ana sınıflara bakacağım.

TFTPCistemcisi

Bu sınıfın görevi uzak bir sunucuya IP ve port numarasını kullanarak bağlanmak, giriş akışından (bu durumda klavyeden) bir komut okumak, onu ayrıştırmak, sunucuya aktarmak ve bağlı olarak Bir dosyayı göndermeniz veya almanız, aktarmanız veya almanız gerekir.

Sunucuya bağlanmak ve giriş akışından bir komut beklemek için istemciyi başlatma kodu şuna benzer. Burada kullanılan bir dizi global değişken, makalenin dışında, programın tam metninde açıklanmaktadır. Önemsiz olduklarından dolayı, makaleyi aşırı yüklememek için bunlardan alıntı yapmıyorum.

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

Bu kod bloğunda çağrılan yöntemlerin üzerinden geçelim:

Burada dosya gönderilir - bir tarayıcı kullanarak, dosyanın içeriğini birer birer sokete yazdığımız bir bayt dizisi olarak sunarız, ardından onu kapatıp tekrar açarız (en bariz çözüm değil, ancak kaynakların serbest bırakılmasını garanti eder), ardından başarılı aktarımla ilgili bir mesaj görüntüleriz.

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

Bu kod parçası sunucudan veri almayı açıklar. Her şey yine önemsizdir, yalnızca ilk kod bloğu ilgi çekicidir. Soketten tam olarak kaç byte okunması gerektiğini anlayabilmek için aktarılan dosyanın ağırlığını bilmeniz gerekir. Sunucudaki dosya boyutu uzun bir tamsayı olarak temsil edilir, dolayısıyla burada 4 bayt kabul edilir ve bunlar daha sonra bir sayıya dönüştürülür. Bu pek Java yaklaşımı değil, SI'ya oldukça benziyor ama sorununu çözüyor.

O zaman her şey önemsizdir - soketten bilinen sayıda bayt alırız ve bunları bir dosyaya yazarız, ardından bir başarı mesajı görüntüleriz.

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

İstemci penceresine get veya put dışında bir komut girilirse, girişin hatalı olduğunu belirten showErrorMessage işlevi çağrılacaktır. Önemsiz olduğu için alıntı yapmayacağım. Biraz daha ilginç olanı, giriş dizesini alma ve bölme işlevidir. Tarayıcıyı, iki boşlukla ayrılmış ve komutu, kaynak adresini ve hedef adresini içeren bir satır almayı beklediğimiz tarayıcıya aktarıyoruz.

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

Komut gönderme—tarayıcıdan girilen komutu sokete iletir ve gönderilmesini zorlar

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

Seçici, girilen dizeye bağlı olarak programın eylemlerini belirleyen bir işlevdir. Buradaki her şey çok güzel değil ve kullanılan hile, kod bloğunun dışında zorla çıkışa sahip en iyi yöntem değil, ancak bunun ana nedeni, C#'taki delegeler, C++'daki işlev işaretçileri veya Java'daki bazı şeylerin olmamasıdır. en azından bunu güzel bir şekilde uygulamanıza izin veren korkunç ve berbat goto. Kodu biraz daha şık hale getirmeyi biliyorsanız yorumlarda eleştirileri memnuniyetle karşılarım. Bana öyle geliyor ki burada bir String-delege sözlüğüne ihtiyaç var, ancak temsilci yok ...

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

TFTP Sunucusu

Sunucunun işlevselliği, istemcinin işlevselliğinden büyük ölçüde farklıdır, yalnızca komutların ona klavyeden değil soketten gelmesi açısından. Yöntemlerin bir kısmı genel olarak aynı olduğundan bunlardan alıntı yapmayacağım, sadece farklılıklara değineceğim.

Başlamak için, giriş olarak bir bağlantı noktası alan ve giriş verilerini soketten sonsuz bir döngüde işleyen run yöntemi kullanılır.

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

Bir dosyaya yazma akışı açan ve yuvadaki tüm giriş baytlarını yazan writeToFileFromSocket yöntemini saran put yöntemi, yazma tamamlandığında aktarımın başarıyla tamamlandığını belirten bir mesaj görüntüler.

    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 yöntemi sunucu dosyasını alır. Programın istemci tarafındaki bölümde daha önce de belirtildiği gibi, bir dosyayı başarılı bir şekilde aktarmak için, uzun bir tamsayıda saklanan boyutunu bilmeniz gerekir, bu yüzden onu 4 baytlık bir diziye böldüm, bayt bayt aktardım sokete ve ardından bunları istemcide alıp bir sayıya birleştirdikten sonra, dosyayı oluşturan tüm baytları dosyadan giriş akışından okuyarak aktarıyorum.


 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 yöntemi istemcidekiyle aynıdır; tek fark, verileri klavye yerine yuvadan okumasıdır. Kod tıpkı seçici gibi depodadır.
Bu durumda başlatma işlemi ayrı bir kod bloğuna yerleştirilir, çünkü Bu uygulamada, aktarım tamamlandıktan sonra kaynaklar serbest bırakılır ve yeniden kullanılır; böylece bellek sızıntılarına karşı koruma sağlanır.

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

Özetle:

Basit bir veri aktarım protokolü üzerine kendi varyasyonumuzu yazdık ve nasıl çalışması gerektiğini çözdük. Prensip olarak Amerika'yı burada keşfetmedim ve pek fazla yeni şey yazmadım, ancak Habré hakkında benzer makaleler yoktu ve cmd yardımcı programları hakkında bir dizi makale yazmanın bir parçası olarak buna değinmemek imkansızdı.

Bağlantılar:

Kaynak kodu deposu
Kısaca TFTP hakkında
Aynı şey, ama Rusça

Kaynak: habr.com

Yorum ekle