An Easier Way to Write a TCP/IP Server Program

Article ID: 53182

I've written a bunch of articles about TCP/IP programming using sockets. Many of the most popular ones demonstrate how to write your own TCP/IP server programs in RPG. I've demonstrated how to write these server programs using the SBMJOB command and using the spawn() API, and quite a few people have adopted my techniques.

Recently, I've discovered an easier way. i5/OS includes a TCP/IP server named INETD that can do all the work of creating the socket, accepting the connection, and spawning your worker program, saving you from having to write it all yourself!

I must admit that I've used this tool on Unix systems in the past, but I only recently realized that the same tool is available on i5/OS. This article explains how to use INETD for your own RPG socket servers.

What Does INETD Do?

In the November 11, 2005, issue of this newsletter, I explained how to write a TCP/IP server application in RPG. You can read that article at the following link:
http://www.systeminetwork.com/article.cfm?id=51809

As that article explains, you take the same steps in every server program. You call the following APIs in sequence: socket(), bind(), listen(), accept(), and spawn(). Wouldn't it be nice if this was done for you? It would be a simple program that you tell which ports to listen on and which programs to spawn, and it would then handle the rest. That's what INETD does!

In fact, that article provides a sample application that asks the client program to provide a name, then prints "Goodbye, NAME" as a demonstration of how a server program can send and receive information over a network. That sample application was broken into two separate programs, one named SERVER3L that listens for new connections, and one named SERVER3I that performs the communication with the client who connected. With INETD, SERVER3L is completely unnecessary! INETD can wait for the connections and run SERVER3I, all without me having to write any code.

Configuring INETD

To configure INETD, you need to edit a configuration file on the System i. To do that, type the following command:

EDTF '/QIBM/USERDATA/OS400/INETD/inetd.conf'

Here's the default configuration file that IBM provides:

# inetd.conf - Internet Super Server (INETD) configuration file
#
#
# (C) COPYRIGHT International Business Machines Corp. 1998
# All Rights Reserved
# Licensed Materials - Property of IBM
#
# US Government Users Restricted Rights - Use, duplication or
# disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
#
# Format of this file is:
#
# 1) All records beginning with a pound sign (#) are comments.
# 2) All other records identify services that are to be made available
#    by the Internet Super Server.  The record format is:
#    a. Service name (e.g. ftp, telnet...)
#    b. Connection type (stream or dgram)
#    c. Protocol name (tcp or udp)
#    d. Wait flag (wait or nowait)
#    e. User profile name (e.g. QUSER). The profile must be *ENABLED
#       and cannot have *SECADM or *ALLOBJ special authorities.
#    f. Path to program in the QSYS.LIB file system to process the
#       request.
#    g. Program name (received as argv[0])
#    h. Program arguments (0-19)
#
# Examples:
# service-name  stream  tcp  nowait  QUSER  /QSYS.LIB/QGPL.LIB/XYZ.PGM  XYZ
# other-service dgram   udp  wait    QUSER  /QSYS.LIB/QGPL.LIB/ABC.PGM  ABC  Parm1 Parm2
#
#
# Basic services
#echo           stream  tcp  nowait  QTCP   *INTERNAL
#discard        stream  tcp  nowait  QTCP   *INTERNAL
#chargen        stream  tcp  nowait  QTCP   *INTERNAL
#daytime        stream  tcp  nowait  QTCP   *INTERNAL
#time           stream  tcp  nowait  QTCP   *INTERNAL
#echo           dgram   udp  wait    QTCP   *INTERNAL
#discard        dgram   udp  wait    QTCP   *INTERNAL
#chargen        dgram   udp  wait    QTCP   *INTERNAL
#daytime        dgram   udp  wait    QTCP   *INTERNAL
#time           dgram   udp  wait    QTCP   *INTERNAL

Each line of the file that begins with a # (pound) symbol is a comment. In fact, every line in the default configuration is a comment. It you were to start INETD in this form, it'd provide no services, because nothing has been enabled.

The lines at the bottom are some basic services that are standard on Unix systems. IBM has provided those basic services (i.e., echo, discard, chargen, daytime, and time) as part of INETD itself, so if you ever want to enable one, you need not buy any software. Simply enable one of the commented-out lines in INETD.

However, these services aren't what's interesting to me as an RPG programmer. What's interesting to me is the ability to have it run my RPG socket programs (e.g., SERVER3I). The configuration file has six required fields followed by up to 20 optional fields. All the fields are separated from each other with one or more spaces. The comments at the top of the configuration file explain what these fields do.

To run my SERVER3I program, I can add the following line to the end of the preceding configuration file:

server3         stream  tcp  nowait  KLEMSCOT  /QSYS.LIB/CLUBTECH.LIB/SERVER3I.PGM SERVER3I

