Networking HC
Volume Number: | | 5
|
Issue Number: | | 12
|
Column Tag: | | HyperChat
|
Related Info: AppleTalk Mgr
A Look Into Networking
By Donald Koscheka, Ernst & Young, MacTutor Contributing Editor
Note: Source code files accompanying article are located on MacTech CD-ROM or source code disks.
Programming the Network
Network programming is fun. Over the past year, Ive presented a suite of XCMDs that provide the Hypercard developer with network access. Rather than present a new XCMD this month, I thought it might be interesting to revisit some of the xcmds that we developed during the year. If you dont have access to back issues of Mactutor, you can obtain the source code for these xcmds from this magazine.
Programming the network is not unlike programming in Hypertalk. In both instances, you are concerned with the behavior of distributed processes. In HyperTalk, processes are distributed over several card and background objects. On the network, objects are distributed over time.
Time distribution of processes adds a new wrinkle to the programming task: how do you know when a given process has completed? The problem stems from the fact that we operate the network asynchronously; when we issue a request for some network service, we dont wait around for an answer. Rather, we return to Hypercard, leaving the network to handle the request in its own good time.
Figure 1 depicts a card for a prototypical network application called Hyperserver. The card contains two icons, each of which provides some access to the network. Clicking on the volumes icon will cause the card to display a list of available servers. Selecting one of these servers will immediately send a message to the selected server requesting a list of its mounted volumes. Similarly, the catalog icon will return a catalog of the current folder on the server (for now, I leave it to your imagination to determine what other things one can do with such a card).
Figure 1. A prototypical screen for HyperServer
The process of sending a message to a server requires that you first call the call the server. Next you send a get volumes message to the server. Then you must wait for a response from the server. Once the server responds, you hangup).
Waiting for some input before advancing to the next stage suggests a state machine. Figure 2 depicts just such a state machine. Each circle in the diagram is a state. The double circles are termini. They start or begin a process. The arrows are state transitions, their labels tell us what stimulus will force a transition to the next state. The looping arrows in the diagram suggest the concept of waiting. For example, when a call request is made, you cant immediately start talking, you have to wait for the other party to answer first. The process of answering the call triggers a transition to the connected state. Once connected, requests can be made to the server. Since requests imply a response, this state needs to wait for input before moving on. The process of receiving an answer to the request will trigger a transition to the next state (handle response). Once the request is serviced, we can go back to the connected state where we either wait for input and send another request (or hang up if thats appropriate).
State transitions can occur in response to one of two classes of stimuli: external or internal. External stimuli comprise information coming in over the wire. Examples of external stimuli include service requests to a server and responses coming from a server. In figure 2, we model the external stimuli as the Service state and the Handle State. The handle state accepts the input from the remote end as a response to some previous request issued in the send state. For this reason, you should never get to the handle state unless you first pass through the send state.
The respond state in Figure 2 can only be reached from the service state. That is, you can only respond to a stimuli given that a request for service was made from a remote end. (For readability, not all states are depicted in the figure).
Figure 2 The State Machine For HyperServer
Internal stimuli are called requests. Requests are messages that are sent to a server. Once a request is issued, we must wait for a response. For example, calling is an example of a message that request a connection with the server.
Once the server answers the call, we can move to the connect state. The connect state is implemented in the idle method of the stack method in listing 1. In addition to listening for external stimuli via ADSPListen, the idle method also manages the request mechanism.
We synchronize requests using a simple queue. Requests get added to the end of the queue and get serviced from the start of the queue. The queue is stored in the global container message_queue. Each line in message_queue corresponds to a message. The first line in message_queue is the next message to service (not the current message). To execute a message, we first move it into the message_pending container. Every other element in the queue then moves up in the line.
The message_pending item will be executed on the next activation of the idle method. If a message is sent that requires some external response, it should be followed by a PENDING message. Pending does nothing which is why its useful as a blocking mechanism. The only way for the queue to advance is to have some method activate the NextMessage method. So PENDING will remain the current message in the queue until some external stimulus unblocks it.
For example, to issue a call to a remote called Allen you might execute something to the effect of:
AddMessage Call"e&Allen"e
AddMessage PENDING
On the next pass through the idle method, the Call message will be executed invoking the call method in the script. The call method dials up the remote end and activates nextMessage which advance the queue to the next message, in this case the current message becomes PENDING.
The queue cannot be unblocked until the server answers the call or the call times out. When the call is answered (in ADSPListen), the connectionMade method is activated unblocking the PENDING message.
Some messages have dont care results. When we hangup on a remote end, we generally dont care to wait around to see if the other guy hung up. Thus the HangUp message is not followed by a pending message.
As a rule, if you have a message that issues a request, you must follow the message with a PENDING message will tells the state machine to wait for an external stimulus before proceeding.
Listing 1 is the stack script that implements this state machine plus the response handlers (not shown in Fig. 1). Before running the state machine, we install HyperADSP in the openstack method. Similarly, we shut down HyperADSP in the closestack method.
Think of the idle method as implementing the connect state and the rest of the script should be a little easier to read. For brevity, Ive left out the methods that originate the requests, but theyre easy enough to reconstruct (or get a copy of this months disk for a demo).
-----------------------------------------------
-- Following are the stack methods for opening
-- closing and listening for traffic on the
-- network
--
-- Due to space limitations, this stack is not
-- complete. This months source disk contains
-- a complete demo of this stack.
-----------------------------------------------
on openstack
global globalADSPData, globalSKTData, globalNBPData, myEntityName
global HyperADSPData, adspcaller, LookupTable
global message_queue, message_pending
set the cursor to 4
put empty into card field current Server
put empty into card field Subscribers
put empty into card field Current Pathname
put empty into card field Current Catalog
put empty into card field log
put empty into message_pending
put empty into message_queue
if globaladspdata is empty then
set the cursor to arrow
adspinstall
if the result is not empty then
put the result
else
nbpOpen
if the result is not empty then
answer the result with OK
else
nbpRegisterName , HyperServer
log Welcome to HyperADSP!
end if
end if
else
log HyperADSP already open!
end if
end openstack
on idle
global message_pending
--
-- if no message is pending, get one from the queue
-- if the queue is empty, do nothing
--
if message_pending is empty then NextMessage
if message_pending is not empty then
send message_pending
end if
adspListen getTheMessage
pass idle
end idle
on getTheMessage
global hyperadspdata, adspcaller, message_pending, incoming
set cursor to 4
set lockscreen to true
put hyperadspdata into incoming
get line 1 of incoming
--
-- Notice how we turn the messages around here.
-- a put message tells us to take the incoming data
-- a get message tells us to handle an incoming request
if word 1 of it is put then Receive incoming
if word 1 of it is get then Handle incoming
set lockscreen to false
set cursor to arrow
end gettheMessage
on closeStack
adspRemove
if the result is not empty then
answer the result with OK
else
nbpClose
end if
end closestack
----------------------------------------------
-- Network Callback methods.
--
-- These methods are executed in response to
-- some activity on the network
--
-- connectionMade : answer an incoming call
-- doALookup: Update the names table
----------------------------------------------
on connectionMade
global adspcaller
if adspcaller is not empty then
-- now that the connection is made, we can dismiss the calling
-- message from the queue:
Log Connect to:&&adspcaller
put empty into adspcaller
NextMessage
end if
end connectionMade
on DoALookup
-----------------------------------------------
--
-- DoALookup returns a list of names along with
-- their registered types. There is a catch
-- that we need to handle: If we want to display
--the name without the type, we need to delete
-- the last item in the line.
--
-- For example:
-- Koscheka, Don, HyperServer
-- needs to be reported as:
-- Koscheka, Don.
--
-- Note that asking for item1 of the line wont
-- do since the comma is part of the registered
-- name.
--
-----------------------------------------------
global lookupTable, currentZone
set the cursor to 4
put GetZoneInfo( TRUE ) into currentZone
put nbplookupnames( HyperServer, currentZone ) into lookupTable
repeat with i = 1 to the number of lines in lookupTable
get the number of items in line i of lookupTable
put empty into item it of line i of lookupTable
get the number of characters in line i of lookupTable
if character it of line i of lookupTable is , then
put empty into character it of line i of lookupTable
end if
put line i of lookupTable into line i of card field subscribers
end repeat
set the cursor to arrow
End DoALookUp
----------------------------------------------
-- Generic utilities
--
-- SelectLine : return the content of the line
-- clicked in by the user (card field only)
--
-- isBlank: return true if the field is empty
-- or contains just white space.
----------------------------------------------
on log mess
-- the idea for a network log is borrowed
-- from Chris Allen.
repeat while the number of chars of cd fld Log > 15000
delete line 1 of cd fld Log
end repeat
put &&mess&return after cd fld log
end log
Function SelectLine theField
--
-- select a line from the given card field
-- Accepts the short name of a card field as input
--
put empty into my_answer
--
-- When selecting a line from a field,
-- you need to take into account that the
-- field may have been scrolled.
--
-- To determine the number of scrolled lines
-- divide the number of scrolled pixels by the
-- number of pixels per line
--
put the scroll of card field theField into box_top
put the textheight of card field theField into tsize
put round( box_top/tsize ) into lineNum
--
-- You get a mouseup in global coordinates
-- To find the line hit, you need to convert
-- to local coordinates.
--
put item 2 of the rect of card field theField into x_global_offset
put item 2 of the clickloc into x_hit
-- Once you find the mousehit point,
-- convert it to a line relative to top of box.
-- This relative line is added to the number
-- of scrolled lines to determine the actual
-- line number (e.g. if 10 lines have been scrolled
-- then the first line in the field is 11, not 1).
--
subtract x_global_offset from x_hit
add trunc( x_hit/tsize) to lineNum
--
-- in the arithmetic, line numbers will be
-- normalized to 0 rather than 1. Hypercard
-- starts at line 1 so adjust the offset.
--
add 1 to lineNum
put line lineNum of card field theField into my_answer
return my_answer
end SelectLine
function is_blank string
get character 1 of string
if it is space or it is empty or it is tab or it is return then
return true
else
return false
end if
end is_blank
----------------------------------------------
-- Queue Management
--
-- These routines handle management of the message
-- queue. Excuse the logic; if Hypercard has an
-- Achilles heel, its the lack of useful data
-- structures.
--
-- Addmessage : add a message to end of queue
-- RemoveMessage: remove a message from the queue
----------------------------------------------
on AddMessage the_memo
global message_queue
get the number of lines in message_queue
put the_memo into line it+1 of message_queue
end addMessage
on NextMessage
--
-- removing a message implies that the removed
-- message get put into the message_pending container
-- This method should only be called once the pending
-- message has been serviced
--
global message_queue, message_pending
if message_pending is not empty then log message_pending
put line 1 of message_queue into message_pending
delete line 1 of message_queue
end NextMessage
-----------------------------------------------
-- Message Management
--
-- These routines handle management of the possible
-- messages that can be received by the network.
-- all messages have the form:
--
-- Line 1: Message Type (token)
-- Line 2: Message Sender (string)
-- Line 3..n: Message dependent data
--
-----------------------------------------------
Function BuildHeader mess, name
put mess into line 1 of temp
put name into line 2 of temp
return temp
end BuildHeader
on call someone
adspCall someone
NextMessage
-- notice that this message needs to be pended also
-- The block will be cleared by the connectionMade method
end call
on hangup someone
ADSPHangup someone
NextMessage
end hangup
on PENDING
-- This is a wait for completion message. Because
-- it does nothing, it blocks the message_queue until
-- some data is received. The receive method will clear
-- the pending message. Only the nextMessage method can
-- unblock the queue. Notice that nextMessage should get
-- called only after the expected data is returned.
end PENDING
on getcatalog server, folder
global myEntityName
--
-- Client is asking for a directory of the requested
-- folder. Service the request...
--
if server is not empty then
put BuildHeader( get catalog, myentityname) into send_this_message
put folder into line 3 of send_this_message
ADSPTalk server, send_this_message
end if
NextMessage
end getcatalog
on getVolumeList server
global myEntityName
if server is not empty then
put BuildHeader(get volumes, myEntityName ) into send_this_message
Log server
log send_this_message
adsptalk server, send_this_message
end if
NextMessage
end getvolumeList
----------------------------------------------
--
-- Message request methods. Requests are assembled
-- by these methods and added to the request queue
--
----------------------------------------------
on request_volumes
global message_queue
put selectline( the short name of the target ) into theServer
if theServer is not empty then
put theServer into card field current server
AddMessage Call&"e&theserver"e
AddMessage Pending
AddMessage getVolumelist&"e&theserver"e
AddMessage Pending
AddMessage Hangup&"e&theserver"e
end if
end request_volumes
on request_catalog
global message_queue
put card field current server into theServer
if theServer is not empty then
AddMessage Call&"e&theserver"e
AddMessage Pending
put quote&cd fld current pathname"e into temp
AddMessage getcatalog&"e&theserver"e&,&temp
AddMessage Pending
AddMessage Hangup&"e&theserver"e
end if
end request_catalog
-----------------------------------------------
-- Message response methods. These methods will
-- respond to a particular message from a remote
-- end in whatever manner is appropriate. Note
-- that responses dont get queued, they go
-- right out on the line.
--
-- Response messages take the same form as request
-- messages. Notice that the response methods
-- use myentityname to tell the client which server
-- is providing the response.
-----------------------------------------------
on Receive something
--
-- A request that was made in the past is
-- being addressed, do something with the
-- incoming data (for now, just display it)
--
if volume is in line 1 of something then
log something
end if
if catalog is in line 1 of something then
delete line 1 of something
put line 1 of something into cd fld current server
delete line 1 of something
put something into cd fld current catalog
end if
NextMessage
end receive something
on Handle request
--
-- incoming caller is making a request, do
-- something about it
--
get word 2 of line 1 of request
put line 2 of request into to_client
if catalog is in it then
put line 3 of request into of_folder
return_catalog of_folder, to_client
end if
if volumes is in it then
return_volume_list to_client
end if
end handle
on return_catalog folder, client
global myEntityName
if client is not empty then
put BuildHeader( put catalog, myEntityName ) into the_response
Put GetCatalog( , directoryID ) into temp
put the_response&return&temp into the_response
ADSPTalk client, the_response
end if
end return_catalog
on return_volume_list client
global myentityname
if client is not empty then
put BuildHeader( put volumes, myentityname )¬
into the_response
Put Volumes() into temp
put the_response&return&temp into the_response
ADSPTalk client, the_response
end if
end return_volume_list
Listing 1. Stack Methods for HyperServer