REXX Tutorial

A sample remote control application

The next sample shown in this tutorial is a remote control application. It consists of a server process waiting for clients to connect, and offering them several commands to be executed on the server. Multiple clients are able to connect to the server at the same time.

The following commands are available to the client when connected to the server:

From what we have seen in the previous example it will not be too difficult to extend the client and server logic to handle the commands from the client. In order to handle multiple connection requests at the same time we will have to split up the processing on the server into two programs. The first program will create a socket and bind it to a well known port address, waiting for a client to connect. As soon as a client connects to the server the second program will be started in another session taking over the communication and processing the commands sent by the client. Programs such as the first one are called a 'daemon process'. These daemons normally run in the background waiting for clients and from their nature do not allocate too much system resources. The second program contains the actual program logic and is called the 'handler process'.

Remote control client

Let's have a look at the client program first. The program listing will be shown in this tutorial function by function with comments for each function. It has basically the same logic for initialization of the communication with the server except that the communication will not stop before the user has entered the 'QUIT' command. This is the source code for the main program of the client:


    /* RCLIENT.CMD - IBM REXX Sample Program                */

    Parse Arg Server



    /* check command line arguments, server is required     */

    If Server = "" Then

     Do

       Say "Usage: RCLIENT Servername"
       Say "  Servername may contain a port number separated",

           "with a colon."
       Exit 1

     End



    /* Load REXX Socket library if not already loaded       */

    If RxFuncQuery("SockLoadFuncs") Then

     Do

       Call RxFuncAdd "SockLoadFuncs","RXSOCK","SockLoadFuncs"
       Call SockLoadFuncs

     End



    /* Connect to remote control server                     */

    Socket = Connect(Server)

    If Socket = -1 Then

      Exit 1



    /* loop until QUIT command was entered                  */

    Do Until Command = "QUIT"
      Say "Please enter one of: 'DIR [path]', 'TYPE name'",

          " or 'QUIT'"
      Parse Pull CommandLine

      Parse Upper Var CommandLine Command Option

      If Length(Command) > 0 Then

        Call SendCommand Socket, CommandLine

    End



    /* Close connection to server                           */

    Call Close Socket

    Exit

The client program requires the alias name of the remote control server as a commandline argument. In addition to the alias you can choose to use a different well known port address on the server instead of the predefined value of 1234. To specify another port address you have to append a colon and the new port number to the server alias (without any inserted blanks). To connect to server 'myserver' at port 4321 you would start the client with:


    RCLIENT myserver:4321

If a valid server alias was supplied as an argument the program connects to the server by calling the function 'Connect'. If for any reason no socket could be created to connect to the server, this function will return -1 and the main program will be terminated.

The main part of the client program is the loop where the user is asked to enter a command to be executed on the server until 'QUIT' is entered. The command read from the keyboard will be sent to the server with the 'SendCommand' function that will also display the response from the server. Finally, if the user has entered 'QUIT' to leave the client, the socket communication will be closed and the program terminated.

The 'Connect' function itself separates the server alias from the port number (if specified), determines the IP address of the server, creates a socket and connects it to the specified server. On completion it will return the socket handle to the caller or -1 if an error occurred:



    /********************************************************/

    /*                                                      */

    /* Function:  Connect                                   */

    /* Purpose:   Create a socket and connect it to server. */

    /* Arguments: Server - server name, may contain port no.*/

    /* Returns:   Socket number if successful, -1 otherwise */

    /*                                                      */

    /********************************************************/

    Connect: Procedure

      Parse Arg Server



      /* if the servername has a port address specified     */

      /* then use this one, otherwise use the default port  */

      /* for the remote control server (1234)               */

      Parse Var Server Server ":" Port

      If Port = "" Then

        Port = 1234



      /* resolve server name alias to dotted IP address     */

      rc = SockGetHostByName(Server, "Host.!")

      If rc = 0 Then

       Do

         Say "Unable to resolve server:" Server

         Return -1

       End



      /* create a TCP socket                                */

      Socket = SockSocket("AF_INET", "SOCK_STREAM", "0")

      If Socket < 0 Then

       Do

         Say "Unable to create socket"
         Return -1

       End



      /* connect the new socket to the specified server     */

      Host.!family = "AF_INET"
      Host.!port = Port

      rc = SockConnect(Socket, "Host.!")

      If rc < 0 Then

       Do

         Say "Unable to connect to server:" Server

         Call Close Socket

         Return -1

       End



      Return Socket

