Introduction
EchoStream
is a full-featured echoing stream implementation for
.NET. In a nutshell, an echoing stream "unions" two other streams, and anything
that is written to it is in-turn written to the two underlying streams. This is
sometimes called a "tee" stream. EchoStream
supports this tee
functionality as well as a particular kind of echoing read, in which reads
always come from one of the two streams and anything that is read is echoed to
the second. Finally, EchoStream
includes extensive error handling options that
allow a user to choose exactly how echoing errors should be handled.
Background
Back when I did a Java version of my current project, I found a class called
TeeOutputStream (or TeeStream) invaluable. It was written by Anil Hemrajani and
found somewhere on Sun's Java Developer Site.
I modified and extended that class, and wrote an accompanying TeeInputStream,
and life was good.
Now that our active development platform is .NET, I found myself needing the
exact same functionality. However, I was unable to find a ready-made
implementation myself, so I decided to write one from scratch. The result is
EchoStream
.
EchoStream
is implemented in C#, and my code
examples are in that language as well.
Using the Code
The simplest use of EchoStream
is as a tee, or write-only, stream. One
example of when you might need this is when doing network communications. You
may have a requirement to log everything that you send over the network stream
to a local file. The following code demonstrates this use case.
byte[] outBytes = GetDataToWrite();
NetStream netStream = GetNetworkStream();
FileStream logStream = GetLogStream();
EchoStream stream = new EchoStream(
netStream, logStream, EchoStream.StreamOwnership.OwnNone
);
stream.Write(outBytes, 0, outBytes.Length);
stream.Close();
The source code is pretty self-explanatory. As long as the EchoStream
is open, writes to it are propagated into the
two streams that were passed to its constructor.
As alluded to in the code above, you can use the EchoStream.StreamOwnership
enumeration to control whether
the EchoStream
closes neither, either one, or both of its
constituent streams whenever it is closed itself.
If you write anything directly to either of the underlying streams, EchoStream
won't know anything about it, and the write will
not be echoed. This may be a useful thing to do in some circumstances.
EchoStream
never buffers its input, so interspersing writes to the
underlying streams with writes to an EchoStream
should
always be safe.
Now, it just so happens that if you have a requirement to log anything going
out of your application through a stream, you will probably also have to log
anything coming into it. EchoStream
handles this as
well, by identifying one of its input streams as its PrimaryStream
,
and the other as its SlaveStream
. When writing, the primary
stream and the slave stream work identically (well, almost; see below). However,
when reading, the echo stream always reads from its primary stream, never from
its slave stream. Anything that it reads from its primary stream is then
"echoed", or written, into the slave stream. Thus the name of the class.
EchoStream
takes the first stream passed to its
constructor as the primary stream, and the second one as the slave stream. These
streams are thereafter accessible through the PrimaryStream
and
SlaveStream
properties.
This code shows how to read from an echo stream.
NetStream netStream = GetNetworkStream();
FileStream logStream = GetLogStream();
EchoStream stream = new EchoStream(
netStream, logStream, EchoStream.StreamOwnership.OwnNone
);
byte[] inBytes = new byte[4096];
int nRead = stream.Read(inBytes, 0, inBytes.Length);
As another example, say you need to read and write to the network stream, but
you need your input and output to go to two separate streams. Because
EchoStream
never does any buffering itself, you can accomplish this
simply by creating two EchoStream
objects with the same primary
stream, but different slave streams, as in the following code.
byte[] outBytes = GetDataToWrite();
NetStream netStream = GetNetworkStream();
FileStream outLogStream = GetOutLogStream();
FileStream inLogStream = GetInLogStream();
EchoStream outStream = new EchoStream(
netStream, outLogStream, EchoStream.StreamOwnership.OwnNone
);
EchoStream inStream = new EchoStream(
netStream, inLogStream, EchoStream.StreamOwnership.OwnNone
);
outStream.Write(outBytes, 0, outBytes.Length);
byte[] inBytes = new byte[4096];
int nRead = stream.Read(inBytes, 0, inBytes.Length);
stream.Close();
Exception Handling
The example above will hum right along until one day it runs on a client's
machine who has 20 megs of free disk space. Suddenly, it will throw an exception
and the network operation will fail, even though nothing bad happened with the
network. In post-mortem analysis, it may come up that the 25 megs of data you
were transferring were never stored on disk, but yet somehow a disk space error
occurred. The answer, of course, is the log files. The way that code is written,
writes to your log files must be successful in order for the network operation
to also be successful.
For some cases, you may find that writing to the slave stream is just as
important as writing to the primary stream, and both must succeed for the
operation to succeed. However, in the case I just described above, let's assume
that communication over the network stream should not be interrupted even if
something had happens while writing to the log streams.
EchoStream
treats the primary and slave streams
differently in terms of exception handling, even in the write case. (This is the
one difference for the write case noted above). Whenever both streams will be
modified by an operation, EchoStream
always modifies the primary
stream first, and always propogates any exception that is thrown back to the
caller. After the primary stream has been successfully modified, however,
EchoStream
's error handling abilities come into play.
EchoStream
supports a set of properties that control its
behavior when exceptions occur while writing to the slave stream. These
properties are as follows.
SlaveReadFailAction
SlaveReadFailFilter
SlaveWriteFailAction
SlaveWriteFailFilter
SlaveSeekFailAction
SlaveSeekFailFilter
LastReadResult
For each of the primary operations on a stream, EchoStream
supports a pair of properties that specify an
action to take on failure, and an optional filter to use. The action can be one
of Propogate
, Ignore
or Filter
.
The default action is Propogate
. When this action
is set, any exception caused by an operation on a slave stream after the primary
stream has already been modified is allowed to propagate out of
EchoStream
. This is the most efficient behavior because
EchoStream
does not need to enter an expensive
try
block at any point during the operation.
The action that is most useful for the scenario described above is Ignore
. When this action is set, any exception caused by an
operation on a slave stream after the primary stream has been modified is
silently caught and ignored by EchoStream
. So for the
example described above, adding these lines to the code before the first read or
write would have solved the problem of a logging failure causing the entire
operation to fail.
outStream.SlaveReadFailAction = EchoStream.SlaveFailAction.Ignore;
outStream.SlaveWriteFailAction = EchoStream.SlaveFailAction.Ignore;
outStream.SlaveSeekFailAction = EchoStream.SlaveFailAction.Ignore;
inStream.SlaveReadFailAction = EchoStream.SlaveFailAction.Ignore;
inStream.SlaveWriteFailAction = EchoStream.SlaveFailAction.Ignore;
inStream.SlaveSeekFailAction = EchoStream.SlaveFailAction.Ignore;
Also, in this particular case, where we simply want to ignore all possible
exceptions that the logging streams can cause, we could use the following
write-only shortcut property.
outStream.SlaveFailActions = EchoStream.SlaveFailAction.Ignore;
inStream.SlaveFailActions = EchoStream.SlaveFailAction.Ignore;
This is more maintainable for this common case because you don't have to go
back and change your code later if new exception-related properties are added to
EchoStream
; using the shortcut properties shields you from that.
The final action, Filter
, is probably the
least common but definitely the most flexible of the possible actions. You
cannot set any of the "action" properties to this value directly. Instead, it is
set implicitly whenever you set one of the filter properties, as in the
following code.
inStream.SlaveWriteFailFilter = new EchoStream.SlaveFailHandler(OnWriteFail);
Debug.Assert(inStream.SlaveWriteFailAction == EchoStream.SlaveFailAction.Filter);
...
private EchoStream.SlaveFailAction OnWriteFail(
object oSender, EchoStream.SlaveFailMethod failMethod,
Exception exc)
{
}
As you can see, using a filter allows you to examine the exception that
occurred and instruct the EchoStream
on how to proceed. You can use
the same method to handle read, write and seek failures by using the
failMethod
parameter to distinguish between them, or you can
register different methods for each case. Also, just as with the "action"
properties, you can set a handler for all filters at once by using the
SlaveFailFilters
method.
A final note is in order for the Propogate
case.
You may decide that, for maximum efficiency, you want to avoid the expensive
try
blocks inside EchoStream
and instead allow
exceptions to propogate back out to a handler that you install in your own code,
somewhere such that it doesn't have to be entered once for each read and write
operation. You handler may handle the exception somehow (perhaps by detaching
the EchoStream
and moving to writing only to the primary stream,
for instance if the logging streams have "gone bad" due to bad disk space) and
then restart your read or write loop. This will work fine, except in the case
where the exception happened during a read operation. In that case, the
exception caused the return value from Read
to be lost from your
point of view, so you can't tell how much was read from the stream in order to
process the results of the successful read on the primary stream that happened
before the slave stream threw an exception. That is where the
LastReadResult
property comes in. LastReadResult
always reflects the result of the last Read
operation that
occurred on the stream, allowing you to pick up where you left off.
Additional Methods and Properties
Read
, Write
have been covered extensively now, but
there are other methods that can potentially modify the underlying streams.
The Seek
and Position
properties both
work in exactly the same way. They use the SlaveSeekFailAction
and
SlaveSeekFailFilter
properties in exactly the same way as described
for Read
and Write
. Additionally, these properties
attempt to work correctly on both underlying streams. For example, imagine that
you write "I see a little silhoueto" to a stream and then make it the primary
stream in an EchoStream
. Imagine also that you have not yet
written anything to your slave stream. Writing " of a goat." to the echo stream
results in the primary stream containing "I see a little silhouette of a goat.",
and the slave stream containing simply "of a goat." Now you realize that you've
badly misspelled "lamb" and written "goat" instead, so you decide to fix the
mistake. If you set the position of the stream to the length of the "I see..."
string, that position will be set directly on the primary stream, but a relative
position will be computed for the slave stream based on the change in position
of the primary stream. The result is that you get what you expect: the stream
pointer for the primary stream goes back to the position just after the 'o' in
"silhouette", and the stream pointer for the slave stream goes back to position
zero.
Similar to Seek
and Position
is
SetLength
. This method changes the size of the slave stream by the
same amount that it changes the size of the primary stream, rather than setting
them both to the same size. This is in the same spirit as the implementation of
Seek
and Position
.
The final method of Stream
that can modify the
stream itself is Flush
. Flush
, however, is just a
special case of write, delayed due to buffering. EchoStream
never
buffers reads or writes, but its constituent streams may, so it is still
possible for calling Flush
on an EchoStream
to cause
an exception in one of the underlying streams. Because Flush
is
just a special case of Write
, however, EchoStream
just
uses the same exception-handling properties used by Write
. In this
way, any buffering that is done by the underlying streams is relatively
transparent to your error handling code, as long you remember that
Flush
may cause the Write
exception-handling
framework to be called into action.
Points of Interest
The initial implementation of EchoStream
was very simple and
straightforward. It was not until I began to write documentation for the methods
and properties of the class that I realized just how woefully inadequate the
class was in terms of exception handling. The nature of streams, such as the
fact that many streams do not support seek operations or those operations are
slow, prevent me from writing the methods of the class in an entirely
exception-safe manner (in which either both streams are successfully changed, or
neither are). However, because one of the two streams in the echo stream is so
often going to be subordinate to the other (at least in the use cases I have
thought of), it made sense to try to isolate callers from certain failures, or
to give them control over what happens when failures occur with that subordinate
stream. Thus was born the exception handling facilities for
EchoStream
, which were entirely absent in the Java Tee streams that
were the inspiration for EchoStream.
Adding this error handling code increased the size of the source (including
documentation) from ~360 lines to ~850 lines, and greatly increased the
complexity of some of the core methods of the class. I feel confident, however,
that with these error handling capabilities the class is much more robust and
that, after any hidden bugs have been plied out of it through use by myself and
any of you in the CodeProject community, it will be a very useful and reliable
tool.
Finally, by necessity, the code downloads for EchoStream
also include my
Covidimus.Diagnostics.Debug
class. That class is not covered
in this article, but you may want to take a look at it; it may prove useful to
you.
Enhancements
The most likely enhancement I can see for this class is actually something
that I probably will never add to it due to efficiency concerns, and that is the
ability to have multiple slave streams. I do not wish to add this capability
directly to EchoStream
because it implies keeping an array of slave
streams and iterating over them for each modifying operation. Each slave stream
would need to have errors handled separately for maximum robustness (meaning
that any try blocks that must be entered will need to be entered once for each
stream in the list). Even in the Propogate
case, I just don't want
to add the overhead of iterating over an array to the EchoStream
methods when the most common case will be iteration over a single item. It is
possible to chain multiple EchoStream
objects together to get this
behavior, but this is less efficient still than iterating over an array would
be. The real solution, in my opinion, would be to develop a class parallel to
EchoStream
called MulticastEchoStream
, which adds
support for having multiple slaves. I will leave development of such a beast as
an exercise for the reader.
History
- April 7, 2003 - Initial Posting