今天,將最後一個流模型例子給記錄一下,代碼同樣來自於網上。由於一些原因,導致心情不是很好,還是按照既定計劃,將該demo的筆記記錄一下。源碼地址:地址。
它是基於 windows的iocp完成的,所以是異步非阻塞io。 最近看了很多的關於io說明的,各種帖子看的我頭大,始終還是沒徹底搞懂,估計是沒接觸過太多,境界還沒到。但是毫無疑問的是,windows的iocp是屬於異步非阻塞io的。
代碼結構:
服務端流程:
#include "main.h"
int main(){
CServerSocket* serverSocket = new CServerSocket();
serverSocket->prepareEnvironment();
delete serverSocket;
}
之前是想將裏面的關鍵方法進行抽象來着,但是後面精力問題,就直接放到一個方法裏面的了。
服務端關鍵代碼:
......
//創建 IOCP的內核對象
//參數說明:已經打開的文件句柄或者空句柄(一般是客戶端的句柄),已經存在的IOCP句柄,完成鍵,真正併發同時執行最大線程數
HANDLE completionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,0);
......
//基於處理器的核心數量創建線程
for(DWORD i=0;i<(mySysInfo.dwNumberOfProcessors*2);++i){
//創建服務器工作線程,並將完成端口傳遞到該線程
HANDLE threadHandle = CreateThread(NULL,0,ServerWorkThread,completionPort,0,NULL);
CloseHandle(threadHandle);
}
......
int bindResult = bind(srvSocket,(SOCKADDR*)&srvAddr, sizeof(SOCKADDR));
......
//將socket設置爲監聽模式
int listenResult = listen(srvSocket,10);
......
//創建用於發送數據的線程
HANDLE sendThread = CreateThread(NULL,0,ServerSendThread,0,0,NULL);
......
主要作用是: 在程序啓動之初,根據cpu的數量,創建相應的信息接收線程,信息接收線程要與一個完成端口進行綁定。 另外,啓動一個信息發送線程,目前程序的實現是向所有連接的客戶端發送信息。 但是也很容易做到向特定的 套接字客戶端發送。
信息接收線程的核心代碼如下:
while(true){
bRet = GetQueuedCompletionStatus(completionPort, &bytesTransferred, (PULONG_PTR)&perHandleData, (LPOVERLAPPED*)&ipOverLapped, INFINITE);
LpperIOData perIoData=NULL;
perIoData = (LpperIOData)CONTAINING_RECORD(ipOverLapped,PerIOData,overlapped);
......
第二行中,是阻塞等待通知的。 它的通知機制是: 通過io完成端口,利用overlapped的結構。 其中,getQueuedCompletionStatus中,第一個參數爲完成端口,倒數第二個參數 爲完成端口攜帶消息的介質。倒數帶三個參數 相當於傳遞的消息類型,這裏爲包含socket的結構體。
此外要注意,重疊機制獲取完數據後,需要設置重疊狀態:
//爲下一個重疊調用 建立單 I/O 操作數據
perIoData = (LpperIOData)GlobalAlloc(GPTR, sizeof(LpperIOData));
ZeroMemory(&(perIoData->overlapped), sizeof(OVERLAPPED)); //清空內存
perIoData->databuff.len= 1024;
perIoData->databuff.buf=perIoData->buffer;
perIoData->operationType=0;//read;
WSARecv(perHandleData->socket, &(perIoData->databuff), 1, (&recvBytes), &flags, &(perIoData->overlapped), NULL);
代碼中的順序是不能亂序的,在我第一次寫這個代碼的時候,當時對c++指針,內存分配啥的還不是很清楚,結果導致這個項目中原來的代碼運行不成功。 之後各種嘗試,最後試成功了。 同時,當時遇到一個奇怪的現象,那就是運行是正常的,但是調試偶爾會出錯。 現在想來,可能跟完成端口有關係。
我本地的cpu核心數是4核,所有在當前例子中,開了八個信息接收線程,這八個線程會同時阻塞在完成端口。根據我完成端口狀態,一次只能允許一個併發,所以當完成端口有信號時,只會觸發一個線程響應。 這個是可以控制的。
接着看看信息發送線程,比較簡單:
//發送信息的線程執行函數
DWORD WINAPI ServerSendThread(LPVOID ipParam){
while (1){
char talk[200];
gets(talk);
if (talk ==""){
return 0;
}
WaitForSingleObject(hMutex,INFINITE);
for(int i=0;i<clientGroup.size();++i){
send(clientGroup[i]->socket,talk,200,0);
}
ReleaseMutex(hMutex);
}
return 0;
}
以上的信息接收時的打印,和 信息輸入時的控制檯錄入,我最開始使用的時 std::cout,於std::cin的標準c++庫函數。 但是經測試,並不能正確獲取值。 當時網上查找說的是 ,它們不是線程安全的原因。 但是我現在想了下,可能恰恰因爲它們是線程安全的才導致這樣的問題。 控制檯對一個程序來說,資源是唯一的,由於該資源被其它資源佔用,所有導致當前使用線程不能正確的得到想要的結果。
客戶端的代碼也比較簡單:
int main(){
CClientSocket* clientSocket= new CClientSocket();
clientSocket->prepareEnvironment();
delete clientSocket;
}
同樣的理由,並未對方法進行拆分。
HANDLE sendThread = CreateThread(NULL,0,SendMessageThread,NULL,0,NULL);
HANDLE receiveThread = CreateThread(NULL,0,ReceiveMessageThread,NULL,0,NULL);
此時客戶端採用了 讀寫均單獨開始一個線程,這樣的好處在於,可以拜託類似於 http應用層協議那種 一收一發的固定通信模式。 當前,它也可支持原來的通信模式,這主要看使用者意願。
發送線程代碼:
DWORD WINAPI SendMessageThread(LPVOID ipParam){
while (1){
string talk;
getline(cin,talk);
WaitForSingleObject(bufferMutex,INFINITE);
if("quit"==talk){
talk.push_back('\0');
send(clientSocket,talk.c_str(),200,0);
break;
}
std::cout<<"\nI Say:(\"quit\" to exit):"<<talk<<"\n";
send(clientSocket,talk.c_str(),200,0);
ReleaseSemaphore(bufferMutex,1,NULL);
Sleep(5000);
}
return 0;
}
接收線程代碼:
DWORD WINAPI ReceiveMessageThread(LPVOID ipParam){
while (1){
char recvBuf[300];
recv(clientSocket,recvBuf,200,0);
WaitForSingleObject(bufferMutex,INFINITE);
std::cout<<"Server Says:"<<recvBuf<<std::endl;
ReleaseSemaphore(bufferMutex,1,NULL);
if(recvBuf[0]=='\0'){
cout<<"接收線程關閉!";
break;
}
}
return 0;
}
注意到這兩個線程,實際上是對線程退出時機做了預設的。 這個層面來說,它實際上便相當於一個特殊意義上的應用層協議了。
運行效果: