การเขียนซอฟต์แวร์ด้วยฟังก์ชันการทำงานของยูทิลิตี้ไคลเอ็นต์-เซิร์ฟเวอร์ Windows ตอนที่ 02

จากการดำเนินการต่อในบทความต่อเนื่องที่เกี่ยวข้องกับการใช้งานยูทิลิตี้คอนโซล Windows แบบกำหนดเองเราไม่สามารถช่วยได้นอกจากสัมผัส TFTP (Trivial File Transfer Protocol) - โปรโตคอลการถ่ายโอนไฟล์อย่างง่าย

คราวที่แล้ว มาดูทฤษฎีสั้นๆ ดูโค้ดที่ใช้ฟังก์ชันการทำงานคล้ายกับที่ต้องการ แล้ววิเคราะห์ รายละเอียดเพิ่มเติม-อยู่ระหว่างดำเนินการ

ฉันจะไม่คัดลอกและวางข้อมูลอ้างอิง ลิงก์ที่ปกติแล้วจะพบได้ในตอนท้ายของบทความ ฉันจะบอกเพียงว่าโดยพื้นฐานแล้ว TFTP เป็นรูปแบบที่เรียบง่ายของโปรโตคอล FTP ซึ่งการตั้งค่าการควบคุมการเข้าถึงมี ถูกลบออกแล้ว และอันที่จริงไม่มีอะไรอยู่ที่นี่นอกจากคำสั่งสำหรับรับและถ่ายโอนไฟล์ อย่างไรก็ตาม เพื่อให้การใช้งานของเราดูหรูหราขึ้นอีกเล็กน้อยและปรับให้เข้ากับหลักการเขียนโค้ดในปัจจุบัน ไวยากรณ์จึงเปลี่ยนไปเล็กน้อย - นี่ไม่ได้เปลี่ยนหลักการทำงาน แต่อินเทอร์เฟซ IMHO จะกลายเป็นตรรกะมากขึ้นเล็กน้อย และ ผสมผสานด้านบวกของ FTP และ TFTP

โดยเฉพาะอย่างยิ่ง เมื่อเปิดตัว ไคลเอนต์จะร้องขอที่อยู่ IP ของเซิร์ฟเวอร์และพอร์ตที่ TFTP แบบกำหนดเองเปิดอยู่ (เนื่องจากความไม่เข้ากันกับโปรโตคอลมาตรฐาน ฉันจึงถือว่าเหมาะสมที่จะปล่อยให้ผู้ใช้สามารถเลือกพอร์ตได้) หลังจากนั้น การเชื่อมต่อเกิดขึ้นซึ่งเป็นผลมาจากการที่ไคลเอนต์สามารถส่งคำสั่งใดคำสั่งหนึ่ง - รับหรือใส่เพื่อรับหรือส่งไฟล์ไปยังเซิร์ฟเวอร์ ไฟล์ทั้งหมดจะถูกส่งในโหมดไบนารี่เพื่อลดความซับซ้อนของตรรกะ

ในการใช้โปรโตคอล ฉันใช้คลาส 4 แบบดั้งเดิม:

  • TFTPClient
  • TFTPเซิร์ฟเวอร์
  • TFTPClientTester
  • TFTPServerTester

เนื่องจากคลาสการทดสอบมีไว้เพื่อการดีบักคลาสหลักเท่านั้น ฉันจะไม่วิเคราะห์คลาสเหล่านั้น แต่โค้ดจะอยู่ในพื้นที่เก็บข้อมูล ลิงก์ไปยังคลาสนั้นอยู่ท้ายบทความ ตอนนี้ฉันจะดูชั้นเรียนหลัก

TFTPClient

งานของคลาสนี้คือการเชื่อมต่อกับเซิร์ฟเวอร์ระยะไกลโดยใช้ IP และหมายเลขพอร์ต อ่านคำสั่งจากสตรีมอินพุต (ในกรณีนี้คือแป้นพิมพ์) แยกวิเคราะห์ ถ่ายโอนไปยังเซิร์ฟเวอร์ และขึ้นอยู่กับว่าคุณ จำเป็นต้องส่งหรือรับไฟล์ ถ่ายโอนหรือรับ

รหัสสำหรับเรียกใช้ไคลเอ็นต์เพื่อเชื่อมต่อกับเซิร์ฟเวอร์และรอคำสั่งจากอินพุตสตรีมจะมีลักษณะดังนี้ ตัวแปรส่วนกลางจำนวนหนึ่งที่ใช้ในที่นี้มีการอธิบายไว้นอกบทความในข้อความฉบับเต็มของโปรแกรม เนื่องจากเรื่องไม่สำคัญฉันจึงไม่อ้างอิงเพื่อไม่ให้บทความมากเกินไป

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

มาดูเมธอดที่ถูกเรียกในบล็อคโค้ดนี้กันดีกว่า:

ที่นี่ไฟล์ถูกส่ง - โดยใช้สแกนเนอร์เรานำเสนอเนื้อหาของไฟล์เป็นอาร์เรย์ของไบต์ซึ่งเราเขียนทีละตัวลงในซ็อกเก็ตหลังจากนั้นเราจะปิดและเปิดใหม่อีกครั้ง (ไม่ใช่วิธีแก้ปัญหาที่ชัดเจนที่สุด แต่ รับประกันการปล่อยทรัพยากร) หลังจากนั้นเราจะแสดงข้อความเกี่ยวกับการถ่ายโอนที่สำเร็จ

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

ส่วนของโค้ดนี้อธิบายการดึงข้อมูลจากเซิร์ฟเวอร์ ทุกอย่างเป็นเรื่องเล็กน้อยอีกครั้งมีเพียงโค้ดบล็อกแรกเท่านั้นที่น่าสนใจ เพื่อให้เข้าใจได้อย่างแน่ชัดว่าต้องอ่านจำนวนไบต์จากซ็อกเก็ต คุณจำเป็นต้องทราบว่าไฟล์ที่ถ่ายโอนมีน้ำหนักเท่าใด ขนาดไฟล์บนเซิร์ฟเวอร์จะแสดงเป็นจำนวนเต็มยาว ดังนั้นจึงยอมรับขนาด 4 ไบต์ที่นี่ ซึ่งต่อมาจะถูกแปลงเป็นตัวเลขเดียว นี่ไม่ใช่แนวทาง Java มากนัก แต่ค่อนข้างคล้ายกับ SI แต่สามารถแก้ปัญหาได้

จากนั้นทุกอย่างก็ไม่สำคัญ - เราได้รับจำนวนไบต์ที่ทราบจากซ็อกเก็ตและเขียนลงในไฟล์หลังจากนั้นเราจะแสดงข้อความแสดงความสำเร็จ

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

หากมีการป้อนคำสั่งอื่นนอกเหนือจาก get หรือ put ลงในหน้าต่างไคลเอ็นต์ ฟังก์ชัน showErrorMessage จะถูกเรียกใช้ เพื่อระบุว่าอินพุตไม่ถูกต้อง เนื่องจากเรื่องไร้สาระ ฉันจะไม่อ้างอิงมัน สิ่งที่น่าสนใจกว่านั้นคือฟังก์ชั่นการรับและแยกสตริงอินพุต เราส่งเครื่องสแกนเข้าไป ซึ่งเราคาดว่าจะได้รับบรรทัดที่คั่นด้วยช่องว่างสองช่องและมีคำสั่ง ที่อยู่ต้นทาง และที่อยู่ปลายทาง

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

การส่งคำสั่ง—ส่งคำสั่งที่ป้อนจากเครื่องสแกนไปยังซ็อกเก็ตและบังคับให้ส่ง

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

ตัวเลือกคือฟังก์ชันที่กำหนดการทำงานของโปรแกรมโดยขึ้นอยู่กับสตริงที่ป้อน ทุกอย่างที่นี่ไม่ได้สวยงามมากนัก และเคล็ดลับที่ใช้ก็ไม่ใช่วิธีที่ดีที่สุดในการบังคับออกนอกบล็อกโค้ด แต่สาเหตุหลักคือไม่มีบางสิ่งใน Java เช่น ผู้รับมอบสิทธิ์ใน C# ตัวชี้ฟังก์ชันจาก C++ หรือที่ อย่างน้อยก็ข้ามไปที่น่ากลัวและแย่มากซึ่งทำให้คุณสามารถนำไปใช้ได้อย่างสวยงาม หากคุณรู้วิธีทำให้โค้ดดูหรูหราขึ้นอีกหน่อย ฉันยินดีรับฟังคำวิจารณ์ในความคิดเห็น สำหรับฉันดูเหมือนว่าจำเป็นต้องใช้พจนานุกรม String-delegate ที่นี่ แต่ไม่มีผู้รับมอบสิทธิ์ ...

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

