s c h e m a t i c s : c o o k b o o k

/ Cookbook.PingPongTutorial

This Web


WebHome 
WebChanges 
TOC (with recipes)
NewRecipe 
WebTopicList 
WebStatistics 

Other Webs


Chicken
Cookbook
Erlang
Know
Main
Plugins
Sandbox
Scm
TWiki  

Schematics


Schematics Home
Sourceforge Page
SchemeWiki.org
Original Cookbook
RSS

Scheme Links


Schemers.org
Scheme FAQ
R5RS
SRFIs
Scheme Cross Reference
PLT Scheme SISC
Scheme48 SCM
MIT Scheme scsh
JScheme Kawa
Chicken Guile
Bigloo Tiny
Gambit LispMe
GaucheChez

Lambda the Ultimate
TWiki.org

The Ping-Pong Duet (Tutorial)

The first application is a client-server combination of staggering simplicity. The client issues requests that consist of the symbol 'ping. The server, upon receipt of this request, responds with the symbol 'pong. One time. The client prints the server's response. That's all.

First, the two programs must agree on a common port where the server will listen so the client can connect.

500 Can't connect to 127.0.0.1:8778 (connect: Connection refused)

To test your programs, pick a "random" port number between 1025 and 65535. The bigger numbers are more barren, so you are less likely to interfere with another service that already exists on your machine.

The client is easier to write. It simply uses TCP-CONNECT to establish a connection to the server.

500 Can't connect to 127.0.0.1:8778 (connect: Connection refused)