The 'SendCommand' function sends the specified command to the server where it will be processed. The response from the server will be read and acknowledged line by line. The reason for this implementation is the following: by sending only one line from the server and then waiting for an acknowledgement from the client we save some program logic to handle more than one line received on the client at once. This could happen, when multiple 'SockSend' calls on the server are processed before the client reads the results from the socket. In this case all the data would be read from the socket at once. We could add CR/LF characters to the data sent to the client but then we also would have to split the lines on the client to be able to recognize the end of transmission string. By sending line by line we can always assume that the end of transmission indicator will be received as a standalone string. The value used for the acknowledgement from the client to the server is not relevant, as long as it is not an empty string.


    /********************************************************/

    /*                                                      */

    /* Procedure: SendCommand                               */

    /* Purpose:   Send a command via the specified socket   */

    /*            and display the full response from server.*/

    /* Arguments: Socket - active socket number             */

    /*            Command - command string                  */

    /* Returns:   nothing                                   */

    /*                                                      */

    /********************************************************/

    SendCommand: Procedure

      Parse Arg Socket, Command



      /* send the command to the remote control server      */

      Call SockSend Socket, Command

      Do Forever

        BytesRcvd = SockRecv(Socket, "RcvData", 1024)



        /* error or end of response encountered             */

        If BytesRcvd <= 0 |,

           RcvData = ">>>End_of_transmission<<<" Then

          Leave



        /* display response and send acknowledge to server  */

        Say RcvData

        Call SockSend Socket, "OK!"
      End



      Say "----- end of output from command:" Command "-----"
      Return

The 'Close' function finally shuts down the used socket and closes it. According to the socket documentation a call to 'SockClose' should be sufficient to close down the socket connection. However experience shows that a preceding call to 'SockShutDown' cleans up the environment better and the sample programs can be run again immediately without experiencing problems with ports being still in use.


    /********************************************************/

    /*                                                      */

    /* Procedure: Close                                     */

    /* Purpose:   Close the specified socket.               */

    /* Arguments: Socket - active socket number             */

    /* Returns:   nothing                                   */

    /*                                                      */

    /********************************************************/

    Close: Procedure

      Parse Arg Socket

      Call SockShutDown Socket, 2

      Call SockClose Socket

      Return

Remote control daemon

This is the source code for the daemon program running on the server:


    /* RSERVERD.CMD - IBM REXX Sample Program               */

    Parse Arg Port

    If Port = "" Then

      Port = 1234



    /* Load REXX Socket library if not already loaded       */

    If RxFuncQuery("SockLoadFuncs") Then

     Do

       Call RxFuncAdd "SockLoadFuncs","RXSOCK","SockLoadFuncs"
       Call SockLoadFuncs

     End

    /* Open socket at well known port and wait for clients  */

    Socket = ListenPort(Port)

    If Socket = -1 Then

      Exit 1



    /* close the socket when program is interrupted         */

    Signal On Halt

    Do Forever

      /* wait for client to connect and start handler       */

      Say "Waiting for client to connect."
      Say "Press Ctrl-C to exit program."
      ClientSocket = SockAccept(Socket)

      Say "Client connected, starting handler process."
      "start rserverh.cmd" ClientSocket

    End



    Halt:

    Call Close Socket

    Exit

The logic of the remote control daemon is quite simple. It creates a socket, connects it to a well known port address (which can be either the default value for our application or a user defined port address) and then enters an endless loop waiting for clients to connect. As soon as a client has connected it starts a new session with the handler program which then takes over the communication with the client. The loop waiting for clients is an infinite loop which can only be interrupted by stopping the program with Ctrl-C. A signal handler for the 'HALT' signal has been added to cover this event to correctly close down the socket.