TFTPเซิร์ฟเวอร์

ฟังก์ชันการทำงานของเซิร์ฟเวอร์แตกต่างจากฟังก์ชันการทำงานของไคลเอ็นต์โดยรวม เฉพาะในคำสั่งนั้นเท่านั้นที่เข้ามา ไม่ใช่จากแป้นพิมพ์ แต่มาจากซ็อกเก็ต โดยทั่วไปวิธีการบางอย่างจะเหมือนกันดังนั้นฉันจะไม่ให้ แต่จะพูดถึงความแตกต่างเท่านั้น

ในการเริ่มต้นใช้วิธีการรันซึ่งรับพอร์ตเป็นอินพุตและประมวลผลข้อมูลอินพุตจากซ็อกเก็ตในลูปนิรันดร์

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

เมธอด put ซึ่งล้อมเมธอด writeToFileFromSocket ที่เปิดสตรีมการเขียนไปยังไฟล์และเขียนไบต์อินพุตทั้งหมดจากซ็อกเก็ต จะแสดงข้อความที่ระบุว่าการถ่ายโอนเสร็จสมบูรณ์เมื่อการเขียนเสร็จสมบูรณ์

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

วิธีการรับดึงไฟล์เซิร์ฟเวอร์ ดังที่ได้กล่าวไว้แล้วในส่วนฝั่งไคลเอ็นต์ของโปรแกรม เพื่อให้ถ่ายโอนไฟล์ได้สำเร็จ คุณต้องทราบขนาดของไฟล์ โดยจัดเก็บไว้ในจำนวนเต็มยาว ดังนั้นฉันจึงแยกมันออกเป็นอาร์เรย์ 4 ไบต์ แล้วถ่ายโอนแบบไบต์ต่อไบต์ ไปที่ซ็อกเก็ตจากนั้นเมื่อได้รับและประกอบมันบนไคลเอนต์เป็นตัวเลขฉันจะถ่ายโอนไบต์ทั้งหมดที่ประกอบเป็นไฟล์อ่านจากสตรีมอินพุตจากไฟล์


 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 นั้นเหมือนกับในไคลเอนต์ ข้อแตกต่างเพียงอย่างเดียวคืออ่านข้อมูลจากซ็อกเก็ตแทนที่จะอ่านจากคีย์บอร์ด รหัสอยู่ในพื้นที่เก็บข้อมูล เช่นเดียวกับตัวเลือก
ในกรณีนี้ การเริ่มต้นจะอยู่ในบล็อกโค้ดที่แยกจากกัน เนื่องจาก ภายในการดำเนินการนี้ หลังจากการถ่ายโอนเสร็จสิ้น ทรัพยากรจะถูกปล่อยออกมาและถูกครอบครองอีกครั้ง - อีกครั้งเพื่อป้องกันการรั่วไหลของหน่วยความจำ

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

เพื่อสรุป:

เราเพิ่งเขียนรูปแบบของเราเองเกี่ยวกับโปรโตคอลการถ่ายโอนข้อมูลแบบธรรมดา และได้ทราบว่ามันควรทำงานอย่างไร โดยหลักการแล้ว ฉันไม่ได้ค้นพบอเมริกาที่นี่และไม่ได้เขียนสิ่งใหม่ ๆ มากนัก แต่ไม่มีบทความที่คล้ายกันเกี่ยวกับHabré และในการเขียนบทความชุดเกี่ยวกับยูทิลิตี้ cmd มันเป็นไปไม่ได้ที่จะไม่แตะต้องมัน

อ้างอิง:

ที่เก็บซอร์สโค้ด
สั้น ๆ เกี่ยวกับ TFTP
สิ่งเดียวกัน แต่เป็นภาษารัสเซีย

ที่มา: will.com

เพิ่มความคิดเห็น