Viết phần mềm với chức năng tiện ích client-server của Windows, phần 02

Tiếp tục loạt bài viết liên tục dành cho việc triển khai tùy chỉnh các tiện ích bảng điều khiển Windows, chúng tôi không thể không đề cập đến TFTP (Giao thức truyền tệp tầm thường) - một giao thức truyền tệp đơn giản.

Như lần trước, chúng ta hãy lướt qua lý thuyết một cách ngắn gọn, xem mã triển khai chức năng tương tự như chức năng được yêu cầu và phân tích nó. Thêm chi tiết - dưới phần cắt

Tôi sẽ không sao chép-dán thông tin tham khảo, các liên kết mà theo truyền thống có thể được tìm thấy ở cuối bài viết, tôi sẽ chỉ nói rằng về cốt lõi, TFTP là một biến thể đơn giản của giao thức FTP, trong đó cài đặt kiểm soát truy cập có đã bị xóa và thực tế ở đây không có gì ngoại trừ các lệnh nhận và truyền tệp . Tuy nhiên, để làm cho việc triển khai của chúng tôi thanh lịch hơn một chút và thích ứng với các nguyên tắc viết mã hiện tại, cú pháp đã được thay đổi một chút - điều này không thay đổi nguyên tắc hoạt động, nhưng giao diện, IMHO, trở nên hợp lý hơn một chút và kết hợp những mặt tích cực của FTP và TFTP.

Đặc biệt, khi khởi chạy, máy khách yêu cầu địa chỉ IP của máy chủ và cổng mà TFTP tùy chỉnh được mở (do không tương thích với giao thức chuẩn, tôi cho rằng việc để cho người dùng khả năng chọn một cổng là phù hợp), sau đó một kết nối xảy ra, do đó máy khách có thể gửi một trong các lệnh - nhận hoặc đặt, nhận hoặc gửi tệp đến máy chủ. Tất cả các tệp được gửi ở chế độ nhị phân để đơn giản hóa logic.

Để triển khai giao thức, theo truyền thống tôi sử dụng 4 lớp:

  • TFTPClient
  • Máy chủ TFTP
  • TFTPClientTester
  • TFTPServerTester

Do thực tế là các lớp kiểm tra chỉ tồn tại để gỡ lỗi những lớp chính nên tôi sẽ không phân tích chúng, nhưng mã sẽ nằm trong kho lưu trữ; bạn có thể tìm thấy liên kết đến nó ở cuối bài viết. Bây giờ tôi sẽ xem xét các lớp chính.

TFTPClient

Nhiệm vụ của lớp này là kết nối với máy chủ từ xa bằng cách sử dụng số IP và cổng của nó, đọc lệnh từ luồng đầu vào (trong trường hợp này là bàn phím), phân tích cú pháp, chuyển nó đến máy chủ và tùy thuộc vào việc bạn có cần gửi hoặc nhận một tập tin, chuyển nó hoặc nhận.

Mã khởi chạy máy khách để kết nối với máy chủ và đợi lệnh từ luồng đầu vào trông như thế này. Một số biến toàn cục được sử dụng ở đây được mô tả bên ngoài bài viết, trong toàn bộ nội dung của chương trình. Do tính tầm thường của chúng nên tôi không trích dẫn để không làm bài viết bị quá tải.

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

Chúng ta hãy xem qua các phương thức được gọi trong khối mã này:

Ở đây, tệp được gửi - bằng cách sử dụng máy quét, chúng tôi trình bày nội dung của tệp dưới dạng một mảng byte, chúng tôi ghi từng byte một vào ổ cắm, sau đó chúng tôi đóng và mở lại (không phải là giải pháp rõ ràng nhất, nhưng nó đảm bảo giải phóng tài nguyên), sau đó chúng tôi hiển thị thông báo chuyển thành công.

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

Đoạn mã này mô tả việc lấy dữ liệu từ máy chủ. Mọi thứ lại trở nên tầm thường, chỉ có khối mã đầu tiên được quan tâm. Để hiểu chính xác cần đọc bao nhiêu byte từ ổ cắm, bạn cần biết tệp được chuyển nặng bao nhiêu. Kích thước tệp trên máy chủ được biểu thị dưới dạng số nguyên dài, do đó 4 byte được chấp nhận ở đây, sau đó được chuyển đổi thành một số. Đây không phải là một cách tiếp cận Java, nó khá giống với SI, nhưng nó giải quyết được vấn đề của nó.