The 'ListenPort' function of the daemon program creates a new socket at a well known port address waiting for clients to connect:



    /********************************************************/

    /*                                                      */

    /* Function:  ListenPort                                */

    /* Purpose:   Create a socket, bind it to a port and    */

    /*            listen at the port for connecting clients.*/

    /* Arguments: Port - port number                        */

    /* Returns:   Socket number if successful, -1 otherwise */

    /*                                                      */

    /********************************************************/

    ListenPort: Procedure

      Parse Arg Port



      /* create a TCP socket                                */

      Socket = SockSocket("AF_INET", "SOCK_STREAM", "0")

      If Socket < 0 Then

       Do

         Say "Unable to create socket"
         Return -1

       End



      /* find out local IP address and bind socket to port  */

      Host.!addr = SockGetHostId()

      Host.!family = "AF_INET"
      Host.!port = Port



      rc = SockBind(Socket, "Host.!")

      If rc < 0 Then

       Do

         Say "Unable to bind to port:" Port

         Call Close Socket

         Return -1

       End



      /* listen at the port, allow 5 clients in queue       */

      rc = SockListen(Socket, 5)

      If rc < 0 Then

       Do

         Say "Unable to listen at port:" Port

         Call Close Socket

         Return -1

       End



      Return Socket

The 'ListenPort' creates a connection request queue for up to 5 concurrent connection requests from clients. If any of the socket calls fails the function will return a value of -1 to indicate a failure. If no problem occurred the socket handle will be returned.

The last function in the daemon program is the 'Close'. Since it is identical to the 'Close' function of the client program it is not listed again.

Remote control handler

The last part of the remote control application is the handler process. When the handler is started from the daemon process it will receive the socket handle which is used to communicate with the client. It will then print out some information on the connected client. The dotted IP address of the connected client can be retrieved with a call to 'SockGetPeerName' and the alias name is then retrieved with a call to 'SockGetHostByAddr'. This information is printed on the screen and then a loop is entered where the handler waits for commands from the client. The command is received and processed in the function 'ReceiveRequest'. This function returns the command string which is used to leave the loop when the command 'QUIT' has been received. To clean up the resources the socket will be closed and finally the OS/2 session in which the handler process is running will be terminated.

As already mentioned in the client program the protocol used for the communication between client and handler expects that every line sent from the handler is acknowledged by the client. The end of the result of a processed command will be marked with a special 'End of transmission' indicator.

The handler process uses two functions to send lines back to the client. The function 'Answer' sends a single line to the client and waits for an acknowledgement while the function 'AnswerQueue' is used to send all lines available in the REXX session queue to the client. The latter function makes it extremely easy to implement the two commands provided by the server. Both commands can be executed on the server workstation without modifications by the OS/2 command processor. The output of the commands will be captured and redirected into the REXX session queue by piping the output into RXQUEUE.EXE. Unnamed session queues are used in this example because such a queue is needed for every handler process running on the server. Named queues are shared among all processes on a PC and are therefore not suited for our purposes.

