TCP/IP Communications Commands

TCP/IP Communications Commands

TclX includes a basic command set suitable for creating IP-based client/server applications. While the network-oriented commands are not so elaborate as those in (for example) the 'scotty' Tcl extension, they are perfectly adequate for writing IP servers -- run by inetd or stand-alone -- and clients. Note: Tcl7.5 (and later) provides TCP/IP server/client features; previous versions did not. At 7.4 and earlier, TclX was the simplest way to add client/server capability to Tcl.

So, why would anyone want to write clients and servers using Tcl?

Suppose I have a Sun with a licensed copy of Sybase DBlib on it. I have also an AIX machine without DBlib, for which I don't want to build any elaborate Sybase code (so I don't need DBlib) -- but I have one little utility (a phone book lookup tool, let's say) that queries the Sybase data. This tool runs on all the Suns just fine. But my AIX user also wants to run this one utility natively on that machine. I could now go out and buy DBlib for AIX. Or I could run a server on the Sun machine that performs this one simple query and returns an answer to a client on any remote machine.

In any case where machine X has access to data or services that machine Y does not, it's possible to make a client process on Y contact a server process on X to get an answer. A machine without NFS could get access to selected files on a remote disk in this way, for example.

Alternatively, let's say that I have an SNMP monitoring tool (which I do) that has a very slow startup (it sure does). It takes more than 45 seconds just to load the Tcl code, initialize the SNMP monitor agent, etc. By that time whatever network condition I just noticed may be over. I would like to have the monitor running as a daemon (server) so I could contact that server process and request a quick statistical snapshot of my ethernet.

Whenever a process is expensive or slow to initialize, but you want to get a quick answer out of it, one solution is to start it up and let it run as a server, making quick connections to it thereafter from client processes without all that overhead. Statistics-gathering and monitoring processes are particularly appopriate subjects for this technique.

A TclX server can accept and manage simultaneous connections. Interprocess communication takes place in the form of strings passed over an IP socket, which is read with gets or read, and written with puts or the server_send command.

The server_create command establishes a TCP/IP socket on the local host, returning a file handle associated with that socket. The file handle does not become ready for reading until there's a successful connection request. The syntax is

	server_create ?-myip IPaddress? ?-myport PortNumber? \
		?-backlog integerVal?
The -myip option allows you to choose between multiple network interfaces on a multi-homed machine. The -myport option permits you to choose your IP port number. If the port number is already in use, you will get an error. The -backlog option limits the length of the pending connections queue (the default is 5, and on some BSD-derived systems the limit is always 5 regardless of your attempts to change it).

Once a server is established on a host, other processes can connect to it using the server_connect command. The basic syntax of server_connect is

	server_connect ?options...? hostDesig serviceDesig
where the host may be designated by a name or IP address, and the service by a name or port number.

The options enable you to configure the TCP/IP connection. You can force the socket to be buffered (-buf) or unbuffered (-nobuf). If it is unbuffered, you get only one file ID for both reading and writing. The -twoids option forces the return of two file IDs, the first one usable for reading and the second for writing. (You must close both of them separately to shut down the socket.) The -myip option works as it does with server_create, and is useful only for multi-homed hosts. -myport lets you assign a port number for the client end of the connection.

After a client attempts to connect, the server has to accept the connection using the server_accept command. The syntax is

	server_accept ?options? fileID
where the fileID is one returned previously by server_create. Possible options are -buf, -nobuf, and -twoids, as above.

Either end of the connection can now write to the other using the server_send command:

	server_send ?options? fileID sendString
You might wonder why the authors added this command, when puts would work just as well. The server_send command is actually better for socket writes, as it has some error detection built in to handle lost connections and other IP-layer problems. You won't need a flush command if you use server_send, even if the socket is buffered.

The options for server_send are -nonewline (the default behaviour is to tack a newline onto the string sent), -dontroute (suppress routing and use only the direct interface), and -outofband (send out-of-band data).

Here are two simple Tcl scripts, a client and a server. The client is the simpler of the two, so let's start there (even though in practice we would probably write the server first). We decide in advance that the server will be listening on port 3011, so that's where the client attempts to connect.

# we send you a Hello and you send us a World.
#
set port 3011
#
set fp [server_connect sunny.mafia.org 3011]
#
while {1} {

        puts -nonewline stdout "What shall we say? " 
        gets stdin what

        server_send $fp "$what"

        select $fp {} {}

        set err [catch {gets $fp answer} res]

        if {$err} {
                echo "No more server!\n$res"
                exit 0
        }

        echo "Server answered:  $answer"

}

This client gets a string from the user and sends it to the server. It then gets the server's answer and shows it to the user.

The server is almost as simple-minded:

# You send us a Hello and we send you a World
#
set fp [server_create -myport 3011]
#
while {1} {

#	check the socket and wait 4 seconds if it's not ready
        set res [select $fp {} {} 4.0]

        lassign $res rd wr ex
#	When the socket becomes readable, connect to the client
#	and start listening.
        if {$rd == "$fp"} {
                set cfp [clientConnect $fp]
                clientListen $cfp
        }

}

proc clientConnect {fp} {

#	accept the client connection and return the client-specific
#	file ID

        set err [catch {set cfp [server_accept $fp]} res]

        if {$err} {
                echo "error completing connection:\n$res"
                exit 1
        }

	return $cfp 
}

proc clientListen {fp} {

#	Listen to the client on private file ID fp until
#	it says "Die" and we die

        while {1} {

                set err [catch {gets $fp client_said} res]

#	if the gets failed, the client has vanished

                if {$err} {
                        echo "No more client!\n$res" 
                        exit 0
                }

#	if you get a null string, the client didn't say anything yet

                if {$client_said == ""} {
                        sleep 1
                        continue
                }

                if {$client_said == "Hello"} {
                        server_send $fp "World"
                        continue
                }

                if {$client_said == "Die"} {
                        server_send $fp "Shutting down"
                        close $fp
                        exit 0
                }

                server_send $fp "Say what?"

        }
}

The server responds to two keywords: Hello and Die. It responds to Hello by answering "World", and to Die by exiting. (If it had been started by inetd via the services database, the server would automatically restart when any attempt was made to connect to 3011; but in this case the server was run manually, so it runs exactly once.) Otherwise it just says, "Say what?" and keeps running. Here's how the client actually looks in operation:

578) sunny.cia.org.de: test.client
What shall we say? howdy
Server answered:  Say what?
What shall we say? yahoo!
Server answered:  Say what?
What shall we say? Hello
Server answered:  World
What shall we say? Die 
Server answered:  Shutting down
What shall we say? bye bye
No more server! 
error reading "file4": Connection reset by peer
Obviously, far more error-checking should be done for a production application. However, this gives you a very basic model from which to build your own clients and servers.