كتابة البرامج مع وظائف الأدوات المساعدة لخادم العميل في Windows، الجزء 02

استمرارًا لسلسلة المقالات المستمرة المخصصة للتطبيقات المخصصة للأدوات المساعدة لوحدة التحكم في Windows، لا يسعنا إلا أن نتطرق إلى TFTP (بروتوكول نقل الملفات البسيط) - وهو بروتوكول نقل ملفات بسيط.

كما في المرة الأخيرة، دعنا نتناول النظرية بإيجاز، ونرى الكود الذي ينفذ وظيفة مشابهة للوظيفة المطلوبة، ثم نقوم بتحليلها. مزيد من التفاصيل - تحت الخفض

لن أقوم بنسخ المعلومات المرجعية ولصقها، والتي يمكن العثور على روابط لها تقليديًا في نهاية المقالة، سأقول فقط أن 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++، أو في على الأقل goto الرهيب والرهيب، والذي يسمح لك بتنفيذ هذا بشكل جميل. إذا كنت تعرف كيفية جعل الكود أكثر أناقة، فأنا أرحب بالنقد في التعليقات. يبدو لي أن هناك حاجة إلى قاموس سلسلة المفوض هنا، ولكن لا يوجد مفوض ...

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

تعرض طريقة الوضع، التي تغلف طريقة 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());
        }
    }

تقوم طريقة get باسترداد ملف الخادم. كما ذكرنا سابقًا في القسم الخاص بالعميل من البرنامج، لنقل ملف بنجاح، تحتاج إلى معرفة حجمه، المخزن في عدد صحيح طويل، لذلك قمت بتقسيمه إلى مصفوفة مكونة من 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());
        }
    }

كي تختصر:

لقد كتبنا للتو نسختنا الخاصة من بروتوكول نقل البيانات البسيط واكتشفنا كيف ينبغي أن يعمل. من حيث المبدأ، لم أكتشف أمريكا هنا ولم أكتب الكثير من الأشياء الجديدة، ولكن لم تكن هناك مقالات مماثلة عن حبري، وكجزء من كتابة سلسلة من المقالات حول أدوات cmd المساعدة، كان من المستحيل عدم التطرق إليها.

المراجع:

مستودع كود المصدر
باختصار حول TFTP
نفس الشيء، ولكن باللغة الروسية

المصدر: www.habr.com

إضافة تعليق