Streams are classes that specialize in data transfer. You use them to read and write data to and from files, networks, memory, strings, and the Internet. Here's an overview of the common stream classes in C#:
You can determine the capability of a stream using the CanRead , CanWrite , and CanSeek properties. For streams that support seeking, use the Seek method and the Position property to get or set your current position in the stream, and the Length property to get the length of a stream. To write data to a stream, you use the Write method; to read data from a stream, you use the Read method. You open a stream with the stream class's constructor; calling Close closes the stream. Some streams perform buffering of the underlying data to improve performance. For those streams, you can use the Flush method to clear any internal buffers and make sure that all data has been written out; the Close method also flushes internal data buffers. Usually, you're responsible for setting up your own data buffers, but the BufferedStream class provides the capability of wrapping a buffered stream around another stream in order to improve read and write performance. Let's get to some examples. The basic, low-level stream for working with files is FileStream , so we'll start with that. Reading and Writing Binary FilesThe FileStream class lets you treat files in binary format, as simple bytes. Using the Read and Write methods of this class, you can read bytes from a file and write a set number of bytes out to a file. This class treats its data as a simple byte stream, without interpreting that data, as the text streams we'll see in a few pages do. You can see the significant public properties of FileStream in Table 5.7, and the significant public methods of that class in Table 5.8. Table 5.7. Significant Public FileStream Properties
Table 5.8. Significant Public FileStream Methods
The best way to understand streams is to see them in action. Here's an example, ch05_04.cs, which treats its own source code as a binary file, reads it into a buffer, and writes that buffer out to a file named ch05_04.bak, providing you with a backup copy of this application's source code. This example uses the FileStream class's constructor to open files for reading and writing. As with other streams, there are various overloaded forms of this stream's constructor; here's the one we'll use: FileStream(string, FileMode, FileAccess, FileShare ); Here are the members of the FileMode enumeration, indicating how you want to open the file:
Here are the members of the FileAccess enumeration, indicating what you want to do with the file:
And here are the members of the FileShare enumeration, indicating how you want other processes to be able to work with the file at the same time:
Here's how we use the FileStream constructor to open ch05_04.cs for reading, and ch05_04.bak for writing (this code will create ch05_04.bak if necessary, or if that file exists, open it and truncate it to zero length): FileStream input = new FileStream("ch05_04.cs", FileMode.Open, FileAccess.Read, FileShare.None); FileStream output = new FileStream("ch05_04.bak", FileMode.OpenOrCreate, FileAccess.Write, FileShare.None); FileStream output = File.OpenWrite("ch05_04.bak"); We'll use the FileStream Read method to read data from the source file into a byte array buffer and the Write method to write that data to the target file. You pass the Read method the buffer to use for data, the offset in that buffer to store data at, and the length of the buffer. This method returns the number of bytes it's read, so we can keep looping until it runs out of data to read. The Write method writes data to the backup file; you pass it the data buffer, the offset in the buffer where your data starts, and the number of bytes to write. You can see this at work in ch05_04.cs, Listing 5.4.
Listing 5.4 Opening and Writing Binary Files (ch05_04.cs)using System.IO; class ch05_04 { public static void Main() { int numberBytes; byte[] dataBuffer = new System.Byte[4096]; FileStream input = new FileStream("ch05_04.cs", FileMode.Open, FileAccess.Read, FileShare.None); FileStream output = new FileStream("ch05_04.bak", FileMode.OpenOrCreate, FileAccess.Write, FileShare.None); while ((numberBytes = input.Read(dataBuffer, 0, 4096)) > 0) { output.Write(dataBuffer, 0, numberBytes); } input.Close(); output.Close(); } } When you run ch05_04, it opens its own source code, ch05_04.cs, and copies that code over, byte-by-byte, to ch05_04.bak. Note that file handling is one of the most exception-prone things you can do in programming, so in general you should enclose your file-handling operations in a try / catch block like this: using System.IO; class ch05_04 { public static void Main() { int numberBytes; byte[] dataBuffer = new System.Byte[4096]; try { FileStream input = new FileStream("ch05_04.cs", FileMode.Open, FileAccess.Read, FileShare.None); FileStream output = new FileStream("ch05_04.bak", FileMode.OpenOrCreate, FileAccess.Write, FileShare.None); while ((numberBytes = input.Read(dataBuffer, 0, 4096)) > 0) { output.Write(dataBuffer, 0, numberBytes); } input.Close(); output.Close(); } catch(System.Exception e) { System.Console.WriteLine(e.Message); } } } You can find which exceptions file-handling operations throw in the C# documentation; for example, the FileStream constructor can throw ArgumentException , ArgumentNullException , FileNotFoundException , IOException , and DirectoryNotFoundException exceptions. Although we're going to omit try / catch handling in this chapter for the sake of brevity, bear in mind that you should use it in general when working with files in a production environment. Creating Buffered StreamsIn the previous example, we created our own buffer to read and write data with. On the other hand, it turns out that sometimes, larger or smaller buffer sizes can improve file-handling performance. For that reason, you can wrap a FileStream object in a BufferedStream object, which will use its own internal buffer to maximize performance. All you have to do is to pass the FileStream object to the BufferedStream constructor and then use the returned BufferedStream object from then on. You can see this in action in ch05_05.cs, Listing 5.5. Listing 5.5 Buffered File I/O (ch05_05.cs)using System.IO; class ch05_05 { public static void Main() { int numberBytes; byte[] dataBuffer = new System.Byte[4096]; FileStream input = new FileStream("ch05_05.cs", FileMode.Open, FileAccess.Read, FileShare.None); FileStream output = new FileStream("ch05_05.bak", FileMode.OpenOrCreate, FileAccess.Write, FileShare.None); BufferedStream bufferedInput = new BufferedStream(input); BufferedStream bufferedOutput = new BufferedStream(output); while ((numberBytes = bufferedInput.Read(dataBuffer, 0, 4096)) > 0) { bufferedOutput.Write(dataBuffer, 0, numberBytes); } bufferedOutput.Flush(); bufferedInput.Close(); bufferedOutput.Close(); } } Reading and Writing Text FilesC# also supports the StreamReader and StreamWriter classes for working with text files. Text files are binary files like any other, but their data is Unicode text arranged into lines (in C#, a line is a sequence of characters followed by a line feed [ "\n" ] or a carriage return immediately followed by a line feed [ "\r\n" ]). What's special about the StreamReader and StreamWriter classes is that they offer the ReadLine and WriteLine methods that let you handle your data in a line-oriented way. You can see the significant public methods of the StreamWriter class in Table 5.9, and the significant public methods of the StreamReader class in Table 5.10. Table 5.9. Significant Public StreamWriter Methods
Table 5.10. Significant Public StreamReader Methods
Here's an example, ch05_06.cs, which uses StreamReader to read a text file from disk and StreamWriter to write a copy of the file. This example opens its own source file, ch05_06.cs, and copies it to ch05_06.bak using text stream methods. In particular, this example uses the WriteLine and ReadLine methods to write and read whole lines of text at a time, as you'll see in ch05_06.cs, Listing 5.6. (Note that the ReadLine method returns null if there's no more data to read, letting us know when the program is finished reading the available data.) Listing 5.6 Working with Text Files (ch05_06.cs)using System.IO; class ch05_06 { public static void Main() { StreamReader input = new StreamReader("ch05_06.cs"); StreamWriter output = new StreamWriter("ch05_06.bak"); string inputString; while ((inputString = input.ReadLine())!= null) { output.WriteLine(inputString); } input.Close(); output.Close(); } } When you run ch05_06.cs, it copies itself over into ch05_06.bak, line by line, using ReadLine and WriteLine , treating its data as text in a line-oriented way (as opposed to the FileStream class's Read and Write methods we saw earlier, which treat their data as binary). Note that an alternative to reading line by line in a loop is to use ReadToEnd , which reads all the text from your current position in the file, returning a single string. Working with Asynchronous I/OUp to this point, we've simply read data from a file and waited for the read operation to finish before doing anything. As we start working with network I/O, where things can be a lot slower, it won't be as easy to wait for reading and writing operations to complete. For that reason, the .NET Framework supports asynchronous I/O through the BeginRead and BeginWrite methods of the Stream class. You can call BeginRead to read a bufferfull of data, or BeginWrite to write a bufferfull of data, and then go on to do other work (we'll use a for loop for that purpose). You'll be called back when the read or write operation is complete. Here's how it might work if you wanted to read a large file while doing other work at the same time. In this example, we'll create a method named StartReading to start the reading operation and then go on with other work. This method opens this example's source code file and calls the BeginRead method to read from the source code file into a buffer. To call BeginRead , you pass it the data buffer ( dataBuffer here), the offset in the buffer at which to start reading (0), the number of bytes to read ( dataBuffer.Length , using the Length array property), the delegate ( asyncDelegate ) to a callback method ( OnBufferFull ) which will be called when the buffer is full, and a state variable in which BeginRead can record the state of the current read operation ( null ). After the call to BeginRead , we'll turn to some other workexecuting a for loop one million times: FileStream input; byte[] dataBuffer; System.AsyncCallback asyncDelegate; void StartReading() { input = new FileStream("ch05_07.cs", FileMode.Open, FileAccess.Read, FileShare.None); dataBuffer = new byte[512]; asyncDelegate = new System.AsyncCallback(this.OnBufferFull); input.BeginRead(dataBuffer, 0, dataBuffer.Length, asyncDelegate, null); for (int loopIndex = 0; loopIndex < 1000000; loopIndex++){} } While the loop executes, BeginRead fills the buffer with data. After a bufferfull of data has been read, the method OnBufferFull is called with an IAsyncResult argument, temporarily interrupting the for loop. You can pass the IAsyncResult argument to the stream's EndRead or EndWrite methods to get the number of bytes actually read or written, and we'll display that number here. If the number of bytes read is greater than 0, we'll go back to read more data into the buffer with another call to BeginRead : void OnBufferFull(System.IAsyncResult asyncResult) { int numberBytes = input.EndRead(asyncResult); System.Console.WriteLine(numberBytes); if (numberBytes > 0) { input.BeginRead(dataBuffer, 0, dataBuffer.Length, asyncDelegate, null); } } You can see the whole code in Listing 5.7, where we create an object of the ch05_07 class so we can call non-static methods from Main , and call the StartReading method to start the asynchronous read process. (Note that to justify asynchronous reading here, you should use this kind of code to read in a huge file instead of just the sample's own source code.) Listing 5.7 Asynchronous File Reading (ch05_07.cs)using System.IO; public class ch05_07 { FileStream input; byte[] dataBuffer; System.AsyncCallback asyncDelegate; public static void Main() { ch05_07 appObj = new ch05_07(); appObj.StartReading(); } void StartReading() { input = new FileStream("ch05_07.cs", FileMode.Open, FileAccess.Read, FileShare.None); dataBuffer = new byte[512]; asyncDelegate = new System.AsyncCallback(this.OnBufferFull); input.BeginRead(dataBuffer, 0, dataBuffer.Length, asyncDelegate, null); for (int loopIndex = 0; loopIndex < 1000000; loopIndex++){} } void OnBufferFull(System.IAsyncResult asyncResult) { int numberBytes = input.EndRead(asyncResult); System.Console.WriteLine(numberBytes); if (numberBytes > 0) { input.BeginRead(dataBuffer, 0, dataBuffer.Length, asyncDelegate, null); } } } The ch05_07.cs example is 1006 bytes long, and this is what you see when you run it. Note that it took two asynchronous buffered reads (512 + 494 = 1006) to read the file: C:\>ch05_07 512 494 0 Working with Network I/OIn C#, you can read and write data using streams on the Internet much as we've already been doing with files. Network I/O is based on sockets , and a socket represents a connection to another socket somewhere on the network for the purposes of communication. You can use UDP or TCP/IP protocols with sockets, and we'll use the more common TCP/IP protocol here. The type of applications we're going to build here are client/server applications. You build the client with the TcpClient class, and you build the server using the TcpListener class. Typically, the server listens for client connections, and when a client connects, a new Socket object is created. Using that socket, you can create a NetWorkStream object to send data to the client. Creating a Network ServerLet's see how this works by creating a client/server TCP/IP application now. We'll start by building a server first, which will send its own source code to any client that connects to it, using an Internet socket. The server is based on the TcpListener class, and we'll use the Socket and NetworkStream classes to build it. You can see the significant public methods of the TcpListener class in Table 5.11, the significant public methods of the Socket class in Table 5.12, and the significant public methods of the NetworkStream class in Table 5.13. Table 5.11. Significant Public TcpListener Methods
Table 5.12. Significant Public Socket Methods
Table 5.13. Significant Public NetworkStream Methods
Servers listen for connections to a client, and you can pass the TcpListener class an IP address (an Internet address of the form xxx.xxx.xxx.xxx ; for example, microsoft.com 's IP address is 207.46.134.222 ) and a port to listen on (ports range from 0 to 65,535). In this example, we'll use the constant IPAddress.Any to indicate that we want to listen for connections on any network interface, and the port number 65512 . After you've created a TcpListener object in a method we'll call Listen , you start listening for connections from the client with the Start method: private void Listen() { TcpListener tcpListener = new TcpListener(IPAddress.Any, 65512); tcpListener.Start(); . . . We'll do the actual listening with an endless while loop, calling the TCP/IP listener's AcceptSocket method to create a new socket; if that socket's Connected property is true , we've connected to the client. In that case, we use the Socket object to create a new NetworkStream object, and use the NetworkStream object to create a new StreamWriter object so we can use the WriteLine method to write to the client. We'll also create a StreamReader object so we can read in this example's own source code. Here's how we read in that source code and send it to the client in the while loop: private void Listen() { TcpListener tcpListener = new TcpListener(IPAddress.Any, 65512); tcpListener.Start(); System.Console.WriteLine("Listening..."); while(true) { Socket socket = tcpListener.AcceptSocket(); if (socket.Connected) { System.Console.WriteLine("Connected..."); NetworkStream networkStream = new NetworkStream(socket); StreamWriter output = new StreamWriter(networkStream); StreamReader input = new StreamReader("ch05_08.cs"); string inputString; System.Console.WriteLine("Sending..."); while((inputString = input.ReadLine()) != null) { output.WriteLine(inputString); output.Flush(); } . . . } System.Console.WriteLine("Quitting..."); } }
Note that after each WriteLine operation we call the Flush method to make sure all data was sent to the client and not cached. This is always a good idea with sockets. All that's left is to call Listen from Main , close the streams when we're done, and break out of the while loop to finish the server's code. You can see how that works in the entire code in Listing 5.8. Listing 5.8 A Network Server (ch05_08.cs)using System.IO; using System.Net; using System.Net.Sockets; public class ch05_08 { public static void Main() { ch05_08 appObject = new ch05_08(); appObject.Listen(); } private void Listen() { TcpListener tcpListener = new TcpListener(IPAddress.Any, 65512); tcpListener.Start(); System.Console.WriteLine("Listening..."); while(true) { Socket socket = tcpListener.AcceptSocket(); if (socket.Connected) { System.Console.WriteLine("Connected..."); NetworkStream networkStream = new NetworkStream(socket); StreamWriter output = new StreamWriter(networkStream); StreamReader input = new StreamReader("ch05_08.cs"); string inputString; System.Console.WriteLine("Sending..."); while((inputString = input.ReadLine()) != null) { output.WriteLine(inputString); output.Flush(); } networkStream.Close(); input.Close(); output.Close(); socket.Close(); System.Console.WriteLine("Disconnected..."); break; } System.Console.WriteLine("Quitting..."); } } } Creating a Network ClientThe next step is to create the client using the TcpClient class. You can see the significant public methods of the TcpClient class in Table 5.14. Table 5.14. Significant Public TcpClient Methods
We'll need a TcpClient object to create the client application for this example. You can pass the TcpClient constructor the name of a remote host and a port number to connect on, like this: ("www.microsoft.com", 80) , where port 80 is the one used for HTTP communication. In this example, we'll run both the client and the server on the same machine, using port 65512, so the name of the host will be "localHost" . To use the StreamReader ReadLine method to read text from the server, we'll use the TcpClient object's GetStream method to get its underlying NetWorkStream stream, and pass that stream to the StreamReader constructor like this: TcpClient client = new TcpClient("localHost", 65512); NetworkStream network = client.GetStream(); System.IO.StreamReader input = new System.IO.StreamReader(network); All that's left is to use ReadLine to keep reading text from the server, and then to close the connection, as you see in Listing 5.9. Listing 5.9 A Network Client (ch05_09.cs)using System.Net.Sockets; public class ch05_09 { static public void Main() { TcpClient client = new TcpClient("localHost", 65512); NetworkStream network = client.GetStream(); System.IO.StreamReader input = new System.IO.StreamReader(network); string outputString; while((outputString = input.ReadLine()) != null) { System.Console.WriteLine(outputString); } input.Close(); network.Close(); } } That's all you need. Now start the server, ch05_08.exe, in one DOS window, and then the client, ch05_09.exe, in a second DOS window. The two will connect on port 65512; the server will send its own source code, ch05_08.cs, to the client, which will display it. Here's what you see when you run the server: C:\>ch05_08 Listening... Connected... Sending... Disconnected... Here's what you see when you run the client. As you can see, we've connected using Internet sockets and ports: C:\>ch05_09 using System.IO; using System.Net; using System.Net.Sockets; public class ch05_08 { . . . Supporting Asynchronous Network I/OThe previous example used synchronous network I/O, but if you have a number of clients all trying to connect to your server at once, it's a better idea to use asynchronous I/O. To handle multiple clients , for example, you might create a new class, Client , and create a new object of that class to handle each new client as requests come in. To perform the actual asynchronous reads and writes in that object, you can use BeginRead and BeginWrite , as we did earlier in this chapter. You can see an example of this in ch05_10.cs, which is an asynchronous network I/O server, and ch05_11.cs, which is an asynchronous client. In this case, the server will be set up for both asynchronous reads and writes. The client will send it some text (in this example, that's the text message "Network Streaming!" ), and the server will read that text asynchronously and write it back asynchronously, giving the server the time it needs to handle other clients. In order to handle multiple clients, you can pass the connected socket to the Client class's constructor when a new client connects, thus creating a new Client object that will handle the new client. This enables you to accept as many incoming connections as you want to: public static void Main() { TcpListener tcpListener = new TcpListener(IPAddress.Any, 65512); tcpListener.Start(); System.Console.WriteLine("Waiting for connection..."); while (true) { Socket socket = tcpListener.AcceptSocket(); if (socket.Connected) { Client client = new Client(socket); } } }
You can see how the Client class works in Listing 5.10; when you create an object of this class, its constructor starts reading from the client with BeginRead . When text has been read, BeginWrite is called to write the received text back to the client, and the code tries to read more from the client. If there's no more to read, the code closes the connection. Listing 5.10 An Asynchronous Network Server (ch05_10.cs)using System.Net; using System.Net.Sockets; public class ch05_10 { public static void Main() { TcpListener tcpListener = new TcpListener(IPAddress.Any, 65512); tcpListener.Start(); System.Console.WriteLine("Waiting for connection..."); while (true) { Socket socket = tcpListener.AcceptSocket(); if (socket.Connected) { Client client = new Client(socket); } } } } class Client { byte[] dataBuffer; Socket socket; NetworkStream networkStream; System.AsyncCallback readDelegate; System.AsyncCallback writeDelegate; public Client(Socket socket) { this.socket = socket; dataBuffer = new byte[512]; networkStream = new NetworkStream(socket); System.Console.WriteLine("Connected to a client..."); readDelegate = new System.AsyncCallback(this.OnRead); writeDelegate = new System.AsyncCallback(this.OnWrite); networkStream.BeginRead(dataBuffer, 0, dataBuffer.Length, readDelegate, null); } private void OnRead(System.IAsyncResult asyncResult) { int numberBytes = networkStream.EndRead(asyncResult); if (numberBytes > 0) { string outputString = System.Text.Encoding.ASCII.GetString( dataBuffer, 0, numberBytes - 2); System.Console.WriteLine("Read this text: \"{0}\".", outputString); networkStream.BeginWrite(dataBuffer, 0, numberBytes, writeDelegate, null); } else { System.Console.WriteLine( "Read operation with this client finished..."); networkStream.Close(); networkStream = null; socket.Close(); socket = null; } } private void OnWrite(System.IAsyncResult asyncResult) { networkStream.EndWrite(asyncResult); System.Console.WriteLine("Sent text back to client..."); networkStream.BeginRead(dataBuffer, 0, dataBuffer.Length, readDelegate, null); } } The client in this case needs to connect to the server with a NetworkStream object, create a StreamWriter object to write to the server, and create a StreamReader object to read from the server. You can see how that works in the client's code, ch05_11.cs, Listing 5.11. Listing 5.11 An Asynchronous Network Client (ch05_11.cs)using System.Net.Sockets; public class ch05_11 { static public void Main() { ch05_11 appObject = new ch05_11(); } ch05_11() { NetworkStream networkStream; System.Console.WriteLine("Connecting to server..."); TcpClient tcpSocket = new TcpClient("localHost", 65512); networkStream = tcpSocket.GetStream(); string outputString = "Network streaming!"; System.Console.WriteLine("Sending this message to server: \"{0}\".", outputString); System.IO.StreamWriter output = new System.IO.StreamWriter(networkStream); output.WriteLine(outputString); output.Flush(); System.IO.StreamReader input = new System.IO.StreamReader(networkStream); string inputString = input.ReadLine(); System.Console.WriteLine("Got this message from server: \"{0}\".", inputString); System.Console.WriteLine("Disconnecting from server..."); networkStream.Close(); } } When you start the server, ch05_10.exe, you'll see this: C:\>ch05_10 Waiting for connection... Starting a client, ch05_11.exe, in a new DOS window shows how the client can send the message, "Network Streaming!" to the server, and how the server sends it back: C:\c#\ch05>ch05_11 Connecting to server... Sending this message to server: "Network streaming!". Got this message from server: "Network streaming!". Disconnecting from server... Here's what the server displays after the connection is made: C:\>ch05_10 Waiting for connection... Connected to a client... Read this text: "Network streaming!". Sent text back to client... Read operation with this client finished... You can make multiple connections to this server, running the client application in multiple DOS windows. Each time you do, a new Client object will be created to handle the new connection, and read/write operations with the new client will be handled asynchronously. That means that one client doesn't have to wait until the server is done with all other clients before it gets any attention, which is how it's done in the real world. Working with Internet StreamsThe FCL also includes the HttpWebRequest and HttpWebResponse classes to let you work directly with HTTP streams on the Web. We'll take a look at how to send a request to a Web server, requesting the Web page at www.microsoft.com, and reading the Web server's response when it comes in. You can see the significant public properties of the HttpWebRequest class, which enable you to send commands to Web servers just as browsers do, in Table 5.15, and its significant public methods in Table 5.16. Table 5.15. Significant Public HttpWebRequest Properties
Table 5.16. Significant Public HttpWebRequest Methods
The HttpWebResponse class lets you handle what the Web server sends back to you. You'll find the significant public properties of the HttpWebResponse class in Table 5.17, and its significant public methods in Table 5.18. Table 5.17. Significant Public HttpWebResponse Properties
Table 5.18. Significant Public HttpWebResponse Methods
Browsers work by sending the kinds of requests to Web servers that we'll send here, and by receiving the kinds of responses we'll get in return. In this next example, the application will act much like a browser when we use HttpWebRequest to send a request for the HTML of the page www.microsoft.com , and HttpWebResponse to create a StreamReader object so we can read that page. To create the request for that page, you don't use the HttpWebRequest constructor directly; you use the Create method of its base class, WebRequest . To get the response from the Web site, you use the GetResponse method of the WebRequest class. And to get a StreamReader stream corresponding to the Web page itself, you use the WebResponse class's GetResponseStream method, which returns a Stream object that you can pass on to the StreamReader constructor: HttpWebRequest webRequest = (HttpWebRequest) WebRequest.Create("http://www.microsoft.com"); HttpWebResponse webResponse = (HttpWebResponse) webRequest.GetResponse(); StreamReader input = new StreamReader(webResponse.GetResponseStream()); Now that we have a StreamReader object, all we have to do is to read the Web page using the ReadLine method, as you see in ch05_12.cs, Listing 5.12. Listing 5.12 An Asynchronous Network Client (ch05_12.cs)using System.IO; using System.Net; public class ch05_12 { static public void Main( string[] Args ) { HttpWebRequest webRequest = (HttpWebRequest) WebRequest.Create("http://www.microsoft.com"); HttpWebResponse webResponse = (HttpWebResponse) webRequest.GetResponse(); StreamReader input = new StreamReader(webResponse.GetResponseStream()); string inputString; while ((inputString = input.ReadLine()) != null) { System.Console.WriteLine(inputString); } input.Close(); } } When you run ch05_12, it downloads the HTML of the Web page at www.microsoft.com and displays it. That's all there is to it. |