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.

Figure 8-1. Simple blocking socket sample

 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).

Figure 8-2. Multithreaded blocking sockets example

 // 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.

Figure 8-3. Making a socket nonblocking

 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.



Network Programming for Microsoft Windows
Linux Server Hacks, Volume Two: Tips & Tools for Connecting, Monitoring, and Troubleshooting
ISBN: 735615799
EAN: 2147483647
Year: 1998
Pages: 159

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net