Chapter 22. Streams

For many applications, data is held in memory and accessed as though it were a three-dimensional solid; when you need to access a variable or an object, use its name, and, presto, it is available to you. When you want to move your data into or out of a file, across the network, or over the Internet, however, your data must be streamed.[17] In a stream, data flows much like bubbles in a stream of water.

Typically, the endpoint of a stream is a backing store. The backing store provides a source for the stream, like a lake provides a source for a river. Typically, the backing store is a file, but it is also possible for the backing store to be a network or web connection.

Files and directories are abstracted by classes in the .NET Framework. These classes provide methods and properties for creating, naming, manipulating, and deleting files and directories on your disk.

The .NET Framework provides buffered and unbuffered streams, as well as classes for asynchronous I/O. With asynchronous I/O, you can instruct the .NET classes to read your file; while they are busy getting the bits off the disk, your program can be working on other tasks. The asynchronous I/O tasks notify you when their work is done. The asynchronous classes are sufficiently powerful and robust that you might be able to avoid creating threads explicitly (see Chapter 21).

Streaming into and out of files is no different from streaming across the network, and the second part of this chapter will describe streaming using both TCP/IP and web protocols.

To create a stream of data, your object will typically be serialized, or written to the stream as a series of bits. The .NET Framework provides extensive support for serialization, and the final part of this chapter walks you through the details of taking control of the serialization of your object.

Files and Directories

Before looking at how you can get data into and out of files, let’s start by examining the support provided for file and directory manipulation.

The classes you need are in the System.IO namespace. These include the File class, which represents a file on disk, and the Directory class, which represents a directory (also known in Windows as a folder).

Working with Directories

The Directory class exposes static methods for creating, moving, and exploring directories. All the methods of the Directory class are static; therefore, you can call them all without having an instance of the class.

The DirectoryInfo class is a similar class, but one that has nothing but instance members (i.e., no static members at all). DirectoryInfo derives from FileSystemInfo, which in turn derives from MarshalByRefObject. The FileSystemInfo class has a number of properties and methods that provide information about a file or directory.

Table 22-1 lists the principal methods of the Directory class, and Table 22-2 lists the principal methods of the DirectoryInfo class, including important properties and methods inherited from FileSystemInfo.

Table 22-1. Principal methods of the Directory class

Method

Use

CreateDirectory( )

Creates all directories and subdirectories specified by its path parameter

GetCreationTime( )

Returns and sets the time the specified directory was created

GetDirectories( )

Gets named directories

GetLogicalDrives( )

Returns the names of all the logical drives in the form <drive>:

GetFiles( )

Returns the names of files matching a pattern

GetParent( )

Returns the parent directory for the specified path

Move( )

Moves a directory and its contents to a specified path

Table 22-2. Principal methods and properties of the DirectoryInfo class

Method or property

Use

Attributes

Inherits from FileSystemInfo; gets or sets the attributes of the current file

CreationTime

Inherits from FileSystemInfo; gets or sets the creation time of the current file

Exists

Public property Boolean value, which is true if the directory exists

Extension

Public property inherited from FileSystemInfo; that is, the file extension

FullName

Public property inherited from FileSystemInfo; that is, the full path of the file or directory

LastAccessTime

Public property inherited from FileSystemInfo; gets or sets the last access time

LastWriteTime

Public property inherited from FileSystemInfo; gets or sets the time when the current file or directory was last written to

Name

Public property name of this instance of DirectoryInfo

Parent

Public property parent directory of the specified directory

Root

Public property root portion of the path

Create( )

Public method that creates a directory

CreateSubdirectory( )

Public method that creates a subdirectory on the specified path

Delete( )

Public method that deletes a DirectoryInfo and its contents from the path

GetDirectories( )

Public method that returns a DirectoryInfo array with subdirectories

GetFiles( )

Public method that returns a list of files in the directory

GetFileSystemInfos( )

Public method that retrieves an array of FileSystemInfo objects

MoveTo( )

Public method that moves a DirectoryInfo and its contents to a new path

Refresh( )

Public method inherited from FileSystemInfo; refreshes the state of the object

Creating a DirectoryInfo Object

To explore a directory hierarchy, you need to instantiate a DirectoryInfo object. The DirectoryInfo class provides methods for getting not just the names of contained files and directories, but also FileInfo and DirectoryInfo objects, allowing you to dive into the hierarchical structure, extracting subdirectories and exploring these recursively.

You instantiate a DirectoryInfo object with the name of the directory you want to explore:

string path = Environment.GetEnvironmentVariable("SystemRoot");
DirectoryInfo dir = new DirectoryInfo(path);

Tip

Remember that the at (@) sign before a string creates a verbatim string literal in which it isn’t necessary to escape characters such as the backslash. I covered this in Chapter 10.

You can ask that DirectoryInfo object for information about itself, including its name, full path, attributes, the time it was last accessed, and so forth. To explore the subdirectory hierarchy, ask the current directory for its list of subdirectories:

DirectoryInfo[] directories = dir.GetDirectories( );

This returns an array of DirectoryInfo objects, each of which represents a directory. You can then recurse into the same method, passing in each DirectoryInfo object in turn:

foreach (DirectoryInfo newDir in directories)
{
    dirCounter++;
    ExploreDirectory(newDir);
}

The dirCounter static int member variable keeps track of how many subdirectories have been found altogether. To make the display more interesting, add a second static int member variable, indentLevel, which will be incremented each time you recurse into a subdirectory, and will be decremented when you pop out. This will allow you to display the subdirectories indented under the parent directories. Example 22-1 shows the complete listing.

Example 22-1. Recursing through subdirectories
using System;
using System.IO;

namespace RecursingDirectories
{
    class Tester
    {
        // static member variables to keep track of totals
        // and indentation level
        static int dirCounter = 1;
        static int indentLevel = −1; // so first push = 0

        public static void Main( )
        {
            Tester t = new Tester( );

            // choose the initial subdirectory
            string theDirectory =
            Environment.GetEnvironmentVariable("SystemRoot");
            // Mono and Shared Source CLI users on Linux, Unix or
            // Mac OS X should comment out the preceding two lines
            // of code and uncomment the following:
            //string theDirectory = "/tmp";

            // call the method to explore the directory,
            // displaying its access date and all
            // subdirectories

            DirectoryInfo dir = new DirectoryInfo(theDirectory);

            t.ExploreDirectory(dir);

            // completed. print the statistics
            Console.WriteLine(
                "

{0} directories found.
",
                    dirCounter);
        }
        // Set it running with a directoryInfo object
        // for each directory it finds, it will call
        // itself recursively

        private void ExploreDirectory(DirectoryInfo dir)
        {
            indentLevel++; // push a directory level

            // create indentation for subdirectories
            for (int i = 0; i < indentLevel; i++)
                Console.Write(" "); // two spaces per level

            // print the directory and the time last accessed
            Console.WriteLine("[{0}] {1} [{2}]
",
                indentLevel, dir.Name, dir.LastAccessTime);

            // get all the directories in the current directory
            // and call this method recursively on each
            DirectoryInfo[] directories = dir.GetDirectories( );
            foreach (DirectoryInfo newDir in directories)
            {
                dirCounter++; // increment the counter
                ExploreDirectory(newDir);
            }
            indentLevel−−; // pop a directory level
        }
    }
}

Output (excerpt):
   [2] logiscan [5/1/2001 3:06:41 PM]

   [2] miitwain [5/1/2001 3:06:41 PM]

 [1] Web [5/1/2001 3:06:41 PM]

   [2] printers [5/1/2001 3:06:41 PM]

     [3] images [5/1/2001 3:06:41 PM]

   [2] Wallpaper [5/1/2001 3:06:41 PM]

363 directories found.

Warning

You must add using System.IO; to the top of your file; Visual Studio 2008 doesn’t do this automatically.

This program will throw an exception in Vista as you attempt to read into directories that are protected by the operating system. That is a good thing; it means Vista is doing what it should.

The program begins by identifying a directory (SystemRoot, usually C:WinNT or C:Windows) and creating a DirectoryInfo object for that directory. It then calls ExploreDirectory, passing in that DirectoryInfo object. ExploreDirectory displays information about the directory, and then retrieves all the subdirectories.

The list of all the subdirectories of the current directory is obtained by calling GetDirectories. This returns an array of DirectoryInfo objects. ExploreDirectory is the recursive method; each DirectoryInfo object is passed into ExploreDirectory in turn. The effect is to push recursively into each subdirectory, and then to pop back out to explore sister directories until all the subdirectories of %SystemRoot% are displayed. When ExploreDirectory finally returns, the calling method prints a summary.

Working with Files

The DirectoryInfo object can also return a collection of all the files in each subdirectory found. The GetFiles( ) method returns an array of FileInfo objects, each of which describes a file in that directory. The FileInfo and File objects relate to one another, much as DirectoryInfo and Directory do. Like the methods of Directory, all the File methods are static; like DirectoryInfo, all the methods of FileInfo are instance methods.

Table 22-3 lists the principal methods of the File class; Table 22-4 lists the important members of the FileInfo class.

Table 22-3. Principal public static methods of the File class

Method

Use

AppendText( )

Creates a StreamWriter that appends text to the specified file

Copy( )

Copies an existing file to a new file

Create( )

Creates a file in the specified path

CreateText( )

Creates a StreamWriter that writes a new text file to the specified file

Delete( )

Deletes the specified file

Exists( )

Returns true if the specified file exists

GetAttributes( ), SetAttributes( )

Gets or sets the FileAttributes of the specified file

GetCreationTime( ), SetCreationTime( )

Returns or sets the creation date and time of the file

GetLastAccessTime( ), SetLastAccessTime( )

Returns or sets the last time the specified file was accessed

GetLastWriteTime( ), SetLastWriteTime( )

Returns or sets the last time the specified file was written to

Move( )

Moves a file to a new location; can be used to rename a file

OpenRead( )

Public static method that opens a FileStream on the file

OpenWrite( )

Creates a read/write Stream on the specified path

Table 22-4. Methods and properties of the FileInfo class

Method or property

Use

Attributes( )

Inherits from FileSystemInfo; gets or sets the attributes of the current file

CreationTime

Inherits from FileSystemInfo; gets or sets the creation time of the current file

Directory

Public property that gets an instance of the parent directory

Exists

Public property Boolean value that is true if the directory exists

Extension

Public property inherited from FileSystemInfo; that is, the file extension

FullName

Public property inherited from FileSystemInfo; that is, the full path of the file or directory

LastAccessTime

Public property inherited from FileSystemInfo; gets or sets the last access time

LastWriteTime

Public property inherited from FileSystemInfo; gets or sets the time when the current file or directory was last written to

Length

Public property that gets the size of the current file

Name

Public property Name of this DirectoryInfo instance

AppendText( )

Public method that creates a StreamWriter that appends text to a file

CopyTo( )

Public method that copies an existing file to a new file

Create( )

Public method that creates a new file

Delete( )

Public method that permanently deletes a file

MoveTo( )

Public method to move a file to a new location; can be used to rename a file

Open( )

Public method that opens a file with various read/write and sharing privileges

OpenRead( )

Public method that creates a read-only FileStream

OpenText( )

Public method that creates a StreamReader that reads from an existing text file

OpenWrite( )

Public method that creates a write-only FileStream

Example 22-2 modifies Example 22-1, adding code to get a FileInfo object for each file in each subdirectory. That object is used to display the name of the file, along with its length and the date and time it was last accessed.

Example 22-2. Exploring files and subdirectories
using System;
using System.IO;

using System.Collections;

namespace ExploringFilesAndSubdirectories
{
    class Tester
    {
        // static member variables to keep track of totals
        // and indentation level
        static int dirCounter = 1;
        static int indentLevel = −1; // so first push = 0
        static int fileCounter = 0;

        public static void Main(  )
        {
            Tester t = new Tester(  );
            //Console.WriteLine("GetEnvironmentVariables: ");
            //IDictionary    environmentVariables =
            //   Environment.GetEnvironmentVariables(  );
            //foreach (DictionaryEntry de in environmentVariables)
            //    {
            //        Console.WriteLine("  {0} = {1}", de.Key, de.Value);
            //    }

            //return;


            // choose the initial subdirectory
            string theDirectory =
                Environment.GetEnvironmentVariable("SystemRoot");
            // Mono and Shared Source CLI users on Linux, Unix or
            // Mac OS X should comment out the preceding two lines
            // of code and uncomment the following:
            //string theDirectory = "/tmp";

            // call the method to explore the directory,
            // displaying its access date and all
            // subdirectories
            DirectoryInfo dir = new DirectoryInfo(theDirectory);

            t.ExploreDirectory(dir);

            // completed. print the statistics

            Console.WriteLine(
                "

{0} files in {1} directories found.
",
                fileCounter, dirCounter);
        }

        // Set it running with a directoryInfo object
        // for each directory it finds, it will call
        // itself recursively
        private void ExploreDirectory(DirectoryInfo dir)
        {
            indentLevel++; // push a directory level
            // create indentation for subdirectories
            for (int i = 0; i < indentLevel; i++)
                Console.Write(" "); // two spaces per level

            // print the directory and the time last accessed
            Console.WriteLine("[{0}] {1} [{2}]
",
                indentLevel, dir.Name, dir.LastAccessTime);

            // get all the files in the directory and
            // print their name, last access time, and size
            try
            {
                FileInfo[] filesInDir = dir.GetFiles(  );

                foreach (FileInfo file in filesInDir)
                {
                    // indent once more to put files
                    // under their directory
                    for (int i = 0; i < indentLevel + 1; i++)
                        Console.Write(" "); // two spaces per level

                    Console.WriteLine("{0} [{1}] Size: {2} bytes",
                    file.Name,
                    file.LastWriteTime,
                    file.Length);
                    fileCounter++;
                }
                // get all the directories in the current directory
                // and call this method recursively on each
                DirectoryInfo[] directories = dir.GetDirectories(  );
                foreach (DirectoryInfo newDir in directories)
                {
                    dirCounter++; // increment the counter
                    ExploreDirectory(newDir);
                }
                indentLevel−−; // pop a directory level
            }
            catch { }  // skip over the ones Vista doesn't like
        }
    }
}

Output (excerpt):

 0.LOG [8/30/2007 8:26:05 PM] Size: 0 bytes
 AC3API.INI [1/14/1999 2:04:06 PM] Size: 231 bytes
 actsetup.log [7/1/2004 11:13:11 AM] Size: 3848 bytes
 Blue Lace 16.bmp [8/29/2002 6:00:00 AM] Size: 1272 bytes
 BOOTSTAT.DAT [8/30/2007 8:25:03 PM] Size: 2048 bytes
44760 files in 8251 directories found.

The example is initialized with the name of the SystemRoot directory. It prints information about all the files in that directory, and then recursively explores all the subdirectories and all their subdirectories (your output might differ). This can take quite a while to run because the SystemRoot directory tree is rather large (in this case, 44,760 files in 8,251 directories).

Tip

In this version, we used a try/catch block to catch the exception thrown when we tried to get information about directories that are protected by Vista, and so the program was able to run to completion (though the count of files and directories is diminished by the uncounted secured directories):

 try
 {
     FileInfo[] filesInDir = dir.GetFiles(  );
     //...
     indentLevel−−; // pop a directory level
 }
 catch { }

Modifying Files

As you saw from Table 22-3 and Table 22-4, you can use the FileInfo class to create, copy, rename, and delete files. The next example creates a new subdirectory, copies files in, renames some, deletes others, and then deletes the entire directory.

Tip

To set up these examples, create a est directory and copy the media directory from WinNT or Windows into the est directory. Don’t work on files in the system root directly; when working with system files, you want to be extraordinarily careful.

The first step is to create a DirectoryInfo object for the test directory (adjust theDirectory appropriately if you are on a Mac OS X, Linux, or Unix system):

string theDirectory = @"c:	estmedia";
DirectoryInfo dir = new DirectoryInfo(theDirectory);

Next, create a subdirectory within the test directory by calling CreateSubDirectory on the DirectoryInfo object. You get back a new DirectoryInfo object, representing the newly created subdirectory:

string newDirectory = "newTest";
DirectoryInfo newSubDir =
    dir.CreateSubdirectory(newDirectory);

You can now iterate over the test and copy files to the newly created subdirectory:

FileInfo[] filesInDir = dir.GetFiles(  );
foreach (FileInfo file in filesInDir)
{
    string fullName = newSubDir.FullName +
                     "\" + file.Name;
    file.CopyTo(fullName);
    Console.WriteLine("{0} copied to newTest",
       file.FullName);
}

Notice the syntax of the CopyTo method. This is a method of the FileInfo object. Pass in the full path of the new file, including its full name and extension.

Once you’ve copied the files, you can get a list of the files in the new subdirectory and work with them directly:

