< Day Day Up > |
5.8. System.IO: Classes to Read and Write Streams of DataThe System.IO namespace contains the primary classes used to move and process streams of data. The data source may be in the form of text strings, as discussed in this chapter, or raw bytes of data coming from a network or device on an I/O port. Classes derived from the Stream class work with raw bytes; those derived from the TexTReader and TextWriter classes operate with characters and text strings (see Figure 5-6). We'll begin the discussion with the Stream class and look at how its derived classes are used to manipulate byte streams of data. Then, we'll examine how data in a more structured text format is handled using the TexTReader and TextWriter classes. Figure 5-6. Selected System.IO classesThe Stream ClassThis class defines the generic members for working with raw byte streams. Its purpose is to abstract data into a stream of bytes independent of any underlying data devices. This frees the programmer to focus on the data stream rather than device characteristics. The class members support three fundamental areas of operation: reading, writing, and seeking (identifying the current byte position within a stream). Table 5-10 summarizes some of its important members. Not included are methods for asynchronous I/O, a topic covered in Chapter 13, "Asynchronous Programming and Multithreading."
These methods and properties provide the bulk of the functionality for the FileStream, MemoryStream, and BufferedStream classes, which we examine next. FileStreamsA FileStream object is created to process a stream of bytes associated with a backing store梐 term used to refer to any storage medium such as disk or memory. The following code segment demonstrates how it is used for reading and writing bytes: try { // Create FileStream object FileStream fs = new FileStream(@"c:\artists\log.txt", FileMode.OpenOrCreate, FileAccess.ReadWrite); byte[] alpha = new byte[6] {65,66,67,68,69,70}; //ABCDEF // Write array of bytes to a file // Equivalent to: fs.Write(alpha,0, alpha.Length); foreach (byte b in alpha) { fs.WriteByte(b);} // Read bytes from file fs.Position = 0; // Move to beginning of file for (int i = 0; i< fs.Length; i++) Console.Write((char) fs.ReadByte()); //ABCDEF fs.Close(); catch(Exception ex) { Console.Write(ex.Message); } As this example illustrates, a stream is essentially a byte array with an internal pointer that marks a current location in the stream. The ReadByte and WriteByte methods process stream bytes in sequence. The Position property moves the internal pointer to any position in the stream. By opening the FileStream for ReadWrite, the program can intermix reading and writing without closing the file. Creating a FileStreamThe FileStream class has several constructors. The most useful ones accept the path of the file being associated with the object and optional parameters that define file mode, access rights, and sharing rights. The possible values for these parameters are shown in Figure 5-7. Figure 5-7. Options for FileStream constructorsThe FileMode enumeration designates how the operating system is to open the file and where to position the file pointer for subsequent reading or writing. Table 5-11 is worth noting because you will see the enumeration used by several classes in the System.IO namespace.
The FileAccess enumeration defines how the current FileStream may access the file; FileShare defines how file streams in other processes may access it. For example, FileShare.Read permits multiple file streams to be created that can simultaneously read the same file. MemoryStreamsAs the name suggests, this class is used to stream bytes to and from memory as a substitute for a temporary external physical store. To demonstrate, here is an example that copies a file. It reads the original file into a memory stream and then writes this to a FileStream using the WriteTo method: FileStream fsIn = new FileStream(@"c:\manet.bmp", FileMode.Open, FileAccess.Read); FileStream fsOut = new FileStream(@"c:\manetcopy.bmp", FileMode.OpenOrCreate, FileAccess.Write); MemoryStream ms = new MemoryStream(); // Input image byte-by-byte and store in memory stream int imgByte; while ((imgByte = fsIn.ReadByte())!=-1){ ms.WriteByte((byte)imgByte); } ms.WriteTo(fsOut); // Copy image from memory to disk byte[] imgArray = ms.ToArray(); // Convert to array of bytes fsIn.Close(); fsOut.Close(); ms.Close(); BufferedStreamsOne way to improve I/O performance is to limit the number of reads and writes to an external device梡articularly when small amounts of data are involved. Buffers have long offered a solution for collecting small amounts of data into larger amounts that could then be sent more efficiently to a device. The BufferedStream object contains a buffer that performs this role for an underlying stream. You create the object by passing an existing stream object to its constructor. The BufferedStream then performs the I/O operations, and when the buffer is full or closed, its contents are flushed to the underlying stream. By default, the BufferedStream maintains a buffer size of 4096 bytes, but passing a size parameter to the constructor can change this. Buffers are commonly used to improve performance when reading bytes from an I/O port or network. Here is an example that associates a BufferedStream with an underlying FileStream. The heart of the code consists of a loop in which FillBytes (simulating an I/O device) is called to return an array of bytes. These bytes are written to a buffer rather than directly to the file. When fileBuffer is closed, any remaining bytes are flushed to the FileStream fsOut1. A write operation to the physical device then occurs. private void SaveStream() { Stream fsOut1 = new FileStream(@"c:\captured.txt", FileMode.OpenOrCreate, FileAccess.Write); BufferedStream fileBuffer = new BufferedStream(fsOut1); byte[] buff; // Array to hold bytes written to buffer bool readMore=true; while(readMore) { buff = FillBytes(); // Get array of bytes for (int j = 0;j<buff[16];j++){ fileBuffer.WriteByte(buff[j]); // Store bytes in buffer } if(buff[16]< 16) readMore=false; // Indicates no more data } fileBuffer.Close(); // Flushes all remaining buffer content fsOut1.Close(); // Must close after bufferedstream } // Method to simulate I/O device receiving data private static byte[] FillBytes() { Random rand = new Random(); byte[] r = new Byte[17]; // Store random numbers to return in array for (int j=0;j<16;j++) { r[j]= (byte) rand.Next(); if(r[j]==171) // Arbitrary end of stream value { r[16]=(byte)(j); // Number of bytes in array return r; } } System.Threading.Thread.Sleep(500); // Delay 500ms return r; } Using StreamReader and StreamWriter to Read and Write Lines of TextUnlike the Stream derived classes, StreamWriter and StreamReader are designed to work with text rather than raw bytes. The abstract TextWriter and TexTReader classes from which they derive define methods for reading and writing text as lines of characters. Keep in mind that these methods rely on a FileStream object underneath to perform the actual data transfer. Writing to a Text FileStreamWriter writes text using its Write and WriteLine methods. Note their differences:
The StreamWriter object is created using one of several constructors: Syntax (partial list): public StreamWriter(string path) public StreamWriter(stream s) public StreamWriter(string path, bool append) public StreamWriter(string path, bool append, Encoding encoding)
This example creates a StreamWriter object from a FileStream and writes two lines of text to the associated file: string filePath = @"c:\cup.txt"; // Could use: StreamWriter sw = new StreamWriter(filePath); // Use FileStream to create StreamWriter FileStream fs = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.ReadWrite); StreamWriter sw2 = new StreamWriter(fs); // Now that it is created, write to the file sw2.WriteLine("The world is a cup"); sw2.WriteLine("brimming\nwith water."); sw2.Close(); // Free resources Reading from a Text FileA StreamReader object is used to read text from a file. Much like StreamWriter, an instance of it can be created from an underlying Stream object, and it can include an encoding specification parameter. When it is created, it has several methods for reading and viewing character data (see Table 5-12).
This code creates a StreamReader object by passing an explicit FileStream object to the constructor. The FileStream is used later to reposition the reader to the beginning of the file. String path= @"c:\cup.txt"; if(File.Exists(path)) { FileStream fs = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite); StreamReader reader = new StreamReader(fs); // or StreamReader reader = new StreamReader(path); // (1) Read first line string line = reader.ReadLine(); // (2) Read four bytes on next line char[] buff = new char[4]; int count = reader.Read(buff,0,buff.Length); // (3) Read to end of file string cup = reader.ReadToEnd(); // (4) Reposition to beginning of file // Could also use reader.BaseStream.Position = 0; fs.Position = 0; // (5) Read from first line to end of file line = null; while ((line = reader.ReadLine()) != null){ Console.WriteLine(line); } reader.Close(); } Core Note
StringWriter and StringReaderThese two classes do not require a lot of discussion, because they are so similar in practice to the StreamWriter and StreamReader. The main difference is that these streams are stored in memory, rather than in a file. The following example should be self-explanatory: StringWriter writer = new StringWriter(); writer.WriteLine("Today I have returned,"); writer.WriteLine("after long months "); writer.Write("that seemed like centuries"); writer.Write(writer.NewLine); writer.Close(); // Read String just written from memory string myString = writer.ToString(); StringReader reader = new StringReader(myString); string line = null; while ((line = reader.ReadLine()) !=null) { Console.WriteLine(line); } reader.Close(); The most interesting aspect of the StringWriter is that it is implemented underneath as a StringBuilder object. In fact, StringWriter has a GetStringBuilder method that can be used to retrieve it:
StringWriter writer = new StringWriter();
writer.WriteLine("Today I have returned,");
// Get underlying StringBuilder
StringBuilder sb = writer.GetStringBuilder();
sb.Append("after long months ");
Console.WriteLine(sb.ToString());
writer.Close();
Core Recommendation
Encryption with the CryptoStream ClassAn advantage of using streams is the ability to layer them to add functionality. We saw earlier how the BufferedStream class performs I/O on top of an underlying FileStream. Another class that can be layered on a base stream is the CryptoStream class that enables data in the underlying stream to be encrypted and decrypted. This section describes how to use this class in conjunction with the StreamWriter and StreamReader classes to read and write encrypted text in a FileStream. Figure 5-8 shows how each class is composed from the underlying class. Figure 5-8. Layering streams for encryption/decryptionCryptoStream is located in the System.Security.Cryptography namespace. It is quite simple to use, requiring only a couple of lines of code to apply it to a stream. The .NET Framework provides multiple cryptography algorithms that can be used with this class. Later, you may want to investigate the merits of these algorithms, but for now, our interest is in how to use them with the CryptoStream class. Two techniques are used to encrypt data: assymmetric (or public key) and symmetric (or private key). Public key is referred to as asymmetric because a public key is used to decrypt data, while a different private key is used to encrypt it. Symmetric uses the same private key for both purposes. In our example, we are going to use a private key algorithm. The .NET Framework Class Library contains four classes that implement symmetric algorithms:
We use the DES algorithm in our example, but we could have chosen any of the others because implementation details are identical. First, an instance of the class is created. Then, its key and IV (Initialization Vector) properties are set to the same key value. DES requires these to be 8 bytes; other algorithms require different lengths. Of course, the key is used to encrypt and decrypt data. The IV ensures that repeated text is not encrypted identically. After the DES object is created, it is passed as an argument to the constructor of the CryptoStream class. The CryptoStream object simply treats the object encapsulating the algorithm as a black box. The example shown here includes two methods: one to encrypt and write data to a file stream, and the other to decrypt the same data while reading it back. The encryption is performed by WriteEncrypt, which receives a FileStream object parameter encapsulating the output file and a second parameter containing the message to be encrypted; ReadEncrypt receives a FileStream representing the file to be read. fs = new FileStream("C:\\test.txt", FileMode.Create, FileAccess.Write); MyApp.WriteEncrypt(fs, "Selected site is in Italy."); fs= new FileStream("C:\\test.txt",FileMode.Open, FileAccess.Read); string msg = MyApp.ReadEncrypt(fs); Console.WriteLine(msg); fs.Close(); WriteEncrypt encrypts the message and writes it to the file stream using a StreamWriter object that serves as a wrapper for a CrytpoStream object. CryptoStream has a lone constructor that accepts the file stream, an object encapsulating the DES algorithm logic, and an enumeration specifying its mode. // Encrypt FileStream private static void WriteEncrypt(FileStream fs, string msg) { // (1) Create Data Encryption Standard (DES) object DESCryptoServiceProvider crypt = new DESCryptoServiceProvider(); // (2) Create a key and Initialization Vector ? // requires 8 bytes crypt.Key = new byte[] {71,72,83,84,85,96,97,78}; crypt.IV = new byte[] {71,72,83,84,85,96,97,78}; // (3) Create CryptoStream stream object CryptoStream cs = new CryptoStream(fs, crypt.CreateEncryptor(),CryptoStreamMode.Write); // (4) Create StreamWriter using CryptoStream StreamWriter sw = new StreamWriter(cs); sw.Write(msg); sw.Close(); cs.Close(); } ReadEncrypt reverses the actions of WriteEncrypt. It decodes the data in the file stream and returns the data as a string object. To do this, it layers a CryptoStream stream on top of the FileStream to perform decryption. It then creates a StreamReader from the CryptoStream stream that actually reads the data from the stream. // Read and decrypt a file stream. private static string ReadEncrypt(FileStream fs) { // (1) Create Data Encryption Standard (DES) object DESCryptoServiceProvider crypt = new DESCryptoServiceProvider(); // (2) Create a key and Initialization Vector crypt.Key = new byte[] {71,72,83,84,85,96,97,78}; crypt.IV = new byte[] {71,72,83,84,85,96,97,78}; // (3) Create CryptoStream stream object CryptoStream cs = new CryptoStream(fs, crypt.CreateDecryptor(),CryptoStreamMode.Read); // (4) Create StreamReader using CryptoStream StreamReader sr = new StreamReader(cs); string msg = sr.ReadToEnd(); sr.Close(); cs.Close(); return msg; } |
< Day Day Up > |