Socket Modes

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.

Non-blocking Mode

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.



Network Programming for Microsoft Windows
Network Programming for Microsoft Windows (Microsoft Professional Series)
ISBN: 0735605602
EAN: 2147483647
Year: 2001
Pages: 172
Authors: Anthony Jones

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