ONDotNet.com    
 Published on ONDotNet.com (http://www.ondotnet.com/)
 See this if you're having trouble printing code examples


C# I/O and Networking

by Raffi Krikorian
07/17/2001

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

Read into a portion of a byte array

The read( byte[] b, int off, int len ) method in java.io.InputStream

The Read( byte[] buffer, int offset, int length ) method in System.IO.Stream

Write one byte

The write( int b ) method in java.io.OutputStream

The WriteByte( byte value ) method in System.IO.Stream

Write an entire byte arrayjava.io.OutputStream

The write( byte[] b ) method in

No specific method -- use the method that requires an offset/count pair

Write a portion of a byte array

The write( byte[] b, int off, int len ) method in java.io.OutputStream

The Write( byte[] buffer, int offset, int length ) method in System.IO.Stream

One piece of reminder advice for those Java programmers out there -- do not forget to catch IOException. Unlike Java, the C# compiler is not going to enforce exceptions at compile time.

Comment on this articleYour thoughts for Raffi on C# and its approach to this.
Post your comments

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:

IAsyncResult iar sbs.BeginRead( buffer, 0, 1, new AsyncCallback( = callback ), null );

with the callback method looking like

public void callback( IAsyncResult iar )

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.

Filesystem I/O

Performing disk operations in Java is pretty simple -- it mostly involves manipulating the java.io.File object and using either a java.io.FileInputStream or a java.io.FileOutputStream. As we have seen many times before, C# is like Java but slightly different.

Like Java, C# File objects do not have a concrete tie to the underlying filesystem; it is possible to create File objects for non-existent files, and it is also possible create a File for an existing file and move that file out from underneath the CLR without the C# program noticing until it attempts to open the file. Unlike Java, the File object can play a much more pivotal role as it has static methods such as CreateText or AppendText that will return a stream to the filesystem. In Java, the constructor for the FileInputStream must be used to get the same functionality.

To create a new file for writing to in Java, you just have to use the FileInputStream to

FileOutputStream fos new FileOutputStream( "brand-new-file.txt" =);
fos.write( ... )

but C# allows either a

Stream s File.Create( "brand-new-file.txt" );

or a

StreamWriter sw File.CreateText( "brand-new-file.txt" );

to get a Stream or a StreamWriter to the new file. (Appending in Java is done by setting the "append" boolean in one of the FileOutputStream's constructors.) Java allows for reading from files using the java.io.FileInputStream, while C# has static methods named Open Write and OpenText. Lastly, C# offers more fine-grained control in its Open method -- this method exposes the ability to set the file permissions and access contexts.

Table 3: Manipulating files for reading and writing
The methods to use to either read or write from files in both Java and C#

Function

Java

C#

Create a new file for writing

Use the java.io.FileOutputStream

Either use the static File.Create method, static CreateText, or the instance method CreateText

Write to an existing file

Use the java.io.FileOutputStream

Use either the static or instance OpenWrite method

Append text to a file

Use the java.io.FileOutputStream, but use the constructor that takes the append parameter

Use either the static or instance AppendText method

Read text from a file

Use the java.io.FileInputStream

Make use of the static or instance OpenRead or OpenText methods

Another improvement that C# has made that is worth mentioning for curiosity's sake is the inclusion of a File.Copy method. A problem that most Java programmers who have worked with filesystem I/O notice is the inability to properly move files. java.io.File contains a renameTo method that can rename a file; however, that does not work over filesystem boundaries (disks, networks, etc.). Most of the time, programmers are forced to implement their own move command, which copies the file using both a java.io.FileInputStream and a java.io.FileOutputStream, then deletes the original file. C#'s inclusion of a Copy method makes moving files trivial, though the File.Move command also does not work across volumes and filesystem boundaries.

The C# file-system implementation does not have to deal with the cross-platform issues that the Java model has to cope with. There are no equivalent variables to the java.io.File.pathSeparator or the java.io.File.separator. Unfortunately, this also means that the favorite of the java.io.File constructors

public File( File parent, String child )

does not exist -- instead, C# programmers are left with constructing a new System.IO.File object with

File parent  ...
File child  new File( parent.FullName + "\\" + childName );

Understanding Networking

Both programming languages provide a few layers of abstraction around a base level socket implementation -- granted, Java's java.net.Socket class is far more abstract than C#'s System.Net.Sockets.Socket class.

Table 4: Network architecture tiers in Java and C#
Both Java and C# have different abstraction layers for networking that allow for leveraging different aspects of the interface

Tier

Java

C#

Response/Request

java.net.URL and java.net.URLConnection

System.Net.WebRequest

Protocol

java.net.Socket and java.net.ServerSocket for TCP/IP; java.net.DatagramSocket and java.net.MulticastSocket for UDP

System.Net.Sockets.TCPListener and System.Net.Sockets.TCPClient for TCP/IP; System.Net.Sockets.UDPClient

Raw Socket

N/A

System.Net.Sockets.Socket

The Response/Request tier can be used for HTTP type requests, where one end initiates a connection, sends bytes down the stream, and then blocks while it waits for a set of bytes as a response. For more fluid stream-like operations, the protocol tier can be very useful (we will cover TCP/IP operations below). Most Java programmers, unless highly optimizing network operations, do not require fine socket control -- C# still does provide the ability to control raw Berkeley sockets if it is needed.

Response/Request Tier

This tier heavily abstracts away all the networking and provides a stream-like interface to move data back and forth. Java will take a HTTP URL and perform a GET simply by doing a

URL url  new URL( "http://to.post.to.com" );
URLConnection urlConnection  url.openConnection();
InputStream input  urlConnection.getInputStream();
... read stuff from input ...
input.close();

C# mimics this code with its System.Net.WebRequest object:

WebRequest request WebRequestFactory.Create( = "http://to.post.to.com" );
Stream input request.GetResponse().GetResponseStream();
... read stuff from input ...
input.Close();

Both of these implementations will hide the underlying socket creation and HTTP protocol requirements and will simply provide streams that the programmer can use to ship and then receive data. Just like the C# Stream class, the WebRequest class has methods to asynchronously get a request stream to write to or a WebResponse object to read from.

Protocol Tier

The System.Net.Sockets.TCPClient class should seem very familiar to those Java programmers familiar with java.net.Socket, as they are nearly identical; both share a very similar API and share similar functionality, as the programmer does not have to deal with the socket implementation but instead only the return streams to be used.

A simplistic telnet client implementation can be concocted in Java by simply using:

Socket telnet  new Socket( "telnet.host.com", 23 );
OutputStream output  telnet.getOutputStream();
InputStream input  telnet.getInputStream();

and both streams can be used in conjunction to telnet to telnet.host.com. The same program can be written in C# in almost the same fashion:

TCPClient telnet  new TCPClient( "telnet.host.com", 23 );
Stream telnetStream  telnet.GetStream();
StreamWriter output  new StreamWriter( telnetStream );
StreamReader input  new StreamReader( telnetStream );

Also, receiving a TCP/IP connection is nearly identical in both languages, as an incoming socket in Java is set up and then received using:

ServerSocket server  new ServerSocket( 23 );
Socket accept  server.accept();

while C# allows for:

TCPListener server  new TCPListener( 23 );
server.Start();
Socket accept  server.Accept();

In both languages, each socket that is accepted needs to be dealt with separately. In Java, the preferred way (until Java 1.4) is to spawn a thread for each individual socket that is received. The same can be done for the C# sockets; however, the Socket class provides the ability to use an event-driven interface with the "select" method. (Programming sockets in an event-driven model is outside the scope of this article.)

Raw Socket Tier

Here, we probably venture into unfamiliar territory for most Java programmers. Java-only programmers rarely need to know anything about the Berkeley socket implementation, as it is being abstracted away by the java.net.Socket and java.net.DatagramSocket classes. By manipulating this Berkeley socket class properly, the familiar Java functionality of streams can be achieved.

Now we have a C# repertoire that includes the most powerful abstractions from Java -- the ability to perform I/O and networking. Check back for the next article in this series, which will cover multi-threading to allow for parallel operations.

Raffi Krikorian makes a career of hacking everything and anything. Professionally, he is the founding partner at Synthesis Studios: a technological design and consulting firm that orchestrates his disjointed train of thought.


Return to ONJava.com.

Copyright © 2009 O'Reilly Media, Inc.