Many objects need to wait for time to pass or for an external event to occur, but because their methods must return immediately, they can't do the obvious or natural thing. Instead, they use the "state machine" technique. You might see this motif repeatedly throughout the code.
To illustrate this, let's consider a simple networking class that lets you send a file to a remote machine, assuming the connection is all set up. Normally, you might write it like this:
#define BUFSIZE 1024But in the world of nonblocking programming, you can't do this, because it loops until some external event happens (i.e. the socket accepts the whole file), which is a major no-no. The work of that loop has to be moved into a function that does a sliver of the work on each call. The main program agrees to ensure that this function is called periodically. The way you do this is turn the local variables of the loop into member variables of the class, and the body of the loop into a method. For instance:
class filesender_t {
public:
/* Send a file on the given socket.
* 'filename' is the name of the file to send.
* 'socket' is an open network connection.
* On exit, socket is closed.
*/
void sendFile(const char *filename, int socket)
{
int fd;
int nread;
int nwrite, i;
char buf[BUFSIZE];
/* Open the file */
fd = open(filename, O_RDONLY);
if (fd < 0)
fatal_error("open failed");
/* Send the file, one chunk at a time */
do { /* loop in time! */
/* Get one chunk of the file from disk */
nread = read(fd, buf, BUFSIZE);
if (nread == 0) {
/* All done; close the file and the socket. */
close(fd);
close(socket);
break;
}
/* Send the chunk */
for (i=0; i<nread; i += nwrite) { /* loop in time! */
/* write might not take it all in one call,
* so we have to try until it's all written
*/
nwrite = write(socket, buf + i, nread - i);
if (nwrite < 0)
fatal_error("write failed");
}
}
}
}
/*----------------------------------------------------------------------<br /> Portable function to set a socket into nonblocking mode.<br /> Calling this on a socket causes all future read() and write() calls on<br /> that socket to do only as much as they can immediately, and return <br /> without waiting.<br /> If no data can be read or written, they return -1 and set errno<br /> to EAGAIN (or EWOULDBLOCK).<br /> Thanks to Bjorn Reese for this code.<br />----------------------------------------------------------------------*/<br />int setNonblocking(int fd)<br />{<br /> int flags;<br /><br /> /* If they have O_NONBLOCK, use the Posix way to do it */<br />#if defined(O_NONBLOCK)<br /> /* Fixme: O_NONBLOCK is defined but broken on SunOS 4.1.x and AIX 3.2.5. */<br /> if (-1 == (flags = fcntl(fd, F_GETFL, 0)))<br /> flags = 0;<br /> return fcntl(fd, F_SETFL, flags | O_NONBLOCK);<br />#else<br /> /* Otherwise, use the old way of doing it */<br /> flags = 1;<br /> return ioctl(fd, FIOBIO, &flags);<br />#endif<br />} <br /><br />#define BUFSIZE 1024<br />class filesender_t {<br /> int m_fd; /* file being sent */<br /> char m_buf[BUFSIZE]; /* current chunk of file */<br /> int m_buf_len; /* bytes in buffer */<br /> int m_buf_used; /* bytes used so far; <= m_buf_len */<br /> enum { IDLE, SENDING } m_state; /* what we're doing */<br /><br />public:<br /> filesender() {<br /> m_state = IDLE; /* not doing anything initially */<br /> }<br /><br /> /* Start sending a file on the given socket.<br /> * Set the socket to be nonblocking.<br /> * 'filename' is the name of the file to send.<br /> * 'socket' is an open network connection.<br /> */<br /> void sendFile(const char *filename, int socket)<br /> {<br /> int nread; <br /> int nwrite, i;<br /><br /> /* Force the network socket into nonblocking mode */<br /> setNonblocking(socket);<br /><br /> /* Open the file */<br /> m_fd = open(filename, O_RDONLY);<br /> if (m_fd < 0)<br /> fatal_error("open failed");<br /><br /> /* Start sending it */<br /> m_buf_len = 0;<br /> m_buf_used = 0;<br /> m_socket = socket;<br /> m_state = SENDING;<br /> }<br /><br /> /* Continue sending the file started by sendFile().<br /> * Call this periodically.<br /> * Returns nonzero when done.<br /> */<br /> int handle_io()<br /> {<br /> if (m_state == IDLE)<br /> return 2; /* nothing to do */<br /><br /> /* If buffer empty, fill it */<br /> if (m_buf_used == m_buf_len) {<br /> /* Get one chunk of the file from disk */<br /> m_buf_len = read(m_fd, m_buf, BUFSIZE);<br /> if (m_buf_len == 0) {<br /> /* All done; close the file and the socket. */<br /> close(m_fd);<br /> close(m_socket);<br /> m_state = IDLE;<br /> return 1;<br /> }<br /> m_buf_used = 0;<br /> }<br /><br /> /* Send one chunk of the file */<br /> assert(m_buf_len > m_buf_used);<br /> nwrite = write(m_socket, m_buf + m_buf_used, m_buf_len - m_buf_used);<br /> if (nwrite < 0) <br /> fatal_error("write failed");<br /> m_buf_used += nwrite;<br /> return 0;<br /> }<br />}<br />You can imagine the user code calling this in a simple loop, as in the following example, which prints the file to stdout as if it were a network connection:
main()<br />{<br /> filesender_t c;<br /> int sock = fileno(stdout);<br /><br /> c.sendFile("foo.txt", sock);<br /> do {<br /> int done = c.handle_io();<br /> if (done) <br /> break;<br /> }<br />}<br />This may seem like a lot of work for nothing -- all we did was turn the loop inside out -- but notice that you can now send multiple files at the same time, e.g.
#define MAXCLIENTS 10<br />main()<br />{<br /> filesender_t filesenders[MAXCLIENTS];<br /> int n_filesenders;<br /> int listenfd;<br /> struct sockaddr_in servaddr;<br /><br /> /* Set up to be a daemon listening on port 8000 */<br /> listenfd = socket(AF_INET, SOCK_STREAM, 0);<br /> memset(&servaddr, 0, sizeof(servaddr));<br /> servaddr.sin_family = AF_INET;<br /> servaddr.sin_addr.s_addr = htonl(INADDR_ANY);<br /> servaddr.sin_port = htons(8000);<br /> bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr));<br /> /* Force the network socket into nonblocking mode */<br /> setNonblocking(listenfd);<br /><br /> /* Wait for connections, and send anyone who connects <br /> * the contents of foo.txt<br /> */<br /> n_filesenders = 0;<br /> do {<br /> /* If we don't have a full set of clients already,<br /> * listen for a new connection.<br /> */<br /> if (n_filesenders < MAXCLIENTS) {<br /> int fd = accept(listenfd, NULL, NULL);<br /> if (fd != -1) {<br /> /* Someone connected. Send them the file */<br /> filesenders[n_filesenders].sendFile("foo.txt", fd);<br /> n_filesenders++;<br /> }<br /> }<br /> /* Pump data out to all the connected clients */<br /> for (int i=0; i<n_filesenders; i++)<br /> filesenders[i].handle_io();<br /> }<br />}<br />This example isn't very realistic, as it wastes CPU time, doesn't re-use the elements of filesenders[] when they're done, and doesn't do enough error checking, but you get the idea.
The next trick needed to get good performance out of this approach is to use poll() to sleep until some file descriptor is ready for I/O, and then call handle_io() on just that one file descriptor.
See also: