Winsock I/O Methods

Источник: Anthony Jones and Jim Ohlund "Windows Network Programming" - published by Microsoft Press, 1999

Socket Modes

As we mentioned earlier, Windows sockets perform I/O operations in two socket operating modes: blocking and nonblocking. 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 nonblocking mode, the Winsock functions return immediately. Applications running on the Windows CE and Windows 95 (with Winsock 1) platforms, which support very few of the I/O models, require you to take certain steps with blocking and nonblocking 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 code snippet in Figure 8-1 illustrates this model.

SOCKET  sock;  
char    buff[256];  
int     done = 0;
    ...    


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 the data (reading the data actually removes it from the system's buffer) is considered bad programming practice and should be avoided at all costs. 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 actual recv call that removes the data from the system buffer. What can be done to avoid this? The idea is 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 through the use of 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.

Figure 8-2 illustrates this approach by providing two functions, one responsible for reading network data (ReadThread) and one for performing the computations on the data (ProcessThread).

// Initialize critical section (data) and create   
// an auto-reset event (hEvent) before creating the  
// two threads
  
CRITICAL_SECTION data;  
HANDLE           hEvent;  
TCHAR            buff[MAX_BUFFER_SIZE];  
int              nbytes;
    ...    
// Reader thread  
void ReadThread(void)
{
      int nTotal = 0,
          nRead = 0,
          nLeft = 0,
          nBytes = 0;
          while (!done)
          {
             nTotal = 0;
             nLeft = NUM_BYTES_REQUIRED;
             while (nTotal != NUM_BYTES_REQUIRED)
             {
                EnterCriticalSection(&data);
                nRead = recv(sock, &(buff[MAX_BUFFER_SIZE - nBytes]),
                  nLeft);
                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.

Nonblocking Mode

The alternative to blocking sockets is nonblocking sockets. Nonblocking sockets are a bit more challenging to use, but they are every bit as powerful as blocking sockets, with a few advantages. Figure 8-3 illustrates how to create a socket and put it into nonblocking mode.

SOCKET        s;
unsigned long ul = 1;
int           nRet;


s = socket(AF_INET, SOCK_STREAM, 0);
nRet = ioctlsocket(s, FIOBIO, (unsigned long *) &ul);
if (nRet == SOCKET_ERROR)
{
      // Failed to put the socket into nonblocking mode
}  

Once a socket is placed in nonblocking mode, Winsock API calls 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 a successful return code is encountered. Table 8-2 describes the meaning of WSAEWOULDBLOCK when returned by commonly used Winsock calls.

Because nonblocking calls frequently fail with the WSAEWOULDBLOCK error, you should check all return codes and be prepared for failure at any time. The pitfall 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 earlier. Winsock's socket I/O models can help an application determine when a socket is available for reading and writing.

Table 8-2. WSAEWOULDBLOCK errors on nonblocking 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.

Each socket mode-blocking and nonblocking-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, nonblocking sockets are more difficult in the sense that 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.

Socket I/O Models

Essentially five types of socket I/O models are available that allow Winsock applications to manage I/O: select, WSAAsyncSelect, WSAEventSelect, overlapped, and completion port. This section explains the features of each I/O model and outlines how to use the model to develop an application that can manage one or more socket requests. On the companion CD, you will find one or more sample applications for each I/O model demonstrating how to develop a simple TCP echo server using the principles described in each model.

The select Model

The select model is the most widely available I/O model 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 ability 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 whether there is data on a socket and whether a socket can be written to. The whole 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 nonblocking 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: one for checking readability (readfds), one for writability (writefds), and one for out-of-band data (exceptfds). Essentially, the fd_set data type represents a collection of sockets. The readfds set identifies sockets that meet one of the following conditions:

For example, when you want to test a socket for readability, you must add your socket to the readfds set and wait for the select function to complete. When the select call completes, you have to determine whether your socket is still part of the readfds set. If so, the socket is readable-you can begin to retrieve data from the socket. 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 whether the I/O activities described above 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 steps describe the basic flow of an application that uses select with one or more socket handles:

  1. Initialize each fd_set of interest, using the FD_ZERO macro.
  2. Assign socket handles to each of the fd_set sets of interest, using the FD_SET macro.
  3. Call the select function, and wait until I/O activity sets one or more of the socket handles in each fd_set set provided. When select completes, it returns the total number of socket handles that are set in all of the fd_set sets and updates each set accordingly.
  4. Using the return value of select, your application can determine which application sockets have I/O pending by checking each fd_set set using the FD_ISSET macro.
  5. After determining which sockets have I/O pending in each of the sets, process the I/O and go to step 1 to continue the select process.

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 above to determine whether a particular socket is part of a set. Figure 8-4 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 WSAAsyncSelect Model

Winsock provides a useful asynchronous I/O model that allows an application to receive Windows message-based notification of network events on a socket. This is accomplished by calling the WSAAsyncSelect function after creating a socket. This model originally existed in Winsock 1.1 implementations to help application programmers cope with the cooperative multitasking message-based environment of 16-bit Windows platforms, such as Windows for Workgroups. Applications can still benefit from this model, especially if they manage window messages in a standard Windows procedure, normally referred to as a winproc. This model is also used by the Microsoft Foundation Class (MFC) CSocket object.

Message notification

To use the WSAAsyncSelect model, your application must first create a window using the CreateWindow function and supply a window procedure (winproc) support function for this window. You can also use a dialog box with a dialog procedure instead of a window because dialog boxes are windows. For our purposes, we will demonstrate this model using a simple window with a supporting window procedure. Once you have set up the window infrastructure, you can begin creating sockets and turning on window message notification by calling the WSAAsyncSelect function, which is defined as

int WSAAsyncSelect(
      SOCKET s,
      HWND hWnd,
      unsigned int wMsg,
      long lEvent
);

The s parameter represents the socket we are interested in. The hWnd parameter is a window handle identifying the window or the dialog box that receives a message when a network event occurs. The wMsg parameter identifies the message to be received when a network event occurs. This message is posted to the window that is identified by the hWnd window handle. Normally applications set this message to a value greater than the Windows WM_USER value to avoid confusing a network window message with a predefined standard window message. The last parameter, lEvent, represents a bitmask that specifies a combination of network events-listed in Table 8-3-that the application is interested in. Most applications are typically interested in the FD_READ, FD_WRITE, FD_ACCEPT, FD_CONNECT, and FD_CLOSE network event types. Of course, the use of the FD_ACCEPT or the FD_CONNECT type depends on whether your application is a client or a server. If your application is interested in more than one network event, simply set this field by performing a bitwise OR on the types and assigning them to lEvent. For example:

WSAAsyncSelect(s, hwnd, WM_SOCKET,
       FD_CONNECT | FD_READ | FD_WRITE | FD_CLOSE);

This allows our application to get connect, send, receive, and socket-closure network event notifications on socket s. It is impossible to register multiple events one at a time on the socket. Also note that once you turn on event notification on a socket, it remains on unless the socket is closed by a call to closesocket or the application changes the registered network event types by calling WSAAsyncSelect (again, on the socket). Setting the lEvent parameter to 0 effectively stops all network event notification on the socket.

When your application calls WSAAsyncSelect on a socket, the socket mode is automatically changed from blocking to the nonblocking mode that we described earlier. As a result, if a Winsock I/O call such as WSARecv is called and has to wait for data, it will fail with error WSAEWOULDBLOCK. To avoid this error, applications should rely on the user-defined window message specified in the wMsg parameter of WSAAsyncSelect to indicate when network event types occur on the socket.

Event Type Meaning
FD_READ The application wants to receive notification of readiness for reading.
FD_WRITE The application wants to receive notification of readiness for writing.
FD_OOB The application wants to receive notification of the arrival of out-of-band (OOB) data.
FD_ACCEPT The application wants to receive notification of incoming connections.
FD_CONNECT The application wants to receive notification of a completed connection or a multipoint join operation.
FD_CLOSE The application wants to receive notification of socket closure.
FD_QOS The application wants to receive notification of socket Quality of Service (QOS) changes.
FD_GROUP_QOS The application wants to receive notification of socket group Quality of Service (QOS) changes (reserved for future use with socket groups).
FD_ROUTING_INTERFACE_CHANGE The application wants to receive notification of routing interface changes for the specified destination(s).
FD_ADDRESS_LIST_CHANGE The application wants to receive notification of local address list changes for the socket's protocol family.

After your application successfully calls WSAAsyncSelect on a socket, the application begins to receive network event notification as Windows messages in the window procedure associated with the hWnd parameter window handle. A window procedure is normally defined as

LRESULT CALLBACK WindowProc(
      HWND hWnd,
      UINT uMsg,
      WPARAM wParam,
      LPARAM lParam
);

The hWnd parameter is a handle to the window that invoked the window procedure. The uMsg parameter indicates which message needs to be processed. In our case, we will be looking for the message defined in the WSAAsyncSelect call. The wParam parameter identifies the socket on which a network event has occurred. This is important if you have more than one socket assigned to this window procedure. The lParam parameter contains two important pieces of information-the low word of lParam specifies the network event that has occurred, and the high word of lParam contains any error code.

When network event messages arrive at a window procedure, the application should first check the lParam high-word bits to determine whether a network error has occurred on the socket. There is a special macro, WSAGETSELECTERROR, which returns the value of the high-word bits error information. After the application has verified that no error occurred on the socket, the application should determine which network event type caused the Windows message to fire by reading the low-word bits of lParam. Another special macro, WSAGETSELECTEVENT, returns the value of the low-word portion of lParam.