15. Streams and I/O || C# 10 Flashcards
Can you name 3 concepts on which the .NET stream architecture centers?
- Backing stores
- decorators
- adapters
What is a ‘backing store’?
A backing store is the endpoint that makes input and output useful, such as a file or network connection. Precisely, it is either or both of the following:
- A source from which bytes can be sequentially read;
- A destination to which bytes can be sequentially written;
What is a Stream?
A stream exposes a standard set of methods for reading, writing and positioning. Unlike an array, for which all the backing data exists in memory at once, a stream deals with data serially - either one byte at a time or in blocks of manageable size. Hence, a stream can use a small, fixed amount of memory regardless of the size of its backing store.
The abstract Stream class is the base for all streams. It defines methods and properties for three fundamental operations: reading, writing and seeking;
How can we categorize streams?
Streams fall into two categories:
- Backing store streams: These are hardwired to a particular type of backing store, such as
FileStream
orNetworkStream
; - Decorator streams: These feed off another stream, transforming the data in some way, such as
DeflateStream
orCryptoStream
;
To summarize, backing store streams provide the raw data, decorator streams provide transparent binary transformations such as encryption
Can you name the benefits of Decorator streams?
Decorator streams have the following architectural benefits:
1. They liberate backing store streams from needing to implement such features as compression and encryption themselves;
2. Streams don’t suffer a change of interface when decorated;
3. You connect decorators at runtime;
4. You can chain decorators together (e.g. a compressor followed by an encryptor);
What is an adaptor in streams as a topic?
If we consider that backing stores and decorator streams deal exclusively in bytes, when applications often work at higher level, such as text or XML, we need some sort of translation between the types.
Adaptors bridge this gap by wrapping a stream in a class with specialized methods typed to a particular format. For example, a text reader exposes ReadLine
method, an XML writer exposes a WriteAttributes
method.
An adaptor wraps a stream, just like a decorator. Unlike a decorator, however, an adapter is not itself a stream; it typically hides the byte-oriented methods completely.
Adapters offer typed methods for dealing with higher level types such as strings and XML.
Explain how the Read method in Streams work
Read
receives a block of data from the stream into an array. It returns the number of bytes received, which is always either less than or equal to the count argument. If it’s less than count, it means that either the end of the stream has been reached or the stream is giving you the data in smaller chunks (as is often the case with network streams). In either case, the balance of bytes in the array will remain unwritten, their previous values preserved.
With Read
you can be certain you’ve reached the end of the stream only when the method returns 0
What does the Seek
method allows to do?
The Seek
method allows you to move relative to the current position or the end of the stream.
With nonseekable stream (such as an encryption stream) the only way to determine its length is to read it completely through. Furthermore if you need to reread a previous section, you must close the stream and start fresh with a new one.
Describe disposal semantics of the stream
In general streams follow sandard disposal semantics:
-
Dispose
andClose
are identical in function; - Disposing or closing a stream repeatedly causes no error.
Closing a decorator stream closes both the decorator and its backing store stream. With a chain of decorators, closing the outermost decorator closes the whole lot.
Are streams thread-safe?
As a rule, streams are not thread-safe, meaning two threads cannot concurrently read or write to the same stream without possible error.
As a workaround there is a Synchronized
method, that returns a thread-safe wrapper. The wrapper works by obtaining an exclusive lock around each read, write or seek, ensuring that only one thread can perform such an operation at a time.
FileStream class, besides others, has two methods - OpenWrite
and Create
. Both of them write content into the file, but what are the differences?
The differences shows up when we try to write something into a file that already have some content written in it. Create
method truncates any existing content while OpenWrite
leaves existing content intact with the stream positioned at zero. If you write fewer bytes than were previously in the file, OpenWrite
leaves you with a mixture of old and new content.
What’s the difference between ReadLines and ReadAllLines?
They do the same thing except ReadLines returns lazily evaluated IEnumerable<string>
. This is more efficient because it doesnt load the entire file into memory at once.
What is a MemoryStream?
MemoryStream uses an array as a backing store. This partly defeats the purpose of having a stream because the entire backing store must reside in memory at once. MemoryStream is still usefull when you need random access to a nonseekable stream.
You can conver MemoryStream into byte array by calling ToArray. The GetBuffer method does the same job more efficiently by returning a direct reference to the underlying storage array.
Also fun fact that calling Flush on MemoryStream does absolutely nothing :)
What is a PipeStream?
PipeStream provides a simple means by which one process can communicate with another through the operating system’s protocol. There are two kinds of pipe:
1. Anonymous pipe (faster):
Allows one-way communication between a parent and child process on the same computer;
2. Named pipe (more flexible):
Allows two-way communication between arbitrary processes on the same computer or different computers across a network.
A pipe is good for interprocess communication on a single computerL it doesn’t rely on a network transport, which means no network protocol overhead, and it has no issues with firewalls.
A pipe is good for interprocess comunication on a single computer: it doesn’t rely on a network transport, which means no network protocol overhead, and it has no issues with firewalls.
Pipes are stream based, so one process waits to receive a series of bytes while another process sends them.
Define PipeStream class
PipeStream is an abstract class with four concrete subtypes. Two are used for anonymous pipes and other two for named pipes.
Named pipes are simpler to use.
What are Named Pipes?
With the named pipes, the parties communicate through a pipe of the same name. The protocol defines two distinct roles: the client and server. Communication happens between the client and server as follows:
* The server instantiates a NamedPipeServerStream and then calls WaitForConnection;
* The client instantiates a NamedPipeClientStream and then calls Connect (with an optional timeout)
The two parties then read and write the streams to communicate.
(using var s = new NamedPipeServerStream("pipedream")){ s.WaitForConnection(); s.WriteByte(100); // Send the value 100 Console.WriteLine(s.ReadByte()); } (using var c = new NamedPipeClientStream("pipedream")){ c.Connect(); Console.WriteLine(s.ReadByte()); c.WriteByte(200); // Send the value 200 back }
Named pipes are bidirectional by default, so either party can read or write their stream. This means that the client and server must agree on some protocol to coordinate their action, so both parties don’t end up sending or receiving at once.
What is a message transmission and why do we need it?
Since you cannot determine whether a PipeStream has finished reading a message simply by waiting for Read to turn 0, you need to agree between client and server on the length of the message. To help with messages longer than one byte, pipes provide a message transmission mode. If this is enabled a party calling Read can know when a message is complete by checking the IsMessageComplete property.
(using var s = new NamedPipeServerStream("pipedream", PipeDirection.InOut, 1, PipeTransmissionMode.Message)){ s.WaitForConnection(); byte[] msg = Encoding.UTF8.GetBytes("Hello"); s.Write(msg, msg.Length); Console.WriteLine(Encoding.UTF8.GetString(ReadMessage(s))); } (using var c = new NamedPipeClientStream("pipedream")){ c.Connect(); c.ReasMode = PipeTransmissionMode.Message; Console.WriteLine(Encoding.UTF8.GetString(ReadMessage(c))); byte[] message = Encoding.UTF8.GetBytes("Hello right back!"); c.Write(message, 0, message.Length);
What are anonymous pipes and what are they used for?
An anonymous pipe provides a one-way communication stream between a parent and child process. Instead of using a system-wide name, anonymous pipes tune in through a private handle.
As with named pipes, there are distinct client and server roles. The system of communication is a little different, however, and proceeds as follows:
* The server instantiates an AnonymousPipeServerStream, commiting to a PipeDirection of In or Out.
* The server calls GetClientHandleAsString to obtain an identifier for the pipe, which it then passes to the client;
* The child process instantiates an AnonymousPipeClientStream, specifying the opposite PipeDirection;
* The server releases the local handle that was generated in Step 2, by calling DisposeLocalCopyOfClienthandle;
* The parent and child processes communicate by reading/writing the stream;
Because anonymous streams are unidirectional, a server must create two pipes for bidirectional communication.
As with named pipes, the client and server must coordinate their sending and receiving and agree on on the lenght of each transmission. Anonymous pipes don’t support message mode.
Here is an analogy from chatGPT :)
Imagine you have two people, Alice and Bob, who want to communicate, but they can only send messages in one direction. They each have their own dedicated messenger to send messages to the other person. In this modified analogy:
- Alice represents one part of your C# program (e.g., the server).
- Bob represents another part of your C# program (e.g., the client).
- Two Messengers represent the two sets of anonymous pipes, one for each direction of communication (Alice’s messenger for sending messages to Bob, and Bob’s messenger for sending messages to Alice).
Here’s how it works:
- Sending Messages from Alice to Bob (Alice’s Messenger): When Alice wants to send a message (data) to Bob, she writes the message on a note (puts data into her messenger’s pipe). Her dedicated messenger (pipe) takes the note and delivers it to Bob.
- Receiving Messages from Alice (Bob’s Messenger): Bob, on the other hand, has his own messenger (pipe) to receive messages from Alice. His messenger (pipe) delivers the note (data) to Bob, and he reads the message (data) written on it.
- Sending Messages from Bob to Alice (Bob’s Messenger): If Bob wants to send a message (data) to Alice, he writes it on a note and hands it to his dedicated messenger (pipe). The messenger takes the note and delivers it to Alice.
- Receiving Messages from Bob (Alice’s Messenger): Alice, in turn, has her own messenger (pipe) to receive messages from Bob. Her messenger (pipe) delivers the note (data) to Alice, and she reads the message (data) written on it.
So, in the case of anonymous pipes in C#, you need two separate sets of pipes: one for sending data in each direction of communication. This unidirectional setup allows you to achieve bidirectional communication by using two sets of pipes—one for each direction—similar to how Alice and Bob each have their dedicated messengers for sending messages to the other person.
What is a BufferedStream? What buffering gives to a stream?
BufferedStream decorates or wraps another stream with buffering capabilities and it is one of a number of decorator stream types in .NET.
Buffering improves performance by reducing round trips to the backing store.
Why would you need a BufferStream together with FileStream if it already comes with built-in buffering?
In this case its only use might be in enlarging the buffer on an already-constructed FileStream.
Why do we need Stream Adapters?
A Stream deals only in bytes; to read or write data types such as strings, integers or XML elements, you must plug in an adapter; Here are the main catogories:
* Text adapters (for string and character data): TextReader, TextWriter, StreamReader, StreamWriter, StringReader, StringWriter;
* Binary adapters (for primitive types such as int, bool, string and float): BinaryReader, BinaryWriter;
* XML adapters: XmlReader, XmlWriter;
Can you name one additional difference between the TextReader/TextWriter and StreamReader/StreamWriter that is not just the type of data accepted?
TextReader and TextWriter are by themselves just abstract classes with no connection to a stream or backing store. The StreamReader and StreamWriter types, however, are connected to an underlying byte-oriented stream, so they must convert between characters and bytes. They do so through Encoding class, ehich you choose when constructing StreamReader or StreamWriter.
What is the simpliest of encodings and why?
The simpliest of encodings is ASCII because each character is represented by one byte.
Do you know why in UTF encoding first 127 encodes to a single byte, but remaining characters encode to a variable number of bytes?
Since the simpliest encondings is ASCII and it takes those first 127 characters of the Unicode set into its single byte, the UTF-8 also does the same for ASCII compatibility, but the rest characters then takes other amount of memory.