程式扎記: [ NP in MS ] Winsock I/O Methods (Part2 : Socket I/O Models 之一 - select, WSAAsyncSelect Model)

標籤

2011年4月30日 星期六

[ NP in MS ] Winsock I/O Methods (Part2 : Socket I/O Models 之一 - select, WSAAsyncSelect Model)

Preface : 
Essentially, six types of socket I/O models are available that allow Winsock applications to manage I/O: blocking, select, WSAAsyncSelect, WSAEventSelect,overlapped, and completion port. 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. On the companion CD, you will find sample applications for each I/O model demonstrating how to develop a simple TCP echo server using the principles described in each model. 
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 withioctlsocket. 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. 

The blocking Model : 
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 : 
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 : 

  1. int select(  
  2.     int nfds,  
  3.     fd_set FAR * readfds,  
  4.     fd_set FAR * writefds,  
  5.     fd_set FAR * exceptfds,  
  6.     const struct timeval FAR * timeout  
  7. );  
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 writeability (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 : 
* Data is available for reading.
* Connection has been closed, reset, or terminated.
* If listen has been called and a connection is pending, the accept function will succeed.

The writefds set identifies sockets in which one of the following is true: 
* Data can be sent.
* If a non-blocking connect call is being processed, the connection has succeeded.

Finally, the exceptfds set identifies sockets in which one of the following is true: 
* If a non-blocking connect call is being processed, the connection attempt failed.
* OOB data is available for reading.

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 selectcall 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 : 
  1. struct timeval   
  2. {  
  3.     long tv_sec;   
  4.     long tv_usec;  
  5. };  
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 : 
* FD_ZERO(*set) Initializes set to the empty set. A set should always be cleared before using.
* FD_CLR(s, *set) Removes socket s from set.
* FD_ISSET(s, *set) Checks to see if s is a member of set and returns TRUE if so.
* FD_SET(s, *set) Adds socket s to set.

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 : 
1. Initialize each fd_set of interest by using the FD_ZERO macro.

2. Assign socket handles to each of the fd_set sets of interest by 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.

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

4. 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 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 : 
  1. SOCKET  s;  
  2. fd_set  fdread;  
  3. int     ret;  
  4.   
  5. // Create a socket, and accept a connection  
  6.   
  7. // Manage I/O on the socket  
  8. while(TRUE)  
  9. {  
  10.     // Always clear the read set before calling   
  11.     // select()  
  12.     FD_ZERO(&fdread);  
  13.   
  14.     // Add socket s to the read set  
  15.     FD_SET(s, &fdread);  
  16.   
  17.     if ((ret = select(0, &fdread, NULL, NULL, NULL))   
  18.         == SOCKET_ERROR)   
  19.     {  
  20.         // Error condition  
  21.     }  
  22.   
  23.     if (ret > 0)  
  24.     {  
  25.         // For this simple case, select() should return  
  26.         // the value 1. An application dealing with   
  27.         // more than one socket could get a value   
  28.         // greater than 1. At this point, your   
  29.         // application should check to see whether the   
  30.         // socket is part of a set.  
  31.   
  32.         if (FD_ISSET(s, &fdread))  
  33.         {  
  34.             // A read event has occurred on socket s  
  35.         }  
  36.     }  
  37. }  
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. 

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. Before we continue, however, we need to make one subtle distinction. The WSAAsyncSelect and WSAEventSelect models provide asynchronous notification of the capability to read or write data. It does not provide asynchronous data transfer like the overlapped and completion port models. 
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, usually referred to as a winproc. This model is also used by the Microsoft Foundation Class (MFC) CSocketobject. 
- 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 it. 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 : 
  1. int WSAAsyncSelect(  
  2.     SOCKET s,  
  3.     HWND hWnd,  
  4.     unsigned int wMsg,  
  5.     long lEvent  
  6. );  
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. Applications usually 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 5-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. 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 : 
  1. WSAAsyncSelect(s, hwnd, WM_SOCKET,   
  2.     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 toclosesocket 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 non-blocking mode that we described previously. 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. 
 

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 : 
  1. LRESULT CALLBACK WindowProc(  
  2.     HWND hWnd,  
  3.     UINT uMsg,  
  4.     WPARAM wParam,  
  5.     LPARAM lParam  
  6. );  
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 your case, you 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, that 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. 
The following example demonstrates how to manage window messages when using the WSAAsyncSelect I/O model. The code highlights the steps needed to develop a basic server application and removes the programming details of developing a fully featured Windows application : 
  1. #define WM_SOCKET WM_USER + 1  
  2. #include   
  3. #include   
  4.   
  5. int WINAPI WinMain(HINSTANCE hInstance,   
  6.     HINSTANCE hPrevInstance, LPSTR lpCmdLine,  
  7.     int nCmdShow)  
  8. {  
  9.     WSADATA wsd;  
  10.     SOCKET Listen;  
  11.     SOCKADDR_IN InternetAddr;  
  12.     HWND Window;  
  13.   
  14.     // Create a window and assign the ServerWinProc  
  15.     // below to it  
  16.   
  17.     Window = CreateWindow();  
  18.     // Start Winsock and create a socket  
  19.   
  20.     WSAStartup(MAKEWORD(2,2), &wsd);  
  21.     Listen = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP);  
  22.     // Bind the socket to port 5150  
  23.     // and begin listening for connections  
  24.   
  25.     InternetAddr.sin_family = AF_INET;  
  26.     InternetAddr.sin_addr.s_addr = htonl(INADDR_ANY);  
  27.     InternetAddr.sin_port = htons(5150);  
  28.   
  29.     bind(Listen, (PSOCKADDR) &InternetAddr,  
  30.         sizeof(InternetAddr));  
  31.   
  32.     // Set up window message notification on  
  33.     // the new socket using the WM_SOCKET define  
  34.     // above  
  35.   
  36.     WSAAsyncSelect(Listen, Window, WM_SOCKET,  
  37.         FD_ACCEPT │ FD_CLOSE);  
  38.   
  39.     listen(Listen, 5);  
  40.   
  41.     // Translate and dispatch window messages  
  42.     // until the application terminates  
  43.     while (1) {  
  44.      // ...  
  45. }  
  46. }  
  47.   
  48. BOOL CALLBACK ServerWinProc(HWND hDlg,UINT wMsg,  
  49.     WPARAM wParam, LPARAM lParam)  
  50. {  
  51.     SOCKET Accept;  
  52.   
  53.     switch(wMsg)  
  54.     {  
  55.         case WM_PAINT:  
  56.             // Process window paint messages  
  57.             break;  
  58.   
  59.         case WM_SOCKET:  
  60.   
  61.             // Determine whether an error occurred on the  
  62.             // socket by using the WSAGETSELECTERROR() macro  
  63.   
  64.             if (WSAGETSELECTERROR(lParam))  
  65.             {  
  66.                  // Display the error and close the socket  
  67.                 closesocket( (SOCKET) wParam);  
  68.                 break;  
  69.             }  
  70.   
  71.             // Determine what event occurred on the  
  72.             // socket  
  73.   
  74.             switch(WSAGETSELECTEVENT(lParam))  
  75.             {  
  76.                 case FD_ACCEPT:  
  77.   
  78.                     // Accept an incoming connection  
  79.                     Accept = accept(wParam, NULL, NULL);  
  80.   
  81.                     // Prepare accepted socket for read,  
  82.                     // write, and close notification  
  83.   
  84.                     WSAAsyncSelect(Accept, hDlg, WM_SOCKET,  
  85.                         FD_READ │ FD_WRITE │ FD_CLOSE);  
  86.                     break;  
  87.   
  88.                 case FD_READ:  
  89.   
  90.                     // Receive data from the socket in  
  91.                     // wParam  
  92.                     break;  
  93.   
  94.                 case FD_WRITE:  
  95.   
  96.                     // The socket in wParam is ready  
  97.                     // for sending data  
  98.                     break;  
  99.   
  100.                 case FD_CLOSE:  
  101.   
  102.                     // The connection is now closed  
  103.                     closesocket( (SOCKET)wParam);  
  104.                     break;  
  105.             }  
  106.             break;  
  107.     }  
  108.     return TRUE;  
  109. }  
One final detail worth noting is how applications should process FD_WRITE event notifications. FD_WRITE notifications are sent under only three conditions : 
* After a socket is first connected with connect or WSAConnect
* After a socket is accepted with accept or WSAAccept
* When a sendWSASendsendto, or WSASendTo operation fails with WSAEWOULDBLOCK and buffer space becomes available

Therefore, an application should assume that sends are always possible on a socket starting from the first FD_WRITE message and lasting until a send,WSASend, sendto, or WSASendTo returns the socket error WSAEWOULDBLOCK. After such failure, another FD_WRITE message notifies the application that sends are once again possible. 
The WSAAsyncSelect model offers many advantages; foremost is the capability to handle many connections simultaneously without much overhead, unlike the select model's requirement of setting up the fd_set structures. The disadvantages are having to use a window if your application requires no windows (such as a service or console application). Also, having a single window procedure to service all the events on thousands of socket handles can become a performance bottleneck (meaning this model doesn't scale very well). 

補充說明 : 
* [ NP in MS ] Part2 : Socket I/O Models 之二 - WSAEventSelect Model 
* [ NP in MS ] Part2 : Socket I/O Models 之三 - Overlapped Model

沒有留言:

張貼留言

網誌存檔

關於我自己

我的相片
Where there is a will, there is a way!