Sau đó, mọi thứ đều tầm thường - chúng tôi nhận được một số byte đã biết từ ổ cắm và ghi chúng vào một tệp, sau đó chúng tôi hiển thị thông báo thành công.

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

Nếu một lệnh khác ngoài get hoặc put được nhập vào cửa sổ máy khách, hàm showErrorMessage sẽ được gọi, cho biết rằng dữ liệu nhập vào không chính xác. Vì tầm thường nên tôi sẽ không trích dẫn. Thú vị hơn một chút là chức năng nhận và tách chuỗi đầu vào. Chúng tôi chuyển máy quét vào đó, từ đó chúng tôi mong đợi nhận được một dòng cách nhau bằng hai dấu cách và chứa lệnh, địa chỉ nguồn và địa chỉ đích.

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

Gửi lệnh—truyền lệnh được nhập từ máy quét đến ổ cắm và buộc gửi lệnh đó

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

Bộ chọn là một hàm xác định hành động của chương trình tùy thuộc vào chuỗi đã nhập. Mọi thứ ở đây không đẹp lắm và thủ thuật được sử dụng không phải là thủ thuật tốt nhất buộc phải thoát ra ngoài khối mã, nhưng lý do chính cho điều này là do Java thiếu một số thứ, như đại biểu trong C#, con trỏ hàm từ C++ hoặc tại ít nhất là goto khủng khiếp và khủng khiếp, cho phép bạn thực hiện điều này một cách đẹp đẽ. Nếu bạn biết cách làm cho mã thanh lịch hơn một chút, tôi hoan nghênh những lời chỉ trích trong phần bình luận. Đối với tôi, có vẻ như ở đây cần có một từ điển đại biểu chuỗi, nhưng không có đại biểu ...

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

Máy chủ TFTP

Chức năng của máy chủ nói chung khác với chức năng của máy khách, chỉ ở chỗ các lệnh đến với nó không phải từ bàn phím mà từ ổ cắm. Một số phương pháp nhìn chung giống nhau nên tôi sẽ không trích dẫn mà chỉ đề cập đến những điểm khác biệt.

Để bắt đầu, phương thức chạy được sử dụng, phương thức này nhận một cổng làm đầu vào và xử lý dữ liệu đầu vào từ ổ cắm trong một vòng lặp vĩnh cửu.

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

Phương thức put bao bọc phương thức writeToFileFromSocket mở luồng ghi vào tệp và ghi tất cả byte đầu vào từ ổ cắm, hiển thị thông báo cho biết quá trình truyền đã hoàn tất thành công khi quá trình ghi hoàn tất.

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

Phương thức get lấy tệp máy chủ. Như đã đề cập trong phần phía máy khách của chương trình, để truyền thành công một tệp, bạn cần biết kích thước của nó, được lưu trữ dưới dạng một số nguyên dài, vì vậy tôi chia nó thành một mảng 4 byte, truyền chúng theo từng byte. vào ổ cắm, sau đó, sau khi nhận và tập hợp chúng trên máy khách thành một số trở lại, tôi chuyển tất cả các byte tạo nên tệp, đọc từ luồng đầu vào từ tệp.


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

Phương thức getAndParseInput giống như trong máy khách, điểm khác biệt duy nhất là nó đọc dữ liệu từ socket thay vì từ bàn phím. Mã nằm trong kho lưu trữ, giống như bộ chọn.
Trong trường hợp này, việc khởi tạo được đặt trong một khối mã riêng biệt, bởi vì trong quá trình triển khai này, sau khi quá trình truyền hoàn tất, các tài nguyên sẽ được giải phóng và được sử dụng lại - một lần nữa để cung cấp khả năng bảo vệ chống rò rỉ bộ nhớ.

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

Tóm lại:

Chúng tôi vừa viết biến thể của riêng mình trên một giao thức truyền dữ liệu đơn giản và tìm ra cách hoạt động của nó. Về nguyên tắc, tôi không khám phá nước Mỹ ở đây và không viết nhiều điều mới, nhưng không có bài viết tương tự nào về Habré, và là một phần của việc viết một loạt bài về tiện ích cmd, không thể không nhắc đến nó.

Links:

Kho lưu trữ mã nguồn
Sơ lược về TFTP
Điều tương tự, nhưng bằng tiếng Nga

Nguồn: www.habr.com

Thêm một lời nhận xét