هدف بررسی کامل مباحث Streaming در دات نت فریمورک میباشد.
Stream چیست؟
دنبالهای از بایتها که میتوان آنها را از یک backing store (انبار پشتیبان) خواند یا در آن نوشت.
Backing Store
یک رسانه ذخیره سازی از جمله Disk-Drive، Memory و Network Location میباشد که به عنوان منبع یا مقصدی برای خواندن و نوشتن بایتها به صورت دنبالهای، میتوان از آن استفاده کرد.
زمانی که قرار است داده ذخیره شده به صورت Stream مصرف شود، مزیت مقیاس پذیری را نیز خواهید داشت. لذا لازم نیست با مشکل محدودیت حافظه نیز درگیر شوید.
آشنایی با معماری Streaming در دات نت
Streaming در دات نت، توسط سه مفهوم: backing store، decorators و adapters در برگرفته شده است.
کلاسی به نام Stream در دات نت، برای ارائه یکسری متد مشترک برای Reading، Writing و Positioning در نظر گرفته شده است که همچنین کلاس پایه Backing Store Streams و Decorator Streams نیز میباشد.
اعضای کلاس Stream را میتوان به شکل زیر گروه بندی کرد:
در نظر داشته باشید که Stream ها، دارای اشاره گری به مکان جاری تحت عنوان Pointer نیز میباشند. مقدار پیش فرض آن «صفر» میباشد و زمانی که شروع به خواندن از Stream کنید، این خواندن از مکانی شروع میشود که Pointer به آنجا اشاره میکند. به شکل زیر توجه کنید:
اگر قرار باشد 3 بایت اول خوانده شود، لذا حالت زیر را خواهیم داشت:
همانطور که مشخص است، Pointer مربوط به Stream به اولین خانهای اشاره میکند که در Readهای بعدی قرار است خوانده شود. در نهایت با خواندن دو بایت دیگر، حالت زیر را خواهیم داشت:
برای Reading و Writing متدهای زیر در کلاس System.IO.Stream در نظر گرفته شدهاند:
(Read(byte[] buffer,int offset,int count
buffer: آرایهای از بایتها برای نگهداری دادهی خوانده شده از Stream
offset: برخلاف تصور، اندیسی است که مکان شروع ذخیره سازی در buffer را مشخص میکند و نه مکان شروع خواندن از Stream
count: بیشترین تعداد بایت برای خواندن از Stream میباشد. با توجه به اینکه ممکن است به انتهای Stream رسیده باشیم یا اینکه در شرایطی مثلا در Network Streamها چه بسا خود Stream تصمیم بگیرد تعداد بایت کمتری از این مقدار Count را برای ما ارائه دهد. از این رو همیشه مقداری که برای Count مشخص میکنید همان مقداری نیست که متد Read برای شما برگشت خواهد داد.
return: تعداد بایتهایی که خوانده شده است یا اگر به انتهای Stream رسیده باشیم «0» برگشت خواهد داد. از این رو تکه کد زیر برای خواندن کل داده به یکباره، قابل اطمینان نخواهد بود.
byte[] dataToRead=new byte[stream.Length]; int bytesRead=stream.Read(dataToRead,0,dataToRead.Length);
راه حل جایگزین میتواند به شکل زیر باشد:
static byte[] ReadBytes(Stream stream) { // dataToRead will hold the data read from the stream byte[] dataToRead = new byte[stream.Length]; //this is the total number of bytes read. this will be incremented //and eventually will equal the bytes size held by the stream int totalBytesRead = 0; //this is the number of bytes read in each iteration (i.e. chunk size) int chunkBytesRead = 1; while (totalBytesRead < dataToRead.Length && chunkBytesRead > 0) { chunkBytesRead = stream.Read(dataToRead, totalBytesRead, dataToRead.Length - totalBytesRead); totalBytesRead = totalBytesRead + chunkBytesRead; } return dataToRead; }
byte[] data = new BinaryReader (s).ReadBytes (1000);
return: یک بایت را از مکان فعلی که Pointer به آن اشاره میکند، میخواند. اگر خروجی «-1» باشد، به انتهای Stream رسیده اید.
برخلاف انتظار، خروجی این متد از نوع int میباشد؛ چرا که لازم است «-1» را نیز در برگیرد.
با توجه به حالت FileStream که فقط برای Append کردن وهله سازی شده است، امکان خواندن را نخواهید داشت. بنابراین زمانیکه از کلاس شخص ثالثی برای خواندن از Stream استفاده میکنید، بهصلاح است (به منظور Defensive Programming) که از متد CanRead قبل خواندن بهره ببرید.
(Write(byte[] array,int offset,int count
array: آرایه ای از بایتها که قرار است در Stream درج شوند.
offset: اندیس شروع array برای درج کردن در Stream را مشخص میکند.
count: بیشترین تعداد بایتی که از array در Stream درج خواهد شد.
برای درج یک بایت در Stream استفاده میشود.
برای تشخص پشتیبانی کردن Stream از عملیات درج کردن مورد استفاده قرار خواهد گرفت.
با انجام هر یک از عملیات Read و Write برروی Stream، باعث تغییر مکان Pointer مربوط به آن خواهید شد. در صورتیکه نیاز است به صورت انتخابی مکان خاصی از Stream را برای شروع درج کردن یا خواندن انتخاب کنید، Seeking کمک کننده خواهد بود.
باید توجه داشت که پشتیبانی از این عملیات به backing store مورد استفاده وابسته میباشد. از این رو باید دانست که MemoryStream و FileStream از Seeking پشتیبانی کرده ولی در مقابل NetworkStream، PipeStream و همچنین Decorator Streams به غیر از BufferedStream قابلیت Seeking را ندارند. BufferedStream با ایجاد پوششی برروی یک Stream به اصطلاح non-seekable، امکان Seeking درون Buffer داخلی خود را مهیا خواهد کرد.
برای عملیات Seeking نیز اعضایی در کلاس پایه System.IO.Stream در نظر گرفته شده است:
برای تنظیم مکان Pointer در Stream استفاده خواهد شد.
متدی برای تنظیم طول Stream، که اگر value ارسال شده کوچکتر از طول فعلی Stream باشد، آن را کوتاه کرده و در غیر این صورت، Stream موردنظر گسترش خواهد یافت. برای استفاده از این متد، Stream مورد نظر باید قابلیت Writing و Seeking را داشته باشد.
پراپرتی فقط خواندنی که طول Stream را مشخص میکند. در صورتیکه Stream مورد نظر Seekable باشد، میتوان از این پراپرتی بهر برد؛ این بدین معنی است که اگر با یک Stream از نوع non-seekable کار میکنید، در صورت استفاده از این خصوصیت، تمام بایتهای Stream خوانده شده و بعد از قرار گرفتن در یک buffer (به عنوان مثال در memory)، محاسبه خواهد شد.
Position
پراپرتی برای خواندن یا تنظیم مکان فعلی Pointer مربوط به Stream، میباشد. برای استفاده از آن لازم است Stream مورد استفاده Seekable باشد.
مشخص میکند که Stream مورد استفاده Seekable می باشد یا خیر.
به طور خلاصه با استفاده از متد Seek انعطاف پذیری بالایی خواهید داشت. با مقدار دهی پراپرتی Position، این مقدار همیشه نسبت به ابتدای Stream در نظر گرفته خواهد شد (شکل زیر)؛ این در حالی است که با استفاده از متد Seek میتوان مشخص کرد که مقدار Offset تنظیم شده نسبت به ابتدا، مکان جاری و یا انتهای Stream میباشد.
مثال:
using (FileStream fs = File.Create(@"C:\files\testfile3.txt")) { // position is 0 long pos = fs.Position; // sets the position to 1 fs.Position = 1; byte[] arrbytes = { 100, 101 }; //writes the content of arrbytes into current position - which is 1 fs.Write(arrbytes, 0, arrbytes.Length); //position is now 3 as its advanced by write pos = fs.Position; fs.Position = 0; byte[] readdata1 = ReadBytes(fs); }
Closing and Flushing
کلاس پایه System.IO.Stream اینترفیس IDisposable را پیاده سازی کرده است؛ لذا بهتر است برای آزاد سازی منابع از جمله: file handle در FileStream یا socket handle در NetworkStream، بعد از استفاده، متد Dispose آنها را فراخوانی کنید یا با وهله سازی آنها در بدنه using، این فراخوانی به صورت ضمنی انجام شود.
نکته: باید توجه کنید که با Close (معادل Dispose) شدن decorator streamها ، backing store stream داخلی آنها نیز Close خواهد شد.
با توجه به اینکه I/O عملیات پرهزینهای میباشد، برخی از انواع Streamها به منظور بهبود کارآیی از یک مکانیزم بافر داخلی استفاده میکنند. به این شکل که عملیات Write، داده را به جای آنکه درون backing store ذخیره سازی کند، درون این بافر ذخیره سازی خواهد کرد. زمانیکه این بافر پر شود یا به صورت صریح متدهای Flush یا Close فراخوانی شده باشند، داده موجود در بافر درون backing store ذخیره خواهد شد. در نتیجه عملیات Read هم میتواند به بخشی از داده اصلی که هم اکنون درون بافر میباشد، دسترسی سریعتری داشته باشد. به عنوان مثال FileStream از این مکانیزم داخلی برخوردار است. سایز پیش فرض این بافر 4KB (قابل تنظیم است) میباشد. برای سایر مواردی که این امکان برایشان وجود ندارد، میتوان از BufferedStream برای Decorate کردن Stream مورد نظر خود استفاده کرد.
نکته: به صورت پیش فرض، Streamها thread-safe نیستند و امکان خواندن و نوشتن همزمان توسط چند thread برروی یک stream مشترک را نخواهید داشت. برای حل این موضوع، متد استاتیکی در کلاس Stream تحت عنوان Synchronized در نظر گرفته شده است که یک thread-safe wrapper را به برروی stream ورودی در نظر گرفته و آن را به عنوان خروجی برگشت خواهد داد.
[HostProtection(SecurityAction.LinkDemand, Synchronization = true)] public static Stream Synchronized(Stream stream) { if (stream == null) throw new ArgumentNullException("stream"); if (stream is Stream.SyncStream) return stream; return (Stream) new Stream.SyncStream(stream); }