C# I/O and Networking

Arguably, the reason that Java has become so popular as a programming language is because of the way it has abstracted away the difficulties in performing input/output and networking operations. C# has taken the same approach and has provided libraries that hide these complications. The previous two articles in this series have focused around the different language structures that a Java programmer needs to know to build simple C# programs; this article is going to focus on a few C# namespaces dealing with I/O and networking, along with some common usage patterns of these libraries.

Understanding Streams

Streams in both Java and C# usually involve reading and writing bytes from and to the console, the filesystem, or the network. In both languages, the stream paradigm is used more generally whenever a program needs to move or operate on a group of bytes.

Java provides two abstract classes, the java.io.InputStream and the java.io.OutputStream, that contain the unimplemented methods that need to be authored to allow a program to read and write from these two stream sub-types. On the other hand, C# unifies these two classes into System.IO.Stream class; instead of having two objects, one for reading and one for writing, a C# Stream object needs to be tested for its capabilities via the CanRead and the CanWrite properties.

Synchronous I/O

Synchronous I/O is syntactically very similar in both languages. Both the Java java.io.InputStream and java.io.OutputStream, along with the C# System.IO.Stream, have methods to operate one byte at a time, along with methods that operate on an array of bytes. (C# lacks the syntactic sugar of operating on a whole array; it instead only knows how to use an array along with an offset/length pair.)

Table 1: Synchronous I/O methods in Java and C#
The methods to use when desiring to work with streams synchronously

Function

Java

C#

Read one byte

The read() method in java.io.InputStream

The ReadByte() method in System.IO.Stream

Read a whole byte array

The read( byte[] b ) method in java.io.InputStream

No such syntactical method -- use the read method that has an offset/count pair

Asynchronous I/O

What Java lacks is a formal way of performing I/O operations asynchronously; there is no "built-in" way to cause a read or a write to occur on a stream, and then check the result later on. The closest simulation in Java is to spawn a java.lang.Thread around a synchronous method and either have the Thread cause a side effect or perform a callback, either with the status of the I/O operation. C# has asynchronous I/O methods built into its libraries.

For example, to perform an asynchronous read( byte[] b ) call in Java, both with a callback and a state that can be checked afterward, a possible implementation could look somewhat like:

// variables to hold the side effects of the read
int read; // to hold the result of the read
IOException exception; // to hold a possible exception
Object wait ...
// some value to block on until the end of the = call
// a wrap around a read on the InputStream variable "is"
( new Thread( new Runnable() {
public void run() {
try {
read is.read();
} catch( IOException error ) {
exception error;
} finally {
synchronized( wait ) {
// wake up all other threads waiting on this
// read
wait.notifyAll();
}
// call a callback method
callback();
}
}
} ) ).start();

This will cause either the value of the read, or the exception that is caught when reading, to be stored in the "read" and "exception" respectively. Another thread may also wait on the variable "wait," or implement the method "callback" to know when the asynchronous read has completed.

In an attempt to clean all this up, C# provides the methods BeginRead and EndRead that wrap up all the above functionality. The signature to BeginRead is similar to the signature of Read, except it takes two more variables -- an AsyncCallback and a state object -- and it returns an IAsyncResult object that can be used later to check on the progress of the asynchronous read. A standard use of BeginRead looks something like:

To see how many bytes have actually been read, the EndRead method call can be called with the IAsyncResult object. Be warned, however, that calling EndRead will block until the BeginRead completes -- to find out the state of the read without blocking, check the IsCompleted property on the IAsyncResult return. Also note that the contents of the buffer variable are not guaranteed until the asynchronous read has completed.

Implementing Streams

Java and C# streams are similar enough that, knowing what you know about Java streams, implementing a C# stream should not be that difficult. The main difference between implementing the two is not only that the appropriate reading or writing methods need be implemented, but also since a C# Stream class may be functioning either as a reader or a writer, the capability properties need to reflect exactly what the Stream can do.

Table 2: Implementing Streams in Java and C#
A list of methods that need to be implemented in the corresponding stream classes

Function

Java

C#

reading

Implement, at minimum, the read() method in java.io.InputStream (as an optimization, the two other read methods in InputStream may also be implemented)

Cause the CanRead property in System.IO.Stream to return true, and author one of the Read methods -- at minimum one of the Read, ReadByte, or BeginRead/EndRead methods

seeking

Have the skip method in java.io.InputStream perform the needed operations to move forward in a file, and also implement the markSupported, mark, and reset methods to move backwards in a stream

Use the CanSeek property in System.IO.Stream to notify the program that this stream can seek, implementing the Seek method

writing

Implement the write method in java.io.OutputStream (again, as an optimization the other write methods in the OutputStream can be written)

Return true from the System.IO.Stream property named CanWrite and write, at minimum, one of Write, WriteByte, or BeginWrite/EndWrite methods

The C# Stream class provides lots of options on what methods to implement for functionality. Overriding Read and Write (both taking a byte array, an offset, and a length) is usually enough, as the default implementations of all the methods make use of the other methods; simply overriding at least one of the read/write methods will add the needed functionality to the entire stream. The default implementation of ReadByte and WriteByte will convert the long value to and from a byte array, whereas the default implementation of the asynchronous BeginRead and BeginWrite methods will execute Read or Write in a separate thread.

Readers and Writers

Most of this article has been spent talking about the System.IO.Stream class in C#; however, some time needs to be devoted to talking about the System.IO.TextReader and the System.IO.TextWriter. These two classes most closely mimic the Java model of I/O, with one class type for reading while another type handles writing. Where the C# Stream object encapsulates knowledge on how to both read and write bytes, the TextReader and TextWriter classes encapsulate reading and writing characters respectively. The most commonly-used classes that derive from the two above are the System.IO.StreamReader and the System.IO.StreamWrtiter classes -- these two can take a Stream object and, optionally, a System.Text.Encoding object to specify how to convert the byte stream to a character stream (C# defaults to using a UTF-8 encoder/decoder).

If access to stream-like functionality is needed, and instead of working with bytes, you are programming for the use of characters, it may be easier to implement sub-classes of the TextReader and TextWriter classes than to deal with the nuances of the Stream class. Although if the Stream is implemented properly, then you can use the StreamReader and the StreamWriter classes to wrap your custom stream.