Menulis perangkat lunak dengan fungsionalitas utilitas klien-server. Windows, bagian 02

Melanjutkan rangkaian artikel yang membahas implementasi kustom dari utilitas konsol. Windows Tidak mungkin untuk tidak menyebutkan TFTP (Trivial File Transfer Protocol) - sebuah protokol transfer file yang sederhana.

Seperti sebelumnya, kita akan mengulas secara singkat teorinya, melihat kode yang mengimplementasikan fungsionalitas serupa dengan yang dibutuhkan, dan menganalisisnya. Detail selengkapnya di bawah ini.

Saya tidak akan menyalin dan menempel informasi referensi, yang tautannya biasanya dapat ditemukan di akhir artikel. Saya hanya akan mengatakan bahwa TFTP pada dasarnya adalah versi sederhana dari protokol FTP, menghilangkan pengaturan kontrol akses dan pada dasarnya hanya terdiri dari perintah transfer dan pengambilan file. Namun, untuk membuat implementasi kami sedikit lebih elegan dan mudah beradaptasi dengan prinsip pengkodean modern, sintaksnya telah sedikit dimodifikasi. Ini tidak mengubah prinsip operasinya, tetapi antarmuka, menurut saya, menjadi sedikit lebih logis dan menggabungkan aspek positif dari FTP dan TFTP.

Secara spesifik, saat diluncurkan, klien meminta alamat IP server dan port tempat TFTP kustom dibuka (karena ketidaksesuaian dengan protokol standar, saya rasa tepat untuk menyerahkan pemilihan port kepada pengguna). Kemudian, koneksi dibuat, memungkinkan klien untuk mengirim salah satu perintahβ€”get atau putβ€”untuk mengambil atau mengirim file ke server. Semua file dikirim dalam mode biner untuk menyederhanakan logika.

Untuk mengimplementasikan protokol tersebut, saya biasanya menggunakan 4 kelas:

  • Klien TFTP
  • Server TFTP
  • TFTPClientTester
  • Penguji Server TFTP

Karena kelas pengujian hanya ada untuk men-debug kelas utama, saya tidak akan membahasnya. Kode tersebut akan ada di repositori; tautannya dapat ditemukan di akhir artikel. Sekarang saya akan membahas kelas-kelas utama.

Klien TFTP

Tugas kelas ini adalah untuk terhubung ke server jarak jauh melalui alamat IP dan nomor port-nya, membaca perintah dari aliran input (dalam hal ini, keyboard), menguraikannya, mengirimkannya ke server, dan, tergantung pada apakah file perlu dikirim atau diterima, mengirimkannya atau menerimanya.

Kode untuk menjalankan klien agar terhubung ke server dan menunggu perintah dari aliran input terlihat seperti ini. Sejumlah variabel global yang digunakan di sini dijelaskan di luar artikel, dalam teks program lengkap. Karena sifatnya yang sepele, saya tidak menyertakannya di sini untuk menghindari kerumitan artikel.

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

Mari kita bahas metode-metode yang dipanggil dalam blok kode ini:

Di sinilah file dikirimβ€”menggunakan pemindai, kita merepresentasikan isi file sebagai larik byte, yang kita tulis satu per satu ke soket, kemudian menutupnya dan membukanya kembali (bukan solusi yang paling jelas, tetapi ini menjamin pelepasan sumber daya), setelah itu kita menampilkan pesan tentang keberhasilan transfer.

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

Cuplikan kode ini menjelaskan cara menerima data dari server. Sekali lagi, semuanya sederhana; hanya blok kode pertama yang menarik. Untuk memahami secara tepat berapa banyak byte yang perlu dibaca dari soket, kita perlu mengetahui ukuran file yang ditransfer. Ukuran file di server direpresentasikan sebagai bilangan bulat panjang (long integer), jadi empat byte diterima di sini, yang kemudian dikonversi menjadi satu angka. Ini bukan pendekatan yang sangat khas Java; ini lebih mirip pendekatan C, tetapi berhasil menyelesaikan tugas.

