標籤:getc send 模型 解釋 鎖定 eva 成員 事件 為什麼
1.選擇(select)模型:
選擇模型:通過一個fd_set集合管理通訊端,在滿足通訊端需求後,通知通訊端。讓通訊端進行工作。避免通訊端進入阻塞模式,進行無謂的等待。選擇模型的核心的FD_SET集合和select函數。通過該函數,我們可以們判斷通訊端上是否存在資料,或者能否向一個通訊端寫入資料。
用途:如果我們想接受多個SOCKET的資料,該怎麼處理呢?
由於當前socket是阻塞的,直接處理是一定完成不了要求的
a.我們會想到多線程,的確可以解決線程的阻塞問題,但開闢大量的線程並不是什麼好的選擇;
b我們可以想到用ioctlsocket()函數把socket設定成非阻塞的,然後用迴圈逐個socket查看當前通訊端是否有資料,輪詢進行。
這種是可以解決問題的,但是會導致頻繁切換狀態到核心去查看是否有資料到達,浪費時間。
c.於是想辦法用只切換一次狀態就知道所有socket的接受緩衝區是否有資料,於是有了select模型
2.select函數:
int select(
int nfds,//忽略,只是為了保持與早期的Berkeley通訊端應用程式的相容
fd_set FAR* readfds,//可讀性檢查(有資料可讀入,串連關閉,重設,終止)
fd_set FAR* writefds,//可寫性檢查(有資料可發出)
fd+set FAR* exceptfds,//帶外資料檢查(帶外資料)
const struct timeval FAR* timeout//逾時
);
3.select模型的工作步驟:
(1)定義一個集合fd_set並初始化為空白
(2)把通訊端加入到fd_set集合
(3)檢查通訊端的可讀寫性
(4)檢查通訊端是否還在fd_set集合上
(5)處理資料
bool UDPNet::SelectSocket(){timeval tv;tv.tv_sec =0;tv.tv_usec = 100;fd_set fdsets;//建立集合FD_ZERO(&fdsets); //初始化集合FD_SET(m_socklisten,&fdsets);//將socket加入到集合中(此例子是一個socket),將多個socket加入時,可以用數組加for迴圈select(NULL,&fdsets,NULL,NULL,&tv);//只檢查可讀性,即fd_set中的fd_read進行操作if(!FD_ISSET(m_socklisten,&fdsets))//檢查 s是否s e t集合的一名成員;如答案是肯定的是,則返回 T R U E。{return false;}return true;}
4.select函數參數詳解:
三個 fd_set參數:一個用於檢查可讀性(readfds),一個用於檢查可寫性(writefds),另一個用於例外資料( excepfds)。
從根本上說,fdset資料類型代表著一系列特定通訊端的集合。其中,
readfds集合包括符合下述任何一個條件的通訊端:
■ 有資料可以讀入。
■ 串連已經關閉、重設或中止。
■ 假如已調用了listen,而且一個串連正在建立,那麼accept函數調用會成功。
writefds集合包括符合下述任何一個條件的通訊端:
■ 有資料可以發出。
■ 如果已完成了對一個非鎖定串連調用的處理,串連就會成功。
最後,exceptfds集合包括符合下述任何一個條件的通訊端:
■ 假如已完成了對一個非鎖定串連調用的處理,串連嘗試就會失敗。
■ 有帶外(out-of-band,OOB)資料可供讀取。
最後一個參數timeout:
對應的是一個指標,它指向一個timeval結構,用於決定select最多等待 I / O操作完成多久的時間。
如 timeout是一個null 指標,那麼select調用會無限期地“鎖定”或停頓下去,直到至少有一個描述符符合指定的條件後結束。
對timeval結構的定義如下:
struct timeval {
long tv_sec;
long tv_usec;
} ;
若將逾時值設定為(0,0),表明select會立即返回,允許應用程式對 select操作進行“輪詢”。出於對效能方面的考慮,應避免這樣的設定。
select成功完成後,會在 fd_set結構中,返回剛好有未完成的I/O操作的所有通訊端控制代碼的總量。
若超過timeval設定的時間,便會返回0。
如何測試一個通訊端是否“可讀”?
必須將自己的通訊端增添到readfds集合,再等待select函數完成。
select完成之後,必須判斷自己的通訊端是否仍為readfds集合的一部分。若答案是肯定的,便表明該通訊端“可讀”,可立即著手從它上面讀取資料。
在三個參數中(readfds、writedfss和exceptfds),任何兩個都可以是空值(NULL);但是,至少有一個不可為空值!在任何不為空白的集合中,必須包含至少一個通訊端控制代碼;
否則, select函數便沒有任何東西可以等待。
不管由於什麼原因,假如select調用失敗,都會返回SOCKET_ERROR
5.select
一個通訊端阻塞或者不阻塞,select就在那裡,它可以針對這2種通訊端使用,對任何一種通訊端的輪詢檢測,逾時時間都是有效,區別就在於:
當select完畢,認為該通訊端可讀時,
1 .阻塞的通訊端,會讓read阻塞,直到讀到所需要的所有位元組;
2 .非阻塞的通訊端,會讓read讀完fd中的資料後就返回,但如果原本你要求讀10個資料,這時唯讀了8個資料,如果你不再次使用select來判斷它是否可讀,而是直接read,很可能返回EAGAIN或=EWOULDBLOCK(BSD風格) ,
此錯誤由在非阻塞通訊端上不能立即完成的操作返回,例如,當通訊端上沒有排隊資料可讀時調用了recv()函數。此錯誤不是嚴重錯誤,相應操作應該稍後重試。對於在非阻塞 SOCK_STREAM通訊端上調用connect()函數來說,報告EWOULDBLOCK是正常的,因為建立一個串連必須花費一些時間。
EWOULDBLOCK的意思是如果你不把socket設成非阻塞(即阻塞)模式時,這個讀操作將阻塞,也就是說資料還未準備好(但系統知道資料來了,所以select告訴你那個socket可讀)。使用非阻塞模式做I/O操作的細心的人會檢查errno是不是EAGAIN、EWOULDBLOCK、EINTR,如果是就應該重讀,一般是用迴圈。如果你不是一定要用非阻塞就不要設成這樣,這就是為什麼系統的預設模式是阻塞。
完整代碼參考:
#include "stdafx.h"#include <WinSock2.h>#include <iostream>using namespace std;#include <stdio.h>#pragma comment(lib,"ws2_32.lib")#define PORT 8000#define MSGSIZE 255#define SRV_IP "127.0.0.1"int g_nSockConn = 0;//請求串連的數目//FD_SETSIZE是在winsocket2.h標頭檔裡定義的,這裡windows預設最大為64//在包含winsocket2.h標頭檔前使用宏定義可以修改這個值struct ClientInfo{ SOCKET sockClient; SOCKADDR_IN addrClient;};ClientInfo g_Client[FD_SETSIZE];DWORD WINAPI WorkThread(LPVOID lpParameter);int _tmain(int argc, _TCHAR* argv[]){//基本步驟就不解釋了,網路編程基礎那篇部落格裡講的很詳細了 WSADATA wsaData; WSAStartup(MAKEWORD(2,2),&wsaData); SOCKET sockListen = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); SOCKADDR_IN addrSrv; addrSrv.sin_addr.S_un.S_addr = inet_addr(SRV_IP); addrSrv.sin_family = AF_INET; addrSrv.sin_port = htons(PORT); bind(sockListen,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR)); listen(sockListen,64); DWORD dwThreadIDRecv = 0; DWORD dwThreadIDWrite = 0; HANDLE hand = CreateThread(NULL,0, WorkThread,NULL,0,&dwThreadIDRecv);//用來處理手法訊息的進程 if (hand == NULL) { cout<<"Create work thread failed\n"; getchar(); return -1; } SOCKET sockClient; SOCKADDR_IN addrClient; int nLenAddrClient = sizeof(SOCKADDR);//這裡用0初試化找了半天才找出錯誤 while (true) { sockClient = accept(sockListen,(SOCKADDR*)&addrClient,&nLenAddrClient);//第三個參數一定要按照addrClient大小初始化 //輸出串連者的地址資訊 //cout<<inet_ntoa(addrClient.sin_addr)<<":"<<ntohs(addrClient.sin_port)<<"has connect !"<<endl; if (sockClient != INVALID_SOCKET) { g_Client[g_nSockConn].addrClient = addrClient;//儲存串連端地址資訊 g_Client[g_nSockConn].sockClient = sockClient;//加入串連者隊列 g_nSockConn++; } } closesocket(sockListen); WSACleanup(); return 0;}DWORD WINAPI WorkThread(LPVOID lpParameter){ FD_SET fdRead; int nRet = 0;//記錄發送或者接受的位元組數 TIMEVAL tv;//設定逾時等待時間 tv.tv_sec = 1; tv.tv_usec = 0; char buf[MSGSIZE] = ""; while (true) { FD_ZERO(&fdRead); for (int i = 0;i < g_nSockConn;i++) { FD_SET(g_Client[i].sockClient,&fdRead); } //只處理read事件,不過後面還是會有讀寫訊息發送的 nRet = select(0,&fdRead,NULL,NULL,&tv); if (nRet == 0) {//沒有串連或者沒有讀事件 continue; } for (int i = 0;i < g_nSockConn;i++) { if (FD_ISSET(g_Client[i].sockClient,&fdRead)) { nRet = recv(g_Client[i].sockClient,buf,sizeof(buf),0); if (nRet == 0 || (nRet == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET)) { cout<<"Client "<<inet_ntoa(g_Client[i].addrClient.sin_addr)<<"closed"<<endl; closesocket(g_Client[i].sockClient); if (i < g_nSockConn-1) { //將失效的sockClient剔除,用數組的最後一個補上去 g_Client[i--].sockClient = g_Client[--g_nSockConn].sockClient; } } else { cout<<inet_ntoa(g_Client[i].addrClient.sin_addr)<<": "<<endl; cout<<buf<<endl; cout<<"Server:"<<endl; //gets(buf); strcpy(buf,"Hello!"); nRet = send(g_Client[i].sockClient,buf,strlen(buf)+1,0); } } } } return 0;}
伺服器的主要步驟:
1.建立監聽通訊端,綁定,監聽
2.建立工作者線程
3.建立一個通訊端組,用來存放當前所有活動的用戶端通訊端,沒accept一個串連就更新一次數組
4.接收用戶端的串連,因為沒有重新定義FD_SIZE宏,伺服器最多支援64個並發串連。最好是記錄下串連數,不要無條件的接受串連
背景工作執行緒
背景工作執行緒是一個死迴圈,依次迴圈完成的動作是:
1.將當前用戶端通訊端加入到fd_read集中
2.調用select函數
3.用FD_ISSET查看時候通訊端還在讀集中,如果是就接收資料。如果接收的資料長度為0,或者發生WSAECONNRESET錯誤,,則
表示用戶端通訊端主動關閉,我們要釋放這個通訊端資源,調整我們的通訊端數組(讓下一個補上)。上面還有個nRet==0的判斷,
就是因為select函數會立即返回,串連數為0會陷入死迴圈。
本文參考:http://blog.csdn.net/rheostat/article/details/9815725
windows下的IO模型之選擇(select)模型