Over the years , network programmers have developed a number of standard techniques for maintaining concurrent I/O. The techniques range from simple tricks that add only a couple of lines of code to the basic server, to methods that more than double the size and complexity of the code. Unfortunately, these techniques are hostage to the peculiarities of how the underlying operating system handles I/O, and this is notoriously variable from one platform to another. As a result, some of the techniques I describe here will only be available on UNIX systems. Moving upward in complexity from the simplest to the most complex, the techniques are the forking server, the multithreaded server, and the multiplexed server. Forking Server The server spends its time in an accept() loop. Each time a new incoming connection is accepted, the server forks, creating an identical child process. The task of handling the child connection's I/O is handed off to the child, and the parent goes back to listening for new connections (Figure 10.1). When the child is finished handling the connection, it simply exits. Figure 10.1. A forking server In a forking server, the multitasking nature of the operating system allows parent and child to run simultaneously . At any point in time there is a single parent process and multiple child processes, each child dedicated to handling a different client connection. This technique is available on platforms that implement fork() , all UNIX versions of Perl, and version 5.6 and higher on Win32 platforms. The Macintosh port of Perl does not currently support fork() . A special case of the multitasked server is the Inetd "super daemon," which can be used to write simple concurrent servers without worrying too much about the details. We look at Inetd at the end of this chapter. Multithreaded Server Next in complexity is multithreading. Conceptually similar to the previous solution, the server calls accept() in a tight loop. Each time accept() returns a connected socket, the server launches a new thread of execution to handle the client session. Threads are similar to processes, but threads share the same memory and other resources. When the thread is done, it exits. In this model, there are multiple simultaneous threads of execution, one handling the main accept() loop, and the others handling client sessions. This technique is available in Perl versions 5.005 and higher, and only on platforms that support threads. The Windows version of Perl supports threads, as do many (but not all) UNIX versions. MacPerl does not currently support multithreading. We discuss multithreading in Chapter 11. Multiplexed Server The most complex technique uses the select() function to interweave communications sessions. This technique takes advantage of the fact that the network is slower than the CPU, and that most of the time in a network server is spent waiting for a socket to become ready for reading or writing. In this technique, the server creates and maintains a pool of filehandles, one for the listen socket, and one for each connected client. Each time through the loop, the server checks the sockets using the select() function to ascertain whether any is ready for reading and writing. If so, the server handles the I/O for that socket and then goes back to waiting with select() . select() is available on all major Perl platforms. Unfortunately, the technique is also the trickiest to use correctly. We discuss multiplexing in Chapters 12 and 13. Built on top of these basic techniques are a number of variations, including preforking, thread pools, and nonblocking I/O. There are also more esoteric methods for achieving concurrency, including signal-driven I/O, asynchronous I/O, and others. We don't cover those here; for further information on these techniques, see [Stevens 1998]. |