The first column (server3) is the service name from my system's service table. The service table is just a file that provides a symbolic name for a port number. For example, because Telnet runs on port 23, there'd be an entry in the services table that says that TELNET=23. If I knew that I wanted to connect to Telnet but didn't know the port number, I could look it up in the services table, and I'd find that Telnet runs on port 23. The same thing can be used for your own services as well. In this example, I've created a service called SERVER3, and I want it to run on port 54321. To achieve that, I add the following entry to the services table:

ADDSRVTBLE SERVICE('server3') PORT(54321) PROTOCOL('tcp')

The INETD configuration has the value server3 in the first column. That tells it to look in the services table to find the port number for my application, and in this example it finds 54321, because that's what I put in my services table.

The second field in the configuration file is the type of socket to create. This corresponds to the second parameter of the socket() API and can be "stream" or "dgram." The TCP protocol is a stream protocol, and UDP is a datagram protocol. So when I write a TCP or UDP application, I need to provide stream or dgram, respectively.

The third field is the actual protocol to use, TCP or UDP.

The fourth field tells INETD how to process each connection. If you specify "wait," INETD passes the socket listening for connections to your program, and it waits for your program to complete before allowing any new connections. This is useful for UDP because UDP uses the same socket both for receiving data and for waiting for connections. If you specify "nowait," it uses the accept() API to get a new socket for the connection and spawns a background job to handle that socket. With "nowait" it does not wait for the job to complete but immediately returns to listening for more connections.

Most of the time, you specify "wait" for UDP-based services, and "nowait" for TCP-based services, but in some situations, you can improve performance by using "wait" for a TCP service. In that instance, your TCP application would have to call the accept() API to process each client. But again, that's not used frequently, so for the purposes of this article, let's assume that you're using "nowait" for TCP services.

The fifth column specifies the user profile that the spawned job runs under. In my example, my program is running as KLEMSCOT (that's my user profile on my system). In a production application, you'd probably want to create a new user profile with the minimum privileges necessary to run your program. Perhaps it'd be called SRV3USER or something fitting to the application you're writing.

The sixth column is the name of the program to run, in IFS naming format. In my example, I have a program named SERVER3I in the CLUBTECH library. In traditional i5/OS naming format, I'd refer to that program as CLUBTECH/SERVER3I. However, in IFS naming format, I list the file system (QSYS.LIB), followed by a path separator (slash), followed by the library the program is in, and finally the name of the program itself.

The seventh column is the name of the program itself. Unix programs and C programs are always passed the program name as their first parameter (the comment references argv[0], which is the first parameter that a C program would receive). This lets you specify the name passed to the program. This parameter has no effect on RPG programs, so it doesn't matter what you put here.

If you list any further parameters in columns 8-20, they are passed as parameters to your program. They are converted to C-style null-terminated strings when your program receives them. I did not specify any parameters in my sample.

To summarize, the configuration line that I provided for server3 listens on port 54321. Each time a client tries to connect to that port, it spawns a new job, and in that new job it runs the RPG program named SERVER3I in library CLUBTECH.

After you have the configuration the way that you want it, you can use the Start TCP Server (STRTCPSVR) command to start the INETD server. For example:

STRTCPSVR SERVER(*INETD)

Writing the RPG Application

When INETD spawns the new job, it receives the connected socket as descriptor 0, 1, and 2. It doesn't matter which one you use in your application. If you like, you can read from descriptor 0 and write to descriptor 1 (which is how stdin/stdout work in C programs), or you can simply do all your reading and writing on descriptor 0. All three descriptors read and write to the same place.

For example, I could use the send() API to write the words 'Hello World' to the connected client application as follows:

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

The preceding code puts the words "Hello World" into an RPG variable, then uses the %LEN built-in function (BIF) to determine the length of that string. It uses the QDCXLATE API to convert the string to ASCII, and then the send() API is used with descriptor 0 to send the data to the client program.

As luck would have it, my SERVER3I program from the previous article already used descriptor 0 for reading and writing, so I didn't have to change it at all. All I needed to do was configure INETD to replace SERVER3L, then start it up, and the SERVER3I program was ready to go.

When your RPG program is done communicating with the client program, it should use the close() API to disconnect.

I've found that most errors get logged to the job log of the QTOGINTD job in the QSYSWRK subsystem. If you run into trouble with INETD, or if you have trouble getting your program to run, that's the first place I suggest looking.

Because my SERVER3I code from the previous article works nicely with INETD, I haven't provided any sample code for this article. Instead, please download the SERVER3I program from the following article:
http://www.systeminetwork.com/article.cfm?id=51809

ProVIP Sponsors

ProVIP Sponsors