filesInDir = newSubDir.GetFiles(  );
foreach (FileInfo file in filesInDir)
{

Create a simple integer variable named counter, and use it to rename every other file:

if (counter++ %2 == 0)
{
    file.MoveTo(fullName + ".bak");
    Console.WriteLine("{0} renamed to {1}",
       fullName,file.FullName);
}

You rename a file by “moving” it to the same directory, but with a new name. You can, of course, move a file to a new directory with its original name, or you can move and rename at the same time.

Rename every other file, and delete the ones you don’t rename:

file.Delete(  );
Console.WriteLine("{0} deleted.", fullName);

Once you’re done manipulating the files, you can clean up by deleting the entire subdirectory:

newSubDir.Delete(true);

The Boolean parameter determines whether this is a recursive delete. If you pass in false, and if this directory has subdirectories with files in it, it throws an exception.

Example 22-3 lists the source code for the complete program. Be careful when running this: when it is done, the subdirectory is gone. To see the renaming and deletions, put a breakpoint on or remove the last line.

Example 22-3. Creating a subdirectory and manipulating files
using System;
using System.IO;

namespace CreatingSubdirectoryManipulatingFile
{
    class Tester
    {
        public static void Main(  )
        {
            // make an instance and run it
            Tester t = new Tester(  );
            string theDirectory = @"c:	estmedia";
            DirectoryInfo dir = new DirectoryInfo(theDirectory);
            t.ExploreDirectory(dir);
        }

        // Set it running with a directory name
        private void ExploreDirectory(DirectoryInfo dir)
        {

            // make a new subdirectory
            string newDirectory = "newTest";
            DirectoryInfo newSubDir =
                dir.CreateSubdirectory(newDirectory);

            // get all the files in the directory and
            // copy them to the new directory
            FileInfo[] filesInDir = dir.GetFiles(  );
            foreach (FileInfo file in filesInDir)
            {
                string fullName = newSubDir.FullName +
                                "\" + file.Name;
                file.CopyTo(fullName);
                Console.WriteLine("{0} copied to newTest",
                                file.FullName);
            }

            // get a collection of the files copied in
            filesInDir = newSubDir.GetFiles(  );

            // delete some and rename others
            int counter = 0;
            foreach (FileInfo file in filesInDir)
            {
                string fullName = file.FullName;

                if (counter++ % 2 == 0)
                {
                    file.MoveTo(fullName + ".bak");
                    Console.WriteLine("{0} renamed to {1}",
                        fullName, file.FullName);
                }
                else
                {
                    file.Delete(  );
                    Console.WriteLine("{0} deleted.", fullName);
                }
            }

            newSubDir.Delete(true); // delete the subdirectory
        }
    }
}
Output (excerpts):
c:	estmediaBach's Brandenburg Concerto No. 3.RMI
 copied to newTest
c:	estmediaBeethoven's 5th Symphony.RMI copied to newTest
c:	estmediaBeethoven's Fur Elise.RMI copied to newTest
c:	estmediacanyon.mid copied to newTest
c:	estmedia
ewTestBach's Brandenburg Concerto
 No. 3.RMI renamed to
c:	estmedia
ewTestBach's Brandenburg Concerto
 No. 3.RMI.bak
c:	estmedia
ewTestBeethoven's 5th Symphony.RMI deleted.
c:	estmedia
ewTestBeethoven's Fur Elise.RMI renamed to
c:	estmedia
ewTestBeethoven's Fur Elise.RMI.bak
c:	estmedia
ewTestcanyon.mid deleted.

Reading and Writing Data

Reading and writing data is accomplished with the Stream class. Remember streams? This is a chapter about streams.[18]

Stream supports synchronous and asynchronous reads and writes. The .NET Framework provides a number of classes derived from Stream, including FileStream, MemoryStream, and NetworkStream. In addition, there is a BufferedStream class that provides buffered I/O, and can be used with any of the other stream classes. Table 22-5 summarizes the principal classes involved with I/O.

Table 22-5. Principal I/O classes of the .NET Framework

Class

Use

Stream

Abstract class that supports reading and writing bytes

BinaryReader/BinaryWriter

Read and write encoded strings and primitive datatypes to and from streams

File, FileInfo, Directory, DirectoryInfo

Provide implementations for the abstract FileSystemInfo classes, including creating, moving, renaming, and deleting files and directories

FileStream

For reading to and from File objects; supports random access to files; opens files synchronously by default; supports asynchronous file access

TextReader, TextWriter, StringReader, StringWriter

TextReader and TextWriter are abstract classes designed for Unicode character I/O; StringReader and StringWriter write to and from strings, allowing your input and output to be either a stream or a string

BufferedStream

A stream that adds buffering to another stream such as a NetworkStream; BufferedStreams can improve the performance of the stream to which they are attached, but note that FileStream has buffering built in

MemoryStream

A nonbuffered stream whose encapsulated data is directly accessible in memory, and is most useful as a temporary buffer

NetworkStream

A stream over a network connection

Binary Files

This section starts by using the basic Stream class to perform a binary read of a file. The term binary read is used to distinguish from a text read. If you don’t know for certain that a file is just text, it is safest to treat it as a stream of bytes, known as a binary file.

The Stream class is chock-a-block with methods, but the most important are Read( ), Write( ), BeginRead( ), BeginWrite( ), and Flush( ). We will cover all of these in the next few sections.

To perform a binary read, begin by creating a pair of Stream objects, one for reading and one for writing:

Stream inputStream = File.OpenRead(
    @"C:	estsource	est1.cs");

Stream outputStream = File.OpenWrite(
    @"C:	estsource	est1.bak");

To open the files to read and write, you use the static OpenRead( ) and OpenWrite( ) methods of the File class. The static overload of these methods takes the path for the file as an argument, as shown previously.

Binary reads work by reading into a buffer. A buffer is just an array of bytes that will hold the data read by the Read( ) method.

Pass in the buffer, the offset in the buffer at which to begin storing the data read in, and the number of bytes to read. InputStream.Read reads bytes from the backing store into the buffer and returns the total number of bytes read.

It continues reading until no more bytes remain:

while ( (bytesRead =
         inputStream.Read(buffer,0,SIZE_BUFF)) > 0 )
{
    outputStream.Write(buffer,0,bytesRead);
}

Each bufferful of bytes is written to the output file. The arguments to Write( ) are the buffer from which to read, the offset into that buffer at which to start reading, and the number of bytes to write. Notice that you write the same number of bytes as you just read.

Example 22-4 provides the complete listing.

Example 22-4. Implementing a binary read and write to a file
using System;
using System.IO;


namespace ImplementingBinaryReadWriteToFile
{
    class Tester
    {
        const int SizeBuff = 1024;

        public static void Main(  )
        {
            // make an instance and run it
            Tester t = new Tester(  );
            t.Run(  );
        }

        // Set it running with a directory name
        private void Run(  )
        {
            // the file to read from
            Stream inputStream = File.OpenRead(
                @"C:	estsource	est1.cs");

            // the file to write to
            Stream outputStream = File.OpenWrite(
                @"C:	estsource	est1.bak");

            // create a buffer to hold the bytes
            byte[] buffer = new Byte[SizeBuff];
            int bytesRead;

            // while the read method returns bytes
            // keep writing them to the output stream
            while ((bytesRead =
            inputStream.Read(buffer, 0, SizeBuff)) > 0)
            {
                outputStream.Write(buffer, 0, bytesRead);
            }

            // tidy up before exiting
            inputStream.Close(  );
            outputStream.Close(  );
        }
    }
}

Tip

Before you run this program, create the C: estsource subdirectory and add a file (containing the source to this program) named test1.cs. As with previous examples, Unix, Linux, and Mac OS X readers should adjust the paths appropriately.

The result of running this program is that a copy of the input file (test1.cs) is made in the same directory and is named test1.bak. You can compare these files using your favorite file comparison tool; they are identical, as shown in Figure 22-1.[19]

File comparison showing the two files are identical
Figure 22-1. File comparison showing the two files are identical

Buffered Streams

In the previous example, you created a buffer to read into. When you called Read( ), a bufferful was read from disk. It might be, however, that the operating system can be much more efficient if it reads a larger (or smaller) number of bytes at once.

A buffered stream object creates an internal buffer, and reads bytes to and from the backing store in whatever increments it thinks are most efficient. It will still fill your buffer in the increments you dictate, but your buffer is filled from the in-memory buffer, not from the backing store. The net effect is that the input and output are more efficient and thus faster.

A BufferedStream object is composed around an existing Stream object that you already have created. To use a BufferedStream, start by creating a normal stream class as you did in Example 22-4:

Stream inputStream = File.OpenRead(
    @"C:	estsourcefolder3.cs");

Stream outputStream = File.OpenWrite(
    @"C:	estsourcefolder3.bak");

Once you have the normal stream, pass that stream object to the buffered stream’s constructor:

BufferedStream bufferedInput =
    new BufferedStream(inputStream);

BufferedStream bufferedOutput =
    new BufferedStream(outputStream);

You can then use the BufferedStream as a normal stream, calling Read( ) and Write( ) just as you did before. The operating system handles the buffering:

while ( (bytesRead =
     bufferedInput.Read(buffer,0,SIZE_BUFF)) > 0 )
{
    bufferedOutput.Write(buffer,0,bytesRead);
}

Remember to flush the buffer when you want to ensure that the data is written out to the file:

bufferedOutput.Flush(  );

This essentially tells the in-memory buffer to flush out its contents.

Tip

Note that all streams should be closed, though the finalizer will eventually close them for you if you just let them go out of scope. In a robust program, you should always explicitly close the buffer.

Example 22-5 provides the complete listing.

Example 22-5. Implementing buffered I/O
using System;
using System.IO;

namespace Programming_CSharp
{
    class Tester
    {
        const int SizeBuff = 1024;

        public static void Main(  )
        {
            // make an instance and run it
            Tester t = new Tester(  );
            t.Run(  );
        }

        // Set it running with a directory name
        private void Run(  )
        {
            // create binary streams
            Stream inputStream = File.OpenRead(
                @"C:	estsourcefolder3.cs");

            Stream outputStream = File.OpenWrite(
                @"C:	estsourcefolder3.bak");

            // add buffered streams on top of the
            // binary streams
            BufferedStream bufferedInput =
                new BufferedStream(inputStream);

            BufferedStream bufferedOutput =
                new BufferedStream(outputStream);
            byte[] buffer = new Byte[SizeBuff];
            int bytesRead;

            while ((bytesRead =
            bufferedInput.Read(buffer, 0, SizeBuff)) > 0)
            {
                bufferedOutput.Write(buffer, 0, bytesRead);
            }

            bufferedOutput.Flush(  );
            bufferedInput.Close(  );
            bufferedOutput.Close(  );
        }
    }
}

With larger files, this example should run more quickly than Example 22-4 did.

Working with Text Files

If you know that the file you are reading (and writing) contains nothing but text, you might want to use the StreamReader and StreamWriter classes. These classes are designed to make text manipulation easier. For example, they support the ReadLine( ) and WriteLine( ) methods that read and write a line of text at a time. You’ve already used WriteLine( ) with the Console object.

To create a StreamReader instance, start by creating a FileInfo object, and then call the OpenText( ) method on that object:

FileInfo theSourceFile =
    new FileInfo (@"C:	estsource	est1.cs");

StreamReader stream = theSourceFile.OpenText(  );

OpenText( ) returns a StreamReader for the file. With the StreamReader in hand, you can now read the file, line by line:

do
{
    text = stream.ReadLine(  );
} while (text != null);

ReadLine( ) reads a line at a time until it reaches the end of the file. The StreamReader will return null at the end of the file.

To create the StreamWriter class, call the StreamWriter constructor, passing in the full name of the file you want to write to:

StreamWriter writer = new
StreamWriter(@"C:	estsourcefolder3.bak",false);

The second parameter is the Boolean argument append. If the file already exists, true will cause the new data to be appended to the end of the file, and false will cause the file to be overwritten. In this case, pass in false, overwriting the file if it exists.

You can now create a loop to write out the contents of each line of the old file into the new file, and while you’re at it, to print the line to the console as well:

do
{
    text = reader.ReadLine(  );
    writer.WriteLine(text);
    Console.WriteLine(text);
} while (text != null);

Example 22-6 provides the complete source code.

Example 22-6. Reading and writing to a text file
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace ReadingWritingToTextFile
{
    class Tester
    {
        public static void Main(  )
        {
            // make an instance and run it
            Tester t = new Tester(  );
            t.Run(  );
        }

        // Set it running with a directory name
        private void Run(  )
        {
            // open a file
            FileInfo theSourceFile = new FileInfo(
                @"C:	estsource	est.cs");

            // create a text reader for that file
            StreamReader reader = theSourceFile.OpenText(  );

            // create a text writer to the new file
            StreamWriter writer = new StreamWriter(
                @"C:	estsource	est.bak", false);
            // create a text variable to hold each line
            string text;

            // walk the file and read every line
            // writing both to the console
            // and to the file
            do
            {
                text = reader.ReadLine(  );
                writer.WriteLine(text);
                Console.WriteLine(text);
            } while (text != null);

            // tidy up
            reader.Close(  );
            writer.Close(  );
        }
    }
}

When this program is run, the contents of the original file are written both to the screen and to the new file. Notice the syntax for writing to the console:

Console.WriteLine(text);

This syntax is nearly identical to that used to write to the file:

writer.WriteLine(text);

The key difference is that the WriteLine( ) method of Console is static, and the WriteLine( ) method of StreamWriter, which is inherited from TextWriter, is an instance method, and thus must be called on an object rather than on the class itself.

Asynchronous I/O

All the programs you’ve looked at so far perform synchronous I/O, meaning that while your program is reading or writing, all other activity is stopped. It can take a long time (relatively speaking) to read data to or from the backing store, especially if the backing store is a slow disk or (horrors!) a source on the Internet.

With large files, or when reading or writing across the network, you’ll want asynchronous I/O, which allows you to begin a read and then turn your attention to other matters while the CLR fulfills your request. The .NET Framework provides asynchronous I/O through the BeginRead( ) and BeginWrite( ) methods of Stream.

The sequence is to call BeginRead( ) on your file and then to go on to other, unrelated work while the read continues, possibly in another thread. When the read completes, you are notified via a callback method. You can then process the data that was read, kick off another read, and then go back to your other work.

In addition to the three parameters you’ve used in the binary read (the buffer, the offset, and how many bytes to read), BeginRead( ) asks for a delegate and a state object.

Tip

This is an instance of the more general async pattern seen throughout .NET (e.g., async stream I/O, async socket operations, async delegate invocation, etc.).

The delegate is an optional callback method, which, if provided, is called when the data is read. The state object is also optional. In this example, pass in null for the state object. The state of the object is kept in the member variables of the test class.

You are free to put any object you like in the state parameter, and you can retrieve it when you are called back. Typically (as you might guess from the name), you stash away state values that you’ll need on retrieval. The developer can use the state parameter to hold the state of the call (paused, pending, running, etc.).

In this example, create the buffer and the Stream object as private member variables of the class:

public class AsynchIOTester
{
    private Stream inputStream;
    private byte[] buffer;
    const int BufferSize = 256;

In addition, create your delegate as a private member of the class:

private AsyncCallback myCallBack; // delegated method

The delegate is declared to be of type AsyncCallback, which is what the BeginRead( ) method of Stream expects.

An AsyncCallback delegate is declared in the System namespace as follows:

public delegate void AsyncCallback (IAsyncResult ar);

Thus, this delegate can be associated with any method that returns void and that takes an IAsyncResult interface as a parameter. The CLR will pass in the IAsyncResult interface object at runtime when the method is called. You only have to declare the method:

void OnCompletedRead(IAsyncResult asyncResult)

and then hook up the delegate in the constructor:

AsynchIOTester(  )
{
    //...
    myCallBack = new AsyncCallback(this.OnCompletedRead);
}

Here’s how it works, step by step. In Main( ), create an instance of the class and tell it to run:

public static void Main(  )
{
    AsynchIOTester theApp = new AsynchIOTester(  );
    theApp.Run(  );
}

The call to new invokes the constructor. In the constructor, open a file and get a Stream object back. Then, allocate space in the buffer, and hook up the callback mechanism:

AsynchIOTester(  )
{
    inputStream = File.OpenRead(@"C:	estsourceAskTim.txt");
    buffer = new byte[BufferSize];
    myCallBack = new AsyncCallback(this.OnCompletedRead);
}

Tip

This example needs a large text file. I’ve copied a column written by Tim O’Reilly (“Ask Tim”) from http://www.oreilly.com into a text file named AskTim.txt. I placed that in the subdirectory testsource on my C: drive. You can use any text file in any subdirectory.

In the Run( ) method, call BeginRead( ), which causes an asynchronous read of the file:

inputStream.BeginRead(
    buffer, // where to put the results
    0, // offset
    buffer.Length, // BufferSize
    myCallBack, // call back delegate
    null); // local state object

Then, go on to do other work. In this case, simulate useful work by counting up to 500,000, displaying your progress every 1,000 iterations:

for (long i = 0; i < 500000; i++)
{
    if (i%1000 == 0)
    {
        Console.WriteLine("i: {0}", i);
    }
}

When the read completes, the CLR will call your callback method:

void OnCompletedRead(IAsyncResult asyncResult)
{

The first thing to do when notified that the read has completed is to find out how many bytes were actually read. Do so by calling the EndRead( ) method of the Stream object, passing in the IAsyncResult interface object passed in by the CLR:

int bytesRead = inputStream.EndRead(asyncResult);

EndRead( ) returns the number of bytes read. If the number is greater than zero, you’ll convert the buffer into a string, and write it to the console, and then call BeginRead( ) again, for another asynchronous read:

if (bytesRead > 0)
{
    String s =
    Encoding.ASCII.GetString (buffer, 0, bytesRead);
    Console.WriteLine(s);
    inputStream.BeginRead(
        buffer, 0, buffer.Length,
        myCallBack, null);
}

The effect is that you can do other work while the reads are taking place, but you can handle the read data (in this case, by outputting it to the console) each time a bufferful is ready. Example 22-7 provides the complete program.

Example 22-7. Implementing asynchronous I/O
using System;
using System.IO;

namespace AsynchronousIO
{
    public class AsynchIOTester
    {
        private Stream inputStream;

        // delegated method
        private AsyncCallback myCallBack;

        // buffer to hold the read data
        private byte[] buffer;

        // the size of the buffer
        const int BufferSize = 256;

        // constructor
        AsynchIOTester(  )
        {
            // open the input stream
            inputStream = File.OpenRead(
                @"C:	estsourceAskTim.txt");

            // allocate a buffer
            buffer = new byte[BufferSize];

            // assign the callback
            myCallBack =
                new AsyncCallback(this.OnCompletedRead);
        }
        public static void Main(  )
        {
            // create an instance of AsynchIOTester
            // which invokes the constructor
            AsynchIOTester theApp = new AsynchIOTester(  );

            // call the instance method
            theApp.Run(  );
        }

        void Run(  )
        {
            inputStream.BeginRead(
                buffer, // holds the results
                0, // offset
                buffer.Length, // (BufferSize)
                myCallBack, // callback delegate
                null); // local state object

            // do some work while data is read
            for (long i = 0; i < 500000; i++)
            {
                if (i % 1000 == 0)
                {
                    Console.WriteLine("i: {0}", i);
                }
            }
        }

        // callback method
        void OnCompletedRead(IAsyncResult asyncResult)
        {
            int bytesRead =
            inputStream.EndRead(asyncResult);

            // if we got bytes, make them a string
            // and display them, then start up again.
            // Otherwise, we're done.
            if (bytesRead > 0)
            {
                String s =
                    Encoding.ASCII.GetString(buffer, 0, bytesRead);
                Console.WriteLine(s);
                inputStream.BeginRead(
                    buffer, 0, buffer.Length, myCallBack, null);
            }
        }
    }
}
Output (excerpt):
i: 47000
i: 48000
i: 49000
Date: January 2001
From: Dave Heisler
To: Ask Tim
Subject: Questions About O'Reilly
Dear Tim,
I've been a programmer for about ten years. I had heard of
O'Reilly books,then...
Dave,
You might be amazed at how many requests for help with
school projects I get;
i: 50000
i: 51000
i: 52000

The output reveals that the program is working on the two threads concurrently. The reads are done in the background while the other thread is counting and printing out every one-thousandth iteration. As the reads complete, they are printed to the console, and then you go back to counting. (I’ve shortened the listings to illustrate the output.)

In a real-world application, you might process user requests or compute values while the asynchronous I/O is busy retrieving or storing to a file or database.

Network I/O

Writing to a remote object on the Internet isn’t very different from writing to a file on your local machine. You might want to do this if your program needs to store its data to a file on a machine on your network, or if you are creating a program that displays information on a monitor connected to another computer on your network.

Network I/O is based on the use of streams created with sockets. Sockets are very useful for client/server and peer-to-peer (P2P) applications, and when making remote procedure calls.

A socket is an object that represents an endpoint for communication between processes communicating across a network. Sockets can work with various protocols, including UDP and TCP. In this section, we will create a TCP/IP connection between a server and a client. TCP/IP is a connection-based stream-like protocol for network communication. Connection-based means that with TCP/IP, once a connection is made, the two processes can talk with one another as though they were connected by a direct phone line.

Tip

Although TCP/IP is designed to talk across a network, you can simulate network communication by running the two processes on the same machine.

It is possible for more than one application on a given computer to be talking to various clients all at the same time (e.g., you might be running a web server, an FTP server, and a program that provides calculation support). Therefore, each application must have a unique ID so that the client can indicate which application it is looking for. That ID is known as a port. Think of the IP address as a phone number and the port as an extension.

The server instantiates a TcpListener and tells the listener to listen for connections on a specific port. The constructor for the TcpListener has two parameters, an IP address and an int representing the port on which that listener should listen.

Client applications connect to a specific IP address. For example, Yahoo!’s IP address is 66.94.234.13. Clients must also connect to a specific port. All web browsers connect to port 80 by default. Port numbers range from 0 to 65535 (e.g., 216); however, some numbers are reserved.[20]

Tip

Ports are divided into the following ranges:

  • 0-1023: well-known ports

  • 1024-49151: registered ports

  • 49152-65535: dynamic and/or private ports

For a list of all the well-known and registered ports, look at http://www.iana.org/assignments/port-numbers.

Once the listener is created, call Start( ) on it, telling the listener to begin accepting network connections. When the server is ready to start responding to calls from clients, call AcceptSocket( ). The thread in which you’ve called AcceptSocket( ) blocks (waiting sadly by the phone, wringing its virtual hands, hoping for a call).

You can imagine creating the world’s simplest listener. It waits patiently for a client to call. When it gets a call, it interacts with that client to the exclusion of all other clients. The next few clients to call will connect, but they will automatically be put on hold. While they are listening to the music and being told their call is important and will be handled in the order received, they will block in their own threads. Once the backlog (hold) queue fills, subsequent callers will get the equivalent of a busy signal. They must hang up and wait for our simple socket to finish with its current client. This model works fine for servers that take only one or two requests a week, but it doesn’t scale well for real-world applications. Most servers need to handle thousands, even tens of thousands, of connections a minute!

To handle a high volume of connections, applications use asynchronous I/O to accept a call and create a socket with the connection to the client. The original listener then returns to listening, waiting for the next client. This way, your application can handle many calls; each time a call is accepted, a new socket is created.

The client is unaware of this sleight of hand in which a new socket is created. As far as the client is concerned, he has connected with the IP address and port he requested. Note that the new socket establishes a connection with the client. This is quite different from UDP, which uses a connectionless protocol. With TCP/IP, once the connection is made, the client and server know how to talk with each other without having to readdress each packet.

Creating a Network Streaming Server

To create a network server for TCP/IP streaming, start by creating a TcpListener object to listen to the TCP/IP port you’ve chosen. I’ve arbitrarily chosen port 65000 from the available port IDs:

IPAddress localAddr = IPAddress.Parse("127.0.0.1");
TcpListener tcpListener = new TcpListener(localAddr, 65000);

Once the TcpListener object is constructed, you can ask it to start listening:

tcpListener.Start(  );

Now, wait for a client to request a connection:

Socket socketForClient = tcpListener.AcceptSocket(  );

The AcceptSocket method of the TcpListener object returns a Socket object that represents a Berkeley socket interface and is bound to a specific endpoint. AcceptSocket( ) is a synchronous method that will not return until it receives a connection request.

Tip

Because the model is widely accepted by computer vendors, Berkeley sockets simplify the task of porting existing socket-based source code from Windows and Unix environments.

Once you have a socket you’re ready to send the file to the client. Create a NetworkStream class, passing the socket into the constructor:

NetworkStream networkStream = new NetworkStream(socketForClient);

Then create a StreamWriter object much as you did before, except this time not on a file, but rather on the NetworkStream you just created:

System.IO.StreamWriter streamWriter = new
    System.IO.StreamWriter(networkStream);

When you write to this stream, the stream is sent over the network to the client. Example 22-8 shows the entire server. (I’ve stripped this server down to its bare essentials. With a production server, you almost certainly would run the request processing code in a thread, and you’d want to enclose the logic in try blocks to handle network problems.)

Example 22-8. Implementing a network streaming server
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace NetworkStreamingServer
{
    public class NetworkIOServer
    {
        public static void Main(  )
        {
            NetworkIOServer app = new NetworkIOServer(  );
            app.Run(  );
        }

        private void Run(  )
        {
            // create a new TcpListener and start it up
            // listening on port 65000

            IPAddress localAddr = IPAddress.Parse("127.0.0.1");
            TcpListener tcpListener = new TcpListener(localAddr, 65000);
            tcpListener.Start(  );

            // keep listening until you send the file
            for (; ; )
            {
                // if a client connects, accept the connection
                // and return a new socket named socketForClient
                // while tcpListener keeps listening
                Socket socketForClient =
                tcpListener.AcceptSocket(  );
                Console.WriteLine("Client connected");

                // call the helper method to send the file
                SendFileToClient(socketForClient);

                Console.WriteLine("Disconnecting from client...");

                // clean up and go home
                socketForClient.Close(  );
                Console.WriteLine("Exiting...");
                break;
            }
        }

        // helper method to send the file
        private void SendFileToClient(
        Socket socketForClient)
        {
            // create a network stream and a stream writer
            // on that network stream
            NetworkStream networkStream =
            new NetworkStream(socketForClient);
            System.IO.StreamWriter streamWriter =
            new System.IO.StreamWriter(networkStream);

            // create a stream reader for the file
            System.IO.StreamReader streamReader =
                new System.IO.StreamReader(
                    @"C:	estsourcemyTest.txt");

            string theString;

            // iterate through the file, sending it
            // line by line to the client
            do
            {
                theString = streamReader.ReadLine(  );

                if (theString != null)
                {
                    Console.WriteLine(
                        "Sending {0}", theString);
                    streamWriter.WriteLine(theString);
                    streamWriter.Flush(  );
                }
            }
            while (theString != null);

            // tidy up
            streamReader.Close(  );
            networkStream.Close(  );
            streamWriter.Close(  );
        }
    }
}

Creating a Streaming Network Client

The client instantiates a TcpClient class, which represents a TCP/IP client connection to a host:

TcpClient socketForServer;
socketForServer = new TcpClient("localHost", 65000);

With this TcpClient, you can create a NetworkStream, and on that stream, you can create a StreamReader:

NetworkStream networkStream = socketForServer.GetStream(  );
System.IO.StreamReader streamReader =
    new System.IO.StreamReader(networkStream);

Now, read the stream as long as there is data on it, outputting the results to the console:

do
{
    outputString = streamReader.ReadLine(  );

    if( outputString != null )
    {
        Console.WriteLine(outputString);
    }
}
while( outputString != null );

Example 22-9 is the complete client.

Example 22-9. Implementing a network streaming client
using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Text;

namespace NetworkStreamingClient
{
    public class Client
    {
        static public void Main(string[] Args)
        {
            // create a TcpClient to talk to the server
            TcpClient socketForServer;
            try
            {
                socketForServer =
                    new TcpClient("localHost", 65000);
            }
            catch
            {
                Console.WriteLine(
                    "Failed to connect to server at {0}:65000",
                    "localhost");
                return;
            }

            // create the Network Stream and the Stream Reader object
            NetworkStream networkStream =
                socketForServer.GetStream(  );
            System.IO.StreamReader streamReader =
                new System.IO.StreamReader(networkStream);

            try
            {
                string outputString;

                // read the data from the host and display it
                do
                {
                    outputString = streamReader.ReadLine(  );

                    if (outputString != null)
                    {
                        Console.WriteLine(outputString);
                    }
                }
                while (outputString != null);
            }
            catch
            {
                Console.WriteLine(
                "Exception reading from Server");
            }

            // tidy up
            networkStream.Close(  );
        }
    }
}

To test this, I created a simple test file named myTest.txt:

This is line one
This is line two
This is line three
This is line four

Here is the output from the server and the client:

Output (Server):

Client connected
Sending This is line one
Sending This is line two
Sending This is line three
Sending This is line four
Disconnecting from client...
Exiting...
Output (Client):

This is line one
This is line two
This is line three
This is line four
Press any key to continue

Tip

If you are testing this on a single machine, run the client and server in separate command windows or individual instances of the development environment. You need to start the server first, or the client will fail, saying it can’t connect. If you aren’t running this on a single machine, you need to replace occurrences of 127.0.0.1 and localhost with the IP address of the machine running the server. If you are running Windows XP Service Pack 2 with the default settings, you will get a Windows Security Alert asking whether you want to unblock the port.

Handling Multiple Connections

As I mentioned earlier, this example doesn’t scale well. Each client demands the entire attention of the server. A server is needed that can accept the connection and then pass the connection to overlapped I/O, providing the same asynchronous solution that you used earlier for reading from a file.

To manage this, create a new server, AsynchNetworkServer, which will nest within it a new class, ClientHandler. When your AsynchNetworkServer receives a client connection, it instantiates a ClientHandler, and passes the socket to that ClientHandler instance.

The ClientHandler constructor will create a copy of the socket and a buffer and open a new NetworkStream on that socket. It then uses overlapped I/O to asynchronously read and write to that socket. For this demonstration, it simply echoes whatever text the client sends back to the client and also to the console.

To create the asynchronous I/O, ClientHandler defines two delegate methods, OnReadComplete( ) and OnWriteComplete( ), that manage the overlapped I/O of the strings sent by the client.

The body of the Run( ) method for the server is very similar to what you saw in Example 22-8. First, create a listener and then call Start( ). Then, create a forever loop and call AcceptSocket( ). Once the socket is connected, instead of handling the connection, create a new ClientHandler and call StartRead( ) on that object.

Example 22-10 shows the complete source for the server.

Example 22-10. Implementing an asynchronous network streaming server
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace AsynchNetworkServer
{
    public class AsynchNetworkServer
    {
        class ClientHandler
        {
            private byte[] buffer;
            private Socket socket;
            private NetworkStream networkStream;
            private AsyncCallback callbackRead;
            private AsyncCallback callbackWrite;

            public ClientHandler(Socket socketForClient)
            {
                socket = socketForClient;
                buffer = new byte[256];
                networkStream =
                    new NetworkStream(socketForClient);

                callbackRead =
                    new AsyncCallback(this.OnReadComplete);

                callbackWrite =
                    new AsyncCallback(this.OnWriteComplete);
            }

            // begin reading the string from the client
            public void StartRead(  )
            {
                networkStream.BeginRead(
                    buffer, 0, buffer.Length,
                    callbackRead, null);
            }

            // when called back by the read, display the string
            // and echo it back to the client
            private void OnReadComplete(IAsyncResult ar)
            {
                int bytesRead = networkStream.EndRead(ar);

                if (bytesRead > 0)
                {
                    string s =
                        System.Text.Encoding.ASCII.GetString(
                            buffer, 0, bytesRead);
                    Console.Write(
                        "Received {0} bytes from client: {1}",
                        bytesRead, s);
                    networkStream.BeginWrite(
                        buffer, 0, bytesRead, callbackWrite, null);
                }
                else
               {
                    Console.WriteLine("Read connection dropped");
                    networkStream.Close(  );
                    socket.Close(  );
                    networkStream = null;
                    socket = null;
                }
            }

            // after writing the string, print a message and resume reading
            private void OnWriteComplete(IAsyncResult ar)
            {
                networkStream.EndWrite(ar);
                Console.WriteLine("Write complete");
                networkStream.BeginRead(
                    buffer, 0, buffer.Length,
                    callbackRead, null);
            }
        }

        public static void Main(  )
        {
            AsynchNetworkServer app = new AsynchNetworkServer(  );
            app.Run(  );
        }

        private void Run(  )
        {
            // create a new TcpListener and start it up
            // listening on port 65000

            IPAddress localAddr = IPAddress.Parse("127.0.0.1");
            TcpListener tcpListener = new TcpListener(localAddr, 65000);
            tcpListener.Start(  );

            // keep listening until you send the file
            for (; ; )
            {
                // if a client connects, accept the connection
                // and return a new socket named socketForClient
                // while tcpListener keeps listening
                Socket socketForClient = tcpListener.AcceptSocket(  );
                Console.WriteLine("Client connected");
                ClientHandler handler =
                    new ClientHandler(socketForClient);
                    handler.StartRead(  );
            }
        }
    }
}

The server starts up and listens to port 65000. If a client connects, the server instantiates a ClientHandler that will manage the I/O with the client while the server listens for the next client.

Tip

In this example, you write the string received from the client to the console in OnReadComplete( ) and OnWriteComplete( ). Writing to the console can block your thread until the write completes. In a production program, you don’t want to take any blocking action in these methods because you are using a pooled thread. If you block in OnReadComplete( ) or OnWriteComplete( ), you may cause more threads to be added to the thread pool, which is inefficient and will harm performance and scalability.

The client code is very simple. The client creates a tcpSocket for the port on which the server will listen (65000), and creates a NetworkStream object for that socket. It then writes a message to that stream and flushes the buffer. The client creates a StreamReader to read on that stream, and writes whatever it receives to the console. Example 22-11 shows the complete source for the client.

Example 22-11. Implementing a client for asynchronous network I/O
using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Text;

namespace AsynchNetworkClient
{
    public class AsynchNetworkClient
    {
        private NetworkStream streamToServer;

        static public int Main(  )
        {
            AsynchNetworkClient client =
                new AsynchNetworkClient(  );
            return client.Run(  );
        }

        AsynchNetworkClient(  )
        {
            string serverName = "localhost";
            Console.WriteLine("Connecting to {0}", serverName);
            TcpClient tcpSocket = new TcpClient(serverName, 65000);
            streamToServer = tcpSocket.GetStream(  );
        }

        private int Run(  )
        {
            string message = "Hello Programming C#";
            Console.WriteLine(
                "Sending {0} to server.", message);

            // create a streamWriter and use it to
            // write a string to the server
            System.IO.StreamWriter writer =
                new System.IO.StreamWriter(streamToServer);
            writer.WriteLine(message);
            writer.Flush(  );

            // Read response
            System.IO.StreamReader reader =
                new System.IO.StreamReader(streamToServer);
            string strResponse = reader.ReadLine(  );
            Console.WriteLine("Received: {0}", strResponse);
            streamToServer.Close(  );
            return 0;
        }
    }
}


Output (Server):
Client connected
Received 22 bytes from client: Hello Programming C#
Write complete
Read connection dropped

Output (Client):
Connecting to localhost
Sending Hello Programming C# to server.
Received: Hello Programming C#

In this example, the network server doesn’t block while it is handling client connections, but rather, it delegates the management of those connections to instances of ClientHandler. Clients should not experience a delay waiting for the server to handle their connections.

Asynchronous Network File Streaming

You can now combine the skills you learned for asynchronous file reads with asynchronous network streaming to produce a program that serves a file to a client on demand.

Your server will begin with an asynchronous read on the socket, waiting to get a filename from the client. Once you have the filename, you can kick off an asynchronous read of that file on the server. As each bufferful of the file becomes available, you can begin an asynchronous write back to the client. When the asynchronous write to the client finishes, you can kick off another read of the file; in this way, you ping-pong back and forth, filling the buffer from the file and writing the buffer out to the client. The client need do nothing but read the stream from the server. In the next example, the client will write the contents of the file to the console, but you could easily begin an asynchronous write to a new file on the client, thereby creating a network-based file copy program.

The structure of the server isn’t unlike that shown in Example 22-10. Once again you will create a ClientHandler class, but this time, you will add an AsyncCallback named myFileCallBack, which you initialize in the constructor along with the callbacks for the network read and write:

myFileCallBack =
    new AsyncCallback(this.OnFileCompletedRead);

callbackRead =
    new AsyncCallback(this.OnReadComplete);

callbackWrite =
    new AsyncCallback(this.OnWriteComplete);

The Run( ) function of the outer class, now named AsynchNetworkFileServer, is unchanged. Once again you create and start the TcpListener class as well as create a forever loop in which you call AcceptSocket( ). If you have a socket, instantiate the ClientHandler and call StartRead( ). As in the previous example, StartRead( ) kicks off a BeginRead( ), passing in the buffer and the delegate to OnReadComplete.

When the read from the network stream completes, your delegated method OnReadComplete( ) is called and it retrieves the filename from the buffer. If text is returned, OnReadComplete( ) retrieves a string from the buffer using the static System.Text.Encoding.ASCII.GetString( ) method:

if( bytesRead > 0 )
{
    string fileName =
        System.Text.Encoding.ASCII.GetString(
        buffer, 0, bytesRead);

You now have a filename; with that, you can open a stream to the file and use the exact same asynchronous file read used in Example 22-7:

inputStream =
    File.OpenRead(fileName);

inputStream.BeginRead(
    buffer, // holds the results
    0, // offset
    buffer.Length, // Buffer Size
    myFileCallBack, // callback delegate
    null); // local state object

This read of the file has its own callback that will be invoked when the input stream has read a bufferful from the file on the server disk drive.

Tip

As noted earlier, you normally shouldn’t take any action in an overlapped I/O method that might block the thread for any appreciable time. The call to open the file and begin reading it is normally pushed off to a helper thread instead of doing this work in OnReadComplete( ). I simplified it for this example to avoid distracting from the issues at hand.

When the buffer is full, OnFileCompletedRead( ) is called, which checks to see whether any bytes were read from the file. If so, it begins an asynchronous write to the network:

if (bytesRead > 0)
{
    // write it out to the client
    networkStream.BeginWrite(
        buffer, 0, bytesRead, callbackWrite, null);
}

If OnFileCompletedRead was called, and no bytes were read, this signifies that the entire file has been sent. The server reacts by closing the NetworkStream and socket, thus letting the client know that the transaction is complete:

networkStream.Close(  );
socket.Close(  );
networkStream = null;
socket = null;

When the network write completes, the OnWriteComplete( ) method is called, and this kicks off another read from the file:

private void OnWriteComplete( IAsyncResult ar )
{
    networkStream.EndWrite(ar);
    Console.WriteLine( "Write complete");

    inputStream.BeginRead(
        buffer, // holds the results
        0, // offset
        buffer.Length, // (BufferSize)
        myFileCallBack, // callback delegate
        null); // local state object

}

The cycle begins again with another read of the file, and the cycle continues until the file has been completely read and transmitted to the client. The client code simply writes a filename to the network stream to kick off the file read:

string message = @"C:	estsourceAskTim.txt";
System.IO.StreamWriter writer =
    new System.IO.StreamWriter(streamToServer);
writer.Write(message);
writer.Flush(  );

The client then begins a loop, reading from the network stream until no bytes are sent by the server. When the server is done, the network stream is closed. Start by initializing a Boolean value to false and creating a buffer to hold the bytes sent by the server:

bool fQuit = false;
while (!fQuit)
{
    char[] buffer = new char[BufferSize];

You are now ready to create a new StreamReader from the NetworkStream member variable streamToServer:

System.IO.StreamReader reader =
    new System.IO.StreamReader(streamToServer);

The call to Read( ) takes three parameters—the buffer, the offset at which to begin reading, and the size of the buffer:

int bytesRead = reader.Read(buffer,0, BufferSize);

Check to see whether the Read( ) returned any bytes; if not, you are done, and you can set the Boolean value fQuit to true, causing the loop to terminate:

if (bytesRead == 0)
    fQuit = true;

If you did receive bytes, you can write them to the console, or write them to a file, or do whatever it is you will do with the values sent from the server:

else
{
    string theString = new String(buffer);
    Console.WriteLine(theString);
}
}

Once you break out of the loop, close the NetworkStream:

streamToServer.Close(  );

Example 22-12 shows the complete annotated source for the server.

Example 22-12. Implementing a client for an asynchronous network file server
using System;
using System.Net.Sockets;
using System.Threading;
using System.Text;

public class AsynchNetworkClient
{
    private const int BufferSize = 256;
    private NetworkStream streamToServer;

    static public int Main(  )
    {
        AsynchNetworkClient client =
            new AsynchNetworkClient(  );
        return client.Run(  );
    }

    AsynchNetworkClient(  )
    {
        string serverName = "localhost";
        Console.WriteLine("Connecting to {0}", serverName);
        TcpClient tcpSocket = new TcpClient(serverName, 65000);
        streamToServer = tcpSocket.GetStream(  );
    }

    private int Run(  )
    {
        string message = @"C:	estsourceAskTim.txt";
        Console.Write("Sending {0} to server.", message);

        // create a streamWriter and use it to
        // write a string to the server
        System.IO.StreamWriter writer =
            new System.IO.StreamWriter(streamToServer);
        writer.Write(message);
        writer.Flush(  );

        bool fQuit = false;

        // while there is data coming
        // from the server, keep reading
        while (!fQuit)
        {
            // buffer to hold the response
            char[] buffer = new char[BufferSize];

            // Read response
            System.IO.StreamReader reader =
                new System.IO.StreamReader(streamToServer);

            // see how many bytes are
            // retrieved to the buffer
            int bytesRead = reader.Read(buffer, 0, BufferSize);
            if (bytesRead == 0) // none? quite
                fQuit = true;
            else // got some?
            {
                // display it as a string
                string theString = new String(buffer);
                Console.WriteLine(theString);
            }
        }
        streamToServer.Close(  ); // tidy up
        return 0;
    }
}

By combining the asynchronous file read with the asynchronous network read, you have created a scalable application that can handle requests from a number of clients.

Web Streams

Instead of reading from a stream provided by a custom server, you can just as easily read from any web page on the Internet.

A WebRequest is an object that requests a resource identified by a URI, such as the URL for a web page. You can use a WebRequest object to create a WebResponse object that will encapsulate the object pointed to by the URI. That is, you can call GetResponse( ) on your WebRequest object to get access to the object pointed to by the URI. What is returned is encapsulated in a WebResponse object. You can then ask that WebResponse object for a Stream object by calling GetResponseStream( ). GetResponseStream( ) returns a stream that encapsulates the contents of the web object (e.g., a stream with the web page).

The next example retrieves the contents of a web page as a stream. To get a web page, you’ll want to use HttpWebRequest. HttpWebRequest derives from WebRequest and provides additional support for interacting with the HTTP protocol.

To create the HttpWebRequest, cast the WebRequest returned from the static Create( ) method of the WebRequestFactory:

HttpWebRequest webRequest =
    (HttpWebRequest) WebRequest.Create
    ("http://www.libertyassociates.com/book_edit.htm");

Create( ) is a static method of WebRequest. When you pass in a URI, an instance of HttpWebRequest is created.

Tip

The method is overloaded on the type of the parameter. It returns different derived types depending on what is passed in. For example, if you pass in a URI, an object of type HttpWebRequest is created. The return type, however, is WebRequest, and so you must cast the returned value to HttpWebRequest.

Creating the HttpWebRequest establishes a connection to a page on your web site. What you get back from the host is encapsulated in an HttpWebResponse object, which is an HTTP protocol-specific subclass of the more general WebResponse class:

HttpWebResponse webResponse =
    (HttpWebResponse) webRequest.GetResponse(  );

You can now open a StreamReader on that page by calling the GetResponseStream( ) method of the WebResponse object:

StreamReader streamReader = new StreamReader(
    webResponse.GetResponseStream(  ), Encoding.ASCII);

You can read from that stream exactly as you read from the network stream. Example 22-13 shows the complete listing.

Example 22-13. Reading a web page as an HTML stream
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace ReadingWebPageAsHTML
{
    public class Client
    {
        static public void Main(string[] Args)
        {
            // create a webRequest for a particular page
            HttpWebRequest webRequest =
                (HttpWebRequest)WebRequest.Create
                    ("http://www.jesseliberty.com/");

            // ask the web request for a webResponse encapsulating
            // that page
            HttpWebResponse webResponse =
                (HttpWebResponse)webRequest.GetResponse(  );

            // get the streamReader from the response
            StreamReader streamReader = new StreamReader(
                webResponse.GetResponseStream(  ), Encoding.ASCII);

            try
            {
                string outputString;
                outputString = streamReader.ReadToEnd(  );
                Console.WriteLine(outputString);
            }
            catch
            {
                Console.WriteLine("Exception reading from web page");
            }
            streamReader.Close(  );
        }
    }
}
Output (excerpt):
<html>
<head>
<title>Liberty Associates</title>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
<script language="JavaScript">
<!--
isNS=(navigator.appName=="Netscape");
activeMenu="";
activeIndex=−1;
activeImg="";

window.onError = null;

function setImage(imgName,index) {
 if(activeImg==imgName)
 return true;
 document.images[imgName].src = rolloverImg[index].src;
 return true;
}

rolloverImg=new Array(  );

The output shows that what is sent through the stream is the HTML of the page you requested. You might use this capability for screen scraping: reading a page from a site into a buffer and then extracting the information you need.

Tip

All examples of screen scraping in this book assume that you are reading a site for which you have copyright permission.

Serialization

When an object is streamed to disk, its various member data must be serialized—that is, written out to the stream as a series of bytes. The object will also be serialized when stored in a database or when marshaled across a context, app domain, process, or machine boundary.

The CLR provides support for serializing an object graph—an object and all the member data of that object. By default, types aren’t serializable. To be able to serialize an object, you must explicitly mark it with the [Serializable] attribute.

The CLR will do the work of serializing your object for you. Because the CLR knows how to serialize all the primitive types, if your object consists of nothing but primitive types (all your member data consists of integers, longs, strings, etc.), you’re all set. If your object consists of other user-defined types (classes), you must ensure that these types are also serializable. The CLR will try to serialize each object contained by your object (and all their contained objects as well), but these objects themselves must be either primitive types or serializable, or else they will not be serialized.

Tip

When an object is marshaled, either by value or by reference, it must be serialized. The difference is only whether a copy is made or a proxy is provided to the client. Objects marked with the [Serializable] attribute are marshaled by value; those that derive from MarshalByRefObject are marshaled by reference, but both are serialized.

Using a Formatter

When data is serialized, it is eventually read by the same program or another program on the same or a different computer. In any case, the code reading the data expects that data to be in a particular format. Most of the time in a .NET application, the expected format is the native binary format or SOAP.

Tip

SOAP is a simple, lightweight, XML-based protocol for exchanging information across the Web. SOAP is highly modular and very extensible. It also leverages existing Internet technologies, such as HTTP and SMTP.

When data is serialized, the format of the serialization is determined by the formatter you apply. Formatter classes implement the interface IFormatter; you are also free to create your own formatter, though very few programmers will ever need or want to! The CLR provides a SoapFormatter for use with web services and a BinaryFormatter that is useful for fast local storage or remoting.

You can instantiate these objects with their default constructors:

BinaryFormatter binaryFormatter =
    new BinaryFormatter(  );

Once you have an instance of a formatter, you can invoke its Serialize( ) method, passing in a stream and an object to serialize. You’ll see how this is done in the next example.

Working with Serialization

To see serialization at work, you need a sample class that you can serialize and then deserialize. You can start by creating a class named SumOf. SumOf has three member variables:

private int startNumber = 1;
private int endNumber;
private int[] theSums;

The member array theSums represents the value of the sums of all the numbers from startNumber through endNumber. Thus, if startNumber is 1 and endNumber is 10, the array will have the values:

1,3,6,10,15,21,28,36,45,55

Each value is the sum of the previous value plus the next in the series. Thus, if the series is 1,2,3,4, the first value in theSums will be 1. The second value is the previous value (1) plus the next in the series (2); thus, theSums[1] will hold the value 3. Likewise, the third value is the previous value (3) plus the next in the series, so theSums[2] is 6. Finally, the fourth value in theSums is the previous value (6) plus the next in the series (4), for a value of 10.

The constructor for the SumOf object takes two integers: the starting number and the ending number. It assigns these to the local values, and then calls a helper function to compute the contents of the array:

public SumOf(int start, int end)
{
    startNumber = start;
    endNumber = end;
    ComputeSums(  );

The ComputeSums helper function fills in the contents of the array by computing the sums in the series from startNumber through endNumber:

private void ComputeSums(  )
{
    int count = endNumber - startNumber + 1;
    theSums = new int[count];
    theSums[0] = startNumber;
    for (int i=1,j=startNumber + 1;i<count;i++,j++)
    {
       theSums[i] = j + theSums[i−1];
    }
}

You can display the contents of the array at any time by using a foreach loop:

private void DisplaySums(  )
{
    foreach(int i in theSums)
    {
        Console.WriteLine("{0}, ",i);
    }
}

Serializing the object

Now, mark the class as eligible for serialization with the [Serializable] attribute:

[Serializable]
class SumOf

To invoke serialization, you first need a fileStream object into which you’ll serialize the SumOf object:

FileStream fileStream =
 new FileStream("DoSum.out",FileMode.Create);

You are now ready to call the formatter’s Serialize( ) method, passing in the stream and the object to serialize. Because this is done in a method of SumOf, you can pass in the this object, which points to the current object:

binaryFormatter.Serialize(fileStream,this);

This serializes the SumOf object to disk.

Deserializing the object

To reconstitute the object, open the file, and ask a binary formatter to DeSerialize it:

public static SumOf DeSerialize(  ){
    FileStream fileStream =
        new FileStream("DoSum.out",FileMode.Open);
    BinaryFormatter binaryFormatter =
        new BinaryFormatter(  );
    SumOf retVal = (SumOf) binaryFormatter.Deserialize(fileStream);
    fileStream.Close(  );
    return retVal;}

To make sure all this works, first, instantiate a new object of type SumOf and tell it to serialize itself. Then, create a new instance of type SumOf by calling the static deserializer and asking it to display its values:

public static void Main(  )
{
    Console.WriteLine("Creating first one with new...");
    SumOf app = new SumOf(1,10);

    Console.WriteLine(
        "Creating second one with deserialize...");
    SumOf newInstance = SumOf.DeSerialize(  );
    newInstance.DisplaySums(  );
}

Example 22-14 provides the complete source code to illustrate serialization and deserialization.

Example 22-14. Serializing and deserializing an object
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;

namespace SerializingDeserializingAnObject
{
    [Serializable]
    class SumOf
    {
        private int startNumber = 1;
        private int endNumber;
        private int[] theSums;

        public static void Main(  )
        {
            Console.WriteLine("Creating first one with new...");
            SumOf app = new SumOf(1, 10);

            Console.WriteLine("Creating second one with deserialize...");
            SumOf newInstance = SumOf.DeSerialize(  );
            newInstance.DisplaySums(  );
        }

        public SumOf(int start, int end)
        {
            startNumber = start;
            endNumber = end;
            ComputeSums(  );
            DisplaySums(  );
            Serialize(  );
        }

        private void ComputeSums(  )
        {
            int count = endNumber - startNumber + 1;
            theSums = new int[count];
            theSums[0] = startNumber;
            for (int i = 1, j = startNumber + 1; i < count; i++, j++)
            {
                theSums[i] = j + theSums[i − 1];
            }
        }

        private void DisplaySums(  )
        {
            foreach (int i in theSums)
            {
                Console.WriteLine("{0}, ", i);
            }
        }

        private void Serialize(  )
        {
            Console.Write("Serializing...");
            // create a file stream to write the file
            FileStream fileStream =
                new FileStream("DoSum.out", FileMode.Create);
            // use the CLR binary formatter
            BinaryFormatter binaryFormatter =
                new BinaryFormatter(  );
            // serialize to disk
            binaryFormatter.Serialize(fileStream, this);
            Console.WriteLine("...completed");
            fileStream.Close(  );
        }

        public static SumOf DeSerialize(  )
        {
            FileStream fileStream =
                new FileStream("DoSum.out", FileMode.Open);
            BinaryFormatter binaryFormatter =
                new BinaryFormatter(  );
            SumOf retVal = (SumOf)binaryFormatter.Deserialize(fileStream);
            fileStream.Close(  );
            return retVal;
        }
    }
}


Output:
Creating first one with new...
1,
3,
6,
10,
15,
21,
28,
36,
45,
55,
Serializing......completed
Creating second one with deserialize...
1,
3,
6,
10,
15,
21,
28,
36,
45,
55,

The output shows that the object was created, displayed, and then serialized. The object was then deserialized and output again, with no loss of data.

Handling Transient Data

In some ways, the approach to serialization demonstrated in Example 22-14 is very wasteful. Because you can compute the contents of the array given its starting and ending numbers, there really is no reason to store its elements to disk. Although the operation might be inexpensive with a small array, it could become costly with a very large one.

You can tell the serializer not to serialize some data by marking it with the [NonSerialized] attribute:

[NonSerialized] private int[] theSums;

If you don’t serialize the array, however, the object you create will not be correct when you deserialize it. The array will be empty. Remember, when you deserialize the object, you simply read it up from its serialized form; no methods are run.

To fix the object before you return it to the caller, implement the IDeserializationCallback interface:

[Serializable]
class SumOf : IDeserializationCallback

Also, implement the one method of this interface: OnDeserialization( ). The CLR promises that if you implement this interface, your class’ OnDeserialization( ) method will be called when the entire object graph has been deserialized. This is just what you want: the CLR will reconstitute what you’ve serialized, and then you have the opportunity to fix up the parts that were not serialized.

This implementation can be very simple. Just ask the object to recompute the series:

public virtual void OnDeserialization (Object sender)
{
    ComputeSums(  );
}

This is a classic space/time trade-off; by not serializing the array, you may make deserialization somewhat slower (because you must take the time to recompute the array), and you make the file somewhat smaller. To see whether not serializing the array had any effect, I ran the program with the digits 1–5,000. Before setting [NonSerialized] on the array, the serialized file was 20 KB. After setting [NonSerialized], the file was 1 KB. Not bad. Example 22-15 shows the source code using the digits 1–5 as input (to simplify the output).

Example 22-15. Working with a nonserialized object
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
namespace WorkingWithNonSerializedObject
{
    [Serializable]
    class SumOf : IDeserializationCallback
    {
        private int startNumber = 1;
        private int endNumber;
        [NonSerialized]
        private int[] theSums;

        public static void Main(  )
        {
            Console.WriteLine("Creating first one with new...");
            SumOf app = new SumOf(1, 5);

            Console.WriteLine("Creating second one with deserialize...");
            SumOf newInstance = SumOf.DeSerialize(  );
            newInstance.DisplaySums(  );
        }

        public SumOf(int start, int end)
        {
            startNumber = start;
            endNumber = end;
            ComputeSums(  );
            DisplaySums(  );
            Serialize(  );
        }

        private void ComputeSums(  )
        {
            int count = endNumber - startNumber + 1;
            theSums = new int[count];
            theSums[0] = startNumber;
            for (int i = 1, j = startNumber + 1; i < count; i++, j++)
            {
                theSums[i] = j + theSums[i − 1];
            }
        }

        private void DisplaySums(  )
        {
            foreach (int i in theSums)
            {
                Console.WriteLine("{0}, ", i);
            }
        }

        private void Serialize(  )
        {
            Console.Write("Serializing...");
            // create a file stream to write the file
            FileStream fileStream =
                new FileStream("DoSum.out", FileMode.Create);
            // use the CLR binary formatter
            BinaryFormatter binaryFormatter =
                new BinaryFormatter(  );
            // serialize to disk
            binaryFormatter.Serialize(fileStream, this);
            Console.WriteLine("...completed");
            fileStream.Close(  );
        }

        public static SumOf DeSerialize(  )
        {
            FileStream fileStream =
                new FileStream("DoSum.out", FileMode.Open);
            BinaryFormatter binaryFormatter =
                new BinaryFormatter(  );
            SumOf retVal = (SumOf)binaryFormatter.Deserialize(fileStream);
            fileStream.Close(  );
            return retVal;
        }

        // fix up the nonserialized data

        public virtual void OnDeserialization(Object sender)
        {
            ComputeSums(  );
        }
    }
}


Output:
Creating first one with new...
1,
3,
6,
10,
15,
Serializing......completed
Creating second one with deserialize...
1,
3,
6,
10,
15,

You can see in the output that the data was successfully serialized to disk and then reconstituted by deserialization. The trade-off of disk storage space versus time doesn’t make a lot of sense with five values, but it makes a great deal of sense with five million values.

So far, you’ve streamed your data to disk for storage and across the network for easy communication with distant programs. There is one other time you might create a stream: to store permanent configuration and status data on a per-user basis. For this purpose, the .NET Framework offers isolated storage.

Isolated Storage

The .NET CLR provides isolated storage to allow the application developer to store data on a per-user basis. Isolated storage provides much of the functionality of traditional Windows .ini files, or the more recent HKEY_CURRENT_USER key in the Windows Registry.

Applications save data to a unique data compartment associated with the application. The CLR implements the data compartment with a data store, which is typically a directory on the filesystem.

Administrators are free to limit how much isolated storage individual applications can use. They can also use security so that less-trusted code can’t call more highly trusted code to write to isolated storage.

What is important about isolated storage is that the CLR provides a standard place to store your application’s data, but it doesn’t impose (or support) any particular layout or syntax for that data. In short, you can store anything you like in isolated storage.

Typically, you will store text, often in the form of name-value pairs. Isolated storage is a good mechanism for saving user configuration information such as login name, the position of various windows and widgets, and other application-specific, user-specific information. The data is stored in a separate file for each user, but the files can be isolated even further by distinguishing among different aspects of the identity of the code (by assembly or by originating application domain).

Using isolated storage is fairly straightforward. To write to isolated storage, create an instance of an IsolatedStorageFileStream, which you initialize with a filename and a file mode (create, append, etc.):

IsolatedStorageFileStream configFile =
 new IsolatedStorageFileStream
 ("Tester.cfg",FileMode.Create);

Now, create a StreamWriter on that file:

StreamWriter writer =
 new StreamWriter(configFile);

Then, write to that stream as you would to any other. Example 22-16 illustrates.

Example 22-16. Writing to isolated storage
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.IsolatedStorage;
using System.Text;

namespace WritingToIsolatedStorage
{
    public class Tester
    {
        public static void Main(  )
        {
            Tester app = new Tester(  );
            app.Run(  );
        }

        private void Run(  )
        {
            // create the configuration file stream
            IsolatedStorageFileStream configFile =
                new IsolatedStorageFileStream
                    ("Tester.cfg", FileMode.Create);

            // create a writer to write to the stream
            StreamWriter writer =
                new StreamWriter(configFile);

            // write some data to the config. file
            String output;
            System.DateTime currentTime = System.DateTime.Now;
            output = "Last access: " + currentTime.ToString(  );
            writer.WriteLine(output);
            output = "Last position = 27,35";
            writer.WriteLine(output);

            // flush the buffer and clean up
            writer.Close(  );
            configFile.Close(  );
        }
    }
}

After running this code, search your hard disk for Tester.cfg. On my machine, this file is found in:

C:Documents and SettingsJesseLocal SettingsApplication Data
IsolatedStoragemipjwcsz.iir2hzvpjcc.p0yStrongName.
mwoxzllzqpx3u0taclp1dti11kpddwyoUrl.a2f4v2g3ytucslmvlpt2wmdxhrhqg1pz
Files

You can read this file with Notepad if what you’ve written is just text:

Last access: 8/26/2007 10:00:57 AM
Last position = 27,35

Or, you can access this data programmatically. To do so, reopen the file:

IsolatedStorageFileStream configFile =
    new IsolatedStorageFileStream
    ("Tester.cfg",FileMode.Open);

Create a StreamReader object:

StreamReader reader =
    new StreamReader(configFile);

Use the standard stream idiom to read through the file:

string theEntry;
do
{
    theEntry = reader.ReadLine(  );
    Console.WriteLine(theEntry);
} while (theEntry != null);
Console.WriteLine(theEntry);

Isolated storage is scoped by assembly (so if you shut down your program and start it later, you can read the configuration file you created, but you can’t read the configuration of any other assembly). Example 22-17 provides the method needed to read the file. Replace the Run( ) method in the previous example, recompile it, and run it (but don’t change its name, or it won’t be able to access the isolated storage you created previously).

Example 22-17. Reading from isolated storage
        private void Run(  )
        {
            // open the configuration file stream
            IsolatedStorageFileStream configFile =
                new IsolatedStorageFileStream
                    ("Tester.cfg", FileMode.Open);

            // create a standard stream reader
            StreamReader reader =
                new StreamReader(configFile);

            // read through the file and display
            string theEntry;
            do
            {
                theEntry = reader.ReadLine(  );
                Console.WriteLine(theEntry);
            } while (theEntry != null);
            reader.Close(  );
            configFile.Close(  );
        }
    }

Output:

Last access: 8/26/2007 11:19:51 PM
Last position = 27,35


[17] * Internet data may also be sent in datagrams.

[18] * With a tip of the hat to Arlo Guthrie.

[19] * My favorite file comparison utility, as shown here, is ExamDiff Pro (http://www.prestosoft.com/ps.asp?page=edp_examdiffpro).

[20] * If you run your program on a network with a firewall, talk to your network administrator about which ports are closed.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.224.32.46