1.選擇(select)模型:
選擇模型:經過一個fd_set集合管理套接字,在知足套接字需求後,通知套接字。讓套接字進行工做。html
選擇模型的核心是FD_SET集合和select函數。經過該函數,咱們能夠們判斷套接字上是否存在數據,或者可否向一個套接字寫入數據。ios
用途:若是咱們想接受多個SOCKET的數據,該怎麼處理呢?編程
因爲當前socket是阻塞的,直接處理是必定完成不了要求的windows
a.咱們會想到多線程,的確能夠解決線程的阻塞問題,但開闢大量的線程並非什麼好的選擇; 數組
b咱們能夠想到用ioctlsocket()函數把socket設置成非阻塞的,而後用循環逐個socket查看當前套接字是否有數據,輪詢進行。服務器
這種是能夠解決問題的,可是會致使頻繁切換狀態到內核去查看是否有數據到達,浪費時間。網絡
c.因而想辦法用只切換一次狀態就知道全部socket的接受緩衝區是否有數據,因而有了select模型,select是阻塞的,Select的好處是能夠同時處理若干個Socket,多線程
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,若是是就應該重讀,通常是用循環。若是你不是必定要用非阻塞就不要設成這樣,這就是爲何系統的默認模式是阻塞。
經過完善select模型能夠獲得IO複用模型,詳情請看:http://www.cnblogs.com/curo0119/p/8461520.html
一個IO模型的阻塞非阻塞指的是數據訪問過程,而不是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並用fd_zero宏初始化爲空
(2)用FD_SET宏,把套接字句柄加入到fd_set集合
(3)調用select函數,檢查每一個套接字的可讀可寫性,select完成後,會返回全部在fd_set集合中有數據到達的socket的socket句柄總數,並對每一個集合進行更新,即沒有數據到達的socket在原集合中會被置成空。
(4)根據select的返回值以及FD_ISSET宏,對FD_SET集合進行檢查
(5)知道了每一個集合中「待決」的I/O操做後,對相應I/O操做進行處理,返回步驟1,繼續select
select函數返回後,會修改FD_SET的結構,刪除不存在待決IO操做的套接字,這也就是爲何咱們以後要用FD_ISSET判斷是否還在集合中的緣由。
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是一個空指針,那麼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優缺點:
優勢:可實現單線程處理多個任務
缺點:
a.等待數據到達的過程以及將數據從內核拷貝到用戶的過程總也存在必定阻塞
b.管理的set數組有必定上限,最可能是64個(可經過重置fd_setsize將上限擴大到1024)
c.select低效是由於每次它都須要輪詢。
完整代碼參考:
#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)) {
//若是在集合中,向下進行相應的IO操做 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;
//i--是由於要從新判斷新的i的位置的socket是否失效 } } 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會陷入死循環。