Node.js स्ट्रीम्स मोठ्या डेटासेटवर कार्यक्षमतेने प्रक्रिया करून, स्केलेबिलिटी आणि प्रतिसादक्षमता वाढवून तुमच्या ऍप्लिकेशनच्या कार्यप्रदर्शनात कशी क्रांती घडवू शकतात ते शिका.
Node.js स्ट्रीम्स: मोठ्या डेटाचे कार्यक्षमतेने व्यवस्थापन
डेटा-चालित ऍप्लिकेशन्सच्या आधुनिक युगात, मोठ्या डेटासेटचे कार्यक्षमतेने व्यवस्थापन करणे अत्यंत महत्त्वाचे आहे. Node.js, त्याच्या नॉन-ब्लॉकिंग, इव्हेंट-ड्रिव्हन आर्किटेक्चरसह, डेटावर व्यवस्थापित करण्यायोग्य तुकड्यांमध्ये प्रक्रिया करण्यासाठी एक शक्तिशाली यंत्रणा प्रदान करते: स्ट्रीम्स. हा लेख Node.js स्ट्रीम्सच्या जगात खोलवर जातो, त्यांचे फायदे, प्रकार आणि संसाधने संपवल्याशिवाय प्रचंड प्रमाणात डेटा हाताळू शकणारे स्केलेबल आणि प्रतिसादक्षम ऍप्लिकेशन्स तयार करण्यासाठी त्यांचे व्यावहारिक उपयोग शोधतो.
स्ट्रीम्स का वापरावे?
पारंपारिकपणे, संपूर्ण फाइल वाचणे किंवा प्रक्रिया करण्यापूर्वी नेटवर्क विनंतीवरून सर्व डेटा प्राप्त करणे, विशेषतः मोठ्या फाइल्स किंवा सतत डेटा फीड हाताळताना, लक्षणीय कामगिरीतील अडथळे निर्माण करू शकते. बफरिंग म्हणून ओळखला जाणारा हा दृष्टिकोन, भरपूर मेमरी वापरू शकतो आणि ऍप्लिकेशनची एकूण प्रतिसादक्षमता कमी करू शकतो. स्ट्रीम्स डेटावर लहान, स्वतंत्र तुकड्यांमध्ये प्रक्रिया करून अधिक कार्यक्षम पर्याय प्रदान करतात, ज्यामुळे तुम्हाला संपूर्ण डेटासेट लोड होण्याची वाट न पाहता, डेटा उपलब्ध होताच त्यावर काम सुरू करण्याची परवानगी मिळते. हा दृष्टिकोन विशेषतः यासाठी फायदेशीर आहे:
- मेमरी व्यवस्थापन: स्ट्रीम्स डेटावर तुकड्यांमध्ये प्रक्रिया करून मेमरीचा वापर लक्षणीयरीत्या कमी करतात, ज्यामुळे ऍप्लिकेशनला संपूर्ण डेटासेट एकाच वेळी मेमरीमध्ये लोड करण्यापासून प्रतिबंधित केले जाते.
- सुधारित कार्यप्रदर्शन: डेटावर हळूहळू प्रक्रिया केल्याने, स्ट्रीम्स लेटेंसी कमी करतात आणि ऍप्लिकेशनची प्रतिसादक्षमता सुधारतात, कारण डेटा आल्यावर त्यावर प्रक्रिया केली जाऊ शकते आणि प्रसारित केली जाऊ शकते.
- वर्धित स्केलेबिलिटी: स्ट्रीम्स ऍप्लिकेशन्सना मोठे डेटासेट आणि अधिक समवर्ती विनंत्या हाताळण्यास सक्षम करतात, ज्यामुळे ते अधिक स्केलेबल आणि मजबूत बनतात.
- रिअल-टाइम डेटा प्रोसेसिंग: व्हिडिओ, ऑडिओ किंवा सेन्सर डेटा स्ट्रीमिंगसारख्या रिअल-टाइम डेटा प्रोसेसिंग परिस्थितीसाठी स्ट्रीम्स आदर्श आहेत, जिथे डेटावर सतत प्रक्रिया आणि प्रसारण करणे आवश्यक असते.
स्ट्रीमचे प्रकार समजून घेणे
Node.js स्ट्रीम्सचे चार मूलभूत प्रकार प्रदान करते, प्रत्येक एका विशिष्ट हेतूसाठी डिझाइन केलेले आहे:
- रीडेबल स्ट्रीम्स: रीडेबल स्ट्रीम्स फाइल, नेटवर्क कनेक्शन किंवा डेटा जनरेटरसारख्या स्त्रोतावरून डेटा वाचण्यासाठी वापरल्या जातात. जेव्हा नवीन डेटा उपलब्ध असतो तेव्हा ते 'data' इव्हेंट्स आणि जेव्हा डेटा स्त्रोत पूर्णपणे वापरला जातो तेव्हा 'end' इव्हेंट्स उत्सर्जित करतात.
- रायटेबल स्ट्रीम्स: रायटेबल स्ट्रीम्स फाइल, नेटवर्क कनेक्शन किंवा डेटाबेससारख्या ठिकाणी डेटा लिहिण्यासाठी वापरल्या जातात. ते डेटा लिहिण्यासाठी आणि त्रुटी हाताळण्यासाठी पद्धती प्रदान करतात.
- डुप्लेक्स स्ट्रीम्स: डुप्लेक्स स्ट्रीम्स रीडेबल आणि रायटेबल दोन्ही असतात, ज्यामुळे डेटा एकाच वेळी दोन्ही दिशांना प्रवाहित होऊ शकतो. ते सामान्यतः सॉकेट्ससारख्या नेटवर्क कनेक्शनसाठी वापरले जातात.
- ट्रान्सफॉर्म स्ट्रीम्स: ट्रान्सफॉर्म स्ट्रीम्स हे एक विशेष प्रकारचे डुप्लेक्स स्ट्रीम आहेत जे डेटा जात असताना त्यात बदल किंवा रूपांतर करू शकतात. ते कॉम्प्रेशन, एनक्रिप्शन किंवा डेटा रूपांतरणासारख्या कार्यांसाठी आदर्श आहेत.
रीडेबल स्ट्रीम्ससोबत काम करणे
विविध स्त्रोतांकडून डेटा वाचण्यासाठी रीडेबल स्ट्रीम्स हा पाया आहे. येथे एका रीडेबल स्ट्रीमचा वापर करून मोठी टेक्स्ट फाईल वाचण्याचे एक मूलभूत उदाहरण आहे:
const fs = require('fs');
const readableStream = fs.createReadStream('large-file.txt', { encoding: 'utf8', highWaterMark: 16384 });
readableStream.on('data', (chunk) => {
console.log(`Received ${chunk.length} bytes of data`);
// Process the data chunk here
});
readableStream.on('end', () => {
console.log('Finished reading the file');
});
readableStream.on('error', (err) => {
console.error('An error occurred:', err);
});
या उदाहरणात:
fs.createReadStream()
निर्दिष्ट फाईलमधून एक रीडेबल स्ट्रीम तयार करते.encoding
पर्याय फाइलचे कॅरेक्टर एन्कोडिंग (या प्रकरणात UTF-8) निर्दिष्ट करतो.highWaterMark
पर्याय बफर आकार (या प्रकरणात 16KB) निर्दिष्ट करतो. हे 'data' इव्हेंट म्हणून उत्सर्जित होणाऱ्या तुकड्यांचा आकार ठरवते.'data'
इव्हेंट हँडलर प्रत्येक वेळी डेटाचा तुकडा उपलब्ध झाल्यावर कॉल केला जातो.'end'
इव्हेंट हँडलर जेव्हा संपूर्ण फाईल वाचली जाते तेव्हा कॉल केला जातो.'error'
इव्हेंट हँडलर वाचन प्रक्रियेदरम्यान त्रुटी आल्यास कॉल केला जातो.
रायटेबल स्ट्रीम्ससोबत काम करणे
रायटेबल स्ट्रीम्स विविध ठिकाणी डेटा लिहिण्यासाठी वापरल्या जातात. येथे एका रायटेबल स्ट्रीमचा वापर करून फाईलमध्ये डेटा लिहिण्याचे एक उदाहरण आहे:
const fs = require('fs');
const writableStream = fs.createWriteStream('output.txt', { encoding: 'utf8' });
writableStream.write('This is the first line of data.\n');
writableStream.write('This is the second line of data.\n');
writableStream.write('This is the third line of data.\n');
writableStream.end(() => {
console.log('Finished writing to the file');
});
writableStream.on('error', (err) => {
console.error('An error occurred:', err);
});
या उदाहरणात:
fs.createWriteStream()
निर्दिष्ट फाईलसाठी एक रायटेबल स्ट्रीम तयार करते.encoding
पर्याय फाइलचे कॅरेक्टर एन्कोडिंग (या प्रकरणात UTF-8) निर्दिष्ट करतो.writableStream.write()
पद्धत स्ट्रीममध्ये डेटा लिहिते.writableStream.end()
पद्धत सूचित करते की स्ट्रीममध्ये अधिक डेटा लिहिला जाणार नाही, आणि ती स्ट्रीम बंद करते.'error'
इव्हेंट हँडलर लेखन प्रक्रियेदरम्यान त्रुटी आल्यास कॉल केला जातो.
स्ट्रीम्स पाईप करणे
पाईपिंग ही रीडेबल आणि रायटेबल स्ट्रीम्स जोडण्यासाठी एक शक्तिशाली यंत्रणा आहे, जी तुम्हाला एका स्ट्रीममधून दुसऱ्या स्ट्रीममध्ये अखंडपणे डेटा हस्तांतरित करण्यास अनुमती देते. pipe()
पद्धत डेटा प्रवाह आणि त्रुटी प्रसाराचे स्वयंचलितपणे व्यवस्थापन करून, स्ट्रीम्स जोडण्याची प्रक्रिया सोपी करते. स्ट्रीमिंग पद्धतीने डेटावर प्रक्रिया करण्याचा हा एक अत्यंत कार्यक्षम मार्ग आहे.
const fs = require('fs');
const zlib = require('zlib'); // For gzip compression
const readableStream = fs.createReadStream('large-file.txt');
const gzipStream = zlib.createGzip();
const writableStream = fs.createWriteStream('large-file.txt.gz');
readableStream.pipe(gzipStream).pipe(writableStream);
writableStream.on('finish', () => {
console.log('File compressed successfully!');
});
हे उदाहरण पाईपिंग वापरून मोठी फाईल कशी कॉम्प्रेस करायची हे दाखवते:
- इनपुट फाईलमधून एक रीडेबल स्ट्रीम तयार केली जाते.
zlib
मॉड्यूल वापरून एकgzip
स्ट्रीम तयार केली जाते, जी डेटा जाताना तो कॉम्प्रेस करेल.- कॉम्प्रेस केलेला डेटा आउटपुट फाईलमध्ये लिहिण्यासाठी एक रायटेबल स्ट्रीम तयार केली जाते.
pipe()
पद्धत स्ट्रीम्सना क्रमाने जोडते: रीडेबल -> gzip -> रायटेबल.- रायटेबल स्ट्रीमवरील
'finish'
इव्हेंट जेव्हा सर्व डेटा लिहिला जातो तेव्हा ट्रिगर होतो, जे यशस्वी कॉम्प्रेशन दर्शवते.
पाईपिंग बॅकप्रेशर स्वयंचलितपणे हाताळते. बॅकप्रेशर तेव्हा उद्भवते जेव्हा एक रीडेबल स्ट्रीम एका रायटेबल स्ट्रीमच्या वापरापेक्षा वेगाने डेटा तयार करत असते. पाईपिंग रायटेबल स्ट्रीम अधिक डेटा स्वीकारण्यास तयार होईपर्यंत डेटाचा प्रवाह थांबवून रीडेबल स्ट्रीमला रायटेबल स्ट्रीमवर भार टाकण्यापासून प्रतिबंधित करते. हे कार्यक्षम संसाधन वापर सुनिश्चित करते आणि मेमरी ओव्हरफ्लो प्रतिबंधित करते.
ट्रान्सफॉर्म स्ट्रीम्स: उडता उडता डेटा सुधारणे
ट्रान्सफॉर्म स्ट्रीम्स डेटा रीडेबल स्ट्रीममधून रायटेबल स्ट्रीममध्ये प्रवाहित होत असताना त्यात बदल किंवा रूपांतर करण्याचा मार्ग प्रदान करतात. ते डेटा रूपांतरण, फिल्टरिंग किंवा एनक्रिप्शनसारख्या कार्यांसाठी विशेषतः उपयुक्त आहेत. ट्रान्सफॉर्म स्ट्रीम्स डुप्लेक्स स्ट्रीम्सकडून वारसा घेतात आणि _transform()
पद्धत लागू करतात जी डेटा रूपांतरण करते.
येथे टेक्स्टला अपरकेसमध्ये रूपांतरित करणाऱ्या ट्रान्सफॉर्म स्ट्रीमचे एक उदाहरण आहे:
const { Transform } = require('stream');
class UppercaseTransform extends Transform {
constructor() {
super();
}
_transform(chunk, encoding, callback) {
const transformedChunk = chunk.toString().toUpperCase();
callback(null, transformedChunk);
}
}
const uppercaseTransform = new UppercaseTransform();
const readableStream = process.stdin; // Read from standard input
const writableStream = process.stdout; // Write to standard output
readableStream.pipe(uppercaseTransform).pipe(writableStream);
या उदाहरणात:
- आम्ही
stream
मॉड्यूलमधूनTransform
क्लासला विस्तारित करणारा एक सानुकूल ट्रान्सफॉर्म स्ट्रीम क्लासUppercaseTransform
तयार करतो. _transform()
पद्धत डेटाच्या प्रत्येक तुकड्याला अपरकेसमध्ये रूपांतरित करण्यासाठी ओव्हरराइड केली जाते.callback()
फंक्शन रूपांतरण पूर्ण झाले आहे हे सूचित करण्यासाठी आणि रूपांतरित डेटा पाइपलाइनमधील पुढील स्ट्रीमला पास करण्यासाठी कॉल केले जाते.- आम्ही रीडेबल स्ट्रीम (स्टँडर्ड इनपुट) आणि रायटेबल स्ट्रीम (स्टँडर्ड आउटपुट) चे इन्स्टन्स तयार करतो.
- आम्ही रीडेबल स्ट्रीमला ट्रान्सफॉर्म स्ट्रीममधून रायटेबल स्ट्रीममध्ये पाईप करतो, जे इनपुट टेक्स्टला अपरकेसमध्ये रूपांतरित करते आणि ते कन्सोलवर प्रिंट करते.
बॅकप्रेशर हाताळणे
बॅकप्रेशर ही स्ट्रीम प्रोसेसिंगमधील एक गंभीर संकल्पना आहे जी एका स्ट्रीमला दुसऱ्या स्ट्रीमवर भार टाकण्यापासून प्रतिबंधित करते. जेव्हा एक रीडेबल स्ट्रीम एका रायटेबल स्ट्रीमच्या वापरापेक्षा वेगाने डेटा तयार करते, तेव्हा बॅकप्रेशर उद्भवते. योग्य हाताळणीशिवाय, बॅकप्रेशरमुळे मेमरी ओव्हरफ्लो आणि ऍप्लिकेशन अस्थिरता येऊ शकते. Node.js स्ट्रीम्स बॅकप्रेशर प्रभावीपणे व्यवस्थापित करण्यासाठी यंत्रणा प्रदान करतात.
pipe()
पद्धत स्वयंचलितपणे बॅकप्रेशर हाताळते. जेव्हा एक रायटेबल स्ट्रीम अधिक डेटा स्वीकारण्यास तयार नसते, तेव्हा रीडेबल स्ट्रीमला रायटेबल स्ट्रीम तयार असल्याचे संकेत देईपर्यंत थांबवले जाईल. तथापि, प्रोग्रामॅटिकरित्या स्ट्रीम्ससोबत काम करताना (pipe()
न वापरता), तुम्हाला readable.pause()
आणि readable.resume()
पद्धती वापरून बॅकप्रेशर मॅन्युअली हाताळण्याची आवश्यकता आहे.
येथे बॅकप्रेशर मॅन्युअली कसे हाताळायचे याचे एक उदाहरण आहे:
const fs = require('fs');
const readableStream = fs.createReadStream('large-file.txt');
const writableStream = fs.createWriteStream('output.txt');
readableStream.on('data', (chunk) => {
if (!writableStream.write(chunk)) {
readableStream.pause();
}
});
writableStream.on('drain', () => {
readableStream.resume();
});
readableStream.on('end', () => {
writableStream.end();
});
या उदाहरणात:
writableStream.write()
पद्धतfalse
परत करते जर स्ट्रीमचा अंतर्गत बफर पूर्ण भरलेला असेल, जे दर्शवते की बॅकप्रेशर होत आहे.- जेव्हा
writableStream.write()
false
परत करते, तेव्हा आम्ही रीडेबल स्ट्रीमला अधिक डेटा तयार करण्यापासून थांबवण्यासाठीreadableStream.pause()
वापरून थांबवतो. 'drain'
इव्हेंट रायटेबल स्ट्रीमद्वारे उत्सर्जित केला जातो जेव्हा तिचा बफर यापुढे पूर्ण भरलेला नसतो, जे दर्शवते की ती अधिक डेटा स्वीकारण्यास तयार आहे.- जेव्हा
'drain'
इव्हेंट उत्सर्जित होतो, तेव्हा आम्ही रीडेबल स्ट्रीमला डेटा तयार करणे सुरू ठेवण्यासाठीreadableStream.resume()
वापरून पुन्हा सुरू करतो.
Node.js स्ट्रीम्सचे व्यावहारिक उपयोग
Node.js स्ट्रीम्स विविध परिस्थितीत उपयोग शोधतात जिथे मोठा डेटा हाताळणे महत्त्वाचे आहे. येथे काही उदाहरणे आहेत:
- फाईल प्रोसेसिंग: मोठ्या फाइल्सचे कार्यक्षमतेने वाचन, लेखन, रूपांतरण आणि कॉम्प्रेशन करणे. उदाहरणार्थ, विशिष्ट माहिती काढण्यासाठी मोठ्या लॉग फाइल्सवर प्रक्रिया करणे, किंवा वेगवेगळ्या फाईल फॉरमॅटमध्ये रूपांतरण करणे.
- नेटवर्क कम्युनिकेशन: मोठ्या नेटवर्क विनंत्या आणि प्रतिसादांना हाताळणे, जसे की व्हिडिओ किंवा ऑडिओ डेटा स्ट्रीमिंग. एका व्हिडिओ स्ट्रीमिंग प्लॅटफॉर्मचा विचार करा जिथे व्हिडिओ डेटा वापरकर्त्यांना तुकड्यांमध्ये स्ट्रीम केला जातो.
- डेटा रूपांतरण: डेटाला वेगवेगळ्या फॉरमॅटमध्ये रूपांतरित करणे, जसे की CSV ते JSON किंवा XML ते JSON. एका डेटा इंटिग्रेशन परिस्थितीचा विचार करा जिथे अनेक स्त्रोतांकडून आलेला डेटा एका एकीकृत फॉरमॅटमध्ये रूपांतरित करणे आवश्यक आहे.
- रिअल-टाइम डेटा प्रोसेसिंग: रिअल-टाइम डेटा स्ट्रीम्सवर प्रक्रिया करणे, जसे की IoT उपकरणांकडून सेन्सर डेटा किंवा स्टॉक मार्केटमधून आर्थिक डेटा. एका स्मार्ट सिटी ऍप्लिकेशनची कल्पना करा जे हजारो सेन्सर्सकडून आलेला डेटा रिअल-टाइममध्ये प्रक्रिया करते.
- डेटाबेस संवाद: डेटाबेसमध्ये आणि डेटाबेसमधून डेटा स्ट्रीम करणे, विशेषतः MongoDB सारख्या NoSQL डेटाबेस जे अनेकदा मोठे डॉक्युमेंट्स हाताळतात. हे कार्यक्षम डेटा आयात आणि निर्यात ऑपरेशन्ससाठी वापरले जाऊ शकते.
Node.js स्ट्रीम्स वापरण्यासाठी सर्वोत्तम पद्धती
Node.js स्ट्रीम्सचा प्रभावीपणे उपयोग करण्यासाठी आणि त्यांचे फायदे जास्तीत जास्त वाढवण्यासाठी, खालील सर्वोत्तम पद्धतींचा विचार करा:
- योग्य स्ट्रीम प्रकार निवडा: विशिष्ट डेटा प्रोसेसिंग आवश्यकतांवर आधारित योग्य स्ट्रीम प्रकार (रीडेबल, रायटेबल, डुप्लेक्स, किंवा ट्रान्सफॉर्म) निवडा.
- त्रुटी योग्यरित्या हाताळा: स्ट्रीम प्रोसेसिंग दरम्यान उद्भवू शकणाऱ्या त्रुटी पकडण्यासाठी आणि व्यवस्थापित करण्यासाठी मजबूत त्रुटी हाताळणी लागू करा. तुमच्या पाइपलाइनमधील सर्व स्ट्रीम्सना त्रुटी लिस्नर्स जोडा.
- बॅकप्रेशर व्यवस्थापित करा: एका स्ट्रीमला दुसऱ्या स्ट्रीमवर भार टाकण्यापासून रोखण्यासाठी बॅकप्रेशर हाताळणी यंत्रणा लागू करा, ज्यामुळे कार्यक्षम संसाधन वापर सुनिश्चित होईल.
- बफर आकार ऑप्टिमाइझ करा: कार्यक्षम मेमरी व्यवस्थापन आणि डेटा प्रवाहासाठी बफर आकार ऑप्टिमाइझ करण्यासाठी
highWaterMark
पर्याय ट्यून करा. मेमरी वापर आणि कार्यप्रदर्शन यांच्यात सर्वोत्तम संतुलन शोधण्यासाठी प्रयोग करा. - साध्या रूपांतरणांसाठी पाईपिंग वापरा: साध्या डेटा रूपांतरणांसाठी आणि स्ट्रीम्स दरम्यान डेटा हस्तांतरणासाठी
pipe()
पद्धतीचा उपयोग करा. - जटिल तर्कासाठी सानुकूल ट्रान्सफॉर्म स्ट्रीम्स तयार करा: जटिल डेटा रूपांतरणांसाठी, रूपांतरण तर्क अंतर्भूत करण्यासाठी सानुकूल ट्रान्सफॉर्म स्ट्रीम्स तयार करा.
- संसाधने स्वच्छ करा: स्ट्रीम प्रोसेसिंग पूर्ण झाल्यावर योग्य संसाधन स्वच्छता सुनिश्चित करा, जसे की फाईल्स बंद करणे आणि मेमरी मोकळी करणे.
- स्ट्रीम कामगिरीचे निरीक्षण करा: अडथळे ओळखण्यासाठी आणि डेटा प्रोसेसिंग कार्यक्षमता ऑप्टिमाइझ करण्यासाठी स्ट्रीम कामगिरीचे निरीक्षण करा. Node.js च्या अंगभूत प्रोफाइलर किंवा तृतीय-पक्ष मॉनिटरिंग सेवांसारखी साधने वापरा.
निष्कर्ष
Node.js स्ट्रीम्स हे मोठा डेटा कार्यक्षमतेने हाताळण्यासाठी एक शक्तिशाली साधन आहे. व्यवस्थापित करण्यायोग्य तुकड्यांमध्ये डेटावर प्रक्रिया करून, स्ट्रीम्स मेमरीचा वापर लक्षणीयरीत्या कमी करतात, कार्यप्रदर्शन सुधारतात आणि स्केलेबिलिटी वाढवतात. विविध स्ट्रीम प्रकार समजून घेणे, पाईपिंगमध्ये प्रभुत्व मिळवणे आणि बॅकप्रेशर हाताळणे हे मजबूत आणि कार्यक्षम Node.js ऍप्लिकेशन्स तयार करण्यासाठी आवश्यक आहे जे प्रचंड प्रमाणात डेटा सहजतेने हाताळू शकतात. या लेखात वर्णन केलेल्या सर्वोत्तम पद्धतींचे पालन करून, तुम्ही Node.js स्ट्रीम्सच्या पूर्ण क्षमतेचा फायदा घेऊ शकता आणि डेटा-केंद्रित कार्यांच्या विस्तृत श्रेणीसाठी उच्च-कार्यक्षमता, स्केलेबल ऍप्लिकेशन्स तयार करू शकता.
आपल्या Node.js डेव्हलपमेंटमध्ये स्ट्रीम्सचा अवलंब करा आणि आपल्या ऍप्लिकेशन्समध्ये कार्यक्षमता आणि स्केलेबिलिटीची एक नवीन पातळी अनलॉक करा. डेटाचे प्रमाण वाढत असताना, डेटावर कार्यक्षमतेने प्रक्रिया करण्याची क्षमता अधिकाधिक महत्त्वाची बनेल, आणि Node.js स्ट्रीम्स या आव्हानांना तोंड देण्यासाठी एक ठोस पाया प्रदान करतात.