At some stage in your development cycle, you need to store data on some persistent media so that when the computer is restarted the data is still be available. In most cases, you either store the data in a database or in files. A file is basically a sequence of characters stored on storage media such as your hard disks, thumb drives, and so on. When you talk about files, you need to understand another associated term — streams. A stream is a channel in which data is passed from one point to another. In .NET, streams are divided into various types: file streams for files held on permanent storage, network streams for data transferred across the network, memory streams for data stored in internal storage, and so forth.
With streams, you can perform a wide range of tasks, including compressing and decompressing data, serializing and deserializing data, and encrypting and decrypting data. This chapter examines:
How to quickly read and write data to files
The concepts of streams
Using the BufferedStream
class to improve the performance of applications reading from a stream
Using the FileStream
class to read and write to files
Using the MemoryStream
class to use the internal memory store as a buffer
Using the NetworkStream
class for network programming
The various types of cryptographic classes available in .NET
Performing compressions and decompression on streams
Serializing and deserializing objects into binary and XML data
The System.IO
namespace in the .NET Framework contains a wealth of classes that allow synchronous and asynchronous reading and writing of data on streams and files. In the following sections, you will explore the various classes for dealing with files and directories.
Remember to import the System.IO
namespace when using the various classes in the System.IO
namespace.
The .NET Framework class library provides two classes for manipulating directories:
DirectoryInfo
class
Directory
class
The DirectoryInfo
class exposes instance methods for dealing with directories while the Directory
class exposes static methods.
The DirectoryInfo
class provides various instance methods and properties for creating, deleting, and manipulating directories. The following table describes some of the common methods you can use to programmatically manipulate directories.
Method | Description |
---|---|
| Creates a directory. |
| Creates a subdirectory. |
| Deletes a directory. |
| Gets the subdirectories of the current directory. |
| Gets the file list from a directory. |
And here are some of the common properties:
Properties | Description |
---|---|
| Indicates if a directory exists. |
| Gets the parent of the current directory. |
| Gets the full path name of the directory. |
| Gets or sets the creation time of current directory. |
Refer to the MSDN documentation for a full list of methods and properties.
To see how to use the DirectoryInfo
class, consider the following example:
static void Main(string[] args) { string path = @"C:My Folder"; DirectoryInfo di = new DirectoryInfo(path); try { //---if directory does not exists--- if (!di.Exists) { //---create the directory--- di.Create(); //---c:My Folder--- //---creates subdirectories--- di.CreateSubdirectory("Subdir1"); //---c:My FolderSubdir1--- di.CreateSubdirectory("Subdir2"); //---c:My FolderSubdir2--- } //---print out some info about the directory--- Console.WriteLine(di.FullName); Console.WriteLine(di.CreationTime); //---get and print all the subdirectories--- DirectoryInfo[] subDirs = di.GetDirectories(); foreach (DirectoryInfo subDir in subDirs) Console.WriteLine(subDir.FullName); //---get the parent of C:My folder--- DirectoryInfo parent = di.Parent; if (parent.Exists) { //---prints out C:--- Console.WriteLine(parent.FullName); } //---creates C:My FolderSubdir3--- DirectoryInfo newlyCreatedFolder = di.CreateSubdirectory("Subdir3"); //---deletes C:My FolderSubdir3--- newlyCreatedFolder.Delete(); } catch (IOException ex) { Console.WriteLine(ex.Message); } catch (Exception ex) { Console.WriteLine(ex.Message); } Console.ReadLine(); }
In this example, you first create an instance of the DirectoryInfo
class by instantiating it with a path (C:My Folder). You check if the path exists by using the Exist
property. If it does not exist, you create the folder (C:My Folder) and then create two subdirectories underneath it (Subdir1 and Subdir2).
Next, you print out the full pathname (using the FullName
property) of the folder and its creation date (using the CreationTime
property). You then get all the subdirectories under C:My Folder and display their full pathnames. You can get the parent of the C:My Folder using the Parent
property.
Finally, you create a subdirectory named Subdir3 under C:My Folder and pass a reference to the newly created subdirectory to the newlyCreatedFolder
object. You then delete the folder, using the Delete()
method.
The Directory
class is similar to DirectoryInfo
class. The key difference between is that Directory
exposes static members instead of instance members. The Directory
class also exposes only methods — no properties. Some of the commonly used methods are described in the following table.
Method | Description |
---|---|
| Creates a subdirectory. |
| Deletes a specified directory. |
| Indicates if a specified path exists. |
| Gets the current working directory. |
| Gets the subdirectories of the specified path. |
| Gets the file list from a specified directory. |
| Sets the current working directory. |
Refer to the MSDN documentation for a full list of methods and properties.
Here's the previous program using the DirectoryInfo
class rewritten to use the Directory
class:
static void Main(string[] args) { string path = @"C:My Folder"; try { //---if directory does not exists--- if (!Directory.Exists(path)) { //---create the directory--- Directory.CreateDirectory(path); //---set the current directory to C:My Folder--- Directory.SetCurrentDirectory(path);
//---creates subdirectories--- //---c:My FolderSubdir1--- Directory.CreateDirectory("Subdir1"); //---c:My FolderSubdir2--- Directory.CreateDirectory("Subdir2"); } //---set the current directory to C:My Folder--- Directory.SetCurrentDirectory(path); //---print out some info about the directory--- Console.WriteLine(Directory.GetCurrentDirectory()); Console.WriteLine(Directory.GetCreationTime(path)); //---get and print all the subdirectories--- string[] subDirs = Directory.GetDirectories(path); foreach (string subDir in subDirs) Console.WriteLine(subDir); //---get the parent of C:My folder--- DirectoryInfo parent = Directory.GetParent(path); if (parent.Exists) { //---prints out C:--- Console.WriteLine(parent.FullName); } //---creates C:My FolderSubdir3--- Directory.CreateDirectory("Subdir3"); //---deletes C:My FolderSubdir3--- Directory.Delete("Subdir3"); } catch (IOException ex) { Console.WriteLine(ex.Message); } catch (Exception ex) { Console.WriteLine(ex.Message); } Console.ReadLine(); }
As you can see, most of the methods in the Directory
class require you to specify the directory you are working with. If you like to specify the directory path by using relative path names, you need to set the current working directory using the SetCurrentDirectory()
method; if not, the default current directory is always the location of your program. Also, notice that some methods (such as GetParent()
) still return DirectoryInfo
objects.
In general, if you are performing a lot of operations with directories, use the
DirectoryInfo
class. Once it is instantiated, the object has detailed information about the directory you are currently working on. In contrast, theDirectory
class is much simpler and is suitable if you are occasionally dealing with directories.
The .NET Framework class library contains two similar classes for dealing with files — FileInfo
and File
.
The File
class provides static methods for creating, deleting, and manipulating files, whereas the FileInfo
class exposes instance members for files manipulation.
Like the Directory
class, the File
class only exposes static methods and does not contain any properties.
Consider the following program, which creates, deletes, copies, renames, and sets attributes in files, using the File
class:
static void Main(string[] args) { string filePath = @"C: emp extfile.txt"; string fileCopyPath = @"C: emp extfile_copy.txt"; string newFileName = @"C: emp extfile_newcopy.txt"; try { //---if file already existed--- if (File.Exists(filePath)) { //---delete the file--- File.Delete(filePath); } //---create the file again--- FileStream fs = File.Create(filePath); fs.Close(); //---make a copy of the file--- File.Copy(filePath, fileCopyPath); //--rename the file--- File.Move(fileCopyPath, newFileName); //---display the creation time--- Console.WriteLine(File.GetCreationTime(newFileName)); //---make the file read-only and hidden--- File.SetAttributes(newFileName, FileAttributes.ReadOnly); File.SetAttributes(newFileName, FileAttributes.Hidden); } catch (IOException ex) { Console.WriteLine(ex.Message); } catch (Exception ex) { Console.WriteLine(ex.Message); } Console.ReadLine(); }
This program first checks to see if a file exists by using the Exists()
method. If the file exists, the program deletes it using the Delete()
method. It then proceeds to create the file by using the Create()
method, which returns a FileStream
object (more on this in subsequent sections). To make a copy of the file, you use the Copy()
method. The Move()
method moves a file from one location to another. Essentially, you can use the Move()
method to rename a file. Finally, the program sets the ReadOnly
and Hidden
attribute to the newly copied file.
In addition to the File
class, you have the FileInfo
class that provides instance members for dealing with files. Once you have created an instance of the FileInfo
class, you can use its members to obtain more information about a particular file. Figure 11-1 shows the different methods and properties exposed by an instance of the FileInfo
class, such as the Attributes
property, which retrieves the attributes of a file, the Delete()
method that allows you to delete a file, and so on.
The File
class contains four methods to write content to a file:
WriteAllText()
— Creates a file, writes a string to it, and closes the file
AppendAllText()
— Appends a string to an existing file
WriteAllLines()
— Creates a file, writes an array of string to it, and closes the file
WriteAllBytes()
— Creates a file, writes an array of byte to it, and closes the file
The following statements show how to use the various methods to write some content to a file:
string filePath = @"C: emp extfile.txt"; string strTextToWrite = "This is a string"; string[] strLinesToWrite = new string[] { "Line1", "Line2" }; byte[] bytesToWrite = ASCIIEncoding.ASCII.GetBytes("This is a string"); File.WriteAllText(filePath, strTextToWrite); File.AppendAllText(filePath, strTextToWrite); File.WriteAllLines(filePath, strLinesToWrite); File.WriteAllBytes(filePath,bytesToWrite);
The File
class also contains three methods to read contents from a file:
ReadAllText()
— Opens a file, reads all text in it into a string, and closes the file
ReadAllLines()
— Opens a file, reads all the text in it into a string array, and closes the file
ReadAllBytes()
— Opens a file, reads all the content in it into a byte array, and closes the file
The following statements show how to use the various methods to read contents from a file:
string filePath = @"C: emp extfile.txt"; string strTextToRead = (File.ReadAllText(filePath)); string[] strLinestoRead = File.ReadAllLines(filePath); byte[] bytesToRead = File.ReadAllBytes(filePath);
The beauty of these methods is that you need not worry about opening and closing the file after reading or writing to it; they close the file automatically after they are done.
When dealing with text files, you may also want to use the StreamReader
and StreamWriter
classes. StreamReader
is derived from the TextReader class, an abstract class that represents a reader that can read a sequential series of characters.
You'll see more about streams in the "The Stream Class" section later in this chapter.
The following code snippet uses the StreamReader
class to read lines from a text file:
try { using (StreamReader sr = new StreamReader(filePath)) { string line; while ((line = sr.ReadLine()) != null) { Console.WriteLine(line); } } } catch (Exception ex) { Console.WriteLine(ex.ToString()); }
In addition to the ReadLine()
method, the StreamReader
class supports the following methods:
Read()
— Reads the next character from the input stream
ReadBlock()
— Reads a maximum of specified characters
ReadToEnd()
— Reads from the current position to the end of the stream
The StreamWriter
class is derived from the abstract TextWriter
class and is used for writing characters to a stream. The following code snippet uses the StreamWriter
class to write lines to a text file:
try { using (StreamWriter sw = new StreamWriter(filePath)) { sw.Write("Hello, "); sw.WriteLine("World!"); } } catch (Exception ex) { Console.WriteLine(ex.ToString()); }
If you are dealing with binary files, you can use the BinaryReader
and BinaryWriter
classes. The following example reads binary data from one file and writes it into another, essentially making a copy of the file:
string filePath = @"C: empVS2008Pro.png"; string filePathCopy = @"C: empVS2008Pro_copy.png"; //---open files for reading and writing--- FileStream fs1 = File.OpenRead(filePath); FileStream fs2 = File.OpenWrite(filePathCopy); BinaryReader br = new BinaryReader(fs1); BinaryWriter bw = new BinaryWriter(fs2); //---read and write individual bytes--- for (int i = 0; i <= br.BaseStream.Length - 1; i++) bw.Write(br.ReadByte()); //---close the reader and writer--- br.Close(); bw.Close();
This program first uses the File
class to open two files — one for reading and one for writing. The BinaryReader
class is then used to read the binary data from the FileStream
, and the BinaryWriter
is used to write the binary data to the file.
The BinaryReader
class contains many different read methods for reading different types of data — Read(), Read7BitEncodedInt(), ReadBoolean(), ReadByte(), ReadBytes(), ReadChar(), ReadChars(), ReadDecimal(), ReadDouble(), ReadInt16(), ReadInt32(), ReadInt64(), ReadSByte(), ReadSingle(), ReadString(), ReadUInt16(), ReadUInt32()
, and ReadUInt64()
.
Now that you have seen how to use the various classes to manipulate files and directories, let's put them to good use by building a simple file explorer that displays all the subdirectories and files within a specified directory.
The following program contains the PrintFoldersinCurrentDirectory()
function, which recursively traverses a directory's subdirectories and prints out its contents:
class Program { static string path = @"C:Program FilesMicrosoft Visual Studio 9.0VC#"; static void Main(string[] args) { DirectoryInfo di = new DirectoryInfo(path); Console.WriteLine(di.FullName); PrintFoldersinCurrentDirectory(di, −1); Console.ReadLine(); } private static void PrintFoldersinCurrentDirectory( DirectoryInfo directory, int level) { level++; //---print all the subdirectories in the current directory--- foreach (DirectoryInfo subDir in directory.GetDirectories()) { for (int i = 0; i <= level * 3; i++) Console.Write(" "); Console.Write("|__"); //---display subdirectory name--- Console.WriteLine(subDir.Name); //---display all the files in the subdirectory--- FileInfo[] files = subDir.GetFiles(); foreach (FileInfo file in files) { //---display the spaces--- for (int i = 0; i <= (level+1) * 3; i++) Console.Write(" "); //---display filename--- Console.WriteLine("* " + file.Name); } //---explore its subdirectories recursively--- PrintFoldersinCurrentDirectory(subDir, level); } } }
Figure 11-2 shows the output of the program.
A stream is an abstraction of a sequence of bytes. The bytes may come from a file, a TCP/IP socket, or memory. In .NET, a stream is represented, aptly, by the Stream
class. The Stream
class provides a generic view of a sequence of bytes.
The Stream
class forms the base class of all other streams, and it is also implemented by the following classes:
BufferedStream
— Provides a buffering layer on another stream to improve performance
FileStream
— Provides a way to read and write files
MemoryStream
— Provides a stream using memory as the backing store
NetworkStream
— Provides a way to access data on the network
CryptoStream
— Provides a way to supply data for cryptographic transformation
Streams fundamentally involve the following operations:
Reading
Writing
Seeking
The Stream
class is defined in the System.IO
namespace. Remember to import that namespace when using the class.
The following code copies the content of one binary file and writes it into another using the Stream
class:
try { const int BUFFER_SIZE = 8192; byte[] buffer = new byte[BUFFER_SIZE]; int bytesRead; string filePath = @"C: empVS2008Pro.png"; string filePath_backup = @"C: empVS2008Pro_bak.png"; Stream s_in = File.OpenRead(filePath); Stream s_out = File.OpenWrite(filePath_backup); while ((bytesRead = s_in.Read(buffer, 0, BUFFER_SIZE)) > 0) { s_out.Write(buffer, 0, bytesRead); } s_in.Close(); s_out.Close(); } catch (Exception ex) { Console.WriteLine(ex.ToString()); }
This first opens a file for reading using the static OpenRead()
method from the File
class. In addition, it opens a file for writing using the static OpenWrite()
method. Both methods return a FileStream
object.
While the
OpenRead()
andOpenWrite()
methods return aFileStream
object, you can actually assign the returning type to aStream
object because theFileStream
object inherits from theStream
object.
To copy the content of one file into another, you use the Read()
method from the Stream
class and read the content from the file into an byte array. Read()
returns the number of bytes read from the stream (in this case the file) and returns 0 if there are no more bytes to read. The Write()
method of the Stream
class writes the data stored in the byte array into the stream (which in this case is another file). Finally, you close both the Stream
objects.
In addition to the Read()
and Write()
methods, the Stream
object supports the following methods:
ReadByte()
— Reads a byte from the stream and advances the position within the stream by one byte, or returns −1
if at the end of the stream
WriteByte()
— Writes a byte to the current position in the stream and advances the position within the stream by 1 byte
Seek()
— Sets the position within the current stream
The following example writes some text to a text file, closes the file, reopens the file, seeks to the fourth position in the file, and reads the next six bytes:
try { const int BUFFER_SIZE = 8192; string text = "The Stream class is defined in the System.IO namespace."; byte[] data = ASCIIEncoding.ASCII.GetBytes(text); byte[] buffer = new byte[BUFFER_SIZE]; string filePath = @"C: emp extfile.txt"; //---writes some text to file--- Stream s_out = File.OpenWrite(filePath); s_out.Write(data, 0, data.Length); s_out.Close(); //---opens the file for reading--- Stream s_in = File.OpenRead(filePath); //---seek to the fourth position--- s_in.Seek(4, SeekOrigin.Begin); //---read the next 6 bytes--- int bytesRead = s_in.Read(buffer, 0, 6); Console.WriteLine(ASCIIEncoding.ASCII.GetString(buffer, 0, bytesRead)); s_in.Close(); s_out.Close(); } catch (Exception ex) { Console.WriteLine(ex.ToString()); }
To improve its performance, the BufferedStream
class works with another Stream
object. For instance, the previous example used a buffer size of 8192 bytes when reading from a text file. However, that size might not be the ideal size to yield the optimum performance from your computer. You can use the BufferedStream
class to let the operating system determine the optimum buffer size for you. While you can still specify the buffer size to fill up your buffer when reading data, your buffer will now be filled by the BufferedStream
class instead of directly from the stream (which in the example is from a file). The BufferedStream
class fills up its internal memory store in the size that it determines is the most efficient.
The BufferedStream
class is ideal when you are manipulating large streams. The following shows how the previous example can be speeded up using the BufferedStream
class:
try { const int BUFFER_SIZE = 8192; byte[] buffer = new byte[BUFFER_SIZE]; int bytesRead; string filePath = @"C: empVS2008Pro.png"; string filePath_backup = @"C: empVS2008Pro_bak.png"; Stream s_in = File.OpenRead(filePath); Stream s_out = File.OpenWrite(filePath_backup); BufferedStream bs_in = new BufferedStream(s_in); BufferedStream bs_out = new BufferedStream(s_out); while ((bytesRead = bs_in.Read(buffer, 0, BUFFER_SIZE)) > 0) { bs_out.Write(buffer, 0, bytesRead); } bs_out.Flush(); bs_in.Close(); bs_out.Close(); } catch (Exception ex) { Console.WriteLine(ex.ToString()); }
You use a BufferedStream
object over a Stream
object, and all the reading and writing is then done via the BufferedStream
objects.
The FileStream
class is designed to work with files, and it supports both synchronous and asynchronous read and write operations. Earlier, you saw the use of the Stream
object to read and write to file. Here is the same example using the FileStream
class:
try { const int BUFFER_SIZE = 8192; byte[] buffer = new byte[BUFFER_SIZE]; int bytesRead; string filePath = @"C: empVS2008Pro.png"; string filePath_backup = @"C: empVS2008Pro_bak.png"; FileStream fs_in = File.OpenRead(filePath); FileStream fs_out = File.OpenWrite(filePath_backup);
while ((bytesRead = fs_in.Read(buffer, 0, BUFFER_SIZE)) > 0) { fs_out.Write(buffer, 0, bytesRead); } fs_in.Dispose(); fs_out.Dispose(); fs_in.Close(); fs_out.Close(); } catch (Exception ex) { Console.WriteLine(ex.ToString()); }
If the size of the file is large, this program will take a long time because it uses the blocking Read()
method. A better approach would be to use the asynchronous read methods BeginRead()
and EndRead()
.
BeginRead()
starts an asynchronous read from a FileStream
object. Every BeginRead()
method called must be paired with the EndRead()
method, which waits for the pending asynchronous read operation to complete. To read from the stream synchronously, you call the BeginRead()
method as usual by providing it with the buffer to read, the offset to begin reading, size of buffer, and a call back delegate to invoke when the read operation is completed. You can also provide a custom object to distinguish different asynchronous operations (for simplicity you just pass in null
here):
IAsyncResult result = fs_in.BeginRead(buffer, 0, BUFFER_SIZE, new AsyncCallback(readCompleted), null);
The following program shows how you can copy the content of a file into another asynchronously:
class Program { static FileStream fs_in; static FileStream fs_out; const int BUFFER_SIZE = 8192; static byte[] buffer = new byte[BUFFER_SIZE]; static void Main(string[] args) { try { string filePath = @"C: empVS2008Pro.png"; string filePath_backup = @"C: empVS2008Pro_bak.png"; //---open the files for reading and writing--- fs_in = File.OpenRead(filePath); fs_out = File.OpenWrite(filePath_backup); Console.WriteLine("Copying file...");
//---begin to read asynchronously--- IAsyncResult result = fs_in.BeginRead(buffer, 0, BUFFER_SIZE, new AsyncCallback(readCompleted), null); //---continue with the execution--- for (int i = 0; i < 100; i++) { Console.WriteLine("Continuing with the execution...{0}", i); System.Threading.Thread.Sleep(250); } } catch (Exception ex) { Console.WriteLine(ex.ToString()); } Console.ReadLine(); } //---when a block of data is read--- static void readCompleted(IAsyncResult result) { //---simulate slow reading--- System.Threading.Thread.Sleep(500); //---reads the data--- int bytesRead = fs_in.EndRead(result); //---writes to another file--- fs_out.Write(buffer, 0, bytesRead); if (bytesRead > 0) { //---continue reading--- result = fs_in.BeginRead(buffer, 0, BUFFER_SIZE, new AsyncCallback(readCompleted), null); } else { //---reading is done!--- fs_in.Dispose(); fs_out.Dispose(); fs_in.Close(); fs_out.Close(); Console.WriteLine("File copy done!"); } } }
Because the reading may happen so fast for a small file, you can insert Sleep()
statements to simulate reading a large file. Figure 11-3 shows the output.
Sometimes you need to manipulate data in memory without resorting to saving it in a file. A good example is the PictureBox
control in a Windows Form. For instance, you have a picture displayed in the PictureBox
control and want to send the picture to a remote server, say a Web Service. The PictureBox
control has a Save()
method that enables you to save the image to a Stream
object.
Instead of saving the image to a FileStream
object and then reloading the data from the file into a byte array, a much better way would be to use a MemoryStream
object, which uses the memory as a backing store (which is more efficient compared to performing file I/O; file I/O is relatively slower).
The following code shows how the image in the PictureBox
control is saved into a MemoryStream
object:
//---create a MemoryStream object--- MemoryStream ms1 = new MemoryStream(); //---save the image into a MemoryStream object--- pictureBox1.Image.Save(ms1, System.Drawing.Imaging.ImageFormat.Jpeg);
To extract the image stored in the MemoryStream
object and save it to a byte array, use the Read()
method of the MemoryStream
object:
//---read the data in ms1 and write to buffer--- ms1.Position = 0; byte[] buffer = new byte[ms1.Length]; int bytesRead = ms1.Read(buffer, 0, (int)ms1.Length);
With the data in the byte array, you can now proceed to send the data to the Web Service. To verify that the data stored in the byte array is really the image in the PictureBox
control, you can load it back to another MemoryStream
object and then display it in another PictureBox
control, like this:
//---read the data in buffer and write to ms2--- MemoryStream ms2 = new MemoryStream(); ms2.Write(buffer,0,bytesRead); //---load it in another PictureBox control--- pictureBox2.Image = new Bitmap(ms2);
The NetworkStream
class provides methods for sending and receiving data over Stream
sockets in blocking mode. Using the NetworkStream
class is more restrictive than using most other Stream implementations. For example, the CanSeek()
properties of the NetworkStream
class are not supported and always return false. Similarly, the Length()
and Position()
properties throw NotSupportedException
. It is not possible to perform a Seek()
operation, and the SetLength()
method also throws NotSupportedException
.
Despite these limitations, the NetworkStream
class has made network programming very easy and encapsulates much of the complexity of socket programming. Developers who are familiar with streams programming can use the NetworkStream
class with ease.
This section leads you through creating a pair of socket applications to illustrate how the NetworkStream
class works. The server will listen for incoming TCP clients and send back to the client whatever it receives.
The following code is for the server application:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Net; using System.Net.Sockets; namespace Server { class Program { const int PORT_NO = 5000; const string SERVER_IP = "127.0.0.1"; static void Main(string[] args) { //---listen at the specified IP and port no.--- IPAddress localAdd = IPAddress.Parse(SERVER_IP); TcpListener listener = new TcpListener(localAdd, PORT_NO); Console.WriteLine("Listening...");
listener.Start(); //---incoming client connected--- TcpClient client = listener.AcceptTcpClient(); //---get the incoming data through a network stream--- NetworkStream nwStream = client.GetStream(); byte[] buffer = new byte[client.ReceiveBufferSize]; //---read incoming stream--- int bytesRead = nwStream.Read(buffer, 0, client.ReceiveBufferSize); //---convert the data received into a string--- string dataReceived = Encoding.ASCII.GetString(buffer, 0, bytesRead); Console.WriteLine("Received : " + dataReceived); //---write back the text to the client--- Console.WriteLine("Sending back : " + dataReceived); nwStream.Write(buffer, 0, bytesRead); client.Close(); listener.Stop(); Console.ReadLine(); } } }
Basically, you use the TcpListener
class to listen for an incoming TCP connection. Once a connection is made, you use a NetworkStream
object to read data from the client, using the Read()
method as well as write data to the client by using the Write()
method.
For the client, you use the TcpClient
class to connect to the server using TCP and, as with the server, you use the NetworkStream
object to write and read data to and from the client:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Net; using System.Net.Sockets; namespace Client { class Program { const int PORT_NO = 5000; const string SERVER_IP = "127.0.0.1"; static void Main(string[] args) { //---data to send to the server--- string textToSend = DateTime.Now.ToString(); //---create a TCPClient object at the IP and port no.---
TcpClient client = new TcpClient(SERVER_IP, PORT_NO); NetworkStream nwStream = client.GetStream(); byte[] bytesToSend = ASCIIEncoding.ASCII.GetBytes(textToSend); //---send the text--- Console.WriteLine("Sending : " + textToSend); nwStream.Write(bytesToSend, 0, bytesToSend.Length); //---read back the text--- byte[] bytesToRead = new byte[client.ReceiveBufferSize]; int bytesRead = nwStream.Read(bytesToRead, 0, client.ReceiveBufferSize); Console.WriteLine("Received : " + Encoding.ASCII.GetString(bytesToRead, 0, bytesRead)); Console.ReadLine(); client.Close(); } } }
Figure 11-4 shows how the server and client look like when you run both applications.
The client-server applications built in the previous section can accept only a single client. A client connects and sends some data to the server; the server receives it, sends the data back to the client, and then exits. While this is a simple demonstration of a client-server application, it isn't a very practical application because typically a server should be able to handle multiple clients simultaneously and runs indefinitely. So let's look at how you can extend the previous server so that it can handle multiple clients simultaneously.
To do so, you can create a class named Client
and code it as follows:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Net.Sockets; namespace Server { class Client { //---create a TCPClient object--- TcpClient _client = null; //---for sending/receiving data--- byte[] buffer; //---called when a client has connected--- public Client(TcpClient client) { _client = client; //---start reading data asynchronously from the client--- buffer = new byte[_client.ReceiveBufferSize]; _client.GetStream().BeginRead( buffer, 0, _client.ReceiveBufferSize, receiveMessage, null); } public void receiveMessage(IAsyncResult ar) { int bytesRead; try { lock (_client.GetStream()) { //---read from client--- bytesRead = _client.GetStream().EndRead(ar); } //---if client has disconnected--- if (bytesRead < 1) return; else { //---get the message sent--- string messageReceived = ASCIIEncoding.ASCII.GetString(buffer, 0, bytesRead); Console.WriteLine("Received : " + messageReceived);
//---write back the text to the client--- Console.WriteLine("Sending back : " + messageReceived); byte[] dataToSend = ASCIIEncoding.ASCII.GetBytes(messageReceived); _client.GetStream().Write(dataToSend, 0, dataToSend.Length); } //---continue reading from client--- lock (_client.GetStream()) { _client.GetStream().BeginRead( buffer, 0, _client.ReceiveBufferSize, receiveMessage, null); } } catch (Exception ex) { Console.WriteLine(ex.ToString()); } } } }
Here, the constructor of the Client
class takes in a TcpClient
object and starts to read from it asynchronously using the receiveMessage()
method (via the BeginRead()
method of the NetworkStream
object). Once the incoming data is read, the constructor continues to wait for more data.
To ensure that the server supports multiple users, you use a TcpListener
class to listen for incoming client connections and then use an infinite loop to accept new connections. Once a client is connected, you create a new instance of the Client
object and continue waiting for the next client. So the Main()
function of your application now looks like this:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Net; using System.Net.Sockets; namespace Server { class Program { const int PORT_NO = 5000; const string SERVER_IP = "127.0.0.1"; static void Main(string[] args) { //---listen at the specified IP and port no.--- IPAddress localAddress = IPAddress.Parse(SERVER_IP); TcpListener listener = new TcpListener(localAddress, PORT_NO); Console.WriteLine("Listening..."); listener.Start();
while (true) { //---incoming client connected--- Client user = new Client(listener.AcceptTcpClient()); } } } }
Figure 11-5 shows the server with two clients connected to it.
The .NET framework contains a number of cryptography services that enable you to incorporate security services into your .NET applications. These libraries are located under the System.Security.Cryptography
namespace and provide various functions such as encryption and decryption of data, as well as other operations such as hashing and random-number generation. One of the core classes that support the cryptographic services is the CryptoStream
class, which links data streams to cryptographic transformations.
This section explores how to use some of the common security APIs to make your .NET applications more secure.
The most common security function that you will perform is hashing. Consider the situation where you need to build a function to authenticate users before they can use your application. You would require the user to supply a set of login credentials, generally containing a user name and a password. This login information needs to be persisted to a database. Quite commonly, developers store the passwords of users verbatim on a database. That's a big security risk because hackers who get a chance to glance at the users' database would be able to obtain the passwords of your users. A better approach is to store the hash values of the users' passwords instead of the passwords themselves. A hashing algorithm has the following properties:
It maps a string of arbitrary length to small binary values of a fixed length, known as a hash value.
The hash value of a string is unique, and small changes in the original string will produce a different hash value.
It is improbable that you'd find two different strings that produce the same hash value.
It is impossible to use the hash value to find the original string.
Then, when the user logs in to your application, the hash value of the password provided is compared with the hash value stored in the database. In this way, even if hackers actually steal the users' database, the actual password is not exposed. One downside to storing the hash values of users' passwords is that in the event that a user loses her password, there is no way to retrieve it. You'd need to generate a new password for the user and request that she change it immediately. But this inconvenience is a small price to pay for the security of your application.
There are many hashing algorithms available in .NET, but the most commonly used are the SHA1 and MD5 implementations. Let's take a look at how they work in .NET.
Using Visual Studio 2008, create a new Console application project. Import the following namespaces:
using System.IO; using System.Security.Cryptography;
Define the following function:
static void Hashing_SHA1() { //---ask the user to enter a password--- Console.Write("Please enter a password: "); string password = Console.ReadLine(); //---hash the password--- byte[] data = ASCIIEncoding.ASCII.GetBytes(password); byte[] passwordHash; SHA1CryptoServiceProvider sha = new SHA1CryptoServiceProvider(); passwordHash = sha.ComputeHash(data); //---ask the user to enter the same password again--- Console.Write("Please enter password again: "); password = Console.ReadLine();
//---hash the second password and compare it with the first--- data = System.Text.Encoding.ASCII.GetBytes(password); if (ASCIIEncoding.ASCII.GetString(passwordHash) == ASCIIEncoding.ASCII.GetString(sha.ComputeHash(data))) Console.WriteLine("Same password"); else Console.WriteLine("Incorrect password"); }
You first ask the user to enter a password, after which you will hash it using the SHA1 implementation. You then ask the user to enter the same password again. To verify that the second password matches the first, you hash the second password and then compare the two hash values. For the SHA1 implementation, the hash value generated is 160 bits in length (the byte array passwordHash
has 20 members: 8 bits × 20 = 160 bits). In this example, you convert the hash values into strings and perform a comparison. You could also convert them to Base64 encoding and then perform a comparison. Alternatively, you can also evaluate the two hash values by using their byte arrays, comparing them byte by byte. As soon as one byte is different, you can conclude that the two hash values are not the same.
To test the function, simply call the Hashing_SHA1()
function in Main()
:
static void Main(string[] args) { Hashing_SHA1(); Console.Read(); }
Figure 11-6 shows the program in action.
You can also use the MD5 implementation to perform hashing, as the following function shows:
static void Hashing_SHA1() { //---ask the user to enter a password--- Console.Write("Please enter a password: "); string password = Console.ReadLine(); //---hash the password--- byte[] data = ASCIIEncoding.ASCII.GetBytes(password); byte[] passwordHash;
MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider(); passwordHash = md5.ComputeHash(data); //---ask the user to enter the same password again--- Console.Write("Please enter password again: "); password = Console.ReadLine(); //---hash the second password and compare it with the first--- data = System.Text.Encoding.ASCII.GetBytes(password); if (ASCIIEncoding.ASCII.GetString(passwordHash) == ASCIIEncoding.ASCII.GetString(md5.ComputeHash(data))) Console.WriteLine("Same password"); else Console.WriteLine("Incorrect password"); }
The main difference is that the hash value for MD5 is 128 bits in length.
With hashing, you simply store the hash value of a user's password in the database. However, if two users use identical passwords, the hash values for these two passwords will be also identical. Imagine a hacker seeing that the two hash values are identical; it would not be hard for him to guess that the two passwords must be the same. For example, users often like to use their own names or birth dates or common words found in the dictionary as passwords. So, hackers often like to use dictionary attacks to correctly guess users' passwords. To reduce the chance of dictionary attacks, you can add a "salt" to the hashing process so that no two identical passwords can generate the same hash values. For instance, instead of hashing a user's password, you hash his password together with his other information, such as email address, birth date, last name, first name, and so on. The idea is to ensure that each user will have a unique password hash value. While the idea of using the user's information as a salt for the hashing process sounds good, it is quite easy for hackers to guess. A better approach is to randomly generate a number to be used as the salt and then hash it together with the user's password.
The following function, Salted_Hashing_SHA1()
, generates a random number using the RNGCryptoServiceProvider
class, which returns a list of randomly generated bytes (the salt). It then combines the salt with the original password and performs a hash on it.
static void Salted_Hashing_SHA1() { //---Random Number Generator--- byte[] salt = new byte[8]; RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider(); rng.GetBytes(salt); //---ask the user to enter a password--- Console.Write("Please enter a password: "); string password = Console.ReadLine();
//---add the salt to the password--- password += ASCIIEncoding.ASCII.GetString(salt); //---hash the password--- byte[] data = ASCIIEncoding.ASCII.GetBytes(password); SHA1CryptoServiceProvider sha = new SHA1CryptoServiceProvider(); byte[] passwordHash; passwordHash = sha.ComputeHash(data); //---ask the user to enter the same password again--- Console.Write("Please enter password again: "); password = Console.ReadLine(); Console.WriteLine(ASCIIEncoding.ASCII.GetString(salt)); //---adding the salt to the second password--- password += ASCIIEncoding.ASCII.GetString(salt); //---hash the second password and compare it with the first--- data = ASCIIEncoding.ASCII.GetBytes(password); if (ASCIIEncoding.ASCII.GetString(passwordHash) == ASCIIEncoding.ASCII.GetString(sha.ComputeHash(data))) Console.WriteLine("Same password"); else Console.WriteLine("Incorrect password"); }
If you use salted hash for storing passwords, the salt used for each password should be stored separately from the main hash database so that hackers do not have a chance to obtain it easily.
Hashing is a one-way process, which means that once a value is hashed, you can't obtain its original value by reversing the process. This characteristic is particularly well suited for authentications as well as digitally signing a document.
In reality, there are many situations that require information to be performed in a two-way process. For example, to send a secret message to a recipient, you need to "scramble" it so that only the recipient can see it. This process of scrambling is known as encryption. Undoing the scrambling process to obtain the original message is known as decryption. There are two main types of encryption: symmetric and asymmetric.
Symmetric encryption is also sometimes known as private key encryption. You encrypt a secret message using a key that only you know. To decrypt the message, you need to use the same key. Private key encryption is effective only if the key can be kept a secret. If too many people know the key, its effectiveness is reduced, and if the key's secrecy is compromised somehow, then the message is no longer secure.
Despite the potential weakness of private key encryption, it is very easy to implement and, computationally, it does not take up too many resources.
For private key encryption (symmetric), the .NET Framework supports the DES, RC2, Rijndael, and TripleDES algorithms.
To see how symmetric encryption works, you will use the RijndaelManaged
class in the following SymmetricEncryption()
function. Three parameters are required — the string to be encrypted, the private key, and the initialization vector (IV). The IV is a random number used in the encryption process to ensure that no two strings will give the same cipher text (the encrypted text) after the encryption process. You will need the same IV later on when decrypting the cipher text.
To perform the actual encryption, you initialize an instance of the CryptoStream
class with a MemoryStream
object, the cryptographic transformation to perform on the stream, and the mode of the stream (Write
for encryption and Read
for decryption):
static string SymmetricEncryption(string str, byte[] key, byte[] IV) { MemoryStream ms = new MemoryStream(); try { //---creates a new instance of the RijndaelManaged class--- RijndaelManaged RMCrypto = new RijndaelManaged(); //---creates a new instance of the CryptoStream class--- CryptoStream cryptStream = new CryptoStream( ms, RMCrypto.CreateEncryptor(key, IV), CryptoStreamMode.Write); StreamWriter sWriter = new StreamWriter(cryptStream); //---encrypting the string--- sWriter.Write(str); sWriter.Close(); cryptStream.Close(); //---return the encrypted data as a string--- return System.Convert.ToBase64String(ms.ToArray()); } catch (Exception ex) { Console.WriteLine(ex.ToString()); return (String.Empty); } }
The encrypted string is returned as a Base64-encoded string. You can check the allowable key sizes for the RijndaelManaged
class by using the following code:
KeySizes[] ks; RijndaelManaged RMCrypto = new RijndaelManaged(); ks = RMCrypto.LegalKeySizes; //---print out the various key sizes--- Console.WriteLine(ks[0].MaxSize); // 256 Console.WriteLine(ks[0].MinSize); // 128 Console.WriteLine(ks[0].SkipSize); // 64
The valid key sizes are: 16 bytes (128 bit), 24 bytes (128 bits + 64 bits), and 32 bytes (256 bits).
You can get the system to generate a random key and IV (which you need to supply in the current example) automatically:
//---generate key--- RMCrypto.GenerateKey(); byte[] key = RMCrypto.Key; Console.WriteLine("Key : " + System.Convert.ToBase64String(key)); //---generate IV--- RMCrypto.GenerateIV(); byte[] IV = RMCrypto.IV; Console.WriteLine("IV : " + System.Convert.ToBase64String(IV));
If the IV is null when it is used, the GenerateIV()
method is called automatically. Valid size for the IV is 16 bytes.
To decrypt a string encrypted using the RijndaelManaged
class, you can use the following SymmetricDecryption()
function:
static string SymmetricDecryption(string str, byte[] key, byte[] IV) { try { //---converts the encrypted string into a byte array--- byte[] b = System.Convert.FromBase64String(str); //---converts the byte array into a memory stream for decryption--- MemoryStream ms = new MemoryStream(b); //---creates a new instance of the RijndaelManaged class--- RijndaelManaged RMCrypto = new RijndaelManaged(); //---creates a new instance of the CryptoStream class--- CryptoStream cryptStream = new CryptoStream( ms, RMCrypto.CreateDecryptor(key, IV), CryptoStreamMode.Read); //---decrypting the stream--- StreamReader sReader = new StreamReader(cryptStream); //---converts the decrypted stream into a string--- String s = sReader.ReadToEnd(); sReader.Close(); return s; } catch (Exception ex) { Console.WriteLine(ex.ToString()); return String.Empty; } }
The following code snippet shows how to use the SymmetricEncryption()
and SymmetricDecryption()
functions to encrypt and decrypt a string:
RijndaelManaged RMCrypto = new RijndaelManaged(); //---generate key--- RMCrypto.GenerateKey(); byte[] key = RMCrypto.Key; Console.WriteLine("Key : " + System.Convert.ToBase64String(key)); //---generate IV--- RMCrypto.GenerateIV(); byte[] IV = RMCrypto.IV; Console.WriteLine("IV : " + System.Convert.ToBase64String(IV)); //---encrypt the string--- string cipherText = SymmetricEncryption("This is a test string.", key, IV); Console.WriteLine("Ciphertext: " + cipherText); //---decrypt the string--- Console.WriteLine("Original string: " + SymmetricDecryption(cipherText, key, IV));
Figure 11-7 shows the output.
Private key encryption requires the key used in the encryption process to be kept a secret. A more effective way to transport secret messages to your intended recipient is to use asymmetric encryption (also known as public key encryption), which involves a pair of keys involved. This pair, consisting of a private key and a public key, is related mathematically such that messages encrypted with the public key can only be decrypted with the corresponding private key. The reverse is also true; messages encrypted with the private key can only be decrypted with the public key. Let's see an example for each scenario.
Before you send a message to your friend Susan, Susan needs to generate the key pair containing the private key and the public key. Susan then freely distributes the public key to you (and all her other friends) but keeps the private key to herself. When you want to send a message to Susan, you use her public key to encrypt the message. Upon receiving the encrypted message, Susan proceeds to decrypt it with her private key. Susan is the only one who can decrypt the message because the key pair works in such a way that only messages encrypted with the public key can be decrypted with the private key. And there is no need to exchange keys, thus eliminating the risk of compromising the secrecy of the key.
Now suppose that Susan sends a message encrypted with her private key to you. To decrypt the message, you need the public key. The scenario may seem odd because the public key is not a secret; everyone knows it. But using this method guarantees that the message has not been tampered with and confirms that it indeed comes from Susan. If the message had been modified, you would not be able to decrypt it. The fact that you can decrypt the message using the public key proves that the message has not been modified.
In computing, public key cryptography is a secure way to encrypt information, but it's computationally expensive because it is time-consuming to generate the key pairs and to perform encryption and decryption. Therefore, it's generally used only for encrypting a small amount of sensitive information.
For public key (asymmetric) encryptions, the .NET Framework supports the DSA and RSA algorithms. The RSA algorithm is used in the following AsymmetricEncryption()
function. This function takes in two parameters: the string to be encrypted and the public key:
static string AsymmetricEncryption(string str, string publicKey) { try { //---Creates a new instance of RSACryptoServiceProvider--- RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(); //---Loads the public key--- RSA.FromXmlString(publicKey); //---Encrypts the string--- byte[] encryptedStr = RSA.Encrypt(ASCIIEncoding.ASCII.GetBytes(str), false); //---Converts the encrypted byte array to string--- return System.Convert.ToBase64String(encryptedStr); } catch (Exception ex) { Console.WriteLine(ex.ToString()); return String.Empty; } }
The encrypted string is returned as a Base64-encoded string. To decrypt a string encrypted with the public key, define the following AsymmetricDecryption()
function. It takes in two parameters (the encrypted string and the private key) and returns the decrypted string.
static string AsymmetricDecryption(string str, string privateKey) { try { //---Creates a new instance of RSACryptoServiceProvider--- RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(); //---Loads the private key--- RSA.FromXmlString(privateKey);
//---Decrypts the string--- byte[] DecryptedStr = RSA.Decrypt(System.Convert.FromBase64String(str), false); //---Converts the decrypted byte array to string--- return ASCIIEncoding.ASCII.GetString(DecryptedStr); } catch (Exception ex) { Console.WriteLine(ex.ToString()); return String.Empty; } }
The following code snippet shows how to use the AsymmetricEncryption()
and AsymmetricDecryption()
functions to encrypt and decrypt a string:
string publicKey, privateKey; RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(); //---get public key--- publicKey = RSA.ToXmlString(false); Console.WriteLine("Public key: " + publicKey); Console.WriteLine(); //---get private and public key--- privateKey = RSA.ToXmlString(true); Console.WriteLine("Private key: " + privateKey); Console.WriteLine(); //---encrypt the string--- string cipherText = AsymmetricEncryption("C# 2008 Programmer's Reference", publicKey); Console.WriteLine("Ciphertext: " + cipherText); Console.WriteLine(); //---decrypt the string--- Console.WriteLine("Original string: " + AsymmetricDecryption(cipherText, privateKey)); Console.WriteLine();
You can obtain the public and private keys generated by the RSA algorithm by using the ToXmlString()
method from the RSACryptoServiceProvider
class. This method takes in a Bool
variable, and returns a public key if the value false
is supplied. If the value true
is supplied, it returns both the private and public keys.
Figure 11-8 shows the output.
The System.IO.Compression
namespace contains classes that provide basic compression and decompression services for streams. This namespace contains two classes for data compression: DeflateStream
and GZipStream
. Both support lossless compression and decompression and are designed for dealing with streams.
Compression is useful for reducing the size of data. If you have huge amount of data to store in your SQL database, for instance, you can save on disk space if you compress the data before saving it into a table. Moreover, because you are now saving smaller blocks of data into your database, the time spent in performing disk I/O is significantly reduced. The downside of compression is that it takes additional processing power from your machine (and requires additional processing time), and you need to factor in this additional time before deciding you want to use compression in your application.
Compression is extremely useful in cases where you need to transmit data over networks, especially slow and costly networks such as General Packet Radio Service (GPRS).connections. In such cases, using compression can drastically cut down the data size and reduce the overall cost of communication. Web Services are another area where using compression can provide a great advantage because XML data can be highly compressed.
But once you've decided the performance cost is worth it, you'll need help deciphering the utilization of these two compression classes, which is what this section is about.
The compression classes read data (to be compressed) from a byte array, compress it, and store the results in a Stream
object. For decompression, the compressed data stored in a Stream
object is decompressed and then stored in another Stream
object.
Let's see how you can perform compression. First, define the Compress()
function, which takes in two parameters: algo
and data
. The first parameter specifies which algorithm to use (GZip or Deflate), and the second parameter is a byte array that contains the data to compress. A MemoryStream
object will be used to store the compressed data. The compressed data stored in the MemoryStream
is then copied into another byte array and returned to the calling function. The Compress()
function is defined as follows:
static byte[] Compress(string algo, byte[] data) { try { //---the ms is used for storing the compressed data--- MemoryStream ms = new MemoryStream(); Stream zipStream = null; switch (algo) { case "Gzip": zipStream = new GZipStream(ms, CompressionMode.Compress, true); break; case "Deflat": zipStream = new DeflateStream(ms, CompressionMode.Compress, true); break; default: return null; } //---compress the data stored in the data byte array--- zipStream.Write(data, 0, data.Length); zipStream.Close(); //---store the compressed data into a byte array--- ms.Position = 0; byte[] c_data = new byte[ms.Length]; //---read the content of the memory stream into the byte array--- ms.Read(c_data, 0, (int)ms.Length); return c_data; } catch (Exception ex) { Console.WriteLine(ex.ToString()); return null; } }
The following Decompress()
function decompresses the data compressed by the Compress()
function. The first parameter specifies the algorithm to use, while the byte array containing the compressed data is passed in as the second parameter, which is then copied into a MemoryStream
object.
static byte[] Decompress(string algo, byte[] data) { try { //---copy the data (compressed) into ms--- MemoryStream ms = new MemoryStream(data);
Stream zipStream = null; //---decompressing using data stored in ms--- switch (algo) { case "Gzip": zipStream = new GZipStream(ms, CompressionMode.Decompress, true); break; case "Deflat": zipStream = new DeflateStream(ms, CompressionMode.Decompress, true); break; default: return null; } //---used to store the de-compressed data--- byte[] dc_data; //---the de-compressed data is stored in zipStream; // extract them out into a byte array--- dc_data = RetrieveBytesFromStream(zipStream, data.Length); return dc_data; } catch (Exception ex) { Console.WriteLine(ex.ToString()); return null; } }
The compression classes then decompress the data stored in the memory stream and store the decompressed data into another Stream
object. To obtain the decompressed data, you need to read the data from the Stream
object. This is accomplished by the RetrieveBytesFromStream()
function, which is defined next:
static byte[] RetrieveBytesFromStream(Stream stream, int bytesblock) { //---retrieve the bytes from a stream object--- byte[] data = null; int totalCount = 0; try { while (true) { //---progressively increase the size of the data byte array--- Array.Resize(ref data, totalCount + bytesblock); int bytesRead = stream.Read(data, totalCount, bytesblock); if (bytesRead == 0) { break; } totalCount += bytesRead; }
//---make sure the byte array contains exactly the number // of bytes extracted--- Array.Resize(ref data, totalCount); return data; } catch (Exception ex) { Console.WriteLine(ex.ToString()); return null; } }
The RetrieveBytesFromStream()
function takes in two parameters — a Stream
object and an integer — and returns a byte array containing the decompressed data. The integer parameter is used to determine how many bytes to read from the stream object into the byte array at a time. This is necessary because you do not know the exact size of the decompressed data in the stream object. And hence it is necessary to dynamically expand the byte array in blocks to hold the decompressed data during runtime. Reserving too large a block wastes memory, and reserving too small a block loses valuable time while you continually expand the byte array. It is therefore up to the calling routine to determine the optimal block size to read.
The block size is the size of the compressed data (data.Length
):
//---the de-compressed data is stored in zipStream; // extract them out into a byte array--- dc_data = RetrieveBytesFromStream(zipStream, data.Length);
In most cases, the uncompressed data is a few times larger than the compressed data, so you would at most expand the byte array dynamically during runtime a couple of times. For instance, suppose that the compression ratio is 20% and the size of the compressed data is 2MB. In that case, the uncompressed data would be 10MB, and the byte array would be expanded dynamically five times. Ideally, the byte array should not be expanded too frequently during runtime because it severely slows down the application. Using the size of the compressed data as a block size is a good compromise.
Use the following statements to test the Compress()
and Decompress()
functions:
static void Main(string[] args) { byte[] compressedData = Compress("Gzip", System.Text.Encoding.ASCII.GetBytes( "This is a uncompressed string")); Console.WriteLine("Compressed: {0}", ASCIIEncoding.ASCII.GetString(compressedData)); Console.WriteLine("Uncompressed: {0}", ASCIIEncoding.ASCII.GetString(Decompress("Gzip", compressedData))); Console.ReadLine(); }
The output is as shown in Figure 11-9.
The compressed data contains some unprintable characters, so you may hear some beeps when it prints. To display the compressed data using printable characters, you can define two helper functions — byteArrayToString()
and stringToByteArray()
:
//---converts a byte array to a string--- static string byteArrayToString(byte[] data) { //---copy the compressed data into a string for presentation--- System.Text.StringBuilder s = new System.Text.StringBuilder(); for (int i = 0; i <= data.Length - 1; i++) { if (i != data.Length - 1) s.Append(data[i] + " "); else s.Append(data[i]); } return s.ToString(); } //---converts a string into a byte array--- static byte[] stringToByteArray(string str) { //---format the compressed string into a byte array--- string[] eachByte = str.Split(" "); byte[] data = new byte[eachByte.Length]; for (int i = 0; i <= eachByte.Length - 1; i++) data[i] = Convert.ToByte(eachByte[i]); return data; }
To use the two helper functions, make the following changes to the statements:
static void Main(string[] args) { byte[] compressedData = Compress("Gzip", System.Text.Encoding.ASCII.GetBytes( "This is a uncompressed string")); string compressedDataStr = byteArrayToString(compressedData); Console.WriteLine("Compressed: {0}", compressedDataStr); byte[] data = stringToByteArray(compressedDataStr); Console.WriteLine("Uncompressed: {0}", ASCIIEncoding.ASCII.GetString(Decompress("Gzip", data))); Console.ReadLine(); }
Figure 11-10 shows the output when using the two helper functions.
Alternatively, you can also convert the compressed data to a Base64-encoded string, like this:
byte[] compressedData = Compress("Gzip", System.Text.Encoding.ASCII.GetBytes( "This is a uncompressed string")); string compressedDataStr = Convert.ToBase64String(compressedData); Console.WriteLine("Compressed: {0}", compressedDataStr); byte[] data = Convert.FromBase64String((compressedDataStr)); Console.WriteLine("Uncompressed: {0}", ASCIIEncoding.ASCII.GetString(Decompress("Gzip", data)));
Figure 11-11 shows the output using the base64 encoding.
Many a time you may need to persist the value of an object to secondary storage. For example, you may want to save the values of a couple of Point
objects representing the positioning of an item on-screen to secondary storage. The act of "flattening" an object into a serial form is known as serialization. The .NET Framework supports binary and XML serialization.
Consider the following class, BookMark
, which is used to stored information about web addresses and their descriptions:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.IO; using System.Runtime.Serialization.Formatters.Binary; namespace Serialization { class Program { static void Main(string[] args) { } } class BookMark { private DateTime _dateCreated; public BookMark() { _dateCreated = DateTime.Now; } public DateTime GetDateCreated() { return _dateCreated; } public string URL { get; set; } public string Description { get; set; } public BookMark NextURL { get; set; } } }
The BookMark
class contains properties as well as private variables. The NextURL
property links multiple BookMark
objects, much like a linked list. Let's now create two BookMark
objects and link them:
static void Main(string[] args) { BookMark bm1, bm2; bm1 = new BookMark { URL = "http://www.amazon.com", Description = "Amazon.com Web site" }; bm2 = new BookMark() { URL = "http://www.wrox.com", Description = "Wrox.com Web site", NextURL = null }; //---link the first BookMark to the next--- bm1.NextURL = bm2; }
You can serialize the objects into a binary stream by writing the Serialize()
function:
static void Main(string[] args) { //... } static MemoryStream Serialize(BookMark bookMark) { MemoryStream ms = new MemoryStream(); FileStream fs = new FileStream( @"C:Bookmarks.dat", FileMode.Create, FileAccess.Write); BinaryFormatter formatter = new BinaryFormatter(); //---serialize to memory stream--- formatter.Serialize(ms, bookMark); ms.Position = 0; //---serialize to file stream--- formatter.Serialize(fs, bookMark); return ms; }
For binary serialization, you need to import the System.Runtime.Serialization.Formatters.Binary
namespace.
The Serialize()
function takes in a single parameter (the BookMark
object to serialize) and returns a MemoryStream
object representing the serialized BookMark
object. You use the BinaryFormatter
class from the System.Runtime.Serialization.Formatters.Binary
namespace to serialize an object. One side effect of this function is that it also serializes the BookMark
object to file, using the FileStream
class.
Before you serialize an object, you need to prefix the class that you want to serialize name with the [Serializable]
attribute:
[Serializable] class BookMark { private DateTime _dateCreated; public BookMark() { _dateCreated = DateTime.Now; } public DateTime GetDateCreated() { return _dateCreated; } public string URL { get; set; } public string Description { get; set; } public BookMark NextURL { get; set; } }
The following statement serializes the bm1 BookMark
object, using the Serialize()
function:
static void Main(string[] args) { BookMark bm1, bm2; bm1 = new BookMark { URL = "http://www.amazon.com", Description = "Amazon.com Web site" }; bm2 = new BookMark() { URL = "http://www.wrox.com", Description = "Wrox.com Web site", NextURL = null }; //---link the first BookMark to the next--- bm1.NextURL = bm2; //========Binary Serialization========= //---serializing an object graph into a memory stream--- MemoryStream ms = Serialize(bm1); }
To prove that the object is serialized correctly, you deserialize the memory stream (that is, "unflatten" the data) and assign it back to a BookMark
object:
static void Main(string[] args) { BookMark bm1, bm2; bm1 = new BookMark { URL = "http://www.amazon.com", Description = "Amazon.com Web site" }; bm2 = new BookMark() { URL = "http://www.wrox.com", Description = "Wrox.com Web site", NextURL = null }; //---link the first BookMark to the next--- bm1.NextURL = bm2; //========Binary Serialization========= //---serializing an object graph into a memory stream--- MemoryStream ms = Serialize(bm1); //---deserializing a memory stream into an object graph--- BookMark bm3 = Deserialize(ms); }
Here is the definition for the DeSerialize()
function:
static void Main(string[] args) { //... } static MemoryStream Serialize(BookMark bookMark) { //... } static BookMark Deserialize(MemoryStream ms) { BinaryFormatter formatter = new BinaryFormatter(); return (BookMark)formatter.Deserialize(ms); }
To display the values of the deserialized BookMark
object, you can print out them out like this:
static void Main(string[] args) { BookMark bm1, bm2; bm1 = new BookMark { URL = "http://www.amazon.com", Description = "Amazon.com Web site" }; bm2 = new BookMark() { URL = "http://www.wrox.com", Description = "Wrox.com Web site", NextURL = null }; //---link the first BookMark to the next--- bm1.NextURL = bm2; //========Binary Serialization========= //---serializing an object graph into a memory stream--- MemoryStream ms = Serialize(bm1); //---deserializing a memory stream into an object graph--- BookMark bm3 = Deserialize(ms); //---print out all the bookmarks--- BookMark tempBookMark = bm3; do { Console.WriteLine(tempBookMark.URL); Console.WriteLine(tempBookMark.Description); Console.WriteLine(tempBookMark.GetDateCreated()); Console.WriteLine("---"); tempBookMark = tempBookMark.NextURL; } while (tempBookMark != null); Console.ReadLine(); }
If the objects are serialized and deserialized correctly, the output is as shown in Figure 11-12.
But what does the binary stream look like? To answer that question, take a look at the c:BookMarks.dat
file that you have created in the process. To view the binary file, simply drag and drop the BookMarks.dat
file into Visual Studio 2008. You should see something similar to Figure 11-13.
A few observations are worth noting at this point:
Private variables and properties are all serialized. In binary serialization, both the private variables and properties are serialized. This is known as deep serialization, as opposed to shallow serialization in XML serialization (which only serializes the public variables and properties). The next section discusses XML serialization.
Object graphs are serialized and preserved. In this example, two BookMark
objects are linked, and the serialization process takes care of the relationships between the two objects.
There are times when you do not want to serialize all of the data in your object. If you don't want to persist the date and time that the BookMark
objects are created, for instance, you can prefix the variable name (that you do not want to serialize) with the [NonSerialized]
attribute:
[Serializable] class BookMark { [NonSerialized] private DateTime _dateCreated; public BookMark() { _dateCreated = DateTime.Now; } public DateTime GetDateCreated()
{ return _dateCreated; } public string URL { get; set; } public string Description { get; set; } public BookMark NextURL { get; set; } }
The dateCreated
variable will not be serialized. Figure 11-14 shows that when the dateCreated
variable is not serialized, its value is set to the default date when the object is deserialized.
You can also serialize an object into an XML document. There are many advantages to XML serialization. For instance, XML documents are platform-agnostic because they are in plain text format and that makes cross-platform communication quite easy. XML documents are also easy to read and modify, which makes XML a very flexible format for data representation.
The following example illustrates XML serialization and shows you some of its uses.
Let's define a class so that you can see how XML serialization works. For this example, you define three classes that allow you to store information about a person, such as name, address, and date of birth. Here are the class definitions:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.VisualBasic; using System.IO; using System.Xml.Serialization; using System.Xml; namespace Serialization { class Program { static void Main(string[] args) { } }
public class Member { private int age; public MemberName Name; public MemberAddress[] Addresses; public DateTime DOB; public int currentAge { get { //---add a reference to Microsoft.VisualBasic.dll--- age = (int)DateAndTime.DateDiff( DateInterval.Year, DOB, DateTime.Now, FirstDayOfWeek.System, FirstWeekOfYear.System); return age; } } } public class MemberName { public string FirstName { get; set; } public string LastName { get; set; } } public class MemberAddress { public string Line1; public string Line2; public string City; public string Country; public string Postal; } }
The various classes are deliberately designed to illustrate the various aspects of XML serialization. They may not adhere to the best practices for defining classes.
Here are the specifics for the classes:
The Member
class contains both private and public members. It also contains a read-only property.
The Member
class contains a public array containing the address of a Member
.
The Member
class contains a variable of Date
data type.
The MemberName
class contains two properties.
The MemberAddress
class contains only public members.
To serialize a Member
object into a XML document, you can use the XMLSerializer
class from the System.Xml.Serialization
namespace:
static void Main(string[] args) { } //========XML Serialization========= static void XMLSerialize(Member mem) { StreamWriter sw = new StreamWriter(@"c:Members.xml"); try { XmlSerializer s = new XmlSerializer(typeof(Member)); s.Serialize(sw, mem); } catch (Exception ex) { Console.WriteLine(ex.ToString()); } finally { sw.Close(); } }
For XML serialization, you need to import the System.Xml.Serialization
namespace.
In the XMLSerialize()
function, you first create a new StreamWriter
object so that you can save the serialized XML string to a file. The Serialize()
method from the XMLSerializer
class serializes the Member
object into an XML string, which is then written to file by using the StreamWriter
class.
To test the XMLSerialize()
function, assume that you have the following object declarations:
static void Main(string[] args) { MemberAddress address1 = new MemberAddress() { Line1 = "One Way Street", Line2 = "Infinite Loop", Country = "SINGAPORE", Postal = "456123" }; MemberAddress address2 = new MemberAddress() { Line1 = "Two Way Street", Country = "SINGAPORE", Postal = "456123" }; Member m1 = new Member() { Name = new MemberName() { FirstName = "Wei-Meng", LastName = "Lee" }, DOB = Convert.ToDateTime(@"5/1/1972"), Addresses = new MemberAddress[] { address1, address2 } }; }
To serialize the Member
object, invoke the XMLSerialize()
method like this:
static void Main(string[] args) { MemberAddress address1 = new MemberAddress() { Line1 = "One Way Street", Line2 = "Infinite Loop", Country = "SINGAPORE", Postal = "456123" }; MemberAddress address2 = new MemberAddress() { Line1 = "Two Way Street", Country = "SINGAPORE", Postal = "456123" }; Member m1 = new Member() { Name = new MemberName() {
FirstName = "Wei-Meng", LastName = "Lee" }, DOB = Convert.ToDateTime(@"5/1/1972"), Addresses = new MemberAddress[] { address1, address2 } }; XMLSerialize(m1); }
Figure 11-15 shows the XML document generated by the XMLSerialize()
function.
As you can see, the object is serialized into an XML document with a format corresponding to the structure of the object. Here are some important points to note:
The City
information is not persisted in the XML document (nor as the Line2
in the second Address
element) because it was not assigned in the objects. You will see later how to persist empty elements, even though a value is not assigned.
All read/write properties in the object are persisted in the XML document, except the read-only currentAge
property in the Member
class.
Only public variables are persisted; private variables are not persisted in XML serialization.
The default name for each element in the XML document is drawn from the variable (or class) name. In most cases this is desirable, but sometimes the element names might not be obvious.
To deserialize the XML document, simply use the Deserialize()
method from the XMLSerializer
class. Define the XMLDeserialize()
function as follows:
static void Main(string[] args) { //... } //========XML Serialization========= static Member XMLDeserialize(string xmlFile) { Member obj; XmlReader xr = XmlReader.Create(xmlFile); try { XmlSerializer s = new XmlSerializer(typeof(Member)); obj = (Member)s.Deserialize(xr); } catch (Exception ex) { Console.WriteLine(ex.ToString()); obj = null; } finally { xr.Close(); } return obj; }
Here, you can use the XmlReader
class's Create()
method to open an XML file for reading. The XmlReader
class is used to read the data from the XML file. The deserialized object is then returned to the calling function.
Remember to import the System.Xml
namespace for the XmlReader
class.
To test the XMLDeserialize()
function, call it directly after an object has been serialized, like this:
static void Main(string[] args) { MemberAddress address1 = new MemberAddress() { Line1 = "One Way Street", Line2 = "Infinite Loop", Country = "SINGAPORE", Postal = "456123"
}; MemberAddress address2 = new MemberAddress() { Line1 = "Two Way Street", Country = "SINGAPORE", Postal = "456123" }; Member m1 = new Member() { Name = new MemberName() { FirstName = "Wei-Meng", LastName = "Lee" }, DOB = Convert.ToDateTime(@"5/1/1972"), Addresses = new MemberAddress[] { address1, address2 } }; XMLSerialize(m1); Member m2 = XMLDeserialize(@"c:Members.xml"); Console.WriteLine("{0}, {1}", m2.Name.FirstName, m2.Name.LastName); Console.WriteLine("{0}", m2.currentAge); foreach (MemberAddress a in m2.Addresses) { Console.WriteLine("{0}", a.Line1); Console.WriteLine("{0}", a.Line2); Console.WriteLine("{0}", a.Country); Console.WriteLine("{0}", a.Postal); Console.WriteLine(); } Console.ReadLine(); }
The output of these statements is shown in Figure 11-16.
Despite the fairly automated task performed by the XMLSerializer
object, you can customize the way the XML document is generated. Here's an example of how you can modify classes with a few attributes:
[XmlRoot("MemberInformation", Namespace = "http://www.learn2develop.net", IsNullable = true)] public class Member { private int age; //---specify the element name to be MemberName--- [XmlElement("MemberName")] public MemberName Name; //---specify the sub-element(s) of Addresses to be Address--- [XmlArrayItem("Address")] public MemberAddress[] Addresses; public DateTime DOB; public int currentAge { get { //---add a reference to Microsoft.VisualBasic.dll--- age = (int)DateAndTime.DateDiff( DateInterval.Year, DOB, DateTime.Now, FirstDayOfWeek.System, FirstWeekOfYear.System); return age; } } } public class MemberName { public string FirstName { get; set; } public string LastName { get; set; } } public class MemberAddress { public string Line1; public string Line2; //---empty element if city is not specified--- [XmlElement(IsNullable = true)] public string City; //---specify country and postal as attribute---
[XmlAttributeAttribute()] public string Country; [XmlAttributeAttribute()] public string Postal; }
When the class is serialized again, the XML document will look like Figure 11-17.
Notice that the root element of the XML document is now <MemberInformation>
. Also, <MemberAddress>
has now been changed to <Address>
, and the <Country>
and <Postal>
elements are now represented as attributes. Finally, the <City>
element is always persisted regardless of whether or not it has been assigned a value.
Here are the uses of each attribute:
[XmlRoot("MemberInformation", Namespace = "http://www.learn2develop.net", IsNullable = true)] public class Member { ...
Sets the root element name of the XML document to MemberInformation
(default element name is Member
, which follows the class name), with a specific namespace. The IsNullable
attribute indicates if empty elements must be displayed.
//---specify the element name to be MemberName--- [XmlElement("MemberName")] public MemberName Name; ...
Specifies that the element name MemberName
be used in place of the current variable name (as defined in the class as Name
).
//---specify the sub-element(s) of Addresses to be Address--- [XmlArrayItem("Address")] public MemberAddress[] Addresses; ...
Specifies that the following variable is repeating (an array) and that each repeating element be named as Address
.
//---empty element if city is not specified--- [XmlElement(IsNullable = true)] public string City; ... Indicates that the document must include the City element even if it is empty.
//---specify country and postal as attribute--- [XmlAttributeAttribute()] public string Country; [XmlAttributeAttribute()] public string Postal; ...
Indicates that the Country
and Postal
property be represented as an attribute.
There is one more thing that you need to note when doing XML serialization. If your class has a constructor (as in the following example), you also need a default constructor:
[XmlRoot("MemberInformation", Namespace = "http://www.learn2develop.net", IsNullable = true)] public class Member { private int age;
public Member(MemberName Name) { this.Name = Name; } //---specify the element name to be MemberName--- [XmlElement("MemberName")] public MemberName Name; ...
This example results in an error when you try to perform XML serialization on it. To solve the problem, simply add a default constructor to your class definition:
[XmlRoot("MemberInformation", Namespace = "http://www.learn2develop.net", IsNullable = true)] public class Member { private int age; public Member() { } public Member(MemberName Name) { this.Name = Name; } ...
XML serialization can help you to preserve the state of your object (just like the binary serialization that you saw in previous section) and makes transportation easy. More significantly, you can use XML serialization to manage configuration files. You can define a class to store configuration information and use XML serialization to persist it on file. By doing so, you have the flexibility to modify the configuration information easily because the information is now represented in XML; at the same time, you can programmatically manipulate the configuration information by accessing the object's properties and methods.
In this chapter, you explored the basics of files and streams and how to use the Stream object to perform a wide variety of tasks, including network communication, cryptography, and compression. In addition, you saw how to preserve the state of objects using XML and binary serialization. In the .NET Framework, the Stream object is extremely versatile and its large number of derived classes is designed to deal with specific tasks such as file I/O, memory I/O, network I/O, and so on.
18.222.20.101