إن متابعة TFTP (Trivial File Transfer Protocol) هي عبارة عن بروتوكول بسيط لنقل الملفات.
ومثلما حدث في المرة الأخيرة ، فإننا نتجاوز النظرية لفترة وجيزة ونرى رمزًا ينفذ وظيفة مماثلة لتلك المطلوبة ، ونحللها. قراءة المزيد - تحت خفض
لن أقوم بنسخ المعلومات المرجعية ، الروابط التي يمكن العثور عليها تقليديًا في نهاية المقالة ، أقول فقط إن TFTP في جوهرها عبارة عن صيغة مبسطة لبروتوكول FTP حيث تتم إزالة إعداد التحكم في الوصول ، وفي الحقيقة لا يوجد شيء هنا باستثناء أوامر استلام الملف ونقله . ومع ذلك ، من أجل جعل تطبيقنا أكثر أناقة قليلاً وتكييفه مع المبادئ الحالية لكود الكتابة ، يتم تغيير بناء الجملة قليلاً - لا يغير مبادئ العمل ، لكن الواجهة IMHO تصبح منطقية أكثر قليلاً وتجمع بين الجوانب الإيجابية لبروتوكول FTP و TFTP.
على وجه الخصوص ، عند بدء التشغيل ، يسأل العميل عن عنوان IP الخاص بالخادم والمنفذ الذي يفتح عليه TFTP المخصص (نظرًا لعدم التوافق مع البروتوكول القياسي ، فقد اعتبرت أنه من المناسب ترك خيار تحديد المنفذ للمستخدم) ، وبعد ذلك يحدث اتصال ، ونتيجة لذلك يمكن للعميل إرسال أحد الأوامر - الحصول على أو وضع ، لتلقي أو إرسال ملف إلى الخادم. يتم إرسال جميع الملفات في الوضع الثنائي - من أجل تبسيط المنطق.
لتنفيذ البروتوكول ، لقد استخدمت تقليديا 4 فئات:
- TFTPClient
- TFTPServer
- 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("\nDone\n"); } 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("\nDONE\n"); } 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); } }
TFTPServer
تختلف وظيفة الخادم عن وظيفة العميل بشكل عام فقط في أن الأوامر الواردة منه لا تأتي من لوحة المفاتيح ، ولكن من المقبس. تتطابق بعض الأساليب ، لذلك لن أقدمها ، ولن أذكر سوى الاختلافات.
للبدء هنا ، يتم استخدام طريقة التشغيل ، والتي تستقبل منفذًا لإدخال البيانات ومعالجتها من المقبس في دورة أبدية.
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("\nDone\n"); }; 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نفس الشيء ، ولكن باللغة الروسية