تستخدم البرامج ، أثناء العمل ، ذاكرة الوصول العشوائي لأجهزة الكمبيوتر. في JavaScript ، في بيئة Node.js ، يمكنك كتابة مشاريع الخوادم بمقاييس مختلفة. تنظيم العمل مع الذاكرة هو دائما مهمة صعبة ومسؤولة. في الوقت نفسه ، إذا كان بعض المبرمجين يشاركون بإحكام شديد في إدارة الذاكرة بلغات مثل C و C ++ ، فإن لدى JS آليات تلقائية ، على ما يبدو ، تزيل بالكامل مسؤولية المبرمج عن العمل الفعال مع الذاكرة. ومع ذلك ، هذا في الواقع ليس هو الحال. يمكن أن تتداخل التعليمة البرمجية المكتوبة بشكل سيء لـ Node.js مع التشغيل العادي للخادم بالكامل الذي يعمل عليه.

ستركز المادة ، التي نترجمها اليوم ، على العمل الفعال مع الذاكرة في بيئة Node.js. على وجه الخصوص ، ستتم مناقشة مفاهيم مثل التدفقات ، المخازن المؤقتة ، وطريقة دفق
pipe()
هنا. سيتم استخدام Node.js v8.12.0 في التجارب. يمكن العثور على مستودع به كود مثال
هنا .
المهمة: نسخ ملف ضخم
إذا طُلب من شخص ما إنشاء برنامج لنسخ الملفات في Node.js ، فمن المرجح أنه سيكتب على الفور عما يظهر أدناه. نقوم بتسمية الملف الذي يحتوي على هذا الرمز
basic_copy.js
.
const fs = require('fs'); let fileName = process.argv[2]; let destPath = process.argv[3]; fs.readFile(fileName, (err, data) => { if (err) throw err; fs.writeFile(destPath || 'output', data, (err) => { if (err) throw err; }); console.log('New file has been created!'); });
يقوم هذا البرنامج بإنشاء معالجات لقراءة وكتابة ملف باسم محدد ويحاول كتابة بيانات الملف بعد قراءته. بالنسبة للملفات الصغيرة ، يعمل هذا النهج.
افترض أن تطبيقنا يحتاج إلى نسخ ملف ضخم (سننظر في ملفات "ضخمة" أكبر من 4 جيجابايت) أثناء عملية النسخ الاحتياطي للبيانات. على سبيل المثال ، لديّ ملف فيديو بحجم 7.4 غيغابايت ، وأنا ، باستخدام البرنامج الموضح أعلاه ، سأحاول نسخه من دليلي الحالي إلى دليل
Documents
. إليك الأمر لبدء النسخ:
$ node basic_copy.js cartoonMovie.mkv ~/Documents/bigMovie.mkv
في أوبونتو ، بعد تنفيذ هذا الأمر ، تم عرض رسالة خطأ تتعلق بتجاوز سعة المخزن المؤقت:
/home/shobarani/Workspace/basic_copy.js:7 if (err) throw err; ^ RangeError: File size is greater than possible Buffer: 0x7fffffff bytes at FSReqWrap.readFileAfterStat [as oncomplete] (fs.js:453:11)
كما ترون ، فشلت عملية قراءة الملف بسبب حقيقة أن Node.js تسمح فقط بقراءة ٢ جيجا بايت من البيانات في المخزن المؤقت. كيف تتغلب على هذا القيد؟ عند إجراء عمليات تستخدم بشكل مكثف نظام الإدخال / الإخراج الفرعي (نسخ الملفات ومعالجتها وضغطها) ، من الضروري مراعاة قدرات الأنظمة والقيود المرتبطة بالذاكرة.
تدفقات والمخازن المؤقتة في Node.js
للتغلب على المشكلة الموضحة أعلاه ، نحتاج إلى آلية يمكننا من خلالها تقسيم كميات كبيرة من البيانات إلى أجزاء صغيرة. سنحتاج أيضًا إلى هياكل بيانات لتخزين هذه الأجزاء والعمل معها. المخزن المؤقت هو بنية بيانات تتيح لك تخزين البيانات الثنائية. بعد ذلك ، نحتاج إلى أن نكون قادرين على قراءة أجزاء من البيانات من القرص وكتابتها على القرص. هذه الفرصة يمكن أن تعطينا التدفقات. دعونا نتحدث عن المخازن المؤقتة والخيوط.
uff العاشقون
يمكن إنشاء
Buffer
عن طريق تهيئة كائن
Buffer
.
let buffer = new Buffer(10);
في إصدارات Node.js الأحدث من 8 ، من الأفضل استخدام البناء التالي لإنشاء مخازن مؤقتة:
let buffer = new Buffer.alloc(10); console.log(buffer);
إذا كان لدينا بالفعل بعض البيانات ، مثل صفيف أو شيء مشابه ، يمكن إنشاء مخزن مؤقت بناءً على هذه البيانات.
let name = 'Node JS DEV'; let buffer = Buffer.from(name); console.log(buffer)
تحتوي المخازن المؤقتة على طرق تسمح لك "بالنظر" إليها ومعرفة ما هي البيانات الموجودة - هذه هي أساليب
toString()
و
toJSON()
.
نحن ، في عملية تحسين الكود ، لن نخلق مخازن مؤقتة بأنفسنا. ينشئ Node.js هياكل البيانات هذه تلقائيًا عند العمل مع التدفقات أو مآخذ الشبكة.
reams تيارات
تيارات ، إذا تحولنا إلى لغة الخيال العلمي ، يمكن مقارنة مع البوابات إلى عوالم أخرى. هناك أربعة أنواع من التدفقات:
- دفق للقراءة (يمكن قراءة البيانات منه).
- تيار للتسجيل (يمكن إرسال البيانات إليه).
- تدفق مزدوج (مفتوح لقراءة البيانات منه وإرسال البيانات إليه).
- تحويل الدفق (دفق مزدوج خاص يتيح لك معالجة البيانات ، على سبيل المثال ، ضغطها أو التحقق من صحتها).
نحتاج إلى تدفقات لأن الهدف الحيوي لواجهة برمجة تطبيقات الدفق في Node.js ، وخاصة طريقة
stream.pipe()
، هو
stream.pipe()
تخزين البيانات مؤقتًا على المستويات المقبولة. يتم ذلك بحيث لا يؤدي العمل مع مصادر ومستقبلات البيانات التي تختلف في سرعات المعالجة المختلفة إلى تجاوز الذاكرة المتوفرة.
بمعنى آخر ، لحل مشكلة نسخ ملف كبير ، نحتاج إلى آلية تسمح لنا بعدم التحميل الزائد للنظام.
التدفقات والمخازن المؤقتة (بناءً على وثائق Node.js)يعرض الرسم البياني السابق نوعين من التدفقات - التدفقات المقروءة والتدفقات القابلة للكتابة. طريقة
pipe()
هي آلية بسيطة للغاية تتيح لك إرفاق مؤشرات ترابط لقراءة مؤشرات الترابط للكتابة. إذا لم يكن المخطط أعلاه واضحًا لك ، فهذا جيد. بعد تحليل الأمثلة التالية ، يمكنك التعامل معها بسهولة. على وجه الخصوص ، سننظر الآن في أمثلة على معالجة البيانات باستخدام طريقة
pipe()
.
الحل 1. نسخ الملفات باستخدام الجداول
النظر في حل مشكلة نسخ ملف ضخم ، والتي تحدثنا عنها أعلاه. يمكن أن يستند هذا الحل إلى موضوعين وسيبدو كما يلي:
- نتوقع ظهور الجزء التالي من البيانات في ساحة القراءة.
- نكتب البيانات المستلمة في الدفق للتسجيل.
- نحن نراقب سير عملية النسخ.
سوف ندعو البرنامج الذي ينفذ هذه الفكرة
streams_copy_basic.js
. هنا كودها:
const stream = require('stream'); const fs = require('fs'); let fileName = process.argv[2]; let destPath = process.argv[3]; const readable = fs.createReadStream(fileName); const writeable = fs.createWriteStream(destPath || "output"); fs.stat(fileName, (err, stats) => { this.fileSize = stats.size; this.counter = 1; this.fileArray = fileName.split('.'); try { this.duplicate = destPath + "/" + this.fileArray[0] + '_Copy.' + this.fileArray[1]; } catch(e) { console.exception('File name is invalid! please pass the proper one'); } process.stdout.write(`File: ${this.duplicate} is being created:`); readable.on('data', (chunk)=> { let percentageCopied = ((chunk.length * this.counter) / this.fileSize) * 100; process.stdout.clearLine(); // process.stdout.cursorTo(0); process.stdout.write(`${Math.round(percentageCopied)}%`); writeable.write(chunk); this.counter += 1; }); readable.on('end', (e) => { process.stdout.clearLine();
نتوقع من المستخدم تشغيل هذا البرنامج لتزويدها باسمين للملف. الأول هو الملف المصدر ، والثاني هو اسم نسخته المستقبلية. نقوم بإنشاء دفقين - دفق للقراءة ودفق للكتابة ، ونقل أجزاء من البيانات من الأولى إلى الثانية. هناك أيضا بعض الآليات المساعدة. يتم استخدامها لمراقبة عملية النسخ وإخراج المعلومات المقابلة إلى وحدة التحكم.
نستخدم آلية الحدث هنا ، على وجه الخصوص ، نحن نتحدث عن الاشتراك في الأحداث التالية:
data
- تسمى عند قراءة قطعة من البيانات.end
- تسمى عند قراءة البيانات من دفق القراءة.error
- يسمى إذا حدث خطأ أثناء قراءة البيانات.
باستخدام هذا البرنامج ، يتم نسخ ملف 7.4 جيجا بايت بدون رسائل خطأ.
$ time node streams_copy_basic.js cartoonMovie.mkv ~/Documents/4kdemo.mkv
ومع ذلك ، هناك مشكلة واحدة. يمكن التعرف عليه من خلال النظر في البيانات المتعلقة باستخدام موارد النظام من خلال عمليات مختلفة.
بيانات استخدام موارد النظاملاحظ أن عملية
node
، بعد نسخ 88٪ من الملف ، تشغل 4.6 جيجابايت من الذاكرة. هذا كثيرًا ، يمكن أن تتداخل معالجة الذاكرة مع عمل البرامج الأخرى.
for أسباب الاستهلاك المفرط للذاكرة
انتبه لسرعة قراءة البيانات من القرص وكتابة البيانات إلى القرص من الرسم التوضيحي السابق (أعمدة قراءة القرص وكتابته). وهي ، هنا يمكنك رؤية المؤشرات التالية:
Disk Read: 53.4 MiB/s Disk Write: 14.8 MiB/s
مثل هذا الاختلاف في سرعات القراءة من سجل البيانات يعني أن مصدر البيانات ينتج عنها أسرع بكثير مما يستطيع المتلقي استلامها ومعالجتها. يجب على الكمبيوتر تخزين أجزاء بيانات القراءة في الذاكرة حتى تتم كتابتها على القرص. نتيجة لذلك ، نرى مثل هذه المؤشرات لاستخدام الذاكرة.
على جهاز الكمبيوتر الخاص بي ، استمر هذا البرنامج لمدة 3 دقائق و 16 ثانية. هنا معلومات حول التقدم المحرز في تنفيذه:
17.16s user 25.06s system 21% cpu 3:16.61 total
الحل 2. نسخ الملفات باستخدام التدفقات ومع الضبط التلقائي لسرعة قراءة البيانات وكتابتها
لمواجهة المشكلة أعلاه ، يمكننا تعديل البرنامج بحيث يتم تكوين سرعات القراءة والكتابة تلقائيًا أثناء نسخ الملفات. وتسمى هذه الآلية الضغط الخلفي. من أجل استخدامه ، نحن لسنا بحاجة إلى القيام بأي شيء خاص. يكفي ، باستخدام طريقة
pipe()
، توصيل دفق القراءة بدفق الكتابة ، وسوف يقوم Node.js بضبط سرعات نقل البيانات تلقائيًا.
اتصل بهذا البرنامج
streams_copy_efficient.js
. هنا كودها:
const stream = require('stream'); const fs = require('fs'); let fileName = process.argv[2]; let destPath = process.argv[3]; const readable = fs.createReadStream(fileName); const writeable = fs.createWriteStream(destPath || "output"); fs.stat(fileName, (err, stats) => { this.fileSize = stats.size; this.counter = 1; this.fileArray = fileName.split('.'); try { this.duplicate = destPath + "/" + this.fileArray[0] + '_Copy.' + this.fileArray[1]; } catch(e) { console.exception('File name is invalid! please pass the proper one'); } process.stdout.write(`File: ${this.duplicate} is being created:`); readable.on('data', (chunk) => { let percentageCopied = ((chunk.length * this.counter) / this.fileSize) * 100; process.stdout.clearLine(); // process.stdout.cursorTo(0); process.stdout.write(`${Math.round(percentageCopied)}%`); this.counter += 1; }); readable.on('error', (e) => { console.log("Some error occurred: ", e); }); writeable.on('finish', () => { process.stdout.clearLine();
يتمثل الاختلاف الرئيسي بين هذا البرنامج والبرنامج السابق في استبدال رمز نسخ أجزاء البيانات بالسطر التالي:
readable.pipe(writeable);
في قلب كل ما يحدث هنا هو طريقة
pipe()
. يتحكم في سرعات القراءة والكتابة ، مما يؤدي إلى حقيقة أن الذاكرة لم تعد مثقلة.
قم بتشغيل البرنامج.
$ time node streams_copy_efficient.js cartoonMovie.mkv ~/Documents/4kdemo.mkv
نحن ننسخ نفس الملف الضخم. الآن دعونا ننظر في كيفية العمل مع الذاكرة ومع نظرة القرص.
باستخدام توجيه الإخراج () ، يتم تكوين سرعات القراءة والكتابة تلقائيًاالآن نرى أن عملية
node
تستهلك 61.9 ميغابايت فقط من الذاكرة. إذا نظرت إلى البيانات الخاصة باستخدام القرص ، يمكنك رؤية ما يلي:
Disk Read: 35.5 MiB/s Disk Write: 35.5 MiB/s
بفضل آلية الضغط الخلفي ، أصبحت الآن سرعات القراءة والكتابة متساوية مع بعضها البعض. بالإضافة إلى ذلك ، يعمل البرنامج الجديد أسرع 13 ثانية من القديم.
12.13s user 28.50s system 22% cpu 3:03.35 total
باستخدام طريقة
pipe()
، تمكنا من تقليل وقت تنفيذ البرنامج وتقليل استهلاك الذاكرة بنسبة 98.68٪.
في هذه الحالة ، 61.9 ميغابايت هو حجم المخزن المؤقت الذي تم إنشاؤه بواسطة دفق قراءة البيانات. يمكننا ضبط هذا الحجم بأنفسنا ، وذلك باستخدام طريقة
read()
للدفق لقراءة البيانات:
const readable = fs.createReadStream(fileName); readable.read(no_of_bytes_size);
قمنا هنا بنسخ الملف في نظام الملفات المحلي ، ومع ذلك ، يمكن استخدام نفس الأسلوب لتحسين العديد من مهام إدخال البيانات الأخرى. على سبيل المثال ، يعمل هذا مع تدفقات البيانات ، مصدرها كافكا ، والمتلقي هو قاعدة البيانات. وفقًا لنفس المخطط ، من الممكن تنظيم قراءة البيانات من قرص ، وضغطها ، كما يقولون ، "سريعًا" ، وإعادة كتابتها مرة أخرى إلى القرص بالفعل في شكل مضغوط. في الواقع ، هناك العديد من الاستخدامات الأخرى للتكنولوجيا الموضحة هنا.
ملخص
كان أحد أهداف هذه المقالة هو توضيح مدى سهولة كتابة البرامج السيئة على Node.js ، على الرغم من أن هذا النظام الأساسي يوفر واجهات برمجة تطبيقات رائعة للمطور. مع بعض الاهتمام بواجهة برمجة التطبيقات هذه ، يمكنك تحسين جودة مشاريع البرامج من جانب الخادم.
أعزائي القراء! كيف يمكنك العمل مع المخازن المؤقتة ومؤشرات الترابط في Node.js؟