Streams in .NET
The .NET Framework provides a rich set of classes for performing operations on various types of streams. Stream is the main class, an abstract class from which all other stream-related classes derive. Since a stream is an abstraction of data as a sequence of bytes, to manipulate these sequences of bytes you have to perform some basic operations such as reading, writing, or seeking. With the Stream class you can perform binary I/O operations on a stream. With TextReader and TextWriter you can perform character I/O operations, whereas with BinaryReader and BinaryWriter you can perform I/O operations on primitive types.
Synchronous and Asynchronous I/O
There are two ways to perform operations on a stream, synchronous or asynchronous-your choice will depend upon the requirements of your application. As we will see in a moment, the Stream class provides methods for both synchronous and asynchronous operations, but first let's discuss some of the advantages and disadvantages of each type of operation.
Synchronous I/O
By default, all operations on streams are performed synchronously-this is the simplest way to perform I/O operations. The disadvantage of synchronous I/O is that it blocks processing until the I/O operation is complete-then the application is allowed to continue processing.
Synchronous I/O is useful for performing operations on small sized files, but with large files the application may give poor performance as it blocks the current thread of execution. Synchronous I/O is not suitable for performing operations over a network where you have little control over the time required to complete the operation. Therefore, synchronous I/O is not a good choice for passing huge streams on a network with low bandwidth or speed. By threading synchronous methods, you can simulate asynchronous I/O.
Asynchronous I/O
In asynchronous I/O, other tasks can be performed while the I/O operation is being completed. When the I/O operation completes, the operating system notifies the caller. Therefore, a separate notification mechanism is required for asynchronous I/O.
This method is useful when an application needs to continue performing other tasks while processing large amounts of data from a stream, or needs to work with slow devices whose rate of access would otherwise slow down the application.
In asynchronous I/O a separate thread is created for each I/O request-this can lead to an extra overhead for the operating system.
The Stream Class
The Stream class in the System.IO namespace is the base class for all the other stream classes, and provides the functionality for performing fundamental operations on stream. If you understand the functionality of the Stream class, then you can easily understand the other derived classes too-the key thing is to learn how to create the different types of stream with each class. In short, all the Stream-derived classes represent various types of stream with common or derived methods from the Stream class, and some extra methods for performing operations on that specific type of stream.
The diagram below illustrates Stream, the derived classes, and various other classes provided for performing operations on stream.
Each derived class is characterized according to its underlying device or backing store. For example, the FileStream class uses files as a backing store for streams, whereas the NetworkStream does not have any backing storage-this class is specially created for transferring streams across a network.
In the table below, some of the main classes derived from Stream are shown, along with a description of their purpose:
Class
Purpose
FileStream
Uses files as backing storage. The most widely used Stream class.
BufferedStream
Uses a buffer as backing storage. Used as intermediate storage for improving performance.
MemoryStream
Uses memory as backing storage and performs faster I/O operations when compared to other streams.
NetworkStream
Does not have any backing storage. Used with other streams for transferring data across a network.
CryptoStream
CryptoStream is used with other stream classes for performing encryption-decryption on streams.
Now let's have a look at the Stream class's members.
Stream Members
Let's start with the public properties-first of all, we have three public properties that tell us if a stream supports a particular feature or not, which can be determined when the stream is created through the relevant constructor:
Property
Description
CanRead
This property is used to check whether the current stream supports reading or not. In general, this property is used before performing any reading operation on a stream. This property returns true if the stream supports reading, otherwise false. A NotSupportedException is thrown if the stream does not support reading and an attempt is made to read from it.
CanSeek
Seeking is used to set the position within the current stream. CanSeek checks whether the current stream supports seeking or not. It returns true if the stream supports seeking, otherwise false. A stream's ability to seek depends on its backing store-media such as a disk or memory will generally allow seeking, but streams without any backing store such as network streams will always returns false. A NotSupportedException is thrown if seeking is attempted on a stream that does not support it.
CanWrite
CanWrite is used before doing any write operation on the current stream to check whether it supports writing or not. This property returns true if stream supports writing, otherwise false. Attempting to write to a stream that does not support writing throws a NotSupportedException.
There are two further properties to determine the size of the stream and the current position within the stream:
Property
Description
Length
This read-only property returns a long value representing the length of the stream in bytes. This property can be used for checking the end of the stream, or for determining the size of an array of bytes-a buffer used for storing the stream.
Position
Position can be used to set or to get the current position within the stream -used for moving around within a stream. To use this property, the stream must support seeking, which can be checked using the CanSeek property discussed above.
The Stream class has a number of methods for moving through the stream, reading and writing from the stream, and managing the stream.
Let's look first at the Seek() method for moving through the stream. Seek() is used for setting the position within stream. This method provides random access to the stream and is generally used while modifying or reading specific contents of stream. Seek() takes a long value and a value from the SeekOrigin enumeration. The long value specifies the offset from reference point specified by SeekOrigin value-this can be Begin, Current, or End, representing the beginning of the stream, the current position, or the end of the stream respectively. (Seek() is the method actually used when you set the Position property.)
The methods for reading to and writing from streams fall into two categories-there are corresponding sets of methods for synchronous and asynchronous operations.
Here are the synchronous methods:
Method
Description
Read()
ReadByte()
Read() and ReadByte() are used for performing synchronous reading from a stream. Read() reads a specified number of bytes and advances the position within the stream by the number of bytes read, whereas ReadByte() reads a single byte from the stream and advances the current position within the stream by one byte.
Note that Read() returns 0 at the end of stream whereas ReadByte() returns -1.
Write()
WriteByte()
These methods are used to perform synchronous writing to a stream. Write() writes a sequences of bytes in a stream and advances the current position within the stream by the number of bytes written, whereas WriteByte() writes a single byte at the current position within a stream, advancing the position by a single byte.
and here are the asynchronous methods:
Method
Description
BeginRead()
BeginWrite()
BeginRead() and BeginWrite() are the methods through which you can perform asynchronous I/O operations.
Both methods take five parameters-a byte array buffer that data is read from or written to, an integer offset which indicates the starting position for reading or writing the data, and an integer count specifying the maximum number of bytes to read or write. The fourth parameter is an optional AsyncCallback delegate, called when the read or write operation is complete. The fifth and last parameter is a user-provided object to distinguish this particular read or write request from the other requests.
Both methods return an IAsyncResult interface that represents the status of an asynchronous operation.
EndRead()
EndWrite()
These methods are used for completing asynchronous I/O operations, waiting for the asynchronous operations to finish.
Finally, let's look at methods for managing a stream:
Method
Description
Flush()
Flush() clears all buffers and moves information to its destination depending upon the state of the Stream object.
Close()
This method is used to free resources such as file handlers and sockets associated with stream. This method automatically flushes any stored data so there is no need to call Flush() before a Close() method. The underlying mechanism for closing stream is different for each stream type-in FileStream it releases all file resources whereas in Networkstream it closes the underlying socket. It's advisable to put a Close() method into a finally block to ensure that the stream is closed regardless of any exceptions thrown.
SetLength
This method is used to set the length of the current stream. Note that if the specified value is less than the current length of the stream then the stream is chopped down. If the specified value is greater than the current length of the stream the stream will expand.
A stream must support both writing and seeking for SetLength()-this can be checked with the CanWrite and CanSeek properties.
When looking at the classes derived from Stream, we won't be discussing these methods any further unless they have meaning specific to that type of stream.
Our first Stream example will be the FileStream class. Once you've seen how to implement the members we've just looked at with the FileStream class, it's a short stretch to understand how to apply them to the other Stream-derived classes.
The FileStream Class
The FileStream class is useful for performing I/O operations on files, and as such, is one of the most important and widely used of all the Stream-derived classes.
With the FileStream class you can perform operations not only on files but also on operating system file handles such as standard input and output devices. You can use this class with other derived classes for creating temporary files-you can serialize objects to a binary file, for example, and then convert them back whenever required. While transferring the file across a network, you can stream the file contents using this class on the server-side and on the client-side you can recreate and store the retrieved stream back into the original file format.
The FileStream constructor allows several ways to create a FileStream object-but they all involve specifying either a string for the file path or a file handle that can be used for physical devices that support streaming.
Creating a FileStream Instance with a File Path
Various methods for creating a FileStream instance by specifying a path to a file are described below. A default buffer of 8192 bytes is allocated to the stream when it is created. This can be changed with one of the constructor overloads we'll meet in a moment.
Specifying the File Path and Mode
By specifying a string representing the path to a file and a value from the FileMode enumeration you can create a FileStream object. The FileMode parameter describes how to open a specified file, and its values are given below:
FileMode Value
Description
Append
Opens or creates a new file for appending data to-the file cannot be used for reading, and the file pointer is set to the end of the file.
Create
Creates a new file, overwriting if the file already exists.
CreateNew
Creates a new file, throwing an exception if the file already exists.
Open
Opens an already existing file, throwing an exception if the file does not exist.
OpenOrCreate
If the file specified exists it is opened, or else a new file is created.
Truncate
Opens a file and deletes its contents, setting the file pointer to the beginning of the file.
Thus to create a FileStream object that creates a new file called C:\Networking\MyStream.txt you would use the following:
// Using file path and file mode
FileStream inF = new FileStream("C:\\Networking\\MyStream.txt",
FileMode.CreateNew);
Specifying File Access
You can create an instance of FileStream by providing additional file access parameters from the FileAccess enumeration. This enumeration allows you to restrict the user to specific operations on the stream.
FileAccess Value
Description
Read
Allow read-only access to the file
Write
Allow write-only access to the file
ReadWrite
Allow both read and write access to the file
The CanRead and CanWrite properties discussed earlier can be used to check the FileAccess permission given to the file.
Thus to create a FileStream object that creates a new file called C:\Networking\MyStream.txt with write-only access you would use the following:
// Using file path, file mode and file access
FileStream inF = new FileStream("C:\\Networking\\MyStream.txt",
FileMode.Open, FileAccess.Write);
Specifying Sharing Permissions
With the addition of sharing permissions you can control access to other stream objects. This is useful when a file is shared between two or more processes. The sharing permissions are determined by a value from the FileShare enumeration that represents various access modes.
FileShare Value
Description
Inheritable
Child process can inherit the file handle. Not supported by Win32.
None
No process (including the current one) can access the file-thus the file cannot be shared.
Read
Gives read-only permission to the current process and other processes.
Write
Gives write-only permission to the current process and other processes.
ReadWrite
Grants read and write permission to the current process and other processes.
// Using file path, file mode, file access and sharing permission
// Open file for writing, other processes will get read-only access
FileStream inF = new FileStream("C:\\Networking\\MyStream.txt ",
FileMode.Open, FileAccess.Write, FileShare.Read),
Specifying Buffer Size
You can also create a FileStream instance by specifying the size of the buffer in addition to the parameters discussed above. Here we set the size of the buffer to 1000 bytes:
//Using path, mode, access, sharing permission and buffer size.
FileStream outF = new FileStream("C:\\Networking\\MyStream.txt",
FileMode.Open, FileAccess.Write,
FileShare.Read, 1000);
Specifying Synchronous or Asynchronous State
With a further Boolean value we can specify whether to use asynchronous or synchronous I/O operations on the stream-a value of true indicates asynchronous.
//Using path, mode, access, sharing permission, buffer size and
// specifying asynchronous operations
FileStream outF = new FileStream("C:\\Networking\\MyStream.txt",
FileMode.Open, FileAccess.Write,
FileShare.Read, 1000, true);
It's also possible to create a FileStream object using a file handle rather than passing a path to a file. A file handle is a unique identifier that the operating system assigns to a file when the file is opened or created. A file handle is represented using the IntPtr structure, which represents an integer of platform-specific length.
Creating a FileStream object with a file handle requires you to specify at least the file handle and the FileAccess. A further overload allows you to define the ownership of the stream-a value of true means the FileStream instance gets exclusive control. With further parameters you can specify the size of the buffer (the default size is still 8192 bytes).
You can obtain a file handle from a FileStream object by using its Handle property:
//Create FileStream instance
FileStream inF = new FileStream("C:\\Networking\\MyStream.txt", FileMode.Open);
//Get the file handle
IntPtr fHandle = inF.Handle;
Reading and Writing with the FileStream
We've spent enough time talking about the methods and properties of the Stream-derived classes that we can use, so let's actually get on with reading from and writing to a FileStream. We'll take a look at both synchronous and asynchronous modes of operation.
Synchronous I/O
Stream provides Read() and Write() methods for performing synchronous read/write operations on a stream.
The following example performs synchronous I/O operations on a file. It also uses the Seek() method to set the position within the stream.
We begin by adding the System.IO namespace for I/O operations, and the System.Text namespace for the methods to convert strings to byte arrays-we'll look more at these later in the chapter.
using System;
using System.IO;
using System.Text;
class SyncIO
{
public static void Main(string[] args)
{
We create a FileStream instance, specifying its FileMode as OpenOrCreate. This will open our file if it exists, otherwise a new file is created. When you first run this example, a file SyncDemo.txt is created in the same folder as the executable.
// Create FileStream instance
FileStream syncF = new FileStream("SyncDemo.txt",FileMode.OpenOrCreate);
Now we are ready to examine various synchronous methods. Let's start with WriteByte(). In the example a character is converted to a byte and then written to a file by using the WriteByte() method. After writing a byte the file position is automatically incremented by one.
syncF.WriteByte(Convert.ToByte('A'));
By using the Write() method we can write more than one character to the file. Here we convert a string to a byte array (with the GetBytes() method of the Encoding class in the System.Text namespace), and this byte array is then written to the file with the Write() method. Write() takes three parameters, the first being the byte array to write, the position or offset in the array from where to start writing, and the length of data to be written-in this case we write the entire byte array from the start:
Console.WriteLine("--Write method demo--");
byte[] writeBytes = Encoding.ASCII.GetBytes(" is a first character.");
syncF.Write(writeBytes, 0, writeBytes.Length);
Thus we have written a single byte with the WriteByte() method, and an entire string (converted to a byte array) with the Write() method. Now we'll read this information back with the corresponding Read() methods.
When you perform read or write operations on a FileStream, the current position (or pointer) of the file automatically increases by the number of bytes read or written. We want to read the bytes just written, so we'll use the Seek() method to set the current position in the stream, in this case back to the beginning of the stream:
// Set pointer at origin
syncF.Seek (0,SeekOrigin.Begin);
Now we can read-we'll read a single byte with the ReadByte() method first of all. A byte is read from the stream (with the position automatically increased by one) and converted back into a char:
Console.WriteLine ("--Readbyte method demo--");
// Read byte and display
Console.WriteLine("First character is ->" +
Convert.ToChar(syncF.ReadByte()));
Now we read the remainder of the file with the Read() method. This method takes three parameters- a byte array which will store the data read, the position or offset in array from where to start reading, and the number of bytes to read. Here we'll read in all the remaining bytes in the file-we're at position 1 in the file, so we want to read in syncF.Length − 1 bytes:
// Use of Read method
Console.WriteLine("----Read method demo----");
// Allocate buffer
byte[] readBuf = new byte[syncF.Length−1];
// Read file
syncF.Read(readBuf,0,(Convert.ToInt32(syncF.Length))−1);
The byte array of data just read in is converted to a string with the Get String() method of the Encoding class and displayed:
// Display contents
Console.WriteLine("The rest of the file is : " +
Encoding.ASCII.GetString(readBuf));
}
}
The output of this code is the following:
Asynchronous I/O
One of the overloads for the FileStream constructor provides the IsAsync flag that defines synchronous or asynchronous state. The FileStream opens asynchronously when you pass true to this flag. Note that the operating system must support asynchronous I/O operation or else it works as synchronous I/O-Windows NT, 2000, and XP support both synchronous and asynchronous I/O.
Asynchronous I/O is a little bit more complex than synchronous I/O. We'll only sketch out the basics of how to implement asynchronous operations here-we'll look at them in more depth in Chapter 4.
A special callback mechanism is needed to implement asynchronous I/O, and an AsyncCallback delegate provides a way for client applications to implement this callback mechanism. This callback delegate is supplied to the BeginRead() or BeginWrite() method.
We'll look at an example for asynchrono us reading. We begin with the usual namespaces, and some static fields to hold a FileStream object and byte array:
using System;
using System.IO;
using System.Text;
using Sys tern.Threading;
public class AsyncDemo
{
// Stream object for reading
static FileStream fileStm;
// Buffer to read
static byte[] readBuf;
We declare an AsyncCallback delegate field for the callback function:
// Async-Call-back delegate
static AsyncCallback Callback;
In the Main() method, we initialize our callback delegate to point to the CallBackFunction() method-this is the method that will be called when the end of the asynchronous read operation is signaled. We'll see more about this process in Chapter 4.
public static void Main(String[] args)
{
Callback = new AsyncCallback(CallBackFunction);
Now we can initialize our FileStream object, specifying asynchronous operations with the final true value:
fileStm = new FileStream(@"C:\Networking\Streams\Test.txt",
FileMode.Open, FileAccess.Read,
FileShare.Read, 64, true);
readBuf= new byte[fileStm.Length];
Now we can use the BeginRead() method to initiate asynchronous read operations on the stream. The callback delegate is passed to the BeginRead() method as its penultimate parameter.
// Call async read
fileStm.BeginRead(readBuf, 0, readBuf.Length, Callback, null);
Data will be read from our FileStream while we continue with other activities-here we'll simply give the appearance of doing some other work by looping, and every so often putting the main thread to sleep. Once the loop has finished, the FileStream is closed.
// Simulation of main execution
for (long i = 0; i < 5000; i++)
{
if (i % 1000 == 0)
{
Console.WriteLine("Executing in Main - " + i.ToString());
Thread.Sleep(10);
}
}
fileStm.Close();
}
Note that if your loop completes before reading has finished then the FileStream is closed anyway- the main thread of our program isn't waiting for BeginRead() to finish. In Chapter 4 we'll look at an example that waits for an asynchronous operation to finish before continuing with the next activity- this can be very important if you need to perform asynchronous operations in a particular order or rely upon one operation completing before you can start the next one.
The callback function, suitably named CallBackFunction() here, is called when the buffer is full. EndRead() is called to complete the asynchronous read. If the end of the file hasn't been reached, BeginRead() is called again, to continue reading, and the contents of our current read are displayed:
static void CallBackFunction(IAsyncResult asyncResult)
{
// Gets called when the buffer is full.
int readB = fileStm.EndRead(asyncResult);
if (readB > 0)
{
fileStm.BeginRead(readBuf, 0, readBuf.Length, Callback, null);
Console.WriteLine(Encoding.ASCII.GetString(readBuf, 0, readB));
}
}
}
And here is the typical output of this example-note that your processor speed, file and buffer size may give you different results:
The BufferedStream Class
A buffer is a reserved area of memory used for storing temporary data, and its main purpose is to improve I/O performance, and it is often used to synchronize data transfer between devices of different speeds. Many online media applications use buffers as intermediate storage. Devices such as printers have their own buffers for storing the data.
In .NET you can implement buffering through the BufferedStream class. A BufferedStream objects wraps another Stream object. The BufferedStream is generally used with NetworkStream to store data in memory. The FileStream already has its own internal buffer and a MemoryStream doesn't require buffering.
A default buffer of 4096 bytes is allocated when you create a buffer with BufferedStream, but you can also specify a custom size for buffer through one of the many constructor overloads.
The following example shows a method for reading a buffered stream. This method takes a Stream object parameter, wraps it in a BufferedStream and performs a read operation. In the same way you can perform other operations on BufferedStream.
// Reading BufferedStream
public static void readBufStream(Stream st)
{
// Compose BufferedStream
BufferedStream bf = new BufferedStream(st);
byte[] inData = new Byte[st.Length];
// Read and display buffered data
bf.Read(inData, 0, Convert.ToInt32(st.Length));
Console.WriteLine(Encoding.ASCII.GetString(inData));
}
The MemoryStream Class
There are situations where an application needs data frequently, such as a lookup table for reference data. In such cases, storing the data in a file can cause delays and reduce the performance of the application. MemoryStream is the solution for such cases where data needs to be stored in memory.
MemoryStream is useful for fast temporary storage. A good example is transferring serialized objects within a process-you can use MemoryStream for temporarily storing serialized objects. This gives better performance than using the FileStream or BufferedStream classes.
Creating a MemoryStream object is quite different from FileStream or BufferedStream. An instance can be created in several ways.
The following example shows how to create and use a MemoryStream-we use the WriteTo() method to write the entire memory stream to a file.
using System;
using System.IO;
using System.Text;
public class memStreamDemoClass
{
public static void Main(String[] args)
{
The MemoryStream instance is created without passing any parameter. Reading and writing data in the MemoryStream is the same as we saw in the FileStream example. Here we are using the Write() method for writing a simple string.
// Create empty Memory stream
MemoryStream mS = new MemoryStream();
byte[] memData = Encoding.ASCII.GetBytes("This will go in Memory!!");
// Write data
mS.Write(memData, 0 , memData.Length);
After writing a string, we'll read it using the Read() method. Note that before reading we set the current position to zero.
// Set pointer at origin
mS.Position = 0;
byte[] inData = new byte [100];
// Read memory
mS.Read(inData, 0, 100);
Console.WriteLine(Encoding.ASCII.GetString(inData));
The WriteTo() method is used to write the entire contents of this memory stream to the file stream.
Stream strm = new FileStream("C:\\Networking\Streams\MemOutput.txt",
FileMode.OpenOrCreate, FileAccess.Write);
mS.WriteTo(strm);
}
}
The NetworkStream Class
Across a network, the data transferred between locations is in the form of a continuous flow or stream. For handling such streams, .NET has a special class-NetworkStream in the System.Net.Sockets namespace, which is used for sending and receiving data through network sockets.
NetworkStream is an unbuffered stream and does not support random access to data-you cannot change the position within the stream and therefore the use of Seek() and Position throws an exception. The CanSeek property always returns false for a NetworkStream object.
As NetworkStream is unbuffered, BufferedStream is usually used along with this class as an intermediate storage medium.
Some of the important members of NetworkStream are described below:
Property
Description
DataAvailable
Returns a Boolean value indicating whether data is available in the stream for reading or not. A value of true indicates that data is available in the stream.
Readable
Used to get or set a Boolean value indicating whether read access is given to the stream or not. This property works in the same way as the CanRead property in other streams.
Socket
Returns the underlying Socket.
Writeable
Used for checking whether the stream can be written to or not. A value of true indicates that the stream is writeable. This property works in the same way as the CanWrite property in other streams.
Each NetworkStream constructor requires at least a Socket-in addition you can specify a Boolean value indicating ownership of a stream and/or a value from the FileAccess enumeration we saw earlier to control read and write permissions. Setting the ownership flag to true gives control of the socket to the NetworkStream object, and by using the Close() method you can close the underlying socket.
A NetworkStream can also be retrieved from a TcpClient-we'll spend a whole chapter on TCP in Chapter 5, but we'll make use of it here to illustrate NetworkStream in a client-server scenario. The TcpClient.Get Stream() method creates a NetworkStream object, passing in its underlying Socket as the constructor parameter.
Let's look at the code for a simple TCP listener using a NetworkStream-the TCP classes are found in the System.Net.Sockets namespace.
using System;
using System.IO;
using System.Text;
using System.Net;
using System.Net.Sockets;
class TCPListenerDemo
{
public static void Main()
{
try
{
The first thing we do is create our TcpListener object to listen on port 5001 here, and start listening with the Start() method. AcceptClient() accepts a connection request, returning a TcpClient. We use its GetStream() method to create our NetworkStream object:
// Create TCP listener
TcpListener listener = new TcpListener(5001);
listener.Start();
TcpClient tc = listener.AcceptClient();
NetworkStream stm = tc.GetStream();
Now we can read data as we have done with the other streams, using the Read() method:
byte[] readBuf = new byte[100];
stm.Read(readBuf,0,100);
//Display Data
Console.WriteLine(Encoding.ASCII.GetString(readBuf));
stm.Close();
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
}
}
To use this example you need a client application for sending some data. Remember to start the listener application before the client application!
Here's the code for the TCP client:
using System;
using System.IO;
using System.Text;
using System.Net;
using System.Net.Sockets;
class TcpClient Example
{
static void Main(string[] args)
{
try
{
We create our TcpClient, and connect to the localhost on port 5001. Once again, we use the GetStream() method to return the underlying NetworkStream:
// Create TCP Client
TcpClient client = new TcpClient();
//Connect using hostname and port
client.Connect ("localhost", 5001);
//Get NetworkStream instance for sending data
NetworkStream stm = client.GetStream();
Now we have our NetworkStream, sending the data is the same process as we have used with the other streams:
byte[] sendBytes = Encoding.ASCII.GetBytes("This data has come from" +
" another place!!!");
stm.Write (sendBytes, 0, sendBytes.Length);
Finally, our TcpClient is closed:
client.Close();
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
Console.WriteLine("The listener has probably not started");
}
}
}
We'll see many more examples of creating client-server applications of differing complexity over the coming chapters.
The CryptoStream Class
For certain types of data, the need to secure its contents is a major consideration when transforming or storing it-the need for securing data is increasing day by day. To make data secure it is generally encrypted with some secret key into an unreadable form. To get back the original contents it is decrypted with a secret key. The secret key used for decryption may be the same as that used for encryption or different depending on the encryption algorithm.
.NET provides the CryptoStream class that links streams to cryptographic transformations. CryptoStream is not actually in the System.IO namespace, but does indeed derive from Stream. The CryptoStream class can be used to perform cryptographic operations on a Stream object.
The CryptoStream constructor takes three parameters-the first is the stream to be used, the second is the cryptographic transformation, and the third parameter specifies read or write access to the cryptographic stream.
We have a variety of cryptographic transformations at our disposal-any cryptographic service provider that implements the ICryptoTransform interface can be used. The following example demonstrates the use of various cryptographic providers-these reside in the System.Security.Cryptography namespace.
using System;
using System.IO;
using System.Text;
using System.Security.Cryptography;
public class crypt
{
public static void Main()
{
First of all, we'll ask the user to choose a service provider. All service providers are derived from the SymmetricAlgorithm class that has a single secret key that is used for both encryption and decryption.
Console.WriteLine("Select Service Provider for CryptoStream");
Console.WriteLine("1 = DESCryptoServiceProvider");
Console.WriteLine{"2 = RC2CryptoServiceProvider");
Console.WriteLine("3 = RijndaelManaged");
Console.WriteLine("4 = TripleBESCryptoServiceProvider");
Console.WriteLine("5 = SymmetricAlgorithm");
// Create des object
SymmetricAlgorithm des = null;
switch (Console.ReadLine())
{
case "1":
des = new DESCryptoServiceProvider();
break;
case "2":
des = new RC2CryptoServiceProvider();
break;
case "3":
des = new RijndaelManaged();
break;
case "4":
des = new TripleDESCryptoServiceProvider();
break;
case "5":
des= SymmetricAlgorithm.Create(); //uses default algorithm
break;
default:
Console.WriteLine ("Wrong selection");
return;
}
A FileStream object is created to save the encrypted data, around which we will wrap our CryptoStream object. The ICryptoTransform interface helps to define the basic operations of the cryptographic transformation that is created using the CreateEncryptor method of the SymmetricAlgorithm class.
FileStream fs = new FileStream("SecretFile.dat", FileMode.Create,
FileAccess.Write);
ICryptoTransform desencrypt = des.CreateEncryptor();
CryptoStream cryptostream = new CryptoStream(fs, desencrypt,
CryptoStreamMode.Write);
Now we encrypt our message. We'll use a simple string, which we convert to a byte array with the GetBytes() method of the Encoding class of the System.Text namespace-we'll talk more about this class later in the chapter. Once we have our byte array, it is written to the CryptoStream with its Write() method.
string theMessage = "A top secret message";
byte[] bytearrayinput = Encoding.Unicode.GetBytes(theMessage);
Console.WriteLine("Original Message : {0} ", theMessage);
cryptostream.Write(bytearrayinput, 0, bytearrayinput.Length);
cryptostream.Close();
fs.Close();
After closing our streams, we can proceed to decrypt our message-in the second part of our code the encrypted message is read from the file and then converted back into the original.
/***********Time to Decrypt...************/
// Create file stream to read encrypted file back
FileStream fsread = new FileStream("SecretFile.dat", FileMode.Open,
FileAccess.ReadWrite);
byte [] encByte = new byte[fsread.Length ];
fsread.Read(encByte,0,encByte.Length);
Here we have defined the size of our byte array by using the Length property of the FileStream to determine the length of the data written to our file. We read the data into this byte array with the Read() method. Before we decrypt, we display the encrypted message to the console, and then set the Position within the FileStream back to zero before continuing.
Console.WriteLine ("Encrypted Message : " +
Encoding.ASCII.GetString(encByte));
fsread.Position = 0;
Decrypting the data uses a similar procedure to that of encrypting the data. A main difference is the CreateDecryptor() method, which is used to create the specified decryptor object. We create a new byte array into which we read from the CryptoStream, and then use the GetString() method of the Encoding class to turn the byte array into a string for displaying.
// Create DES Decryptor from our des instance
ICryptoTransform desdecrypt = des.CreateDecryptor();
CryptoStream cryptostreamDecr = CryptoStream(fsread, desdecrypt,
CryptoStreamMode.Read);
byte[] decrByte = new byte[fsread.Length];
cryptostreamDecr.Read(decrByte,0,(int)fsread.Length);
string output = Encoding.Unicode.GetString(decrByte);
Console.WriteLine("Decrypted Message : {0}" ,output);
cryptostreamDecr.Close();
fsread.Close();
}
}
The screenshot below shows the output of the above code when it is run twice with the Symmetric Algorithm encryption method: