標籤:
非同步與非阻塞區別見我的另外一篇文章Socket 同步/非同步與阻塞/非阻塞區別
select
WSAAsyncSelect
WSAEventSelect
重疊(Overlapped)I/O
IOCP:完成連接埠
Select
首先要使用ioctlsocket設定為非阻塞模式。
然後啟動線程,線程中不停select。
WSAAsyncSelect
WSAAsyncSelect模型是Windows下最簡單易用的一種Socket I/O模型。使用這種模型時,Windows會把網路事件以訊息的形勢通知應用程式。此模型提供了讀寫資料能力的非同步通知,但不提供非同步資料傳送。需要在訊息響應函數裡send(一般為resend)和receive。由於該模型基於Windows訊息機制,必須在應用程式中建立視窗。雖然可以在開發中,確定是否顯示該視窗。
WSAEventSelect
通常與WSACreateEvent、WSAResetEvent、WSACloseEvent、WSAWaitForMultileEvents和WSAEnumNetworkEvents一起使用,無需建立視窗。WSAWaitForMultileEvents檢查是否有Event,WSAEnumNetworkEvents枚舉事件類型,FD_READ、FD_WRITE等。
函數最多可以支援WSA_MAXIMUM_WAIT_EVENTS(64)個對象.該函數會等待網路事件的發生,如果過了指定了時間(dwTimeOut)則返回WSA_WAIT_TIMEOUT,如果在規定的時間內有事件發生,則返回該事件對象的索引(注意:在程式中要想得到發生的事件的真正索引需得用傳回值減去WSA_WAIT_EVENT_0),調用失敗返回WSA_WAIT_FAILED.如果將參數fWaitAll設定成false如果有多個網路事件發生該函數也只返回一個事件對象索引,並且該事件是在事件控制代碼數組中最前面的一個.解決方案是迴圈調用該函數處理後面的受信事件.
重疊(Overlapped)I/O
它和之前模型不同的是,使用重疊模型的代理程式更新緩衝區收發系統直接使用資料,也就是說,如果應用程式投遞了一個10KB大小的緩衝區來接收資料,且資料已經到達通訊端,則該資料將直接被拷貝到投遞的緩衝區。之前的模型都是在通訊端的緩衝區中,當通知應用程式接收後,在把資料拷貝到程式的緩衝區。
——摘自http://zhoumf1214.blog.163.com/blog/static/5241940201211705318496/
以receive為例,之前的模型,需要自己從通訊端的緩衝區拷貝至程式緩衝區,而重疊IO則是作業系統直接將資料拷貝至程式緩衝區。
重疊模型的核心是一個重疊資料結構。若想以重疊方式使用檔案,必須用 FILE_FLAG_OVERLAPPED標誌開啟它。
有2種方式實現:
1. 事件
先WaitForSingleObject/WaitForMultipleObjects或WSAWaitForMultipleEvents函數 ,然後調用(WSA)GetOverlappedResult()函數,最後,使用指標位移定位就可以準確操作接受到的資料了。
與其他事件類別似,最大個數為64.
2. 完成常式(非完成連接埠)
ReadFileEx(),傳遞迴調函數指標。
完成連接埠
所謂“完成連接埠",實際是Win32 、Windows NT以及Windows 2000採用的一種I / O構造機制,除通訊端控制代碼之外,實際上還可接受其他東西(重疊IO好像也可以)。
使用這種模型之前,首先要建立一個 I / O 完成連接埠對象(CreateIoCompletionPort),
NumberOfConcurrentThread參數的特殊之處在於,它定義了在一個完成連接埠上,同時允許執行的線程數量。理想情況下,我們希望每個處理器各自負責一個線程的運行,為完成連接埠提供服務,避免過於頻繁的線程“情境”切換。若將該參數設為 0,表明系統內安裝了多少個處理器,便允許同時運行多少個線程!
1) 建立一個完成連接埠。第四個參數保持為 0,指定在完成連接埠上,每個處理器一次只允許執行一個工作者線程。
2) 判斷系統內到底安裝了多少個處理器。
3) 建立工作者線程,根據步驟 2 )得到的處理器資訊,在完成連接埠上,為已完成的 I / O請求提供服務。在這個簡單的例子中,我們為每個處理器都只建立一個工作者線程。這是由於事先已預計到,到時不會有任何線程進入“掛起”狀態,造成由於線程數量的不足,而使處理器閒置局面(沒有足夠的線程可供執行) 。調用C r e a t e T h r e a d函數時,必須同時提供一個工作者常式,由線程在建立好執行。本節稍後還會詳細討論區對話的職責。
4) 準備好一個監聽通訊端,在連接埠 5 1 5 0上監聽進入的串連請求。
5) 使用a c c e p t函數,接受進入的串連請求。
6) 建立一個資料結構,用於容納“單控制代碼資料” ,同時在結構中存入接受的通訊端控制代碼。
7) 調用C r e a t e I o C o m p l e t i o n P o r t,將自a c c e p t返回的新通訊端控制代碼同完成連接埠關聯到一起。通過完成鍵(C o m p l e t i o n K e y)參數,將單控制代碼資料結構傳遞給 C r e a t e I o C o m p l e t i o n P o r t。
8) 開始在已接受的串連上進行 I / O 操作。在此,我們希望通過重疊 I / O機制,在建立的通訊端上投遞一個或多個非同步 W S A R e c v或W S A S e n d請求。這些 I / O 請求完成後,一個工作者線程會為I / O請求提供服務,同時繼續處理未來的 I / O 請求,稍後便會在步驟 3 )指定的工作者常式中,體驗到這一點。
9) 重複步驟5 ) ~ 8 ),直至伺服器中止。
// IOCP_TCPIP_Socket_Server.cpp#include <WinSock2.h>#include <Windows.h>#include <vector>#include <iostream>using namespace std;#pragma comment(lib, "Ws2_32.lib")// Socket編程需用的動態連結程式庫#pragma comment(lib, "Kernel32.lib")// IOCP需要用到的動態連結程式庫/** * 結構體名稱:PER_IO_DATA * 結構體功能:重疊I/O需要用到的結構體,臨時記錄IO資料 **/const int DataBuffSize = 2 * 1024;typedef struct{OVERLAPPED overlapped;WSABUF databuff;char buffer[ DataBuffSize ];int BufferLen;int operationType;}PER_IO_OPERATEION_DATA, *LPPER_IO_OPERATION_DATA, *LPPER_IO_DATA, PER_IO_DATA;/** * 結構體名稱:PER_HANDLE_DATA * 結構體儲存:記錄單個通訊端的資料,包括了通訊端的變數及通訊端的對應的用戶端的地址。 * 結構體作用:當伺服器串連上用戶端時,資訊儲存到該結構體中,知道用戶端的地址以便於回訪。 **/typedef struct{SOCKET socket;SOCKADDR_STORAGE ClientAddr;}PER_HANDLE_DATA, *LPPER_HANDLE_DATA;// 定義全域變數const int DefaultPort = 6000;vector < PER_HANDLE_DATA* > clientGroup;// 記錄用戶端的向量組HANDLE hMutex = CreateMutex(NULL, FALSE, NULL);DWORD WINAPI ServerWorkThread(LPVOID CompletionPortID);DWORD WINAPI ServerSendThread(LPVOID IpParam);// 開始主函數int main(){// 載入socket動態連結程式庫WORD wVersionRequested = MAKEWORD(2, 2); // 請求2.2版本的WinSock庫WSADATA wsaData;// 接收Windows Socket的結構資訊DWORD err = WSAStartup(wVersionRequested, &wsaData);if (0 != err){// 檢查通訊端庫是否申請成功cerr << "Request Windows Socket Library Error!\n";system("pause");return -1;}if(LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2){// 檢查是否申請了所需版本的通訊端庫WSACleanup();cerr << "Request Windows Socket Version 2.2 Error!\n";system("pause");return -1;}// 建立IOCP的核心對象/** * 需要用到的函數的原型: * HANDLE WINAPI CreateIoCompletionPort( * __in HANDLE FileHandle,// 已經開啟的檔案控制代碼或者空控制代碼,一般是用戶端的控制代碼 * __in HANDLE ExistingCompletionPort,// 已經存在的IOCP控制代碼 * __in ULONG_PTR CompletionKey,// 完成鍵,包含了指定I/O完成包的指定檔案 * __in DWORD NumberOfConcurrentThreads // 真正並發同時執行最大線程數,一般推介是CPU核心數*2 * ); **/HANDLE completionPort = CreateIoCompletionPort( INVALID_HANDLE_VALUE, NULL, 0, 0);if (NULL == completionPort){// 建立IO核心對象失敗cerr << "CreateIoCompletionPort failed. Error:" << GetLastError() << endl;system("pause");return -1;}// 建立IOCP線程--線程裡面建立線程池// 確定處理器的核心數量SYSTEM_INFO mySysInfo;GetSystemInfo(&mySysInfo);// 基於處理器的核心數量建立線程for(DWORD i = 0; i < (mySysInfo.dwNumberOfProcessors * 2); ++i){// 建立伺服器工作器線程,並將完成連接埠傳遞到該線程HANDLE ThreadHandle = CreateThread(NULL, 0, ServerWorkThread, completionPort, 0, NULL);if(NULL == ThreadHandle){cerr << "Create Thread Handle failed. Error:" << GetLastError() << endl;system("pause");return -1;}CloseHandle(ThreadHandle);}// 建立流式通訊端SOCKET srvSocket = socket(AF_INET, SOCK_STREAM, 0);// 綁定SOCKET到本機SOCKADDR_IN srvAddr;srvAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);srvAddr.sin_family = AF_INET;srvAddr.sin_port = htons(DefaultPort);int bindResult = bind(srvSocket, (SOCKADDR*)&srvAddr, sizeof(SOCKADDR));if(SOCKET_ERROR == bindResult){cerr << "Bind failed. Error:" << GetLastError() << endl;system("pause");return -1;}// 將SOCKET設定為監聽模式int listenResult = listen(srvSocket, 10);if(SOCKET_ERROR == listenResult){cerr << "Listen failed. Error: " << GetLastError() << endl;system("pause");return -1;}// 開始處理IO資料cout << "本伺服器已準備就緒,正在等待用戶端的接入...\n";// 建立用於發送資料的線程HANDLE sendThread = CreateThread(NULL, 0, ServerSendThread, 0, 0, NULL);while(true){PER_HANDLE_DATA * PerHandleData = NULL;SOCKADDR_IN saRemote;int RemoteLen;SOCKET acceptSocket;// 接收串連,並分配完成端,這兒可以用AcceptEx()RemoteLen = sizeof(saRemote);acceptSocket = accept(srvSocket, (SOCKADDR*)&saRemote, &RemoteLen);if(SOCKET_ERROR == acceptSocket){// 接收用戶端失敗cerr << "Accept Socket Error: " << GetLastError() << endl;system("pause");return -1;}// 建立用來和通訊端關聯的單控制代碼資料資訊結構PerHandleData = (LPPER_HANDLE_DATA)GlobalAlloc(GPTR, sizeof(PER_HANDLE_DATA));// 在堆中為這個PerHandleData申請指定大小的記憶體PerHandleData -> socket = acceptSocket;memcpy (&PerHandleData -> ClientAddr, &saRemote, RemoteLen);clientGroup.push_back(PerHandleData);// 將單個用戶端資料指標放到用戶端組中// 將接受通訊端和完成連接埠關聯CreateIoCompletionPort((HANDLE)(PerHandleData -> socket), completionPort, (DWORD)PerHandleData, 0);// 開始在接受通訊端上處理I/O使用重疊I/O機制// 在建立的通訊端上投遞一個或多個非同步// WSARecv或WSASend請求,這些I/O請求完成後,工作者線程會為I/O請求提供服務// 單I/O操作資料(I/O重疊)LPPER_IO_OPERATION_DATA PerIoData = NULL;PerIoData = (LPPER_IO_OPERATION_DATA)GlobalAlloc(GPTR, sizeof(PER_IO_OPERATEION_DATA));ZeroMemory(&(PerIoData -> overlapped), sizeof(OVERLAPPED));PerIoData->databuff.len = 1024;PerIoData->databuff.buf = PerIoData->buffer;PerIoData->operationType = 0;// readDWORD RecvBytes;DWORD Flags = 0;WSARecv(PerHandleData->socket, &(PerIoData->databuff), 1, &RecvBytes, &Flags, &(PerIoData->overlapped), NULL);}system("pause");return 0;}// 開始服務背景工作執行緒函數DWORD WINAPI ServerWorkThread(LPVOID IpParam){HANDLE CompletionPort = (HANDLE)IpParam;DWORD BytesTransferred;LPOVERLAPPED IpOverlapped;LPPER_HANDLE_DATA PerHandleData = NULL;LPPER_IO_DATA PerIoData = NULL;DWORD RecvBytes;DWORD Flags = 0;BOOL bRet = false;while(true){bRet = GetQueuedCompletionStatus(CompletionPort, &BytesTransferred, (PULONG_PTR)&PerHandleData, (LPOVERLAPPED*)&IpOverlapped, INFINITE);if(bRet == 0){cerr << "GetQueuedCompletionStatus Error: " << GetLastError() << endl;return -1;}PerIoData = (LPPER_IO_DATA)CONTAINING_RECORD(IpOverlapped, PER_IO_DATA, overlapped);// 檢查在通訊端上是否有錯誤發生if(0 == BytesTransferred){closesocket(PerHandleData->socket);GlobalFree(PerHandleData);GlobalFree(PerIoData);continue;}// 開始資料處理,接收來自用戶端的資料WaitForSingleObject(hMutex,INFINITE);cout << "A Client says: " << PerIoData->databuff.buf << endl;ReleaseMutex(hMutex);// 為下一個重疊調用建立單I/O操作資料ZeroMemory(&(PerIoData->overlapped), sizeof(OVERLAPPED)); // 清空記憶體PerIoData->databuff.len = 1024;PerIoData->databuff.buf = PerIoData->buffer;PerIoData->operationType = 0;// readWSARecv(PerHandleData->socket, &(PerIoData->databuff), 1, &RecvBytes, &Flags, &(PerIoData->overlapped), NULL);}return 0;}// 發送資訊的線程執行函數DWORD WINAPI ServerSendThread(LPVOID IpParam){while(1){char talk[200];gets(talk);int len;for (len = 0; talk[len] != ‘\0‘; ++len){// 找出這個字元組的長度}talk[len] = ‘\n‘;talk[++len] = ‘\0‘;printf("I Say:");cout << talk;WaitForSingleObject(hMutex,INFINITE);for(int i = 0; i < clientGroup.size(); ++i){send(clientGroup[i]->socket, talk, 200, 0);// 發送資訊}ReleaseMutex(hMutex); }return 0;}
Windows 非阻塞或非同步 socket