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.
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 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# | ||
Function |
Java |
C# |
Read one byte |
The |
The |
Read a whole byte array |
The |
No such syntactical method -- use the read method that has an offset/count pair |
Read into a portion of a byte array |
The |
The |
Write one byte |
The |
The |
Write an entire byte |
The |
No specific method -- use the method that requires an offset/count pair |
Write a portion of a byte array |
The |
The |
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.
|
|
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 methodsBeginRead 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.
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# | ||
Function |
Java |
C# |
reading |
Implement, at minimum, the |
Cause the |
seeking |
Have the skip method in |
Use the |
writing |
Implement the |
Return true from the |
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.
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.
|
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 | ||
Function |
Java |
C# |
Create a new file for writing |
Use the |
Either use the static |
Write to an existing file |
Use the |
Use either the static or instance |
Append text to a file |
Use the |
Use either the static or instance |
Read text from a file |
Use the |
Make use of the static or instance |
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 );
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# | ||
Tier |
Java |
C# |
Response/Request |
|
|
Protocol |
|
|
Raw Socket |
N/A |
|
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.
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.
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.)
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.