The server commands itself are processed in the functions 'ProcessDirCommand' and 'ProcessTypeCommand' respectively. This is the complete listing of the handler program:



    /* RSERVERH.CMD - IBM REXX Sample Program               */

    Parse Arg Socket



    /* Load REXX Socket library if not already loaded       */

    If RxFuncQuery("SockLoadFuncs") Then

     Do

       Call RxFuncAdd "SockLoadFuncs","RXSOCK","SockLoadFuncs"
       Call SockLoadFuncs

     End



    /* Show some information about connected client         */

    Call SockGetPeerName Socket, "ClientAddr.!"
    Call SockGetHostByAddr ClientAddr.!addr, "ClientAddr.!"
    Say "Established connection with client '" ||,

        ClientAddr.!name || "'."


    /* Process commands until QUIT is reached               */

    Command = ""
    Do Until Command = "QUIT"
      Command = ReceiveRequest(Socket)

    End



    /* Close socket and OS/2 session                        */

    Call Close Socket

    "Exit"


    /********************************************************/

    /*                                                      */

    /* Function:  ReceiveRequest                            */

    /* Purpose:   Wait for a command from the client and    */

    /*            execute it. Return the identifier of the  */

    /*            command to the caller.                    */

    /* Arguments: Socket - active socket number             */

    /* Returns:   command identifier                        */

    /*                                                      */

    /********************************************************/

    ReceiveRequest: Procedure

      Parse Arg Socket



      /* Wait for the command from the client               */

      BytesRcvd = SockRecv(Socket, "CommandLine", 1024)

      Say "Command line from client:" CommandLine



      Parse Var CommandLine Command Option

      Command = Translate(Command)



      Select

        When Command = "DIR" Then

          Call ProcessDirCommand Socket, Option

        When Command = "TYPE" Then

          Call ProcessTypeCommand Socket, Option

        When Command = "QUIT" Then

          Nop

        Otherwise

          Call Answer Socket, "Invalid command."
      End



      /* send end of answer marker back to client           */

      Call SockSend Socket, ">>>End_of_transmission<<<"
      Return Command



    /********************************************************/

    /*                                                      */

    /* Procedure: Close                                     */

    /* Purpose:   Close the specified socket.               */

    /* Arguments: Socket - active socket number             */

    /* Returns:   nothing                                   */

    /*                                                      */

    /********************************************************/

    Close: Procedure

      Parse Arg Socket

      Call SockShutDown Socket, 2

      Call SockClose Socket

      Return



    /********************************************************/

    /*                                                      */

    /* Procedure: Answer                                    */

    /* Purpose:   Send one answer line back to the client   */

    /*            and wait for acknowledgement from client. */

    /* Arguments: Socket - active socket number             */

    /*            AnswerString - line to send to client     */

    /* Returns:   nothing                                   */

    /*                                                      */

    /********************************************************/

    Answer: Procedure

      Parse Arg Socket, AnswerString

      Call SockSend Socket, AnswerString

      Call SockRecv Socket, "Ack", 256

      Return



    /********************************************************/

    /*                                                      */

    /* Procedure: AnswerQueue                               */

    /* Purpose:   Send all lines from the session queue     */

    /*            back to the client as the answer of the   */

    /*            previous executed command.                */

    /* Arguments: Socket - active socket number             */

    /* Returns:   nothing                                   */

    /*                                                      */

    /********************************************************/

    AnswerQueue: Procedure

      Parse Arg Socket



      /* send answer lines until session queue is empty     */

      Do While Queued() > 0

        Parse Pull Line



        /* empty lines will be sent as a space              */

        If Line = "" Then

          Line = " "


        Call Answer Socket, Line

      End

      Return



    /********************************************************/

    /*                                                      */

    /* Procedure: ProcessDirCommand                         */

    /* Purpose:   Process the DIR command that was received */

    /*            from the client and send back the result. */

    /* Arguments: Socket - active socket number             */

    /*            FileMask - optional file mask for DIR     */

    /* Returns:   nothing                                   */

    /*                                                      */

    /********************************************************/

    ProcessDirCommand: Procedure

      Parse Arg Socket, FileMask



      /* redirect output from DIR command to session queue  */

      "DIR" FileMask " | RXQUEUE"
      Call AnswerQueue Socket

      Return



    /********************************************************/

    /*                                                      */

    /* Procedure: ProcessTypeCommand                        */

    /* Purpose:   Process the TYPE command that was received*/

    /*            from the client and send back the result. */

    /* Arguments: Socket - active socket number             */

    /*            FileName - required filename to type      */

    /* Returns:   nothing                                   */

    /*                                                      */

    /********************************************************/

    ProcessTypeCommand: Procedure

      Parse Arg Socket, FileName



      /* TYPE needs a filename as argument                  */

      If FileName = "" Then

       Do

         Call Answer Socket, "You have to specify a filename",

              " with the TYPE command."
         Return

       End



      /* Check if the file really exists                    */

      If Stream(FileName, "C", "QUERY EXISTS") = "" Then

       Do

         Call Answer Socket, "The specified file '" ||,

              FileName || "' does not exist."
         Return

       End



      /* redirect output from TYPE command to session queue */

      "TYPE" FileName " | RXQUEUE"
      Call AnswerQueue Socket

      Return


[ IBM REXX homepage | Previos page | Next page | Tutorial Index | Object REXX homepage ]
[ IBM homepage | Order | Search | Contact IBM | Help | (C) | (TM) ]
This page is at http://www2.hursley.ibm.com/rexxtut/socktut5.htm