4.3. improvement #3: Creating a Read Line utility

Now, we've got two "API header members", one for socket calls, and one for error handling, this will allow us to greatly simplify our sockets programs, making them much easier for other programmers to read.

However, looking at the code, there's still one area that's a bit difficult to read, and that's the process of reading one "line" at a time from a socket.

You may recall that almost all almost all of the communications that we'll have with internet servers will involve sending and receiving "lines ASCII of text." In fact, this is SO common that I couldn't come up with a simple client program that didn't use them!

Since most RPG programmers are used to thinking of data in terms of "records" and "fields", it might be useful to think of a line of ASCII text as being a "record". These records are, however, almost always variable-length. You determine where the end of the record is by looking for an 'end of line sequence', which consists of one or two control characters.

There are 3 popular end of line sequences. The first one is that each line ends with the CR (Carriage Return) character. This is most commonly used in Apple computers. It is based on the way a typewriter works, when you press the 'Carriage Return' key on a typewriter, the paper feeds up by one line, and then returns to the leftmost position on the page. CR is ASCII value 13 (or Hex x'0D')

The next end of line sequence is the one used by Unix. The ASCII standard states that ends of lines of text should end with the LF ('line feed') character. Many people, especially C programmers, will refer to the LF character as the 'Newline' character. LF is ASCII value 10 (or Hex x'0A')

Finally, we have the end of line sequence used by DOS and Windows. As noted above, a typewriter feeds to the next line when CR is pressed. However, most early printers could "overtype" a line of text, so when you send the CR character it would go back to the start of the SAME line. The LF sequence would feed the paper out one line, but would not return the carriage to the leftmost position. So, to start a new line of text required two characters, a CR followed by an LF. DOS (and eventually Windows) kept this standard by requiring a line of text to end with both the CR character, followed by the LF character. This was a nice compromise between the up & coming UNIX operating system that DOS was inspired by, and the Apple II DOS, which was at that time the most popular home computer in the world.

So enough history, already! The point of all this is, there are many different ways to send and receive lines of text, what we need is a way to read them in an RPG program. In some cases, we'll want to be able to specify different end-of-line sequences, and in some cases we'll want to have the text automatically translated from ASCII to EBCDIC for us to work with.

So, lets write a routine to do this. We'll make it a subprocedure and put it into a service program, since we'll want to do this in virtually every sockets program that we write.

To make this as much like the recv() API as possible, we'll allow the caller to pass us a socket, a pointer, and a max length as our first 3 parameters. We'll also return the length of the data read, just as recv() does, or -1 upon error. So, the prototype for this new routine will look like this (so far):

         D RdLine          PR            10I 0
         D   peSock                      10I 0 value
         D   peLine                        *   value
         D   peLength                    10I 0 value
     

(Note that in the naming conventions of the company that I work for, 'pe' at the start of a variable means 'parameter')

Next, we'll give optional parameters that can be used to tell our subprocedure to translate the data to EBCDIC, and/or change what the characters for line feed and newline are. This will make our prototype look more like this:

         D RdLine          PR            10I 0
         D   peSock                      10I 0 value
         D   peLine                        *   value
         D   peLength                    10I 0 value
         D   peXLate                      1A   const options(*nopass)
         D   peLF                         1A   const options(*nopass)
         D   peCR                         1A   const options(*nopass)
     

Now, if the optional parameters are not passed to us, we need to use reasonable default values. That is, we'll use x'0D' for CR, x'0A' for LF and *OFF (do not translate) for the peXLate parm. This is easy to do by checking how many parms were passed. Like so:

         c                   if        %parms > 3
         c                   eval      wwXLate = peXLate
         c                   else
         c                   eval      wwXLate = *OFF
         c                   endif
         
         c                   if        %parms > 4
         c                   eval      wwLF = peLF
         c                   else
         c                   eval      wwLF = x'0A'
         c                   endif
         
         c                   if        %parms > 5
         c                   eval      wwCR = peCR
         c                   else
         c                   eval      wwCR = x'0D'
         c                   endif
     