This defines CLIENT as a procedure of no arguments (so the body doesn't evaluate until we invoke the procedure). TCP-CONNECT returns two values (look up "multiple return values" in HelpDesk). The first is an input port, to which the server writes data, and the second an output port, from which the server reads data. The naming convention used above helps me keep them straight. The WRITE statement writes 'ping to the port being read by the server. Having written the message, the client closes its ports and exits.

In the above example, we assume both client and server reside on the same machine (hence the use of the hostname "localhost"). The client can reside on an entirely different machine, however.

The server's definition is slightly more complex. Here's the server:

500 Can't connect to 127.0.0.1:8778 (connect: Connection refused)

The server must first create a "listener". The listener is woken up when a network connection comes in on the chosen port. TCP-ACCEPT accepts responses queued at the server.

If we combine these three code fragments (constants, client and server) and run them in a single Scheme session, we ... can't. There's a problem.

If we run the client first, it tries to connect with the server, which isn't yet running, and we get an error saying there's no response from the common port.

If we run the server first, it creates a listener, then executes the TCP-ACCEPT expression. This blocks on a request before it can continue. But we need it to return control to the prompt so we can start the client.

In short, we can't run either one first.

There are three ways out of this jam.

First, we use two separate copies of Scheme (i.e., separate processes). The first process runs the server. The second one runs the client. Note that each process must have the definition of its procedure and the constant definitions. When run after starting the server, the client will return the value pong.

Second, we can just run client and server on different machines. This is really just a special case of the first solution, but it also lets you experiment with connecting to different machines. To do this, you'd have to edit the value associated with SERVER-HOST to be the name of the machine running the server.

Third, we can use threads. (Read up about "threads" in HelpDesk; also see ThreadChapter.) We can thus invoke both the client and server in the same Scheme process by running

500 Can't connect to 127.0.0.1:8778 (connect: Connection refused)

THREAD expects a procedure of no arguments as its first argument, which is exactly what SERVER is.

500 Can't connect to 127.0.0.1:8778 (connect: Connection refused)

In both cases, SERVER exits as soon as it has serviced its request. If you invoke SERVER with THREAD you won't notice this. If you run the client and server in two separate processes, you will.

That concludes our first example.

Queueing up for Tokens

The server and client in the first example ran only once. Typical servers run forever, accepting and servicing requests as they arrive. We'll create such a server as our second example.

We will once again build a client-server combination. The server generates token numbers, much like the machines that issue serial numbers to people in queues. The server initializes at zero, and each request generates the next token number. The client is a function that consumes one argument, which is the number of tokens to receive. It contacts the server as many times as specified in its argument, and returns a list of the resulting tokens.

If we were writing this program without networking, it would look as follows:

500 Can't connect to 127.0.0.1:8778 (connect: Connection refused)

This program behaves as follows:

500 Can't connect to 127.0.0.1:8778 (connect: Connection refused)

Make sure you understand this code before proceeding.

First, establish the server's locus:

500 Can't connect to 127.0.0.1:8778 (connect: Connection refused)

The client is again pretty simple:

500 Can't connect to 127.0.0.1:8778 (connect: Connection refused)

The server's basic structure looks the same:

500 Can't connect to 127.0.0.1:8778 (connect: Connection refused)

except it must do two things: (1) keep track of the last token number, and loop to handle multiple requests. Thus:

500 Can't connect to 127.0.0.1:8778 (connect: Connection refused)

Note that the server does not need to create multiple listeners. It creates the listener for that service just once. It accepts connections from the listener multiple times. Now, assuming the server is running (either in a separate process, on a separate machine, or in its own thread), running the client returns the expected values:

500 Can't connect to 127.0.0.1:8778 (connect: Connection refused)

Note the very subtle yet critical difference between the server above and this one:

500 Can't connect to 127.0.0.1:8778 (connect: Connection refused)

The above server is buggy! Each time through the loop it tries to create a new listener. The first time it succeeds; on the second attempt, the invocation of TCP-LISTEN fails because there is already a listener on that port -- created by this very server!

Variations on the Token Server: Reusing a Connection and Obtaining Consecutive Numbers

There are potentially two problems with the token server above. We address both these problems in this section.

First, the sample interactions with the token server before this section suggest that the tokens will always be consecutive, as they are in the sequential world. In fact, however, the server lives in a concurrent universe. Each time through the loop, TOKEN-CLIENT establishes a fresh connection with the server. Besides being somewhat inefficient, this also means that a different client may connect between two consecutive connections by your client, and may thus grab one or more of the intermediate numbers. So you might see an interaction like

500 Can't connect to 127.0.0.1:8778 (connect: Connection refused)

(You probably won't for small numbers of tokens, but if you set off two processes each requesting a large number of tokens -- say 1,000 each -- from the same server, you ought to find that they aren't all consecutive.)

A related problem is that the client connects to the server each time through its loop. This wastes network resources by repeatedly re-establishing connections. A better solution is for the client and server to have a "dialog": once connected, the server keeps providing the client with tokens until the client has exhausted its demand.

In this rewrite, the client sends the server one of two messages: 'more, as long as it needs more numbers, and 'enough, when it no longer needs any more numbers. The server responds appropriately.

500 Can't connect to 127.0.0.1:8778 (connect: Connection refused)

The NEWLINE and FLUSH-OUTPUT are necessary to make the buffers to be flushed to the network device and transmitted.

This program also guarantees that the tokens will be consecutive, because the server does not service a new client until it is done responding to the current one.

Another way to make more effective use of a connection and to ensure consecutive tokens is to have your server return several (consecutive) tokens at once. How to package up the tokens? All our examples thusfar have transmitted only symbols and numbers. In fact, however, as with i/o operations on any other port, you may use any readable and writeable datum [NOTE: insert HelpDesk reference here]. As a simple example, here's a rewrite of the token server and client where the client simply informs the server of how many tokens it needs, and the server returns a list of that many tokens.

500 Can't connect to 127.0.0.1:8778 (connect: Connection refused)

Notice that the first argument to WRITE is a loop that computes a sequence of tokens.

Both servers in this section come at a price. One long request can tie this server down and make it unable to service other requests. Eventually, the number of pending requests may become too large and new client requests may be denied. (See the second parameter to TCP-LISTEN.)

That concludes our short tour of a ping-pong server.


This tutorial originally appeared in Free Software Magazine as Networking in PLT Scheme, and has been republished here by kind permission of the author, Shriram Krishnamurthi, Assistant Professor of Computer Science at Brown University.


Comments about this recipe

Contributors

-- ShriramKrishnamurthi? - 30 Dec 2005

CookbookForm
TopicType: Recipe
ParentTopic: NetRecipes
TopicOrder: 10

 
 
Copyright © 2004 by the contributing authors. All material on the Schematics Cookbook web site is the property of the contributing authors.
The copyright for certain compilations of material taken from this website is held by the SchematicsEditorsGroup - see ContributorAgreement & LGPL.
Other than such compilations, this material can be redistributed and/or modified under the terms of the GNU Lesser General Public License (LGPL), version 2.1, as published by the Free Software Foundation.
Ideas, requests, problems regarding Schematics Cookbook? Send feedback.
/ You are Main.guest