Streaming: How to Get Bytes From Here to There in Smalltalk
Craig Latta
26 May 1998
I've written a framework for communicating bytes in Smalltalk
with a simple transport-independent interface. It's based on the
stream, which is essentially a sequence of elements that
remembers a position. I use streams to access files, network
connections, serial ports, and other external resources. I also use
them to manipulate "internal" collections in local memory.
In this tutorial we'll see how to create and use streams on
various resources.
creating and using simple streams
We'll start by creating a couple of internal streams and
manipulating them in simple ways. First let's make a global variable
for our examples:
"Make a global variable for referring to an example stream."
Smalltalk at: #ExampleStream put: nil
reading
There are two ways to create a stream on a collection. One is
to send a message to the collection. The other is to send a message to
one of the stream classes, with a collection as a parameter. For
example, the following creates a stream on a collection of characters:
"Create a stream on a collection of characters."
ExampleStream := 'this is merely a test' stream
Now let's read a character from it:
"Read an element from ExampleStream."
ExampleStream next
The answer is the Character $t. You can also read several
characters at once:
"Read several elements from the example stream."
ExampleStream next: 3
The answer is a String, 'his'. Note that the stream remembered
its position within the sequence after reading the first character.
This is why the messages are based around the word next; you're
reading the next elements from the current position.
You can also ask a stream for its entire contents:
"Answer the entire contents of the example stream."
ExampleStream contents
Sending contents doesn't affect the stream's
position. Try sending next again to see this. You can, however,
change the stream's position without reading per se:
"Reposition the example stream two elements forward."
ExampleStream skip: 2.
"Reset the example stream's position to the beginning."
ExampleStream reset
Finally, you can read elements from the collection without
changing the stream's position:
"Read an element from the example stream without changing
the stream's position."
ExampleStream peek.
"Read several elements from the example stream without
changing the stream's position."
ExampleStream peek: 3
Reading from streams is most commonly used in the system for
parsing. For an example, see NetMessage>>initializeFrom:withSize:.
writing
The stream we just created can read, but it can't
write. Writeable streams are instances of a different Class which
understands more messages and whose instances take up slightly more
memory. They're created with a different message, and are typically
created on empty collections:
"Create a stream on an empty String."
ExampleStream := (String new: 16) writeableStream
The messages for writing are similar to those for reading:
"Write a character with the ExampleStream."
ExampleStream nextPut: $h.
"Write several characters with the ExampleStream."
ExampleStream nextPutAll: 'ello world!'
As before, you can ask a stream for its contents. Since
we're writing to the example stream, it will answer differently as
elements are written.
Writeable streams have more diverse positioning behavior, to
avoid ambiguity when positioning backward. You can simply move a
stream's position backward, or you can also retract elements
which have been written. For example, after this:
"Reposition the example stream backward two elements."
ExampleStream skip: -1
The stream's contents is still 'hello world!'. But after this:
"Retract the example stream's previous element."
ExampleStream retract
The stream's contents is 'hello word!' (assuming you performed
the skip: in the previous example).
This touches on a subtle point. A stream's position is a
pointer into sequence, between two elements, before the
first element, or after the last element. For example, if you
reset the example stream, its position will be zero. At the end of
'hello world!', the position is twelve. After writing the first
element, the position is one.
Writeable streams also understand the reading messages. In
addition to writing after positioning backward, several places in the
system actually perform both reading and writing with the same
writeable streams. This makes careful use of positioning behavior very
important.
Streams understand several more messages that we didn't
explore here. For example, you can ask a stream if it's positioned at
the end (with atEnd), and you can search for patterns of
elements (with throughAll:). Browse the Stream and
PositionableStream classes to see them.
streaming over diverse collections
For simplicity, our examples so far have involved collections
of characters. However, you can use streams to manipulate collections
of any objects you like. For example, you could create streams on
collections of colors, musical notes, or instances of any new class
you create. Perhaps the most popular kind of stream content currently
is the integer, specifically those integers between 0 and
255...
streaming bytes to and from the system
Another simplifying characteristic of our examples has been
the internal nature of the collections. We used collections
whose elements were all stored within local memory. Often it's useful
to perform streaming operations with collections located
elsewhere. The very use of the words "collection" and "located" may be
an abstraction in some cases. All that matters is that the elements
are sequenceable. Streams that perform these external exchanges are
called external streams.
The "collection" an external stream manipulates is called an
external resource. The most common external resources are disk
files, serial ports, and sockets. We'll look at creating and using
external streams on these resources.
file streams
External resources are identified by name. In turn, external
streams are typically created from a resource name, or "universal
resource locator" (URL). To create a file stream, you first must
create a filename URL, then send a stream-creation message to the
filename.
"Create a stream on a named file."
ExampleStream := 'file:///c:/autoexec.bat' asFilename stream
Filename URLs understand the same stream-creation messages
that SequenceableCollections do, so you can create writeable streams
in the usual way. Once you have a stream, you can use the usual
messages for reading, writing, positioning, etc.
A new concept here is that of closing. In creating a
stream, you are implicitly making a connection between the stream and
the thing it will manipulate. That connection needs to be severed
gracefully. For an internal stream, this connection is a memory
reference, and is automatically retired by the memory management
system. Therefore, when you're finished using an internal stream you
may simply ignore it. But an external resource's shutdown behavior
often involves action outside the system. External streams need to be
closed explicitly:
"Close an external stream."
ExampleStream close
All external streams are closed this way, regardless of the
kind of external resources to which they have been connected.
It's possible that an error or other unexpected event may
occur between the time an external stream is created and when it
should be closed. The system has a facility for ensuring that actions
occur despite such interruptions:
"Ensure that a file stream is closed, despite the occurrence
of an error just after it is opened."
[
ExampleStream := 'file:///c:/autoexec.bat'
asFilename stream.
3 zork
]
valueEnsuring: [ExampleStream close]
This facility is part of the exception-handling
system. For more information about exception-handling, see the
examples in BlockContext class. Another way of ensuring special
terminating actions is by performing them as part of automatic memory
reclamation. This is commonly known as finalization.
Most underlying filesystems have a permissions
system. Note that you can create a writeable file stream on a
"read-only" file without complaint from the system. It's only when you
actually try to write with such a stream that you'll encounter an
error. We've left it to the underlying filesystem to keep track of
permissions and to report errors (which are then relayed by
Smalltalk).
net streams
External streams on resources other than files are called
net streams. Conceptually, you can think of a net stream as
connected to a sequence of bytes which is undefined at creation
time. This is in contrast to streams created on internal collections
or files, which may be regarded as a predefined sequences of
bytes. Indeed, what you'll read from a typical net stream depends
strongly on what you write to it.
Net streams are created by sending messages to the
NetStream class.
serial port streams
As we've seen, creating an external stream is mainly a matter
of referring to the desired resource appropriately. A serial port
stream is created like this:
"Create a stream on a serial port."
ExampleStream := NetStream onSerialPortAt: 1
Once created, a serial port stream is not yet active. This is
because there are typically several additional parameters which need
to be set to non-default values before actually opening the
resource. For example, you can change the baud rate from the default
of 9600 baud:
"Change the baud rate for a serial port stream."
ExampleStream baudRate: 38400
For the other such messages you can send, see the
control-serial protocol of NetStream. After these parameters
have been set, you can open the stream:
"Open a serial port stream."
ExampleStream open
As with other external streams, don't forget to close serial
port streams after you're finished with them.
socket streams
IP sockets are identified by family (TCP or UDP), port,
hostname, and role (client or server). The NetStream class provides
several messages requiring varying levels of detail (and providing
various defaults). Here's an example TCP client:
"Create a TCP client to the echo port (7) at the host named
netjam.org."
ExampleStream := NetStream tcpClientToPort: 7
atHostNamed: 'netjam.org'
Unlike serial port streams, socket streams are active as soon
as they are created.
other NetStream features
Particular numeric positions are no longer meaningful when
dealing with net streams. This is because the byte sequences being
manipulated usually have no well-defined beginning or end at
stream-creation time. Attempting to get or set the numeric position
of a net stream results in an error. You can, however, peek
forward and skip backward. You can also keep track of the
number of bytes that have been read, and you can reopen a
stream that has been closed.
correspondents
There is also support in the system for modeling the local
side of a conversation involving various net streams. It is provided
by the Correspondent hierarchy. In particular, the
Client class is useful for defining socket-based clients (e.g.,
POPClient), the Server class is useful for defining
socket-based servers (e.g., HTTPServer), and the
SerialCorrespondent class is useful for defining correspondents
which communicate via serial ports. The correspondent framework helps
to organize various details about initializing external resources
(e.g., remembering that 'echo' protocol conversations happen on remote
port 7), and provide a place to put other protocol-specific behavior.
explore and enjoy
We've looked at the basic ideas involved in using streams in
flow. Have fun!
concept hierarchy
stream
sequence of elements
internal resources (collections)
external resources
position
creation
stream
writeableStream
reading
next
next:
peek
peek:
writing
nextPut:
nextPutAll:
positioning
skip
skip:
retract
retract:
position
position:
reset
resetRetainingContents
position indices point between elements
utilities
atEnd
throughAll:
(etc.)
external streams
external resources
files
net resources
serial ports
sockets
IP
TCP
UDP
(numeric positions no longer apply)
(correspondents)
closing
ensured behavior
exception-handling
finalization
reopening
permissions
|