Chapter 4. Improving our client program

Table of Contents
4.1. improvement #1: header files
4.2. improvement #2: error handling
4.3. improvement #3: Creating a Read Line utility
4.4. improvement #4: Creating a Write Line utility
4.5. Packaging our utilities into a service program
4.6. improvement #5: Stripping out the HTTP response
4.7. improvement #6: Sending back escape messages
4.8. improvement #7: Displaying the data using DSPF
4.9. Our updated client program
Written by Scott Klement.

4.1. improvement #1: header files

Now that we've had a "basic example" of sending and receiving some information from a server program, let's take a look at how we can improve our example.

The first improvement that we should make will involve converting all of the prototypes, constants and data structures that we use for sockets into a "header file", and this will be described in this section:

First of all, our example contains about 75 lines of code that are simply prototypes, constants and data structures which will be used in just about every program that we write using sockets. One way that we can improve our program is to place them into their own source member, use the /copy compiler directive to add them into our programs. This gives us the following benefits:

When I create an "API header file", I generally give the member a descriptive name followed by a "_H", which means "header". So my header member relating to sockets is called "SOCKET_H".

Here is an example of what a prototype generally looks like in my header member:

          **----------------------------------------------------------------
          **  struct servent *getservbyname(char *service_name, 
          **                                char *protocol_name);
          **
          **   Retrieve's a service entry structure for a given service
          **   and protocol name.  (Usually used to get a port number for
          **   a named service)
          **
          **   service_name = name of service to get (e.g.: 'http' or 'telnet')    
          **  protocol_name = protocol that service runs under ('tcp', 'udp')
          **
          **  Returns *NULL upon failure, otherwise a pointer to the service 
          **     entry structure
          **----------------------------------------------------------------
         D getservbyname   PR              *   ExtProc('getservbyname')
         D  service_name                   *   value options(*string)
         D  protocol_name                  *   value options(*string)
     

So you can see that it tells me a lot of information. How the prototype was defined in C, a basic description of the function that I'm calling, a description of the parameters, and a description of the return values.

When I need constants in my header file, I'll usually group them according to the API that uses them, and give each a short description, like this:

          * Address Family of the Internet Protocol
         D AF_INET         C                   CONST(2)
          * Socket type for reading/writing a stream of data
         D SOCK_STREAM     C                   CONST(1) 
          * Default values of Internet Protocol
         D IPPROTO_IP      C                   CONST(0)
     

When I place data structures into a header file, I'll do it like this:

         **
         ** Service Database Entry (which service = which port, etc)
         **
         **            struct servent {
         **              char   *s_name;
         **              char   **s_aliases;
         **              int    s_port;
         **              char   *s_proto;
         **            };
         **
         D p_servent       S               *
         D servent         DS                  Based(p_servent)
         D   s_name                        *
         D   s_aliases                     *
         D   s_port                      10I 0
         D   s_proto                       *
     

I'm planning (when time permits) to go through each of my data structures and add a description of each field in the structure, as well. I simply haven't had a chance yet.

You'll also notice that I always declare my data structures (or at least, those I use with the UNIX-type APIs) to be BASED() on a pointer. I do that for three reasons:

  1. many of these structures (in particular, servent and hostent) are "returned" from an API. That means that the API actually allocates the memory for them, rather than my program. So, I need to use a pointer so I can refer to the memory that was allocated by that API.

  2. As alluded to in point #1, when something is BASED() the system doesn't allocate memory to it. Since the header will usually have many different structures that may not be used in every program, it saves memory to only allocate it when needed.

  3. In all but the simplest programs, we will need more than one 'instance' of each data structure. For example, if we wanted to both call bind() to bind our socket to a given port number, and call connect() to connect to a remote port in the same program, we'll need two copies of the sockaddr_in data structure. One containing the address & port number to bind to, the other containing the address & port number to connect to.

    By using BASED() data structures we can do that, simply by allocating two different buffers, one called "connto" and one called "bindto". We can set the address of the sockaddr_in data structure to the area of memory that bindto is in when we want to change or examine the contents of the bindto data, and likewise set the address of sockaddr_in to the area of memory that connto is in when we want to examine or change the connto area.

Point #3 is often difficult for an RPG programmer to understand. You can think of it as being similar in concept to using the MOVE op-code to put data from a character string into a data structure to break the data into fields. It's a little different, however, in that you're not copying all of the data from one place to another, instead you're referencing it in it's original place in memory. Consequently, you don't have to move it back after you've changed it.

Here's a quick example of code snippets that use the functionality that I'm trying to describe in Point #3:

         D connto          S               *
         D bindto          S               *
         D length          S             10I 0
    
         C* How much memory do we need for a sockaddr_in structure?
         C*
         c                   eval      length = %size(sockaddr_in)
    
         C* Allocate space for a 'connect to' copy of the structure:
         C*
         c                   alloc(e)  length        connto
         c                   if        %error
         C**** No memory left?!
         c                   endif
    
         C* Allocate space for a 'bind to' copy of the structure:
         C*
         c                   alloc(e)  length        bindto
         c                   if        %error
         C**** No memory left?!
         c                   endif
    
         C* Set the values of the connto structure:
         c                   eval      p_sockaddr = connto
         c                   eval      sin_family = AF_INET
         c                   eval      sin_addr   = some_ip_address
         c                   eval      sin_port   = some_port_number
         c                   eval      sin_zero   = *ALLx'00'
    
         C* Set the values of the bindto structure:
         C*  Note that each time we do a 'p_sockaddr = XXX' we are effectively
         C*  "switching" which copy of the sockaddr_in structure we're working
         C*  with.
         c                   eval      p_sockaddr = bindto
         c                   eval      sin_family = AF_INET
         c                   eval      sin_addr   = INADDR_ANY
         c                   eval      sin_port   = some_port_number
         c                   eval      sin_zero   = *ALLx'00'
    
         C* call connect()
         c                   if        connect(sock1: connto: length) < 0
         c* Error!
         c                   endif
    
         C* call bind()
         c                   if        bind(sock2: bindto: length) < 0
         c* Error!
         c                   endif
    
         C* examine the contents of connect() version
         c                   eval      p_sockaddr = connto
         c                   if        sin_port = 80
         c*  Do somethign special
         c                   endif
     

Hopefully, by now, you have an idea of how our /copy member (i.e. "API header member") should work. You should be able to go back to the example programs in previous topics in this tutorial and create your own header file.

If you prefer, I will make available the header file that I use for my own sockets programs. This will save you having to create your own, if you do not wish to. You can find my header file here: http://www.scottklement.com/rpg/socktut/qrpglesrc.socket_h

After this point, all of the example programs will use my socket_h header member. Each section that explains a new API call will describe what entries you need to add to your own header, if you're writing your own.