The overlapped I/O model in Winsock offers applications better system performance than any of the I/O models explained so far. The overlapped model's basic design allows your application to post one or more asynchronous I/O requests at a time using an overlapped data structure. At a later point, the application can service the submitted requests after they have completed. This model is available on all Windows platforms except Windows CE. The model's overall design is based on the Windows overlapped I/O mechanisms available for performing I/O operations on devices using the ReadFile and WriteFilefunctions.
Originally, the Winsock overlapped I/O model was available only to Winsock 1.1 applications running on Windows NT. Applications could take advantage of the model by calling ReadFile and WriteFile on a socket handle and specifying an overlapped structure. Since the release of Winsock 2, overlapped I/O has been incorporated into new Winsock functions, such as WSASend and WSARecv. As a result, the overlapped I/O model is now available on all Windows platforms that feature Winsock 2.
Ps.
To use the overlapped I/O model on a socket, you must first create a socket that has the overlapped flag set. See Chapter 2 for more information on creating overlapped enabled sockets. After you successfully create a socket and bind it to a local interface, overlapped I/O operations can commence by calling the Winsock functions listed below and specifying an optional WSAOVERLAPPED structure :
To use overlapped I/O, each function takes a WSAOVERLAPPED structure as a parameter. When these functions are called with a WSAOVERLAPPED structure, they complete immediately—regardless of the socket's mode (described at the beginning of this chapter). They rely on the WSAOVERLAPPED structure to manage the completion of an I/O request. There are essentially two methods for managing the completion of an overlapped I/O request : your application can wait for event object notification or it can process completed requests through completion routines. The first six functions in the list have another parameter in common : a WSAOVERLAPPED_COMPLETION_ROUTINE. This parameter is an optional pointer to a completion routine function that gets called when an overlapped request completes. We will explore the event notification method next. Later in this chapter, you will learn how to use optional completion routines instead of events to process completed overlapped requests.
- Event Notification
The event notification method of overlapped I/O requires associating Windows event objects with WSAOVERLAPPED structures. When I/O calls such asWSASend and WSARecv are made using a WSAOVERLAPPED structure, they return immediately. Typically, you will find that these I/O calls fail with the return value SOCKET_ERROR and that WSAGetLastError reports a WSA_IO_PENDING error status. This error status simply means that the I/O operation is in progress. At a later time, your application will need to determine when an overlapped I/O request completes by waiting on the event object associated with the WSAOVERLAPPED structure. The WSAOVERLAPPED structure provides the communication medium between the initiation of an overlapped I/O request and its subsequent completion, and is defined as :
- typedef struct WSAOVERLAPPED
- {
- DWORD Internal;
- DWORD InternalHigh;
- DWORD Offset;
- DWORD OffsetHigh;
- WSAEVENT hEvent;
- } WSAOVERLAPPED, FAR * LPWSAOVERLAPPED;
When an overlapped I/O request finally completes, your application is responsible for retrieving the overlapped results. In the event notification method, Winsock will change the event-signaling state of an event object that is associated with a WSAOVERLAPPED structure from non-signaled to signaled when an overlapped request finally completes. Because an event object is assigned to the WSAOVERLAPPED structure, you can easily determine when an overlapped I/O call completes by calling the WSAWaitForMultipleEvents function, which we also described in the WSAEventSelect I/O model.
WSAWaitForMultipleEvents waits a specified amount of time for one or more event objects to become signaled. We can't stress this point enough: remember that WSAWaitForMultipleEvents is capable of waiting on only 64 event objects at a time. Once you determine which overlapped request has completed, you need to determine the success or failure of the overlapped call by calling WSAGetOverlappedResult, which is defined as :
- BOOL WSAGetOverlappedResult(
- SOCKET s,
- LPWSAOVERLAPPED lpOverlapped,
- LPDWORD lpcbTransfer,
- BOOL fWait,
- LPDWORD lpdwFlags
- );
If the WSAGetOverlappedResult function succeeds, the return value is TRUE. This means that your overlapped operation has completed successfully and that the value pointed to by lpcbTransfer has been updated. If the return value is FALSE, one of the following statements is true :
Upon failure, the value pointed to by lpcbTransfer will not be updated, and your application should call the WSAGetLastError function to determine the cause of the failure. The following sample of code demonstrates how to structure a simple server application that is capable of managing overlapped I/O on one socket using the event notification described above :
- #define DATA_BUFSIZE 4096
- void main(void)
- {
- WSABUF DataBuf;
- char buffer[DATA_BUFSIZE];
- DWORD EventTotal = 0,
- RecvBytes=0,
- Flags=0;
- WSAEVENT EventArray[WSA_MAXIMUM_WAIT_EVENTS];
- WSAOVERLAPPED AcceptOverlapped;
- SOCKET ListenSocket, AcceptSocket;
- // Step 1:
- // Start Winsock and set up a listening socket
- ...
- // Step 2:
- // Accept an inbound connection
- AcceptSocket = accept(ListenSocket, NULL, NULL);
- // Step 3:
- // Set up an overlapped structure
- EventArray[EventTotal] = WSACreateEvent();
- ZeroMemory(&AcceptOverlapped,
- sizeof(WSAOVERLAPPED));
- AcceptOverlapped.hEvent = EventArray[EventTotal];
- DataBuf.len = DATA_BUFSIZE;
- DataBuf.buf = buffer;
- EventTotal++;
- // Step 4:
- // Post a WSARecv request to begin receiving data
- // on the socket
- if (WSARecv(AcceptSocket, &DataBuf, 1, &RecvBytes,
- &Flags, &AcceptOverlapped, NULL) == SOCKET_ERROR)
- {
- if (WSAGetLastError() != WSA_IO_PENDING)
- {
- // Error occurred
- }
- }
- // Process overlapped receives on the socket
- while(TRUE)
- {
- DWORD Index;
- // Step 5:
- // Wait for the overlapped I/O call to complete
- Index = WSAWaitForMultipleEvents(EventTotal,
- EventArray, FALSE, WSA_INFINITE, FALSE);
- // Index should be 0 because we
- // have only one event handle in EventArray
- // Step 6:
- // Reset the signaled event
- WSAResetEvent(
- EventArray[Index - WSA_WAIT_EVENT_0]);
- // Step 7:
- // Determine the status of the overlapped
- // request
- WSAGetOverlappedResult(AcceptSocket,
- &AcceptOverlapped, &BytesTransferred,
- FALSE, &Flags);
- // First check to see whether the peer has closed
- // the connection, and if so, close the
- // socket
- if (BytesTransferred == 0)
- {
- printf("Closing socket %d\n", AcceptSocket);
- closesocket(AcceptSocket);
- WSACloseEvent(
- EventArray[Index - WSA_WAIT_EVENT_0]);
- return;
- }
- // Do something with the received data
- // DataBuf contains the received data
- ...
- // Step 8:
- // Post another WSARecv() request on the socket
- Flags = 0;
- ZeroMemory(&AcceptOverlapped,
- sizeof(WSAOVERLAPPED));
- AcceptOverlapped.hEvent = EventArray[Index -
- WSA_WAIT_EVENT_0];
- DataBuf.len = DATA_BUFSIZE;
- DataBuf.buf = buffer;
- if (WSARecv(AcceptSocket, &DataBuf, 1,
- &RecvBytes, &Flags, &AcceptOverlapped,
- NULL) == SOCKET_ERROR)
- {
- if (WSAGetLastError() != WSA_IO_PENDING)
- {
- // Unexpected error
- }
- }
- }
- }
This example can easily be expanded to handle more than one socket by moving the overlapped I/O processing portion of the code to a separate thread and allowing the main application thread to service additional connection requests.
Ps.
- Completion Routines :
For newcomers, the completion port model seems overwhelmingly complicated because extra work is required to add sockets to a completion port when compared to the initialization steps for the other I/O models. However, as you will see, these steps are not that complicated once you understand them. Also, the completion port model offers the best system performance possible when an application has to manage many sockets at once. Unfortunately, it's available only on Windows NT, Windows 2000, and Windows XP; however, the completion port model offers the best scalability of all the models discussed so far. This model is well suited to handling hundreds or thousands of sockets.
Essentially, the completion port model requires you to create a Windows completion port object that will manage overlapped I/O requests using a specified number of threads to service the completed overlapped I/O requests. Note that a completion port is actually a Windows I/O construct that is capable of accepting more than just socket handles. However, this section will describe only how to take advantage of the completion port model by using socket handles. To begin using this model, you are required to create an I/O completion port object that will be used to manage multiple I/O requests for any number of socket handles. This is accomplished by calling the CreateIoCompletionPort function, which is defined as :
- HANDLE CreateIoCompletionPort(
- HANDLE FileHandle,
- HANDLE ExistingCompletionPort,
- DWORD CompletionKey,
- DWORD NumberOfConcurrentThreads
- );
When you initially create a completion port object, the only parameter of interest is NumberOfConcurrentThreads; the first three parameters are not significant. The NumberOfConcurrentThreads parameter is special because it defines the number of threads that are allowed to execute concurrently on a completion port. Ideally, you want only one thread per processor to service the completion port to avoid thread context switching. The value 0 for this parameter tells the system to allow as many threads as there are processors in the system. The following code creates an I/O completion port :
- CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
Worker Threads and Completion Ports :
After a completion port is successfully created, you can begin to associate socket handles with the object. Before associating sockets, though, you have to create one or more worker threads to service the completion port when socket I/O requests are posted to the completion port object. At this point, you might wonder how many threads should be created to service the completion port. This is actually one of the more complicated aspects of the completion port model because the number needed to service I/O requests depends on the overall design of your application. It's important to note the distinction between number of concurrent threads to specify when calling CreateIoCompletionPort versus the number of worker threads to create; they do not represent the same thing. We recommended previously that you should have the CreateIoCompletionPort function specify one thread per processor to avoid thread context switching. The NumberOfConcurrentThreads parameter of CreateIoCompletionPort explicitly tells the system to allow only n threads to operate at a time on the completion port. If you create more than n worker threads on the completion port, only n threads will be allowed to operate at a time. (Actually, the system might exceed this value for a short amount of time, but the system will quickly bring it down to the value you specify in CreateIoCompletionPort.) You might be wondering why you would create more worker threads than the number specified by the CreateIoCompletionPort call. As we mentioned previously, this depends on the overall design of your application. If one of your worker threads calls a function—such as Sleep or WaitForSingleObject—and becomes suspended, another thread will be allowed to operate in its place. In other words, you always want to have as many threads available for execution as the number of threads you allow to execute in the CreateIoCompletionPort call. Thus, if you expect your worker thread to ever become blocked, it is reasonable to create more worker threads than the value specified in CreateIoCompletionPort's NumberOfConcurrentThreads parameter.
Once you have enough worker threads to service I/O requests on the completion port, you can begin to associate socket handles with the completion port. This requires calling the CreateIoCompletionPort function on an existing completion port and supplying the first three parameters—FileHandle,ExistingCompletionPort, and CompletionKey—with socket information. The FileHandle parameter represents a socket handle to associate with the completion port. The ExistingCompletionPort parameter identifies the completion port to which the socket handle is to be associated with. The CompletionKey parameter identifies per-handle data that you can associate with a particular socket handle. Applications are free to store any type of information associated with a socket by using this key. We call it per-handle data because it represents data associated with a socket handle. It is useful to store the socket handle using the key as a pointer to a data structure containing the socket handle and other socket-specific information. As we will see later in this chapter, the thread routines that service the completion port can retrieve socket-handle–specific information using this key.
Let's begin to construct a basic application framework from what we've described so far. The following example demonstrates how to start developing an echo server application using the completion port model. In this code, we take the following preparation steps :
Below is the sample code for above steps :
- HANDLE CompletionPort;
- WSADATA wsd;
- SYSTEM_INFO SystemInfo;
- SOCKADDR_IN InternetAddr;
- SOCKET Listen;
- int i;
- typedef struct _PER_HANDLE_DATA
- {
- SOCKET Socket;
- SOCKADDR_STORAGE ClientAddr;
- // Other information useful to be associated with the handle
- } PER_HANDLE_DATA, * LPPER_HANDLE_DATA;
- // Load Winsock
- StartWinsock(MAKEWORD(2,2), &wsd);
- // Step 1:
- // Create an I/O completion port
- CompletionPort = CreateIoCompletionPort(
- INVALID_HANDLE_VALUE, NULL, 0, 0);
- // Step 2:
- // Determine how many processors are on the system
- GetSystemInfo(&SystemInfo);
- // Step 3:
- // Create worker threads based on the number of
- // processors available on the system. For this
- // simple case, we create one worker thread for each
- // processor.
- for(i = 0; i < SystemInfo.dwNumberOfProcessors; i++)
- {
- HANDLE ThreadHandle;
- // Create a server worker thread, and pass the
- // completion port to the thread. NOTE: the
- // ServerWorkerThread procedure is not defined
- // in this listing.
- ThreadHandle = CreateThread(NULL, 0,
- ServerWorkerThread, CompletionPort,
- 0, NULL);
- // Close the thread handle
- CloseHandle(ThreadHandle);
- }
- // Step 4:
- // Create a listening socket
- Listen = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0,
- WSA_FLAG_OVERLAPPED);
- InternetAddr.sin_family = AF_INET;
- InternetAddr.sin_addr.s_addr = htonl(INADDR_ANY);
- InternetAddr.sin_port = htons(5150);
- bind(Listen, (PSOCKADDR) &InternetAddr,
- sizeof(InternetAddr));
- // Prepare socket for listening
- listen(Listen, 5);
- while(TRUE)
- {
- PER_HANDLE_DATA *PerHandleData=NULL;
- SOCKADDR_IN saRemote;
- SOCKET Accept;
- int RemoteLen;
- // Step 5:
- // Accept connections and assign to the completion
- // port
- RemoteLen = sizeof(saRemote);
- Accept = WSAAccept(Listen, (SOCKADDR *)&saRemote,
- &RemoteLen);
- // Step 6:
- // Create per-handle data information structure to
- // associate with the socket
- PerHandleData = (LPPER_HANDLE_DATA)
- GlobalAlloc(GPTR, sizeof(PER_HANDLE_DATA));
- printf("Socket number %d connected\n", Accept);
- PerHandleData->Socket = Accept;
- memcpy(&PerHandleData->ClientAddr, &saRemote, RemoteLen);
- // Step 7:
- // Associate the accepted socket with the
- // completion port
- CreateIoCompletionPort((HANDLE) Accept,
- CompletionPort, (DWORD) PerHandleData, 0);
- // Step 8:
- // Start processing I/O on the accepted socket.
- // Post one or more WSASend() or WSARecv() calls
- // on the socket using overlapped I/O.
- WSARecv(...);
- }
- DWORD WINAPI ServerWorkerThread(LPVOID lpParam)
- {
- // The requirements for the worker thread will be
- // discussed later.
- return 0;
- }
After associating a socket handle with a completion port, you can begin processing I/O requests by posting overlapped send and receive requests on the socket handle. You can now start to rely on the completion port for I/O completion notification. Basically, the completion port model takes advantage of the Windows overlapped I/O mechanism in which Winsock API calls such as WSASend and WSARecv return immediately when called. It is up to your application to retrieve the results of the calls at a later time through an OVERLAPPED structure. In the completion port model, this is accomplished by having one or more worker threads wait on the completion port using the GetQueuedCompletionStatus function, which is defined as :
- BOOL GetQueuedCompletionStatus(
- HANDLE CompletionPort,
- LPDWORD lpNumberOfBytesTransferred,
- PULONG_PTR lpCompletionKey,
- LPOVERLAPPED * lpOverlapped,
- DWORD dwMilliseconds
- );
- Per-handle Data and Per-I/O Operation Data
When a worker thread receives I/O completion notification from the GetQueuedCompletionStatus API call, the lpCompletionKey and lpOverlapped parameters contain socket information that can be used to continue processing I/O on a socket through the completion port. Two types of important socket data are available through these parameters: per-handle data and per-I/O operation data.
The lpCompletionKey parameter contains what we call per-handle data because the data is related to a socket handle when a socket is first associated with the completion port. This is the data that is passed as the CompletionKey parameter of the CreateIoCompletionPort API call. As we noted earlier, your application can pass any type of socket information through this parameter. Typically, applications will store the socket handle related to the I/O request here.
The lpOverlapped parameter contains an OVERLAPPED structure followed by what we call per-I/O operation data, which is anything that your worker thread will need to know when processing a completion packet (echo the data back, accept the connection, post another read, and so on). Per-I/O operation data is any number of bytes contained in a structure also containing an OVERLAPPED structure that you pass into a function that expects an OVERLAPPED structure. A simple way to make this work is to define a structure and place an OVERLAPPED structure as a field of the new structure. For example, we declare the following data structure to manage per-I/O operation data :
- typedef struct
- {
- OVERLAPPED Overlapped;
- char Buffer[DATA_BUFSIZE];
- int BufferLen;
- int OperationType;
- } PER_IO_DATA;
- PER_IO_OPERATION_DATA PerIoData;
- WSABUF wbuf;
- DWORD Bytes, Flags;
- // Initialize wbuf ...
- WSARecv(socket, &wbuf, 1, &Bytes, &Flags, &(PerIoData.Overlapped),
- NULL);
- PER_IO_DATA *PerIoData=NULL;
- OVERLAPPED *lpOverlapped=NULL;
- ret = GetQueuedCompletionStatus(
- CompPortHandle,
- &Transferred,
- (PULONG_PTR)&CompletionKey,
- &lpOverlapped,
- INFINITE);
- // Check for successful return
- PerIoData = CONTAINING_RECORD(lpOverlapped, PER_IO_DATA, Overlapped);
You can determine which operation was posted on this handle by using a field of the per-I/O structure to indicate the type of operation posted. In our example, the OperationType member would be set to indicate a read, write, etc., operation. One of the biggest benefits of per-I/O operation data is that it allows you to manage multiple I/O operations (such as read/write, multiple reads, and multiple writes) on the same handle. You might ask why you would want to post more than one I/O operation at a time on a socket. The answer is scalability. For example, if you have a multiple-processor machine with a worker thread using each processor, you could potentially have several processors sending and receiving data on a socket at the same time.
Before continuing, there is one other important aspect about Windows completion ports that needs to be stressed. All overlapped operations are guaranteed to be executed in the order that the application issued them. However, the completion notifications returned from a completion port are not guaranteed to be in that same order. That is, if an application posts two overlapped WSARecv operations, one with a 10 KB buffer and the next with a 12 KB buffer, the 10 KB buffer is filled first, followed by the 12 KB buffer. The application's worker thread may receive notification from GetQueuedCompletionStatusfor the 12 KB WSARecv before the completion event for the 10 KB operation. Of course, this is only an issue when multiple operations are posted on a socket.
To complete this simple echo server sample, we need to supply a ServerWorkerThread function. The following code outlines how to develop a worker thread routine that uses per-handle data and per-I/O operation data to service I/O requests :
- DWORD WINAPI ServerWorkerThread(
- LPVOID CompletionPortID)
- {
- HANDLE CompletionPort = (HANDLE) CompletionPortID;
- DWORD BytesTransferred;
- LPOVERLAPPED Overlapped;
- LPPER_HANDLE_DATA PerHandleData;
- LPPER_IO_DATA PerIoData;
- DWORD SendBytes, RecvBytes;
- DWORD Flags;
- while(TRUE)
- {
- // Wait for I/O to complete on any socket
- // associated with the completion port
- ret = GetQueuedCompletionStatus(CompletionPort,
- &BytesTransferred,(LPDWORD)&PerHandleData,
- (LPOVERLAPPED *) &PerIoData, INFINITE);
- // First check to see if an error has occurred
- // on the socket; if so, close the
- // socket and clean up the per-handle data
- // and per-I/O operation data associated with
- // the socket
- if (BytesTransferred == 0 &&
- (PerIoData->OperationType == RECV_POSTED ││
- PerIoData->OperationType == SEND_POSTED))
- {
- // A zero BytesTransferred indicates that the
- // socket has been closed by the peer, so
- // you should close the socket. Note:
- // Per-handle data was used to reference the
- // socket associated with the I/O operation.
- closesocket(PerHandleData->Socket);
- GlobalFree(PerHandleData);
- GlobalFree(PerIoData);
- continue;
- }
- // Service the completed I/O request. You can
- // determine which I/O request has just
- // completed by looking at the OperationType
- // field contained in the per-I/O operation data.
- if (PerIoData->OperationType == RECV_POSTED)
- {
- // Do something with the received data
- // in PerIoData->Buffer
- }
- // Post another WSASend or WSARecv operation.
- // As an example, we will post another WSARecv()
- // I/O operation.
- Flags = 0;
- // Set up the per-I/O operation data for the next
- // overlapped call
- ZeroMemory(&(PerIoData->Overlapped),
- sizeof(OVERLAPPED));
- PerIoData->DataBuf.len = DATA_BUFSIZE;
- PerIoData->DataBuf.buf = PerIoData->Buffer;
- PerIoData->OperationType = RECV_POSTED;
- WSARecv(PerHandleData->Socket,
- &(PerIoData->DataBuf), 1, &RecvBytes,
- &Flags, &(PerIoData->Overlapped), NULL);
- }
- }
One final detail not outlined in the last two examples we have presented is how to properly close an I/O completion port—especially if you have one or more threads in progress performing I/O on several sockets. The main thing to avoid is freeing an OVERLAPPED structure when an overlapped I/O operation is in progress. The best way to prevent this is to call closesocket on every socket handle—any overlapped I/O operations pending will complete. Once all socket handles are closed, you need to terminate all worker threads on the completion port. This can be accomplished by sending a special completion packet to each worker thread using the PostQueuedCompletionStatus function, which informs each thread to exit immediately. PostQueuedCompletionStatus is defined as :
- BOOL PostQueuedCompletionStatus(
- HANDLE CompletionPort,
- DWORD dwNumberOfBytesTransferred,
- ULONG_PTR dwCompletionKey,
- LPOVERLAPPED lpOverlapped
- );
The completion port I/O model is by far the best in terms of performance and scalability. There are no limitations to the number of sockets that may be associated with a completion port and only a small number of threads are required to service the completed I/O. For more information on using completion ports to develop scalable, high-performance servers, see Chapter 6.
Supplement :
* [ MSDN 文章收集 ] I/O Concepts : I/O Completion Ports
沒有留言:
張貼留言