فصل ۱۵: معماری Stream، FileStream، MemoryStream و PipeStream در .NET
فروش یا انتشار این ترجمه منوط به داشتن مجوز لازم از صاحب حقوق اثر است.
فصل ۱۵: Streamها و I/O
تصویر تزئینی آغاز فصل ۱۵
این فصل Typeهای بنیادی Input و Output در .NET را توضیح میدهد و بر موضوعهای زیر تأکید دارد:
- معماری Stream در .NET و اینکه چگونه برای Read و Write روی انواع مختلف I/O یک Programming Interface یکنواخت فراهم میکند.
- Classهای کار با File و Directory روی Disk.
- Streamهای تخصصی برای Compression، Named Pipeها و Memory-mapped Fileها.
تمرکز این فصل روی Typeهای Namespace با نام System.IO است؛ جایی که قابلیتهای سطح پایینتر I/O قرار دارند.
معماری Stream
معماری Stream در .NET حول سه Concept میچرخد: Backing Store، Decorator و Adapter، همانطور که در شکل 15-1 نشان داده شده است.
Backing Store نقطهٔ انتهایی است که Input و Output را مفید میکند؛ مانند File یا Network Connection. دقیقتر، Backing Store یکی یا هر دو مورد زیر است:
- Sourceای که Byteها را میتوان از آن بهترتیب Read کرد.
- Destinationای که Byteها را میتوان به آن بهترتیب Write کرد.
شکل 15-1 — معماری Streamبرچسبهای شکل: Stream adapters = Adapterهای Stream؛ Decorator streams = Streamهای Decorator؛ Backing store streams = Streamهای Backing Store؛ Text = متن؛ Bytes = بایتها؛ Raw bytes = بایت خام.
اما Backing Store تا زمانی که در اختیار Programmer قرار نگیرد کاربردی ندارد. Stream Class استاندارد .NET برای این هدف است و مجموعهای استاندارد از Methodها برای Reading، Writing و Positioning ارائه میکند. برخلاف Array که همهٔ Data پشت آن یکباره در Memory وجود دارد، Stream با Data بهشکل ترتیبی کار میکند: یا یک Byte در هر لحظه یا Blockهایی با Size قابل مدیریت. بنابراین بدون توجه به Size Backing Store، Stream میتواند مقدار کوچک و ثابتی Memory مصرف کند.
Streamها به دو Category تقسیم میشوند:
- Backing Store Streamها
- مستقیماً به نوع مشخصی از Backing Store متصلاند؛ مثل
FileStream یا NetworkStream. - Decorator Streamها
- از Stream دیگری تغذیه میشوند و Data را به شکلی Transform میکنند؛ مثل
DeflateStream یا CryptoStream.
Decorator Streamها مزیتهای معماری زیر را دارند:
- Backing Store Streamها را از نیاز به پیادهسازی قابلیتهایی مثل Compression و Encryption در خودشان آزاد میکنند.
- Interface یک Stream با Decorate شدن تغییر نمیکند.
- Decoratorها را در Runtime به هم وصل میکنید.
- میتوانید Decoratorها را Chain کنید؛ مثلاً Compressor و بعد Encryptor.
هم Backing Store Stream و هم Decorator Stream فقط با Byteها سروکار دارند. این انعطافپذیر و Efficient است، اما Applicationها اغلب در Level بالاتری مانند Text یا XML کار میکنند. Adapterها این شکاف را با Wrap کردن Stream در Classی با Methodهای تخصصی و Typed برای Format مشخص پر میکنند. مثلاً Text Reader، Method با نام ReadLine و XML Writer، Method با نام WriteAttributes ارائه میکند.
خلاصه: Backing Store Streamها Raw Data را فراهم میکنند؛ Decorator Streamها Transformهای Binary شفاف مانند Encryption را اضافه میکنند؛ و Adapterها Methodهای Typed برای کار با Typeهای سطح بالاتر مانند String و XML میدهند. شکل 15-1 ارتباط آنها را نشان میدهد. برای Compose کردن Chain کافی است Objectها را به Constructor یکدیگر بدهید.
استفاده از Streamها
Class انتزاعی Stream Base همهٔ Streamهاست. Methodها و Propertyهایی برای سه Operation بنیادی ــ Reading، Writing و Seeking ــ و همچنین کارهای مدیریتی مانند Closing، Flushing و تنظیم Timeoutها تعریف میکند؛ جدول 15-1 را ببینید.
جدول 15-1 — Memberهای Class با نام Stream| Category | Memberها |
| Reading | public abstract bool CanRead { get; }
public abstract int Read (byte[] buffer, int offset, int count)
public virtual int ReadByte(); |
| Writing | public abstract bool CanWrite { get; }
public abstract void Write (byte[] buffer, int offset, int count);
public virtual void WriteByte (byte value); |
| Seeking | public abstract bool CanSeek { get; }
public abstract long Position { get; set; }
public abstract void SetLength (long value);
public abstract long Length { get; }
public abstract long Seek (long offset, SeekOrigin origin); |
| Closing / flushing | public virtual void Close();
public void Dispose();
public abstract void Flush(); |
ادامهٔ جدول 15-1 — Memberهای Stream| Category | Memberها |
| Timeouts | public virtual bool CanTimeout { get; }
public virtual int ReadTimeout { get; set; }
public virtual int WriteTimeout { get; set; } |
| Other | public static readonly Stream Null; // "Null" stream
public static Stream Synchronized (Stream stream); |
نسخههای ناهمگام Methodهای Read و Write نیز وجود دارند که هر دو Task برمیگردانند و بهصورت اختیاری Cancellation Token میپذیرند؛ همچنین Overloadهایی که با Span<T> و Memory<T> کار میکنند و در فصل 23 توضیح میدهیم.
در مثال زیر از File Stream برای Read، Write و Seek استفاده میکنیم:
using System;
using System.IO;
// Create a file called test.txt in the current directory:
using (Stream s = new FileStream ("test.txt", FileMode.Create))
{
Console.WriteLine (s.CanRead); // True
Console.WriteLine (s.CanWrite); // True
Console.WriteLine (s.CanSeek); // True
s.WriteByte (101);
s.WriteByte (102);
byte[] block = { 1, 2, 3, 4, 5 };
s.Write (block, 0, block.Length); // Write block of 5 bytes
Console.WriteLine (s.Length); // 7
Console.WriteLine (s.Position); // 7
s.Position = 0; // Move back to the start
Console.WriteLine (s.ReadByte()); // 101
Console.WriteLine (s.ReadByte()); // 102
// Read from the stream back into the block array:
Console.WriteLine (s.Read (block, 0, block.Length)); // 5
// Assuming the last Read returned 5, we'll be at
// the end of the file, so Read will now return 0:
Console.WriteLine (s.Read (block, 0, block.Length)); // 0
}
برای Read یا Write ناهمگام کافی است بهجای Read/Write، ReadAsync/WriteAsync را Call و Expression را Await کنید؛ همچنین همانطور که در فصل 14 گفتیم باید Keyword با نام async را به Caller اضافه کنیم:
async static void AsyncDemo()
{
using (Stream s = new FileStream ("test.txt", FileMode.Create))
{
byte[] block = { 1, 2, 3, 4, 5 };
await s.WriteAsync (block, 0, block.Length); // Write asychronously
s.Position = 0; // Move back to the start
// Read from the stream back into the block array:
Console.WriteLine (await s.ReadAsync (block, 0, block.Length)); // 5
}
}
Methodهای ناهمگام نوشتن Applicationهای Responsive و Scalable را هنگام کار با Streamهای بالقوه کند ــ بهخصوص Network Streamها ــ آسان میکنند، بدون اینکه Thread را اشغال کنند.
Reading و Writing
یک Stream ممکن است Reading، Writing یا هر دو را پشتیبانی کند. اگر CanWrite مقدار false بدهد Stream فقط Read است؛ اگر CanRead مقدار false بدهد فقط Write است.
Read یک Block از Data را از Stream داخل Array دریافت میکند. تعداد Byteهای دریافتشده را برمیگرداند که همیشه کمتر یا مساوی Argument با نام count است. اگر کمتر از count باشد یعنی یا به End Stream رسیدهایم یا Stream Data را در Chunkهای کوچکتر تحویل میدهد؛ چیزی که در Network Streamها رایج است. در هر دو حالت Byteهای باقیماندهٔ Array نوشته نمیشوند و Value قبلی خود را حفظ میکنند.
راه درست Read کردن Stream هزار Byte با Read:
byte[] data = new byte [1000];
// bytesRead will always end up at 1000, unless the stream is
// itself smaller in length:
int bytesRead = 0;
int chunkSize = 1;
while (bytesRead < data.Length && chunkSize > 0)
bytesRead +=
chunkSize = s.Read (data, bytesRead, data.Length - bytesRead);
برای سادهترکردن این کار، از .NET 7، Class با نام Stream Helper Methodهایی با نام ReadExactly و ReadAtLeast و نسخههای Async هرکدام دارد. کد زیر دقیقاً 1000 Byte میخواند و اگر Stream زودتر تمام شود Exception پرتاب میکند:
byte[] data = new byte [1000];
s.ReadExactly (data); // Reads exactly 1000 bytes
Line آخر معادل این است:
s.ReadExactly (data, offset:0, count:1000);
Type با نام BinaryReader راهحل دیگری ارائه میدهد:
byte[] data = new BinaryReader (s).ReadBytes (1000);
Method با نام ReadByte سادهتر است: فقط یک Byte میخواند و برای نشاندادن End Stream مقدار −1 برمیگرداند. ReadByte در واقع int برمیگرداند نه byte، چون Byte نمیتواند −1 را Return کند.
Methodهای Write و WriteByte Data را به Stream میفرستند. اگر نتوانند Byteهای مشخصشده را بفرستند Exception پرتاب میشود.
Seeking
اگر CanSeek مقدار true بدهد Stream قابل Seek است. روی Stream قابل Seek مانند File Stream، میتوانید Length را Query یا با SetLength تغییر دهید و هر زمان Position محل Read یا Write را عوض کنید. Property با نام Position نسبت به Beginning Stream است؛ اما Method با نام Seek اجازه میدهد نسبت به Position فعلی یا End Stream جابهجا شوید.
در Stream غیرقابل Seek ــ مانند Encryption Stream ــ تنها راه تعیین Length این است که آن را کامل بخوانید. همچنین اگر لازم باشد بخش قبلی را دوباره Read کنید، باید Stream را Close و از ابتدا یک Stream جدید ایجاد کنید.
Closing و Flushing
Streamها باید پس از استفاده Dispose شوند تا Resourceهای زیربنایی مانند File Handle و Socket Handle آزاد شوند. راه سادهٔ تضمین آن Instantiate کردن Stream داخل using Block است. در حالت عمومی Streamها Semantic استاندارد Disposal را دنبال میکنند:
Dispose و Close از نظر Function یکساناند.- Dispose یا Close کردن مکرر Stream خطایی ایجاد نمیکند.
Close کردن Decorator Stream هم خود Decorator و هم Backing Store Stream را Close میکند. در Chain از Decoratorها، Close کردن بیرونیترین Decorator در Head زنجیره، کل مجموعه را Close میکند.
بعضی Streamها برای کاهش Round-trip و بهبود Performance، Data را به/از Backing Store در Buffer داخلی نگه میدارند؛ File Stream مثال خوبی است. بنابراین Dataای که Write میکنید ممکن است بلافاصله به Backing Store نرسد و تا پرشدن Buffer Delay شود. Method با نام Flush هر Data داخلی Bufferشده را فوراً Write میکند. Flush هنگام Close شدن Stream خودکار Call میشود، پس هرگز لازم نیست این کار را انجام دهید:
Timeoutها
اگر CanTimeout مقدار true بدهد Stream از Read و Write Timeout پشتیبانی میکند. Network Streamها Timeout دارند؛ File Stream و Memory Stream ندارند. برای Streamهای پشتیبان Timeout، Propertyهای ReadTimeout و WriteTimeout Timeout دلخواه را بر حسب Millisecond تعیین میکنند؛ 0 یعنی بدون Timeout. Methodهای Read و Write با پرتاب Exception وقوع Timeout را اعلام میکنند.
Methodهای ناهمگام ReadAsync/WriteAsync از Timeout پشتیبانی نمیکنند؛ در عوض میتوانید Cancellation Token به آنها بدهید.
Thread Safety
بهعنوان قاعده، Streamها Thread-safe نیستند؛ یعنی دو Thread نمیتوانند همزمان بدون احتمال Error روی یک Stream Read یا Write کنند. Class با نام Stream از طریق Method استاتیک Synchronized Workaround سادهای میدهد. این Method Streamی از هر Type را میپذیرد و Wrapper Thread-safe برمیگرداند. Wrapper با گرفتن Exclusive Lock دور هر Read، Write یا Seek کار میکند و تضمین میکند در هر زمان فقط یک Thread یکی از این Operationها را انجام دهد.
در عمل، این اجازه میدهد چند Thread همزمان Data را به انتهای یک Stream Append کنند. Activityهای دیگر ــ مانند Read Concurrent ــ Locking بیشتری لازم دارند تا هر Thread بخش مورد نظرش را بخواند. Thread Safety را در فصل 21 کامل بررسی میکنیم.
Backing Store Streamها
شکل 15-2 مهمترین Backing Store Streamهای ارائهشده توسط .NET را نشان میدهد. «Null Stream» نیز از طریق Field استاتیک Null در Stream موجود است. Null Streamها هنگام نوشتن Unit Test میتوانند مفید باشند.
شکل 15-2 — Backing Store Streamهابرچسبهای شکل از نسخهٔ اصلی حفظ شدهاند و نام Typeهای .NET ترجمه نشدهاند.
در بخشهای بعد FileStream و MemoryStream را توضیح میدهیم؛ در بخش پایانی این فصل IsolatedStorageStream را بررسی میکنیم و در فصل 16 به NetworkStream میپردازیم.
FileStream
پیشتر در این بخش استفادهٔ پایهٔ FileStream برای Read و Write کردن Byteهای Data را نشان دادیم. اکنون قابلیتهای ویژهٔ این Class را بررسی کنیم.
Construct کردن FileStream
سادهترین راه Instantiate کردن FileStream استفاده از یکی از Facade Methodهای استاتیک زیر روی Class با نام File است:
FileStream fs1 = File.OpenRead ("readme.bin"); // Read-only
FileStream fs2 = File.OpenWrite ("writeme.tmp"); // Write-only
FileStream fs3 = File.Create ("readwrite.tmp"); // Read/write
OpenWrite و Create اگر File از قبل وجود داشته باشد Behavior متفاوتی دارند. Create Content موجود را Truncate میکند؛ OpenWrite Content را دستنخورده نگه میدارد و Stream را در Position صفر قرار میدهد. اگر Byteهای کمتری از Data قبلی File بنویسید، با OpenWrite ترکیبی از Content قدیم و جدید باقی میماند.
میتوانید FileStream را مستقیم نیز Instantiate کنید. Constructorهای آن به همهٔ Featureها دسترسی میدهند: Filename یا Low-level File Handle، Creation و Access Mode، Optionهای Sharing، Buffering و Security. کد زیر File موجود را برای Read/Write بدون Overwrite باز میکند؛ Keyword با نام using تضمین میکند وقتی fs از Scope خارج شد Dispose شود:
using var fs = new FileStream ("readwrite.tmp", FileMode.Open);
کمی بعد FileMode را دقیقتر بررسی میکنیم.
Shortcut Methodهای Class با نام File
Methodهای استاتیک زیر کل File را در یک Step به Memory میخوانند:
File.ReadAllText یک String برمیگرداند.File.ReadAllLines یک Array از Stringها برمیگرداند.File.ReadAllBytes یک Byte Array برمیگرداند.
Methodهای استاتیک زیر کل File را در یک Step مینویسند:
File.WriteAllTextFile.WriteAllLinesFile.WriteAllBytesFile.AppendAllText که برای Append کردن به Log File عالی است.
Method استاتیک دیگری به نام File.ReadLines وجود دارد که شبیه ReadAllLines است، اما IEnumerable<string> از نوع Lazy-evaluated برمیگرداند. Efficientتر است، چون کل File را یکباره به Memory Load نمیکند. LINQ برای مصرف Result مناسب است؛ کد زیر تعداد Lineهایی را حساب میکند که Length آنها بیش از 80 Character است:
int longLines = File.ReadLines ("filePath")
.Count (l => l.Length > 80);
مشخصکردن Filename
Filename میتواند Absolute باشد ــ مثلاً c:\temp\test.txt یا در Unix، /tmp/test.txt ــ یا Relative به Current Directory باشد، مثل test.txt یا temp\test.txt. از Property استاتیک Environment.CurrentDirectory میتوانید Current Directory را بخوانید یا تغییر دهید.
AppDomain.CurrentDomain.BaseDirectory Application Base Directory را برمیگرداند که در حالت معمول Folder حاوی Executable Program است. برای مشخصکردن Filename بهصورت Relative به این Directory میتوانید Path.Combine را Call کنید:
string baseFolder = AppDomain.CurrentDomain.BaseDirectory;
string logoPath = Path.Combine (baseFolder, "logo.jpg");
Console.WriteLine (File.Exists (logoPath));
در Windows میتوانید با Universal Naming Convention یا UNC Path روی Network Read و Write کنید؛ مانند \\JoesPC\PicShare\pic.jpg یا \\10.1.1.2\PicShare\pic.jpg. برای دسترسی به Windows File Share از macOS یا Unix، ابتدا آن را طبق Instruction مخصوص OS خود روی Filesystem Mount و سپس با Path عادی C# باز کنید.
مشخصکردن FileMode
همهٔ Constructorهای FileStream که Filename میپذیرند، Argument از Enum با نام FileMode نیز لازم دارند. شکل 15-3 نشان میدهد چگونه FileMode انتخاب کنید؛ Resultها مشابه Call کردن Method استاتیک متناظر روی Class با نام File هستند.
شکل 15-3 — انتخاب FileModeنام Enumها و Methodهای C# مطابق متن اصلی حفظ شدهاند.
ساختن FileStream فقط با Filename و FileMode، با یک استثنا، Stream قابل Read و Write میدهد. اگر Argument با نام FileAccess را نیز بدهید میتوانید دسترسی را Downgrade کنید:
[Flags]
public enum FileAccess { Read = 1, Write = 2, ReadWrite = 3 }
کد زیر Stream فقط Read برمیگرداند و معادل File.OpenRead است:
using var fs = new FileStream ("x.bin", FileMode.Open, FileAccess.Read);
...
FileMode.Append مورد متفاوت است: با این Mode یک Stream فقط Write میگیرید. برای Append با پشتیبانی Read/Write باید از FileMode.Open یا FileMode.OpenOrCreate استفاده و سپس به End Stream Seek کنید:
using var fs = new FileStream ("myFile.bin", FileMode.Open);
fs.Seek (0, SeekOrigin.End);
...
Featureهای پیشرفته FileStream
Argumentهای اختیاری دیگری که هنگام Construct کردن FileStream میتوانید بدهید:
- Enum با نام
FileShare که میزان Access Processهای دیگر به همان File را پیش از پایان کار شما تعیین میکند: None، Read ــ Default ــ، ReadWrite یا Write. - Size Buffer داخلی بر حسب Byte؛ Default فعلی 4 KB است.
- Flagی که تعیین میکند برای I/O ناهمگام به Operating System واگذار شود یا نه.
- Enum Flagدار
FileOptions برای درخواست OS Encryption با Encrypted، Delete خودکار File موقت هنگام Close با DeleteOnClose و Hintهای Optimization با RandomAccess و SequentialScan. Flag با نام WriteThrough نیز درخواست میکند OS، Write-behind Caching را Disable کند؛ مناسب File یا Logهای Transactional. Flagهایی که OS زیربنایی پشتیبانی نکند بیصدا Ignore میشوند.
بازکردن File با FileShare.ReadWrite اجازه میدهد Processها یا Userهای دیگر همزمان همان File را Read و Write کنند. برای جلوگیری از Chaos میتوانید توافق کنید پیش از Read یا Write، بخشهای مشخصی از File را با Methodهای زیر Lock کنید.
// Defined on the FileStream class:
public virtual void Lock (long position, long length);
public virtual void Unlock (long position, long length);
اگر بخشی یا همهٔ Section درخواستشدهٔ File از قبل Lock شده باشد، Lock Exception پرتاب میکند.
MemoryStream
MemoryStream از Array بهعنوان Backing Store استفاده میکند. این تا حدی هدف داشتن Stream را نقض میکند، چون کل Backing Store باید یکباره در Memory باشد. با این حال وقتی به Random Access روی Stream غیرقابل Seek نیاز دارید، MemoryStream مفید است. اگر میدانید Source Stream Size قابل مدیریت دارد، میتوانید آن را چنین داخل MemoryStream Copy کنید:
var ms = new MemoryStream();
sourceStream.CopyTo (ms);
با ToArray میتوانید MemoryStream را به Byte Array تبدیل کنید. Method با نام GetBuffer همان کار را Efficientتر انجام میدهد و Reference مستقیم به Storage Array زیربنایی میدهد؛ اما این Array معمولاً از Length واقعی Stream بلندتر است.
PipeStream
PipeStream راه سادهای فراهم میکند تا یک Process از طریق Protocol مربوط به Pipe در Operating System با Process دیگر ارتباط برقرار کند. دو نوع Pipe وجود دارد:
- Anonymous Pipe ــ سریعتر
- ارتباط یکطرفه بین Parent Process و Child Process روی یک Computer.
- Named Pipe ــ انعطافپذیرتر
- ارتباط دوطرفه بین Processهای دلخواه روی همان Computer یا Computerهای مختلف در Network.
Pipe برای Interprocess Communication یا IPC روی یک Computer مناسب است: به Network Transport وابسته نیست، پس Network Protocol Overhead ندارد و با Firewall هم مشکل ندارد.
PipeStream یک Class انتزاعی با چهار Subtype واقعی است. دو Type برای Anonymous Pipe و دو Type برای Named Pipe:
- Anonymous Pipe
AnonymousPipeServerStream و AnonymousPipeClientStream- Named Pipe
NamedPipeServerStream و NamedPipeClientStream
Named Pipeها سادهتر استفاده میشوند، پس ابتدا آنها را توضیح میدهیم.
Named Pipeها
در Named Pipe، طرفها از Pipe با Name یکسان ارتباط میگیرند. Protocol دو Role مشخص میکند: Client و Server. Communication بین Client و Server چنین رخ میدهد:
- Server یک
NamedPipeServerStream Instantiate میکند و سپس WaitForConnection را Call میکند. - Client یک
NamedPipeClientStream Instantiate میکند و سپس Connect را با Timeout اختیاری Call میکند.
سپس دو طرف با Read و Write کردن Streamها ارتباط میگیرند.
مثال Server که یک Byte با Value برابر 100 میفرستد و سپس منتظر دریافت یک Byte میشود:
using var s = new NamedPipeServerStream ("pipedream");
s.WaitForConnection();
s.WriteByte (100); // Send the value 100.
Console.WriteLine (s.ReadByte());
کد Client متناظر:
using var s = new NamedPipeClientStream ("pipedream");
s.Connect();
Console.WriteLine (s.ReadByte());
s.WriteByte (200); // Send the value 200 back.
Named Pipe Streamها بهطور پیشفرض Bidirectional هستند، پس هر دو طرف میتوانند Stream خود را Read یا Write کنند. یعنی Client و Server باید روی Protocolی برای Coordinate کردن Actionها توافق داشته باشند تا هر دو طرف همزمان در حال Send یا Receive نباشند.
همچنین باید دربارهٔ Length هر Transmission توافق وجود داشته باشد. مثال ما ساده بود، چون در هر Direction فقط یک Byte ردوبدل کردیم.
برای کمک به Messageهای بلندتر از یک Byte، Pipeها Message Transmission Mode ارائه میکنند ــ فقط در Windows. اگر فعال باشد، طرفی که Read را Call میکند میتواند با Property با نام IsMessageComplete بفهمد Message Complete شده یا نه. برای نمایش، Helper Method زیر یک Message کامل را از PipeStream دارای Message Mode میخواند؛ یعنی تا وقتی IsMessageComplete مقدار true شود Read میکند:
static byte[] ReadMessage (PipeStream s)
{
MemoryStream ms = new MemoryStream();
byte[] buffer = new byte [0x1000]; // Read in 4 KB blocks
do { ms.Write (buffer, 0, s.Read (buffer, 0, buffer.Length)); }
while (!s.IsMessageComplete);
return ms.ToArray();
}
برای ناهمگامکردن آن، s.Read را با await s.ReadAsync جایگزین کنید.
اکنون میتوانیم Message Mode را فعال کنیم. روی Server هنگام Construct کردن Stream، PipeTransmissionMode.Message را مشخص میکنیم:
using var s = new NamedPipeServerStream ("pipedream", PipeDirection.InOut,
1, PipeTransmissionMode.Message);
s.WaitForConnection();
byte[] msg = Encoding.UTF8.GetBytes ("Hello");
s.Write (msg, 0, msg.Length);
Console.WriteLine (Encoding.UTF8.GetString (ReadMessage (s)));
روی Client پس از Call کردن Connect، Message Mode را با تنظیم ReadMode فعال میکنیم:
using var s = new NamedPipeClientStream ("pipedream");
s.Connect();
s.ReadMode = PipeTransmissionMode.Message;
Console.WriteLine (Encoding.UTF8.GetString (ReadMessage (s)));
byte[] msg = Encoding.UTF8.GetBytes ("Hello right back!");
s.Write (msg, 0, msg.Length);
Anonymous Pipeها
Anonymous Pipe یک Stream ارتباط یکطرفه بین Parent Process و Child Process فراهم میکند. بهجای System-wide Name، Anonymous Pipeها از طریق Private Handle به هم متصل میشوند.
مانند Named Pipe، Roleهای Client و Server جدا هستند؛ اما System ارتباط کمی متفاوت است:
- Server یک
AnonymousPipeServerStream Instantiate میکند و PipeDirection از نوع In یا Out را تعیین میکند. - Server،
GetClientHandleAsString را Call میکند تا Identifier Pipe را بگیرد و سپس آن را به Client میدهد؛ معمولاً بهعنوان Argument هنگام Start کردن Child Process. - Child Process یک
AnonymousPipeClientStream با PipeDirection مخالف Instantiate میکند. - Server Local Handle ساختهشده در Step 2 را با
DisposeLocalCopyOfClientHandle آزاد میکند. - Parent و Child با Read/Write کردن Stream ارتباط میگیرند.
چون Anonymous Pipeها Unidirectional هستند، Server برای ارتباط Bidirectional باید دو Pipe بسازد. Console Program زیر دو Pipe ــ Input و Output ــ میسازد و Child Process را Start میکند. سپس یک Byte به Child میفرستد و یک Byte در پاسخ میگیرد:
class Program
{
static void Main (string[] args)
{
if (args.Length == 0)
// No arguments signals server mode
AnonymousPipeServer();
else
// We pass in the pipe handle IDs as arguments to signal client mode
AnonymousPipeClient (args [0], args [1]);
}
static void AnonymousPipeClient (string rxID, string txID)
{
using var rx = new AnonymousPipeClientStream (PipeDirection.In, rxID);
using var tx = new AnonymousPipeClientStream (PipeDirection.Out, txID);
Console.WriteLine ("Client received: " + rx.ReadByte ());
tx.WriteByte (200);
}
static void AnonymousPipeServer ()
{
using var tx = new AnonymousPipeServerStream (
PipeDirection.Out, HandleInheritability.Inheritable);
using var rx = new AnonymousPipeServerStream (
PipeDirection.In, HandleInheritability.Inheritable);
string txID = tx.GetClientHandleAsString ();
string rxID = rx.GetClientHandleAsString ();
// Create and start up a child process.
// We'll use the same Console executable, but pass in arguments:
string thisAssembly = Assembly.GetEntryAssembly().Location;
string thisExe = Path.ChangeExtension (thisAssembly, ".exe");
var args = $"{txID} {rxID}";
var startInfo = new ProcessStartInfo (thisExe, args);
startInfo.UseShellExecute = false; // Required for child process
Process p = Process.Start (startInfo);
tx.DisposeLocalCopyOfClientHandle (); // Release unmanaged
rx.DisposeLocalCopyOfClientHandle (); // handle resources.
tx.WriteByte (100); // Send a byte to the child process
Console.WriteLine ("Server received: " + rx.ReadByte ());
p.WaitForExit ();
}
}
مانند Named Pipe، Client و Server باید ارسال و دریافت را Coordinate کنند و دربارهٔ Length هر Transmission توافق داشته باشند. Anonymous Pipeها متأسفانه Message Mode ندارند، پس Protocol تعیین Length Message را باید خودتان پیادهسازی کنید. یک راه این است که در چهار Byte نخست هر Transmission، Integerای بفرستید که Length Message بعدی را تعیین کند. Class با نام BitConverter Methodهایی برای تبدیل بین Integer و Array چهار Byte ارائه میدهد.
BufferedStream
BufferedStream Stream دیگری را با قابلیت Buffering Decorate یا Wrap میکند و یکی از چند Decorator Stream Type در .NET است که همگی در شکل 15-4 ــ در مقالهٔ بعد ــ نمایش داده میشوند.