Chapter 6. Handling many sockets at once using select()

Table of Contents
6.1. Overview of handling many clients
6.2. The select() API call
6.3. Utility routines for working with select()
6.4. A combination client/server example
6.5. Blocking vs. non-blocking sockets
6.6. The fcntl() API call
6.7. A multi-client example of our server
6.8. Adding some refinements to our approach
6.9. A Chat Application
Written by Scott Klement.

6.1. Overview of handling many clients

There are three different approaches to making a server program be capable of handing many simultaneous clients. These approaches are:

There are pros and cons to each of these approaches, and special issues that need to be dealt with. This chapter will deal with the first approach, and the next chapter will deal with the second approach.

6.1.1. The "single program" approach:

This approach involves a single program that reads from many clients, and writes to many clients in a loop. The program needs to be careful not to "stop" on any one client's work, but to keep switching between all of the involved clients so they all appear to be working.

The pros of this approach:

  • It is efficient. There is no "startup overhead" such as waiting for a new job to be submitted, and it only needs one set of resources.

  • It allows for easy communication between all of the connected clients. For example, if you wanted to write a chat room where everything being said had to echoed to all of the connected clients, you'd want to use this approach, because all of the sockets are together in one program where data can be copied from one to the other.

The cons of this approach:

  • It's very hard to limit each user's access according to the userid they signed in with, since all actions for all users are being taken by a single program.

  • You can't really call other programs to implement functionality, since your program would have to give up control to the program that you call, and when that happened, all of the socket processing would stop.

  • It's very easy for your code to become very complicated and diffiult to maintain.

Up to this point in the tutorial, all of our socket operations have used "blocking" operations. When I say "blocking", I mean that each operation doesn't return control to our program until it has completed. For example, if we call recv(), the computer will wait until there is data to recv() from the other side of the connection. It doesn't stop waiting until the other side has disconnected, or until some data actually appears.

When working with many different clients, however, this can be a problem. We need to be able to read from whichever client actually sends data to us, without being stuck waiting for one that's not sending data to us.

To solve all these problems, the select() API was created. The select API allows us to specify 3 different "groups" or "sets" of sockets. A "read" set, a "write" set and an "exceptional" set. It can tell us when there is data to be read on each of the sockets, as well as telling us when it's safe to write to each of the sockets. Select() also has a "timeout" value which lets us regain control even when there has been no activity.

Extending our server program into one based on this model will be difficult, so we'll start with something a bit simpler than that.

You'll recall that we tested our server programs by using a TELNET client to simply connect to the port we were listening on. But, what if we wanted to test a client program instead of a server program? We couldn't do that because a TELNET client only connects to servers, not to clients...

What we need is a special server program that would accept a connection from a client program as well as a telnet client. It could then copy the data sent from the client to the telnet socket, and vice-versa.

This program would be relatively easy to write, and would be a good starting example of how to deal with multiple connected clients -- as well as a useful tool -- so that's what we'll start with in this chapter.