(Note that in the naming conventions of the company that I work for, 'ww' at the start of a variable means 'work field that is local to a subprocedure')

Then, just like in the DsplyLine routine that we used in our example client, we'll read one character at a time until we receive a LF character. To make the routine simple, we'll simply drop any CR characters that we come across. (That way, if LF=x'0A' and CR=x'0D', this routine will read using either UNIX or Windows end of line characters without any problems. If you wanted to make it work with Apple or Windows end of lines, you'd simply reverse the values passed as CR and LF)

Finally, if the XLate parameter is set to ON, we'll translate the line to EBCDIC. If not, we'll leave it as it was originally sent. So, the finished routine will look like this:

(This should go in member SOCKUTILR4 in the file QRPGLESRC)

         P RdLine          B                   Export  (1)
         D RdLine          PI            10I 0
         D   peSock                      10I 0 value
         D   peLine                        *   value
         D   peLength                    10I 0 value
         D   peXLate                      1A   const options(*nopass)
         D   peLF                         1A   const options(*nopass)
         D   peCR                         1A   const options(*nopass)
    
         D wwBuf           S          32766A   based(peLine)
         D wwLen           S             10I 0
         D RC              S             10I 0
         D CH              S              1A
         D wwXLate         S              1A
         D wwLF            S              1A
         D wwCR            S              1A
    
          (2)
    
          ** Set default values to unpassed parms:
         c                   if        %parms > 3
         c                   eval      wwXLate = peXLate
         c                   else
         c                   eval      wwXLate = *OFF
         c                   endif
    
         c                   if        %parms > 4
         c                   eval      wwLF = peLF
         c                   else
         c                   eval      wwLF = x'0A'
         c                   endif
    
         c                   if        %parms > 5
         c                   eval      wwCR = peCR
         c                   else
         c                   eval      wwCR = x'0D'
         c                   endif
    
          ** Clear "line" of data:  (3)
         c                   eval      %subst(wwBuf:1:peLength) = *blanks
    
         c                   dow       1 = 1
    
          ** read 1 byte:
         c                   eval      rc = recv(peSock: %addr(ch): 1: 0)
         c                   if        rc < 1
         c                   if        wwLen > 0
         c                   leave
         c                   else
         c                   return    -1
         c                   endif
         c                   endif
    
          ** if LF is found, we're done reading:
         c                   if        ch = wwLF
         c                   leave
         c                   endif
    
          ** any other char besides CR gets added to the string:
         c                   if        ch <> wwCR
         c                   eval      wwLen = wwLen + 1
         c                   eval      %subst(wwBuf:wwLen:1) = ch
         c                   endif
    
          ** if variable is full, exit now -- there's no space left to read data into
         c                   if        wwLen = peLength (4)
         c                   leave
         c                   endif
    
         c                   enddo
    
          ** if ASCII->EBCDIC translation is required, do it here
         c                   if        wwXLate=*ON  and wwLen > 0
         c                   callp     Translate(wwLen: wwBuf: 'QTCPEBC')
         c                   endif
    
          ** return the length
         c                   return    wwLen
         P                 E
     

Personally, I learn things better by typing them in, rather than reading them. Therefore, I recommend that you type the code examples in this tutorial in yourself. However, if you'd like, you can download my copy of SOCKUTILR4 here: http://www.scottklement.com/rpg/socktut/qrpglesrc.sockutilr4

A few notes:

(1)
the opening P-spec declares this procedure with "Export", so we can make it callable from outside of our service program.
(2)
The 'Translate' prototype isn't defined here. That's because we'll make it global to the entire service program.
(3)
Before the recv() loop, we clear the buffer, but we only clear from the start to the 'length' that was passed into our program. This is very important, since if the calling program should send us a variable that's not the entire 32k, we don't want to clear data that might not be allocate to the variable...
(4)
Although we let the caller pass a line length, as written, this procedure has an effective maximum line length of 32,766 chars per line. We COULD make it do an 'unlimited' number by using pointer math, but this would make it more difficult to clear the variable at the start, and at any rate, it'd be very unusual for a program to need to read a line of text bigger than 32k.