Kemudian semuanya menjadi mudah - kita menerima sejumlah byte yang diketahui dari soket dan menuliskannya ke dalam file, setelah itu kita menampilkan pesan keberhasilan.

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

Jika perintah selain "get" atau "put" dimasukkan ke jendela klien, fungsi showErrorMessage akan dipanggil, yang menunjukkan bahwa input tidak valid. Karena sifatnya yang sepele, saya tidak akan menyertakannya. Fungsi untuk menerima dan memisahkan string input agak lebih menarik. Kita memberikannya sebuah scanner, yang darinya kita berharap menerima string yang dipisahkan oleh dua spasi, yang berisi perintah, alamat sumber, dan alamat tujuan.

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

Mengirim perintah - mentransfer perintah yang dimasukkan dari pemindai ke soket dan memaksanya untuk dikirim.

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

Selector adalah fungsi yang menentukan tindakan program berdasarkan string input. Cara kerjanya tidak terlalu elegan dan menggunakan teknik yang agak sederhana untuk memaksa kode keluar dari blok, tetapi alasan utamanya adalah Java kekurangan fitur-fitur tertentu, seperti delegate di C#, pointer fungsi di C++, atau bahkan goto yang ditakuti, yang memungkinkan implementasi yang lebih elegan. Jika Anda tahu cara membuat kode sedikit lebih elegan, silakan bagikan pendapat Anda di kolom komentar. Saya rasa kamus String-delegate diperlukan di sini, tetapi tidak ada delegate...

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

Server TFTP

Fungsionalitas server berbeda dari fungsionalitas klien pada dasarnya hanya pada cara perintah diterima, yaitu melalui soket dan bukan melalui keyboard. Beberapa metode sebenarnya sama, jadi saya tidak akan membahasnya di sini; saya hanya akan menyinggung perbedaannya saja.

Untuk memulai program, digunakan metode run, yang menerima port sebagai input dan memproses data input dari soket dalam sebuah loop tak terbatas.

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

Metode put, yang membungkus metode writeToFileFromSocket, membuka aliran penulisan ke sebuah file dan menulis semua byte input dari soket, serta mencetak pesan tentang keberhasilan penyelesaian transfer setelah penulisan selesai.

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

Metode `get` mengambil file dari server. Seperti yang disebutkan di bagian sisi klien, untuk berhasil mentransfer file, saya perlu mengetahui ukurannya, yang disimpan sebagai bilangan bulat panjang (long integer). Oleh karena itu, saya membaginya menjadi array 4 byte, mentransfernya byte demi byte ke soket, dan kemudian, setelah menerimanya dan menyusunnya kembali menjadi angka di sisi klien, saya mentransfer semua byte yang membentuk file tersebut, yang dibaca dari aliran input file.


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

Metode getAndParseInput identik dengan metode klien, kecuali bahwa metode ini membaca data dari soket dan bukan dari keyboard. Kode tersebut ada di repositori, begitu pula dengan selector.
Dalam hal ini, inisialisasi dipindahkan ke blok kode terpisah, karena dalam kerangka implementasi ini, setelah transfer selesai, sumber daya dilepaskan dan dialokasikan kembali - sekali lagi untuk memastikan perlindungan terhadap kebocoran memori.

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

Sebagai rangkuman:

Kami hanya menulis variasi kami sendiri dari protokol transfer data sederhana dan mencari tahu bagaimana cara kerjanya. Saya sebenarnya tidak membuat terobosan baru di sini atau mengatakan sesuatu yang benar-benar baru, tetapi tidak ada artikel serupa di Habr, dan sebagai bagian dari serangkaian artikel tentang utilitas cmd, tidak mungkin untuk tidak membahasnya.

Бсылки:

Repositori kode sumber
Penjelasan singkat tentang TFTP
Sama saja, tapi dalam bahasa Rusia.

Sumber: www.habr.com

Beli hosting yang andal untuk situs dengan perlindungan DDoS, server VPS VDS πŸ”₯ Beli hosting website andal dengan perlindungan DDoS, server VPS VDS | ProHoster