< Chap 5: Index | Winsock2 I/O Method Main | Select Program Example >
What do we have in this chapter 5 part 1?
|
This chapter focuses on managing I/O in a Windows sockets application. Winsock features socket modes and socket I/O models to control how I/O is processed on a socket. A socket mode simply determines how Winsock functions behave when called with a socket. A socket model, on the other hand, describes how an application manages and processes I/O on a socket. Winsock features two socket modes: blocking and non-blocking. The first part of this chapter describes these modes in detail and demonstrates how an application can use them to manage I/O. As you'll see later in the chapter, Winsock offers some interesting I/O models that help applications manage communication on one or more sockets at a time in an asynchronous fashion: blocking, select(), WSAAsyncSelect(), WSAEventSelect(), overlapped I/O, and completion port. By the chapter's end, we'll review the pros and cons of the various socket modes and I/O models and help you decide which one best meets your application's needs. All Windows platforms offer blocking and non-blocking socket operating modes. However, not every I/O model is available for every platform. Every I/O model is available on Windows NT and later versions.
Socket Modes
As we mentioned, Windows sockets perform I/O operations in two socket operating modes: blocking and non-blocking. In blocking mode, Winsock calls that perform I/O, such as send() and recv() wait until the operation is complete before they return to the program. In non-blocking mode, the Winsock functions return immediately. Applications running on the Windows CE and Windows 95 (with Winsock 1) platforms, which support few of the I/O models, require you to take certain steps with blocking and non-blocking sockets to handle a variety of situations.
Blocking Mode
Blocking sockets cause concern because any Winsock API call on a blocking socket can do just that, block for some period of time. Most Winsock applications follow a producer-consumer model in which the application reads (or writes) a specified number of bytes and performs some computation on that data. The following code snippet illustrates this model: |
SOCKET sock;
char buff[256];
int done = 0, nBytes;
...
while(!done)
{
nBytes = recv(sock, buff, 65);
if (nBytes == SOCKET_ERROR)
{
printf("recv failed with error %d\n", WSAGetLastError());
return;
}
DoComputationOnData(buff);
}
...
The problem with this code is that the recv() function might never return if no data is pending because the statement says to return only after reading some bytes from the system's input buffer. Some programmers might be tempted to peek for the necessary number of bytes in the system's buffer by using the MSG_PEEK flag in recv() or by calling ioctlsocket() with the FIONREAD option. Peeking for data without actually reading it is considered bad programming practice and should be avoided at all costs (reading the data actually removes it from the system's buffer). The overhead associated with peeking is great because one or more system calls are necessary just to check the number of bytes available. Then, of course, there is the overhead of making the recv() call that removes the data from the system buffer. To avoid this, you need to prevent the application from totally freezing because of lack of data (either from network problems or from client problems) without continually peeking at the system network buffers. One method is to separate the application into a reading thread and a computation thread. Both threads share a common data buffer. Access to this buffer is protected with a synchronization object, such as an event or a mutex. The purpose of the reading thread is to continually read data from the network and place it in the shared buffer. When the reading thread has read the minimum amount of data necessary for the computation thread to do its work, it can signal an event that notifies the computation thread to begin. The computation thread then removes a chunk of data from the buffer and performs the necessary calculations.
The following section of code illustrates this approach by providing two functions: one responsible for reading network data (ReadThread()) and one for performing the computations on the data (ReadThread()).
#define MAX_BUFFER_SIZE 4096
// Initialize critical section (data) and create
// an auto-reset event (hEvent) before creating the two threads
CRITICAL_SECTION data;
HANDLE hEvent;
SOCKET sock;
TCHAR buff[MAX_BUFFER_SIZE];
int done=0;
// Create and connect sock
...
// Reader thread
void ReadThread(void)
{
int nTotal = 0,
nRead = 0,
nLeft = 0,
nBytes = 0;
while (!done)
{
nTotal = 0;
nLeft = NUM_BYTES_REQUIRED;
// However many bytes constitutes enough data for processing (i.e. non-zero)
while (nTotal != NUM_BYTES_REQUIRED)
{
EnterCriticalSection(&data);
nRead = recv(sock, &(buff[MAX_BUFFER_SIZE - nBytes]), nLeft, 0);
if (nRead == -1)
{
printf("error\n");
ExitThread();
}
nTotal += nRead;
nLeft -= nRead;
nBytes += nRead;
LeaveCriticalSection(&data);
}
SetEvent(hEvent);
}
}
// Computation thread
void ProcessThread(void)
{
WaitForSingleObject(hEvent);
EnterCriticalSection(&data);
DoSomeComputationOnData(buff);
// Remove the processed data from the input
// buffer, and shift the remaining data to the start of the array
nBytes -= NUM_BYTES_REQUIRED;
LeaveCriticalSection(&data);
}
One drawback of blocking sockets is that communicating via more than one connected socket at a time becomes difficult for the application. Using the foregoing scheme, the application could be modified to have a reading thread and a data processing thread per connected socket. This adds quite a bit of housekeeping overhead, but it is a feasible solution. The only drawback is that the solution does not scale well once you start dealing with a large number of sockets.
The alternative to blocking sockets is non-blocking sockets. Non-blocking sockets are a bit more challenging to use, but they are every bit as powerful as blocking sockets, with a few advantages. The following example illustrates how to create a socket and put it into non-blocking mode.
SOCKET s;
unsigned long ul = 1;
int nRet;
s = socket(AF_INET, SOCK_STREAM, 0);
nRet = ioctlsocket(s, FIONBIO, (unsigned long *) &ul);
if (nRet == SOCKET_ERROR)
{
// Failed to put the socket into non-blocking mode
}
Once a socket is placed in non-blocking mode, Winsock API calls that deal with sending and receiving data or connection management return immediately. In most cases, these calls fail with the error WSAEWOULDBLOCK, which means that the requested operation did not have time to complete during the call. For example, a call to recv() returns WSAEWOULDBLOCK if no data is pending in the system's input buffer. Often additional calls to the same function are required until it encounters a successful return code. Table 5-2 describes the meaning of WSAEWOULDBLOCK when returned by commonly used Winsock calls.
Table 5-2 WSAEWOULDBLOCK Errors on Non-blocking Sockets
|
|
Function Name |
Description |
WSAAccept() and accept() |
The application has not received a connection request. Call again to check for a connection. |
closesocket() |
In most cases, this means that setsockopt was called with the SO_LINGER option and a nonzero timeout was set. |
WSAConnect() and connect() |
The connection is initiated. Call again to check for completion. |
WSARecv(), recv(), WSARecvFrom(), and recvfrom() |
No data has been received. Check again later. |
WSASend(), send(), WSASendTo(), and sendto() |
No buffer space available for outgoing data. Try again later. |
Because non-blocking calls frequently fail with the WSAEWOULDBLOCK error, you should check all return codes and be prepared for failure at any time. The trap many programmers fall into is that of continually calling a function until it returns a success. For example, placing a call to recv() in a tight loop to read 200 bytes of data is no better than polling a blocking socket with the MSG_PEEK flag mentioned previously. Winsock's socket I/O models can help an application determine when a socket is available for reading and writing.
Each socket mode, blocking and non-blocking has advantages and disadvantages. Blocking sockets are easier to use from a conceptual standpoint but become difficult to manage when dealing with multiple connected sockets or when data is sent and received in varying amounts and at arbitrary times. On the other hand, non-blocking sockets are more difficult because more code needs to be written to handle the possibility of receiving a WSAEWOULDBLOCK error on every Winsock call. Socket I/O models help applications manage communications on one or more sockets at a time in an asynchronous fashion.
Essentially, six types of socket I/O models are available that allow Winsock applications to manage I/O:
This section explains the features of each I/O model and outlines how to use it to develop an application that can manage one or more socket requests.
Note that technically speaking, there could be a straight non-blocking I/O model, that is, an application that places all sockets into non-blocking mode with ioctlsocket(). However, this soon becomes unmanageable because the application will spend most of its time cycling through socket handles and I/O operations until they succeed.
Most Winsock programmers begin with the blocking model because it is the easiest and most straightforward model. The Winsock samples in Chapter 1 use this model. As we have mentioned, applications following this model typically use one or two threads per socket connection for handling I/O. Each thread will then issue blocking operations, such as send() and recv().
The advantage to the blocking model is its simplicity. For very simple applications and rapid prototyping, this model is very useful. The disadvantage is that it does not scale up to many connections as the creation of more threads consumes valuable system resources.
The select model is another I/O model widely available in Winsock. We call it the select model because it centers on using the select() function to manage I/O. The design of this model originated on UNIX-based computers featuring Berkeley socket implementations. The select model was incorporated into Winsock 1.1 to allow applications that want to avoid blocking on socket calls the capability to manage multiple sockets in an organized manner. Because Winsock 1.1 is backward-compatible with Berkeley socket implementations, a Berkeley socket application that uses the select() function should technically be able to run without modification.
The select() function can be used to determine if there is data on a socket and if a socket can be written to. The reason for having this function is to prevent your application from blocking on an I/O bound call such as send() or recv() when a socket is in a blocking mode and to prevent the WSAEWOULDBLOCK error when a socket is in a non-blocking mode. The select() function blocks for I/O operations until the conditions specified as parameters are met. The function prototype for select is as follows:
int select(
int nfds,
fd_set FAR * readfds,
fd_set FAR * writefds,
fd_set FAR * exceptfds,
const struct timeval FAR * timeout
);
The first parameter, nfds, is ignored and is included only for compatibility with Berkeley socket applications. You'll notice that there are three fd_set parameters:
Essentially, the fd_set data type represents a collection of sockets. The readfds set identifies sockets that meet one of the following conditions:
The writefds set identifies sockets in which one of the following is true:
Finally, the exceptfds set identifies sockets in which one of the following is true:
For example, when you want to test a socket for readability, you must add it to the readfds set and wait for the select() function to complete. When the select() call completes, you have to determine if your socket is still part of the readfds set. If so, the socket is readable, you can begin to retrieve data from it. Any two of the three parameters (readfds, writefds, exceptfds) can be null values (at least one must not be null), and any non-null set must contain at least one socket handle; otherwise, the select() function won't have anything to wait for. The final parameter, timeout, is a pointer to a timeval structure that determines how long the select() function will wait for I/O to complete. If timeout is a null pointer, select() will block indefinitely until at least one descriptor meets the specified criteria. The timeval structure is defined as:
struct timeval
{
long tv_sec;
long tv_usec;
};
The tv_sec field indicates how long to wait in seconds; the tv_usec field indicates how long to wait in milliseconds. The timeout value {0, 0} indicates select() will return immediately, allowing an application to poll on the select() operation. This should be avoided for performance reasons. When select() completes successfully, it returns the total number of socket handles that have I/O operations pending in the fd_set structures. If the timeval limit expires, it returns 0. If select() fails for any reason, it returns SOCKET_ERROR.
Before you can begin to use select() to monitor sockets, your application has to set up either one or all of the read, write, and exception fd_set structures by assigning socket handles to a set. When you assign a socket to one of the sets, you are asking select() to let you know if the I/O activities just described have occurred on a socket. Winsock provides the following set of macros to manipulate and check the fd_set sets for I/O activity.
For example, if you want to find out when it is safe to read data from a socket without blocking, simply assign your socket to the fd_read set using the FD_SET macro and then call select(). To test whether your socket is still part of the fd_read set, use the FD_ISSET macro. The following five steps describe the basic flow of an application that uses select with one or more socket handles:
When select() returns, it modifies each of the fd_set structures by removing the socket handles that do not have pending I/O operations. This is why you should use the FD_ISSET macro as in step 4 to determine if a particular socket is part of a set. The following code sample outlines the basic steps needed to set up the select() model for a single socket. Adding more sockets to this application simply involves maintaining a list or an array of additional sockets.
SOCKET s;
fd_set fdread;
int ret;
// Create a socket, and accept a connection
// Manage I/O on the socket
while(TRUE)
{
// Always clear the read set before calling select()
FD_ZERO(&fdread);
// Add socket s to the read set
FD_SET(s, &fdread);
if ((ret = select(0, &fdread, NULL, NULL, NULL)) == SOCKET_ERROR)
{
// Error condition
}
if (ret > 0)
{
// For this simple case, select() should return
// the value 1. An application dealing with
// more than one socket could get a value
// greater than 1. At this point, your
// application should check to see whether the socket is part of a set.
if (FD_ISSET(s, &fdread))
{
// A read event has occurred on socket s
}
}
}
The advantage of using select() is the capability to multiplex connections and I/O on many sockets from a single thread. This prevents the explosion of threads associated with blocking sockets and multiple connections. The disadvantage is the maximum number of sockets that may be added to the fd_set structures. By default, the maximum is defined as FD_SETSIZE, which is defined in WINSOCK2.H as 64. To increase this limit, an application might define FD_SETSIZE to something large. This define must appear before including WINSOCK2.H. Also, the underlying provider imposes an arbitrary maximum fd_set size, which typically is 1024 but is not guaranteed to be. Finally, for a large FD_SETSIZE, consider the performance hit of setting 1000 sockets before calling select() followed by checking whether each of those 1000 sockets is set after the call returns.
< Chap 5: Index | Winsock2 I/O Method Main | Select Program Example >