Menulis perisian dengan kefungsian utiliti pelayan klien Windows, bahagian 02

Meneruskan siri artikel berterusan yang ditumpukan kepada pelaksanaan tersuai bagi utiliti konsol Windows, kami tidak boleh tidak menyentuh tentang TFTP (Trivial File Transfer Protocol) - protokol pemindahan fail yang mudah.

Seperti kali terakhir, mari kita semak teori secara ringkas, lihat kod yang melaksanakan fungsi yang serupa dengan yang diperlukan, dan menganalisisnya. Butiran lanjut - di bawah potongan

Saya tidak akan menyalin-tampal maklumat rujukan, pautan kepada yang secara tradisinya boleh didapati di penghujung artikel, saya hanya akan mengatakan bahawa pada terasnya, TFTP ialah variasi ringkas protokol FTP, di mana tetapan kawalan akses telah dialih keluar, dan sebenarnya tiada apa-apa di sini kecuali arahan untuk menerima dan memindahkan fail . Walau bagaimanapun, untuk menjadikan pelaksanaan kami sedikit lebih elegan dan disesuaikan dengan prinsip penulisan kod semasa, sintaks telah diubah sedikit - ini tidak mengubah prinsip operasi, tetapi antara muka, IMHO, menjadi sedikit lebih logik dan menggabungkan aspek positif FTP dan TFTP.

Khususnya, apabila dilancarkan, pelanggan meminta alamat IP pelayan dan port di mana TFTP tersuai dibuka (disebabkan ketidakserasian dengan protokol standard, saya menganggap adalah wajar untuk memberikan pengguna keupayaan untuk memilih port), selepas itu sambungan berlaku, akibatnya pelanggan boleh menghantar salah satu arahan - dapatkan atau letak, untuk menerima atau menghantar fail ke pelayan. Semua fail dihantar dalam mod binari untuk memudahkan logik.

Untuk melaksanakan protokol, saya secara tradisinya menggunakan 4 kelas:

  • TFTPClient
  • TFTPServer
  • TFTPClientTester
  • TFTPServerTester

Disebabkan fakta bahawa kelas ujian wujud hanya untuk menyahpepijat yang utama, saya tidak akan menganalisisnya, tetapi kod itu akan berada dalam repositori; pautan kepadanya boleh didapati di penghujung artikel. Sekarang saya akan melihat kelas utama.

TFTPClient

Tugas kelas ini adalah untuk menyambung ke pelayan jauh dengan nombor ip dan portnya, membaca arahan daripada aliran input (dalam kes ini, papan kekunci), menghuraikannya, memindahkannya ke pelayan, dan, bergantung pada sama ada anda perlu menghantar atau menerima fail, memindahkannya atau mendapatkannya.

Kod untuk melancarkan klien untuk menyambung ke pelayan dan menunggu arahan daripada aliran input kelihatan seperti ini. Sebilangan pembolehubah global yang digunakan di sini diterangkan di luar artikel, dalam teks penuh program. Disebabkan remeh mereka, saya tidak memetiknya supaya tidak membebankan artikel tersebut.

 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 lihat kaedah yang dipanggil dalam blok kod ini:

Di sini fail dihantar - menggunakan pengimbas, kami membentangkan kandungan fail sebagai tatasusunan bait, yang kami tulis satu demi satu ke soket, selepas itu kami menutupnya dan membukanya semula (bukan penyelesaian yang paling jelas, tetapi ia menjamin pelepasan sumber), selepas itu kami memaparkan mesej tentang pemindahan yang berjaya.

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

Serpihan kod ini menerangkan mendapatkan semula data daripada pelayan. Segala-galanya sekali lagi remeh, hanya blok pertama kod yang menarik. Untuk memahami dengan tepat berapa banyak bait yang perlu dibaca dari soket, anda perlu tahu berapa berat fail yang dipindahkan. Saiz fail pada pelayan diwakili sebagai integer panjang, jadi 4 bait diterima di sini, yang kemudiannya ditukar kepada satu nombor. Ini bukan pendekatan yang sangat Java, ia agak serupa untuk SI, tetapi ia menyelesaikan masalahnya.

Kemudian semuanya adalah remeh - kami menerima bilangan bait yang diketahui dari soket dan menulisnya ke fail, selepas itu kami memaparkan mesej kejayaan.

   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 arahan selain get atau put dimasukkan ke dalam tetingkap klien, fungsi showErrorMessage akan dipanggil, menunjukkan bahawa input adalah salah. Disebabkan remeh, saya tidak akan memetiknya. Agak lebih menarik ialah fungsi menerima dan membelah rentetan input. Kami menghantar pengimbas ke dalamnya, dari mana kami menjangkakan untuk menerima baris yang dipisahkan oleh dua ruang dan mengandungi arahan, alamat sumber dan alamat destinasi.

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

Menghantar arahanβ€”menghantar arahan yang dimasukkan daripada pengimbas ke soket dan memaksanya dihantar

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

Pemilih ialah fungsi yang menentukan tindakan program bergantung pada rentetan yang dimasukkan. Semuanya di sini tidak begitu cantik dan helah yang digunakan bukanlah yang terbaik dengan keluar paksa di luar blok kod, tetapi sebab utama untuk ini adalah ketiadaan dalam Java beberapa perkara, seperti perwakilan dalam C#, penunjuk fungsi dari C++, atau di sekurang-kurangnya goto yang dahsyat dan dahsyat, yang membolehkan anda melaksanakan ini dengan indah. Jika anda tahu cara membuat kod itu lebih elegan, saya mengalu-alukan kritikan dalam komen. Nampaknya pada saya kamus perwakilan String diperlukan di sini, tetapi tiada perwakilan...

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

TFTPServer

Fungsi pelayan berbeza daripada kefungsian klien, pada umumnya, hanya dengan arahan yang datang kepadanya bukan dari papan kekunci, tetapi dari soket. Beberapa kaedah umumnya sama, jadi saya tidak akan memetiknya, saya hanya akan menyentuh perbezaannya.

Untuk memulakan, kaedah larian digunakan, yang menerima port sebagai input dan memproses data input daripada soket dalam gelung kekal.

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

Kaedah put, yang membalut kaedah writeToFileFromSocket yang membuka aliran tulis ke fail dan menulis semua bait input daripada soket, memaparkan mesej yang menunjukkan kejayaan menyelesaikan pemindahan apabila 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());
        }
    }

Kaedah get mendapatkan semula fail pelayan. Seperti yang telah disebutkan dalam bahagian di sebelah klien program, untuk berjaya memindahkan fail, anda perlu mengetahui saiznya, disimpan dalam integer yang panjang, jadi saya membahagikannya kepada tatasusunan 4 bait, memindahkannya bait demi bait ke soket, dan kemudian, setelah menerima dan memasangnya pada klien ke dalam nombor kembali, saya memindahkan semua bait yang membentuk fail, membaca dari aliran input daripada fail.


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

Kaedah getAndParseInput adalah sama seperti dalam klien, satu-satunya perbezaan ialah ia membaca data dari soket dan bukannya dari papan kekunci. Kod itu berada dalam repositori, sama seperti pemilih.
Dalam kes ini, permulaan diletakkan dalam blok kod yang berasingan, kerana dalam pelaksanaan ini, selepas pemindahan selesai, sumber dilepaskan dan diduduki semula - sekali lagi untuk memberikan 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());
        }
    }

Untuk Meringkaskan:

Kami baru sahaja menulis variasi kami sendiri pada protokol pemindahan data mudah dan memikirkan cara ia harus berfungsi. Pada dasarnya, saya tidak menemui Amerika di sini dan tidak menulis banyak perkara baru, tetapi tidak ada artikel serupa tentang HabrΓ©, dan sebagai sebahagian daripada menulis satu siri artikel tentang utiliti cmd adalah mustahil untuk tidak menyentuhnya.

Rujukan:

Repositori kod sumber
Secara ringkas tentang TFTP
Perkara yang sama, tetapi dalam bahasa Rusia

Sumber: www.habr.com

Tambah komen