The following commands are available to the client when connected to the server:
/* 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 ExitThe 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:4321If 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 SocketThe '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 "-----" ReturnThe '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
/* 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 ExitThe 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 SocketThe '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.
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