A Multi-tasking Internet Daemon in REXX/MVS

By

Earl Hodil, Open Software Technologies (ehodil@mail.open-softech.com)


Part I.

Introduction.

By now you probably have heard that you can write socket applications in REXX.  Since its introduction on MVS (it was originally on VM) several years ago, the RXSOCKET interface has found limited use in simple, single-task, client-side applications.  Unsurprisingly, there isn't a huge need for applications where the mainframe is cast in the role of the client.  What the market seems to crave, instead, are mainframe-based *server* applications.  Accordingly, IBM and several ISVs have set about providing "canned" web servers, mail servers, etc. to satisfy this need.

As with all pre-packaged solutions, users of mainframe-based Internet servers have to live within the limits of the designer's imagination.  A really good design might satisfy 90% of your requirements.  That last 10%, though, can be a real bear.  And because your 10% is frequently not the same as another company's 10%, you may wait a long time before the vendor supplies exactly what you want.  In the end, you usually wind up settling for what you can get.

Well, you don't need to settle any more.  In this two part series, I'll show you how to create your own multi-tasking server applications using nothing more than REXX/MVS, RXSOCKET, and a little bit of Job Control Language (JCL).  In the first part we'll examine the overall architecture and the specifics of the main controlling task.  In part two we'll cover the role of the subtask.

Let's begin with the architecture.  A server -- both Internet and SNA -- usually has 2 major moving parts.  At the top you have some sort of dispatching program that fields requests for connections.  When a connection request comes in, the dispatching program starts another unit of work and passes the connection to it for processing.  At the bottom we have one or more (usually more) subtasks whose role is to communicate with the client and service its requests.

Accordingly, our Internet server also has 2 parts:

  1. A job (or started task) establishes a listen queue on an agreed-upon port.  This job, often called a "daemon", waits for a connection request. When one is received, it starts a subtask and passes the connection to it. The main task then processes the next connection request (and the next..), until the server is terminated.
  2. Each subtask is actually a job that is submitted to the system by the main job.  The job submission is accomplished by allocating a ddname to an internal reader, and writing lines of JCL to the ddname.  When the sub-job starts, it issues a call to TCP/IP to take the connection from the main task.  At that point it is free to send and receive data without any further assistance from the main task.  When the conversation has ended, the sub-job simply terminates.

That's all there is to it!

Now let's look at the "innerds" of the dispatching task.  To make it all a little more real, we'll assume that we want to build a special-purpose telnet server.  A telnet server like this could be used, for example, to service the requests of a Java application that is running on a desktop system.  Or you might just telnet into it (which is *certainly* what you will do when testing).

Job Control Language.

To run our server, we'll need some JCL.  The JCL shown below is what we'll use.

   //TELND    JOB
   //*
   //*        TELND SERVER MAIN JOB. CANCEL TO TERMINATE.
   //*
   //TELND    EXEC PGM=IRXJCL,PARM='RXINETD 2042'
   //SYSEXEC  DD DISP=SHR,DSN=RXINETD.EXEC
   //SYSTSPRT DD SYSOUT=*
   //SYSTSIN  DD DUMMY
   //JCLOUT   DD SYSOUT=(A,INTRDR),RECFM=FB,LRECL=80
   //JCLIN    DD DATA
   //TELN     JOB
   //*
   //*        TELND SERVER SUB-JOB.
   //*
   //TELNS    EXEC PGM=IRXJCL,
   //         PARM='TELNS ?'
   //SYSEXEC  DD DISP=SHR,DSN=RXINETD.EXEC
   //SYSTSPRT DD SYSOUT=*
   //SYSTSIN  DD DUMMY
   /*

This file contains the JCL for both the main job and the sub-jobs.  The main job runs the REXX interpreter, IRXJCL, to invoke the main job exec, RXINETD.  RXINETD accepts one parameter, the port to listen on.

RXINETD reads the template JCL for the sub-jobs from the JCLIN ddname.  In this case, the data for JCLIN is "in-stream", as is indicated by the "DD DATA".  The internal reader is allocated to the JCLOUT ddname.  It is to this ddname that RXINETD writes when it needs to submit a sub-job.

The sub-jobs also run IRXJCL.  The REXX program they invoke is TELNS.  If there were any special files TELNS needed to access, we could place their allocations in the sub-job's JCL.  In this example, we don't have any, so only the allocations required by IRXJCL are present (Refer to the REXX/MVS Reference, SC28-1883 for more information on IRXJCL).

RXINETD Processing.

Now for the REXX code. The main line of RXINETD is shown below:

   /* RXINETD */
   parse upper arg port
   call Init
   call rxsocket 'initialize', 'rxinetd'
   parse value rxsocket('socket') with rc s
   call rxsocket 'bind', s, 'AF_INET' port
   parse value rxsocket('getclientid') with rc ClientID
   call rxsocket 'listen', s, 10
   AcceptList = ''
   do forever
     CheckList = 'READ' s 'WRITE EXCEPTION' AcceptList
     parse value rxsocket('select',CheckList),
       with rc . 'READ' Rlist 'WRITE' . 'EXCEPTION' Elist
     if Elist <> '' then call CloseSocks
     if Rlist <> '' then call HandOff
   end
   call rxsocket 'terminate', 'rxinetd'
   exit

The socket calls are all pretty standard, so I won't explain them here, except for SELECT (see the API Reference, SC31-7187 or SC31-8516, for the details).  The SELECT call is used to wait on various socket events. There are 3 kinds of events:  READ, WRITE, and EXCEPTION.  A READ event is generated whenever data is available to be read from a socket.  It also indicates that a listening socket has a connection request, which is how it is used in RXINETD.  A WRITE event indicates when a socket may be written to, and an EXCEPTION, as the name implies, indicates an exceptional socket event.  One such exceptional event is the taking of a "given" socket, and that is how it used here.

SELECT accepts a list of sockets, by event, that you are interested in. In RXINETD we are interested in READ events on the main socket, "s", and EXCEPTION events on any of the sockets that have been given to the sub-jobs.  SELECT returns a list of sockets, again by event, that currently satisfy the criteria.  If no socket events have occurred, SELECT waits until one does.

Thus, when the SELECT in RXINETD returns, it is notifying us that a connection request is pending on the main socket, and/or that one of the given sockets has been taken.

The INIT subroutine of RXINETD, shown below, is responsible for preparing the JCL template.  It reads the JCL from JCLIN into a stem array named "jcl.".  It then does 3 things to the template:  1) It adds a "/*EOF" card to the end of the job, so that RXINETD can submit jobs, but leave JCLOUT open.  2) It breaks the job card into several parts, so that the name can be customized for each sub-job. 3) It breaks the EXEC card on the question mark and remembers its position in the array.

INIT also opens the JCLOUT ddname so that job submission can proceed as quickly as possible.  JCLOUT is kept open for the life of the main job.

   Init:
   "execio * diskr JCLIN (stem jcl. finis"
   i = jcl.0 + 1
   jcl.i = '/*EOF'
   jcl.0 = i
   parse var jcl.1 '//' JobPrefix 'JOB' JobSuffix
   JobPrefix = left(JobPrefix,4,'X')
   JobCount = 0
   do i = 2 to jcl.0
     if pos('?',jcl.i) <> 0 then do
       pi=i
       parse var jcl.i ParmPrefix '?' ParmSuffix
       leave i
     end
   end
   "execio 0 diskw JCLOUT (open"

The HANDOFF subroutine (shown below) is called whenever a connection request has been received.  Its first task is to ACCEPT the conversation. The result of the ACCEPT is a new socket descriptor.  HANDOFF adds the socket descriptor to the "accept list" so that the SELECT in RXINETD's mainline code can check for EXCEPTION events.

   HandOff:
   parse value rxsocket('accept',s) with rc as .
   AcceptList = AcceptList as
   JobCount=JobCount+1; if JobCount>9999 then JobCount=1
   JobName = JobPrefix||right(JobCount,4,'0')
   call rxsocket 'givesocket', as, 'AF_INET' JobName
   jcl.1  = left('//'JobName 'JOB' JobSuffix,80)
   jcl.pi = left(ParmPrefix||ClientID'/'as||ParmSuffix,80)
   "execio" jcl.0 "diskw JCLOUT (stem jcl."
   return

The HANDOFF subroutine then constructs a job name for the sub-job, passes the socket to the sub-job using GIVESOCKET, and submits the job by writing the JCL to JCLOUT.  Note that the parmstring passed to the TELNS sub-job is set to contain the client ID of the RXINETD server and the socket descriptor that is being given.  This information is required for the TAKESOCKET call in TELNS.

The final subroutine of RXINETD is CLOSESOCKS.  This routine is invoked whenever EXCEPTION events are indicated for given sockets.  As noted earlier, the EXCEPTION indicates that the sub-job has taken control of the socket.  The sub-job, as we will see shortly, is actually given a new socket descriptor that represents the connection.  However, in order for RXINETD to re-use the now-taken socket descriptor, it must first close it (Why?  Who knows?  IBM makes these rules.  I just follow them.).  If RXINETD did not close these given sockets, eventually it would run out of socket descriptors (by default, you only get 40).  So, that is exactly what CLOSESOCKS does.  It closes sockets that have been successfully given to sub-jobs.

   CloseSocks:
   do while Elist <> ''
     parse var Elist cs Elist
     call rxsocket 'close', cs
     parse var AcceptList FirstPart (cs) LastPart
     AcceptList = FirstPart LastPart
   end
   return

By the way, if PARSE is not in your arsenal, you might want to study its use in CLOSESOCKS.  At the top of the loop, PARSE is used to successively extract socket descriptors from ELIST.  This parse works because it puts the remainder (everything after the first blank-delimited word) back into ELIST.  Thus, each time this statement executes, ELIST is reduced by one word.

The second PARSE uses a variable pattern, "(cs)", to find the now-closed socket descriptor in the ACCEPTLIST variable.  The parse divides the list into everything that comes before "(cs)" and everything that comes after it.  The next statement simply glues the 2 parts back together.

That's all for now.  In the next part we'll examine the TELNS sub-job.