|
Please patronize sponsors of this page!
Bytesmiths no longer is involved in software consulting. Maintenance of this web site is currently subsidised by unrelated business activities. Please pass the word to other interested folks, so I can continue to host this page!
- Bytesmiths Editions -- large, archival, fine-art photography on unusual materials
- Bytesmiths Press -- artists' services: web design/hosting, jury slides, giclee reproductions, opening announcements, brochures, etc.
- Champagne Beadworks -- handcrafted jewelry and beadwork
- Crafted By Carol -- handcrafted jewelry and beadwork
- EcoReality, an organization devoted to establishing a sustainable ecovillage
- Ecovillage Newsletter -- Diana Leafe Christian's news of her travels.
- Gemini Gypsy -- Carole Good-Hanson's fused glass frames
- Green Chipper -- light forestry and environmental services.
- Salt Spring Island Society for Community Education -- community education on our island of 10,000.
- Veggie Van Gogh -- two artists' mobile warehouse and living quarters, petroleum-free!
- Veggiemog -- life and times of Kelly O'Toole's Unimog, running on biodiesel
Your site could be listed here, for as little as $12 per month! Go to Bytesmiths Press for details.
This site has been selected by PC Webopaedia as one of the best on this topic!
This site has been awarded a Links2Go Key Resource Award in the Smalltalk category!
Originally published in The Smalltalk Report, August 1996.
Smalltalk as an Internet Server
by Jan Steinman
In the last issue, we
demonstrated how to turn arbitrary Text objects into
HTML, and in September 1995, we
demonstrated how to modify VisualWorks under ENVY so you could store
all your commentary in styled text. The only thing missing to have
live web access to your Smalltalk project documentation is a server!
We've never been fond of specific solutions to general problems.
It would be easy to hard-code a server that is dedicated to serving
HTML versions of Smalltalk documentation, but there is so much code
that is common to any server that we couldn't ignore the
potential re-use.
For example, a server of any kind has these needs:
- Manage a socket name space -- you can't simply pick any number
for your socket.
- Initialize a socket and prepare it for use.
- Loop forever, waiting for connection requests.
- Record service requests in a log.
- Screen service requests for security reasons.
- Fork off individual service requests, so the main loop isn't
unduly delayed from its primary task of waiting for connections.
- Manage unexpected problems that might occur in a service
request.
- Finally, perform the requested service, and return a result.
The following TCP server framework for VisualWorks hides away all
but the last step, allowing the server author to concentrate on the
actual service being provided, without being distracted by the
mechanics of managing sockets, processes, logs, and exceptions.
Setting up a server
instance
Our server is defined as:
Object
subclass: #TcpServer
instanceVariableNames: 'port socket server service handler
requests logger logProtect'
classVariableNames: 'CanTalkToBlock DefaultHandlers
DefaultServices Portmap PortmapControl '
poolDictionaries: ''
Instances of TcpServer provide stateless services on
Transmission Control Protocol (TCP) Internet domain sockets. Each
instance is uniquely associated with a port number on a given
machine, which must be supplied when creating an instance. Because
port must be unique, we use a class Portmap
registry to maintain this uniqueness. Because this registry will be
accessed from multiple threads of control, it must be protected by a
mutual exclusion mechanism in PortmapControl. We set all
this up when initializing the class, which also sets up the default
security and the DefaultHandlers and
DefaultServices.
TcpServer class
initialize
"Set up long-term state that is used for instance management."
self beSecure.
"If this is a re-initialize, be thread-safe."
PortmapControl == nil ifFalse: [self shutDown].
PortmapControl := Semaphore forMutualExclusion.
Portmap size > 0 ifTrue:
[Portmap copy do: [:server | server terminate]].
Portmap := IdentityDictionary new.
(DefaultHandlers := IdentityDictionary new)
at: 0 put: self nullHandler.
(DefaultServices := IdentityDictionary new)
at: 0 put: self discardService
onPort: portNumber
"Answer an active instance that provides default services for
port <portNumber>."
^Portmap
at: portNumber asInteger
ifAbsent: [(self new port: portNumber asInteger) resume]
The connection security mechanism employs a block that answers a
boolean when passed an incoming socket. If the block answers
false, the connection is dropped immediately. This class
method sets the default connection security, but once the connection
security is passed, an individual server can take extra precautions,
or implement finer graduations of security. We also have utility
methods beFriendly , which allows all connections,
and beLonely , which only allows connections from
the same machine, which can be useful for testing.
TcpServer class
beSecure
"Set the default security to only accept connections from the
same network."
CanTalkToBlock := [:socket |
socket getPeer networkAddress = socket getName networkAddress]
The final part of class initialization declares what to do when an
instance is created for a port number that does not have a default
service block or exception handler. Normally, an instance has its own
service and handler. If not, a default service and/or handler is
fetched from the class for a given port number. If even that fails,
then the "default default" service and/or handler is used. Since zero
is an illegal port number, we use it to hold the "default default"
service and handler.
TcpServer class:
nullHandler
"Answer a handler for when nothing is to be done with errors.
(This is generally not a wise choice!)"
^[:exception :stream | ]
discardService
"Answer a 'discard' service, which is to be used when no service
can be found for a given port."
^[:stream | ]
Finally, accessing methods for the unique identifying information
for an instance must take some special actions.
TcpServer:
port
"Answer the port that is listened to by this server for
requests. If none is given, answer 7, for an echo server."
^port ? [7]
port: portNumber
"Initialize me with state needed for default communication on
the given <portNumber>. Answer myself."
self
port: portNumber
service: nil
handler: nil
logger: System errorLog
The definition of the ? method was published in
our January 1996
column. It simply answers the receiver, unless it is
nil, in which case the argument is evaluated and answered.
Note that we also use a few ENVY utility methods in this code, which
you will need to change if you are not going to use this as an ENVY
documentation server.
With one more method, we will have all the essential base state
needed to implement our server. This is the primary instance
initialization method.
TcpServer:
port: portNumber
service: serviceBlock
handler: exceptionBlock
logger: logStream
"Initialize me with state needed for a particular task. Any
argument can be nil, and will be defaulted suitable for an 'echo'
server that logs to the Transcript.
<portNumber> is an Integer port number to listen to.
<serviceBlock> is a one-argument block that is passed a stream
on a transient socket on <portNumber> when a connection arrives.
<exceptionBlock> is a two-argument block that is passed the
exception and the socket stream when <serviceBlock> has an unhandled
exception.
<logStream> is place to write log messages."
requests := WeakArray with: 0.
service := serviceBlock.
handler := exceptionBlock.
port := portNumber asInteger.
logger := logStream ? [Transcript].
(self class register: self) ifFalse:
[self error: 'You already have a service on this port!
You can only have one service per port per machine.']
Remember the need to keep track of port numbers? This is handled
by the class, which also needs a way to "forget" about port numbers
as their server instances are released. The class also manages
associations between port numbers and the services (and their
exception handlers) that each port provides.
TcpServer class:
register: instance
"Register the given <instance> of myself, unless one is already
registered at that port.
Answer success or failure."
^(Portmap
at: instance port
ifAbsentPut: [PortmapControl critical:
[instance]]) == instance
unregister: instance
"Unregister the given <instance> of myself. Don't complain if I
can't find it."
PortmapControl critical:
[Portmap removeKey: (Portmap
keyAtValue: instance ifAbsent: []) ifAbsent: []]
defaultHandlerFor: portNumber
"Answer an appropriate handler for <portNumber>, or a default
default if none."
^DefaultHandlers at: portNumber ifAbsent: [DefaultHandlers at: 0]
defaultServiceFor: portNumber
"Answer an appropriate service for <portNumber>, or a default
default if none."
^DefaultServices at: portNumber ifAbsent: [DefaultServices at: 0]
Recall that class initialization set up a
nullHandler and a discardService to
be used when nothing else was specified for a given instance on a
given port number. That means we need a way to associate other
handlers and services with ports, so that instances can be tightly
cohesive with a port number, but loosly coupled with a service and
handler.
The "default default" of a discardService with a
nullHandler doesn't make for a very useful server!
TcpServer class:
defaultHandlerFor: portNumber is: twoArgBlock
"Set the exception handler for <portNumber> to <twoArgBlock>.
When evaluated, the two arguments will be:
the <exception> that was the argument to the handle: block,
a read-append <stream> on the transient socket that is being
serviced.
This is not thread safe, and should not be changed by some
server action."
2 = twoArgBlock numArgs
ifFalse: [self error:
'Sorry, I need a two-argument clean block here!']
ifTrue: [DefaultHandlers at: portNumber put: twoArgBlock]
defaultServiceFor: portNumber is: oneArgBlock
"Set the service for <portNumber> to a clean <oneArgBlock>.
When evaluated, the argument will be a read-append <stream> on the
transient socket that is being serviced."
1 = oneArgBlock numArgs
ifFalse: [self error:
'Sorry, I need a one-argument clean block here!']
ifTrue: [DefaultServices at: portNumber put: oneArgBlock]
Making a server
instance useful
Although you need more code for a functional server, at this point
we have the base state needed to create and initialize a server
instance. Now let's put it to work, by deriving the instance state
needed, such as the socket connection and input process.
The basic service and handler are usually lazily initialized from
the class registry of default services and default handlers:
TcpServer:
handler
"Answer a two-argument block that is evaluated upon service
exception. It is passed the exception and a stream. Non-local
returns should not be attempted. The block answer is discarded.
If no handler exists, get one suitable for my port."
^handler ? [handler := self class defaultHandlerFor: self port]
service
"Answer a one-argument clean block that is forked upon service
request. It is passed a stream on the transient socket connection.
Stream closing will be handled by the evaluator. The block answer
is discarded.
If no service has been set, initialize it to one appropriate
for my port."
^service ? [service := self class defaultServiceFor: self port]
This class instance collaboration might not seem necessary, in
fact it isn't. However, the temporality of server instances is very
different from that of port service associations, so it's useful to
bind server instances tightly to a port number, but loosly to a
service.
For example, an instance that is providing World Wide Web service
using Hyper Text Transfer Protocol (HTTP) will be created and
discarded much more frequently than the binding of this service to
port number 80, the default HTTP port number. This reduces coupling
in the time domain, which is often overlooked by designers who
concentrate on reducing behavioral or implementation coupling.
Now that we have a service and a handler, the important stuff can
happen. An independent thread runs the primary
server loop that waits on socket connections, checks
to see if the connection is legal, then services the connection's
request.
TcpServer:
server
"Answer an unscheduled Process that listens for and dispatches
service requests.
This service loop should spend most of its time waiting on a
socket connection, and so has a high priority. The service it
implements is typically time-consuming, and so should be forked at
a low priority, which immediately allows the server to resume
listening for connections."
^server ? [server := [[ | connection |
(CanTalkToBlock value: (connection := self socket accept))
ifTrue: [self serviceRequest: connection readAppendStream]
ifFalse: [[connection close] fork]] repeat] newProcess.
server priority: Processor userInterruptPriority - 1.
server]
socket
"Answer an IOSocketAccessor that listens to my port for service
requests."
^socket ?
[socket := OSErrorHolder existingReferentSignal
handle: [:ex |
logger == nil ifFalse
[logger cr; show: 'You appear to have another
server running on port ', self port printString, ' on this machine.']].
ex returnWith: nil]
do: [IOAccessor defaultForIPC newTCPserverAtPort: self port]]
serviceRequest: stream
"A connection has been accepted on a transient copy of my
socket. <stream> is a read-write stream on that socket. Log the
activity and provide the requested service in a separate thread at
low priority."
self registerRequest: ([self serviceRequestFork: stream]
forkAt: Processor userBackgroundPriority + 1)
serviceRequestFork: stream
"Provide a requested service, based on the socket <stream>,
which at this point has not been read at all. If there is a problem,
invoke an instance-specific handler. This method is forked at a low
priority."
Signal noHandlerSignal
handle: [:ex | self handler value: ex value: stream]
do: [self log: 'Open connection at ',
EmTimeStamp now printString from: stream.
self service value: stream.
self log: 'Close connection at ',
EmTimeStamp now printString from: stream].
OSErrorHolder errorSignal handle: [:ex |] do: [stream close]
Since individual requests are forked off, it is essential to be
able to track them down and kill them if needed, so request threads
are registered in requests, a WeakArray . As
these requests terminate, they are collected as garbage and removed
from requests.
TcpServer:
registerRequest: serviceRequestProcess
"A connection has been accepted and <serviceRequestProcess> has
been forked to deal with it. Hang onto it weakly, so it can be
killed when I'm killed. When it terminates, the scavenger will
remove it from the registry."
(requests includes: 0) ifFalse:
[requests grow replaceAll: nil with: 0].
requests
indexOf: 0
replaceWith: serviceRequestProcess
startingAt: 1
stoppingAt: requests size
Finally, instances need to be started, stopped, and killed. If you
are using ENVY, you'll want to have an application
startUp method that relays to TcpServer
startUp to re-start your servers, and a
shutDown method that relays to TcpServer
shutDown to suspend them. Also remember to have a
removing method that gets rid of all instances by
sending TcpServer initialize.
TcpServer:
resume
"Begin my server."
logger == nil ifFalse:
[logger cr;
nextPutAll: 'Resuming service on port ';
print: self port;
nextPutAll: ' at '; print: EmTimeStamp now; flush].
self server resume
suspend
"Suspend my server in such a way that when it resumes, it opens
a new socket. Terminate any active requests in process."
| count |
logger == nil ifFalse:
[logger cr;
nextPutAll: 'Suspending service on port ';
print: self port;
nextPutAll: ' at '; print: EmTimeStamp now.
count := (requests reject: [:request | request = 0]) size.
count > 0 ifTrue:
[logger space; print: count;
nextPutAll: ' active requests cancelled.'].
logger flush].
requests do: [:request |
request = 0 ifFalse: [request terminate]].
server == nil ifFalse: [server terminate. server := nil].
socket == nil ifFalse: [socket close. socket := nil]
terminate
"Terminate my server and release my state."
self suspend.
self class unregister: self.
logger == nil ifFalse: [logProtect wait. logger close].
socket := server := service := handler := logger := logProtect := nil
TcpServer class:
shutDown
"Suspend all my services so that the image can be quit and
re-started."
PortmapControl critical: [Portmap do: [:server | server suspend]]
startUp
"Re-start all my services."
PortmapControl critical: [Portmap do: [:server | server resume]]
What's left?
We've run out of space, but this implementation sketch should give
you enough "thoughtware" to go off and improvise. We left out the
thread safe log interface (there are problems if multiple processes
write to the global Transcript) and our implementation has
more extensive logging and security features and a home page
facility.
However, we'll leave you with a handler and a service that
implement a complete telnet interface to VisualWorks using this
framework, in the hope it might inspire your own services.
TcpServer class:
textualHandler
"Answer a handler that dumps a textual stack."
"self defaultHandlerFor: 23 is: self textualHandler"
^[:exception :stream | stream
nextPutAll: 'Unhandled exception: ';
nextPutAll: exception errorString; cr;
nextPutAll: exception initialContext printStack]
evaluationServiceLoop
"Answer a service block that loops over lines of input,
evaluating each and sending back the result."
"self defaultServiceFor: 23 is: self evaluationServiceLoop"
^[:stream | stream
next: 6; "Discard garbage characters."
nextPutAll: 'Smalltalk evaluation service'; cr;
nextPutAll: 'Type "self close" to end session'; cr;
nextPutAll: 'Smalltalk> '.
[stream
print: (Compiler evaluate: stream nextLine for: stream);
cr;
nextPutAll: 'Smalltalk> '] repeat]
Conclusion
It is easier to write client server code in VisualWorks than it is
in C, but it is still not easy enough! With a bit of work, you can
build a framework that reduces TCP servers to one or two methods.
In the next issue, we'll polish off this series by tying together
this month's server framework with last month's HTML interface, and
you'll have your Smalltalk project documentation on your company's
Intranet!
Go to the previous column
in the series, or the next
column in the series.
|