TCP/IP Server Programming

Article ID: 51809

In applications that use Transmission Control Protocol (TCP) to communicate over a TCP/IP network, the connection always has two sides: A client side and a server side. The server program is the one that waits patiently for a request to be given, and when it receives a request, it carries it out.

This article discusses creating server programs in ILE RPG for the iSeries.

You can think of a TCP session as being like a telephone call. In the case of a server program, it's like the person who answers the phone at a pizza delivery restaurant:

  • The restaurant sets up a delivery hotline, which rings when someone calls with an order.
  • A person listens for the phone to ring.
  • When a call comes in, that person picks up the receiver to answer the call.
  • The customer and the phone operator talk back and forth about the pizza order.
  • They hang up.
  • The operator goes back to waiting for the next call.

A TCP server program is similar. It does the following:

  • It calls the socket() API to create a new socket, and then it calls the bind() API to assign a port number to that socket.
  • The program puts the socket into listen mode by calling the listen() API. This lets other programs connect to it.
  • The program calls the accept() API to accept the next connection. If nobody is trying to connect at the moment, it waits until a connection is requested.
  • When the connection is accepted, you can send data to the client program by calling the send() API, and you receive data from the client program by calling the recv() API.
  • When you're done, you call close() to disconnect the connection.
  • You now accept the next connection by calling accept() again, and the process continues.

To illustrate this process, I've written a short example program called SERVER1. Here's the section of the code that creates a socket and binds it to port number 54321:

     D listener        s             10I 0
     D bindto          ds                  likeds(sockaddr_in)
        .
        .
         listener = socket(AF_INET: SOCK_STREAM: IPPROTO_TCP);
         if (listener = -1);
            // handle error
         endif;

         bindto = *allx'00';
         bindto.sin_family = AF_INET;
         bindto.sin_addr   = INADDR_ANY;
         bindto.sin_port   = 54321;

         if bind(listener: %addr(bindto): %size(bindto)) = -1;
            // handle error
         endif;

The socket() API accepts three parameters. In this case, I've told it that I want to use IPv4-style IP addresses (AF_INET), and that it is a stream socket (SOCK_STREAM). The stream socket uses the TCP protocol (IPPROTO_TCP). The API creates the socket and returns a number. This number is how the system keeps track of all of the different sockets in use at any one time. Each time you call a sockets-related API, you pass it the number so that it knows which socket you're referring to.

In this example, I stored the socket number (or "socket descriptor") in a variable called listener. This socket's job is to listen for new connections; that's why I picked that name.

I then bind my listener socket to port 54321 by calling the bind() API. The second and third parameters to bind() tell it which TCP/IP network interface and port number you want to bind your socket to. In this case, by specifying INADDR_ANY, I told the API to bind to any network interface -- that means that I can receive connections on any network interface on my system. I also told it that I want port 54321.

Now I need to tell the socket to listen for new connections. To do that, I call the listen() API:

         if listen(listener: 1) = -1;
            // handle error
         endif;

The second parameter to listen() is the number of simultaneous connections that the system allows. In this case, I allow only one. If I have an active connection, nobody else can get through. In most cases, it's better to use a higher number.

Now that my socket is in listen mode, I can accept a new connection. In the following example, I accept a connection, write the words "Hello there!" to it, and then disconnect:

        dow not %shtdn;

             session = accept(listener: *null: *omit);
             if (session = -1);
                // handle error
             endif;

             data = 'Hello there!' + CRLF;
             datalen = %len(%trimr(data));
             QDCXLATE(datalen: data: 'QTCPASC');
             send(session: %addr(data): datalen: 0);

             callp close(session);

         enddo;

The accept() API waits until a connection request is made from another program. When one is made, it is accepted, and a new socket is created to handle that connection. In this case, I assigned the new socket descriptor to a variable called session to make it easy to remember that this socket is for my communications session with the client program. With a name like session, it won't get confused with the listener socket.

The preceding code then converts the words "Hello there!" into ASCII and sends them to the client program. Finally, it calls the close() API to disconnect the session established with the client program. However, the listener session is still available, so I can go back and wait for the next connection request.

To try the example program out, download the code for this article (a link to the code is listed at the end of the article). Compile the SERVER1 program, and then run it. Nothing appears on the screen, but the program sits and waits for connections.

n

Then, at a Windows or Linux PC's command prompt (preferably one on the same LAN as the iSeries), type the following command:

  telnet as400.example.com 54321

The telnet program acts very similarly to opening a socket: You can type the data to send and view the data received. That makes it ideal for testing your server programs as you write them. In this case, I've told it to connect to port 54321 of my iSeries -- the same port that SERVER1 is bound to.

In my telnet session, I see the words "Hello there!" and then telnet sends me a message saying that the connection was closed.

To stop the SERVER1 program when you're done playing with it, log on to another session and type the following command:

NETSTAT *CNN

You should see a screen like the following:

                       Work with TCP/IP Connection Status                       
 Type options, press Enter.                                                     
   3=Enable debug   4=End   5=Display details   6=Disable debug                 
   8=Display jobs                                                               
      Remote           Remote     Local                                         
 Opt  Address          Port       Port       Idle Time  State                   
      *                *          as-netp >  048:03:52  Listen                  
      *                *          as-rmtc >  048:03:33  Listen                  
      *                *          as-sign >  048:03:26  Listen                  
      *                *          54321      000:00:09  Listen                  
      69.76.43.40      2680       telnet- >  000:00:00  Established             
      69.76.43.40      2683       telnet- >  000:00:10  Established             
      192.168.5.1      3158       www        000:00:31  Established             
      192.168.5.129    3621       telnet     000:00:25  Established             
      192.168.5.134    1030       telnet     000:00:13  Established             
      192.168.5.161    3653       telnet     000:00:17  Established             
      192.168.5.161    3654       telnet     000:00:16  Established             
      192.168.5.161    3655       telnet     000:01:40  Established             
                                                                        More... 
 F3=Exit   F5=Refresh   F9=Command line   F11=Display byte counts   F12=Cancel  
 F15=Subset   F22=Display entire field    F24=More keys                         
                                                                                

Use option 4 on the line that shows a listener on port 54321. This causes the accept() API in the SERVER1 program to return -1. When the program receives that error, it ends.

The SERVER1 program has some problems, however:

  • Try running it again immediately after the first test. The bind() API fails, saying, "Address in use."
  • There are no timeouts when sending the data to the client. If a network error occurs, the program could get stuck indefinitely.
  • The example doesn't do any input from the client program.

The first problem is due to the way the TCP protocol works. TCP tries to ensure that any data written to the socket is received by the program on the other end of the connection. Because sockets are buffered, the data isn't necessarily received immediately when you call the send() API. What happens to the data still in the send buffer when you call the close() API? The operating system holds on to that buffer for (by default) 2 minutes, letting the other end catch up. The problem is, this means that the port is still marked as "in use," and therefore, you can't rebind to it!

An easy fix exists, though. You can turn on an option called SO_REUSEADDR (reuse address) for a socket. When this is on, two or more sockets can use the same port number, as long as they aren't all in listen mode. Because the ones in Close-Wait state aren't in listen mode, they won't conflict.

     D reuse           s             10I 0
        .
        .
         reuse = 1;
         setsockopt( listener
                   : SOL_SOCKET
                   : SO_REUSEADDR
                   : %addr(reuse)
                   : %size(reuse) );

You should set this option after the socket has been created and before the bind() API is called. That way, you can run the server repeatedly without problems.

In the article titled "Handling Errors in TCP/IP Programming" (October 27, 2005, article ID 51720), I demonstrate using the signal APIs as a method of making sockets time out. In that article (a link is provided below), I explain the importance of timing out network connections, and I demonstrate how you could use the signal APIs to do that with the connect(), recv(), and send() APIs.

Here's an example of using the same technique to time out the accept() API:

     P taccept         B
     D taccept         PI            10I 0
     D   sock                        10I 0 value
     D   addr                          *   value
     D   size                        10I 0 options(*omit)
     D   timeout                     10I 0 value
     D rc              s             10I 0
      /free
          alarm(timeout);
          rc = accept(sock: addr: size);
          alarm(0);
          return rc;
      /end-free
     P                 E

If you don't understand how that code works, be sure to review the aforementioned article. It explains it in detail. The only difference between this and the tconnect() example from the previous newsletter is that this example calls the accept() API instead of the connect() API.

To demonstrate how to read input from the client program, I add code to the program to ask for the user's name. Because I'm testing this with a telnet client, I want to accept input from the user until he or she presses Enter.

When the Enter key is pressed in a telnet window, the telnet client either sends a line feed (LF) character or both a carriage return (CR) character and an LF character. Different telnet clients handle this differently. To support both, I've written a routine that reads until it receives the LF character. If it also receives the CR character, it simply discards it.

Naturally, this routine also needs to time out if an error occurs. Like the other examples, it uses signals to do that. To make it easy to reuse this routine, I've made it a subprocedure. Here's the code for it:

     P trecvline       B
     D trecvline       PI            10I 0
     D   sock                        10I 0 value
     D   data                     32702A   varying options(*varsize)
     D   size                        10I 0 value
     D   timeout                     10I 0 value
     D rc              s             10I 0
     D char            s              1A
     D p_xdata         s               *
     D xdata           s          32702A   based(p_xdata)
      /free
          alarm(timeout);
          %len(data) = 0;
          size = size - 2;

          dou char = x'0a';

             rc = recv(sock: %addr(char): 1: 0);
             if rc = -1;
               leave;
             endif;

             if char<>x'0d' and char<>x'0a' and %len(data)<size;
                data = data + char;
             endif;

          enddo;

          alarm(0);

          if %len(data) > 0;
             p_xdata = %addr(data) + 2;
             QDCXLATE(%len(data): xdata: 'QTCPEBC');
             return %len(data);
          else;
             return rc;
          endif;

      /end-free
     P                 E

So that this code is as reusable as possible, it accepts a variable-length string for the input. The caller of this routine can use any size variable it wants, as long as it's an alphanumeric field defined with the VARYING keyword. To tell this routine how big that variable is, it passes the size parameter as well.

The trecvline() routine calls the recv() API in a loop, receiving only one character each time. If an error occurs, the routine stops. Otherwise, it adds that character to the data received. It keeps doing that until the character is an ASCII LF character, which is x'0a'.

If it succeeded in receiving some data, it uses the QDCXLATE API to translate the data to EBCDIC. But, there's a problem with this. The QDCXLATE API doesn't understand RPG's VARYING data type; it understands only fixed-length alphanumeric strings. To make the translation work properly, a little pointer logic is used to put a fixed-length string in the same area of memory as the VARYING string. That way, the QDCXLATE routine can work on the fixed-length string without any problems.

In the code download for this article, I've provided a SERVER2 program that has all the fixes that I've just described.

If you run the SERVER2 program and telnet to it (using the same command that you used for the SERVER1 program) you'll see that it now asks you for your name. After you've typed it, the program responds with "Good Bye, " before disconnecting.

If you let it sit on the input field for 15 seconds, it'll give up and disconnect you sooner because of the 15-second timeout that I set when I called the trecvline() subprocedure. I've included a few snippets of the SERVER2 program in the text of this newsletter, so be sure to download the source code and read it over to get all the details.

There's still one big problem: The program handles only one connection at a time. Try connecting with a second telnet session while the first one is still active. You'll see that the second session doesn't do anything until the first is completed. It doesn't matter for the simple programs that I present here, but in a mission-critical application, you most likely need to handle lots of simultaneous connections.

The easiest solution is to split the work up across a few different jobs. Have one program that handles listening for new connection requests, and when one is found, have it submit a new job to handle it. We'll call this the "listener" job. The second program, the one that the listener submits, does the work required by this particular client. We'll call this the "instance" job, because a new copy is submitted for each client that connects.

There's still one more problem with this scenario. Sockets are owned by the job that created them. You can't simply pass a socket descriptor as a parameter to a new job. The new job won't know about that socket and won't be able to read from it.

One solution to this problem is to use the givedescriptor() and takedescriptor() APIs. They can be used to pass a descriptor between the listener and its instances.

Another, much easier, solution is to use the spawn() API instead of the SBMJOB (Submit Job) command to create the new job. The spawn() API can automatically copy descriptors to the job that it creates. These descriptors can be open IFS files, pipes, or sockets.

To demonstrate this concept, I've rewritten SERVER2 as two programs, SERVER3L and SERVER3I. These are the listener and instance programs, and they're included in the source download for this article. To try them out, run the SERVER3L program. You'll see that each time you connect with a telnet client, a new SERVER3I job gets started on the system. Each time the telnet client disconnects, its corresponding SERVER3I job ends.

This is the third article in the TCP/IP programming series. You can find the previous articles at the following links:

Introduction to TCP/IP Programming:
http://www.iseriesnetwork.com/article.cfm?id=51701

Handling Errors in TCP/IP Programming:
http://www.iseriesnetwork.com/article.cfm?id=51720

You can download the source code for this article from the following link:
http://www.pentontech.com/IBMContent/Documents/article/51809_46_TcpProg3.zip

ProVIP Sponsors

ProVIP Sponsors