Windows socket之Select模型開發
Windows socket select模型開發。
套接字select模型是一種比較常用的IO模型。利用該模型可以使Windows socket應用程式可以同時管理多個套接字。
使用select模型,可以使當執行操作的套接字滿足可讀可寫條件時,給應用程式傳送通知。收到這個通知後,應用程式再去呼叫相應的Windows socket API去執行函式呼叫。
Select模型的核心是select函式。呼叫select函式檢查當前各個套接字的狀態。根據函式的返回值判斷套接字的可讀可寫性。然後呼叫相應的
阻塞模式和非阻塞模式的優點和不足:
阻塞模式套接字執行IO操作時,如果執行操作的條件未滿足,執行緒就會阻塞在呼叫的函式上。程式不得不處於等待狀態,但是由於並不知道客戶請求何時到來,因此函式在何時返回不得而知。
非阻塞模式套接字執行IO操作時,在任何時候函式都會立即返回。但程式設計師必須為此編寫更多的程式碼。這增加了開發Windows socket應用程式的難度。另外由於不斷的迴圈呼叫導致程式效率很低。
Select模型是Windows sockets中最常見的IO模型。它利用select函式實現
如:在呼叫recv函式之前,先呼叫select函式,如果系統沒有可讀資料那麼select函式就會阻塞在這裡。當系統存在可讀或可寫資料時,select函式返回,就可以呼叫recv函式接收資料了。
可以看出使用select模型,需要兩次呼叫函式。第一次呼叫select函式第二次socket API。使用該模式的好處是:可以等待多個套接字。
select函式
int select (
Int nfds,//被忽略。傳入0即可。
fd_set *readfds,//可讀套接字集合。
fd_set *writefds,//可寫套接字集合。
fd_set *exceptfds,//錯誤套接字集合。
const struct timeval*timeout);//select函式等待時間。
該函式返回處於就緒態並且已經被包含在fd_set結構中的套接字總數。如果超時則返回0。
第一個引數nfds被忽略。
第二個引數readfds,可讀性套接字集合指標。
第三個引數writefds,可寫性套接字集合指標。
第四個引數exceptfds,檢查錯誤套接字集合指標。
第五個引數timeout,等待時間。
fd_set結構是一個結構體。
typedef struct fd_set
{
u_int fd_count;
socket fd_array[FD_SETSIZE];
}fd_set;
fd_cout表示該集合套接字數量。最大為64.
fd_array套接字陣列。
select函式中需要三個fd_set結構:
一:準備接收資料的套接字集合,即可讀性集合。
二:準備傳送資料的套接字集合,即可寫性集合。
在select函式返回時,會在fd_set結構中,填入相應的套接字。
readfds陣列將包括滿足以下條件的套接字:
1:有資料可讀。此時在此套接字上呼叫recv,立即收到對方的資料。
2:連線已經關閉、重設或終止。
3:正在請求建立連線的套接字。此時呼叫accept函式會成功。
writefds陣列包含滿足下列條件的套接字:
1:有資料可以發出。此時在此套接字上呼叫send,可以向對方傳送資料。
2:呼叫connect函式,並連線成功的套接字。
exceptfds陣列將包括滿足下列條件的套接字:
1:呼叫connection函式,但連線失敗的套接字。
2:有帶外(out of band)資料可讀。
select函式的使用:
在呼叫select函式對套接字進行監視之前,必須將要監視的套接字分配給上述三個陣列中的一個。然後呼叫select函式,再次判斷需要監視的套接字是否還在原來的集合中。就可以知道該集合是否正在發生IO操作。
例如:應用程式想要判斷某個套接字是否存在可讀的資料,需要進行如下步驟:
1:將該套接字加入到readfds集合。
2:以readfds作為第二個引數呼叫select函式。
3:當select函式返回時,應用程式判斷該套接字是否仍然存在於readfds集合。
4:如果該套接字存在與readfds集合,則表明該套接字可讀。此時就可以呼叫recv函式接收資料。否則,該套接字不可讀。
在呼叫select函式時,readfds、writefds和exceptfds三個引數至少有一個為非空。並且在該非空的引數中,必須至少包含一個套接字。否則select函式將沒有任何套接字可以等待。
timeval結構體用於定義select的等待時間。
structure timeval
{
long tv_sec;//秒。
long tv_usec;//毫秒。
};
當timeval為空指標時,select會一直等待,直到有符合條件的套接字時才返回。
當tv_sec和tv_usec之和為0時,無論是否有符合條件的套接字,select都會立即返回。
當tv_sec和tv_usec之和為非0時,如果在等待的時間內有套接字滿足條件,則該函式將返回符合條件的套接字。如果在等待的時間內沒有套接字滿足設定的條件,則select會在時間用完時返回,並且返回值為0。
為了方便使用,windows sockets提供了下列巨集,用來對fd_set進行一系列操作。使用以下巨集可以使程式設計工作簡化。
FD_CLR(s,*set);從set集合中刪除s套接字。
FD_ISSET(s,*set);檢查s是否為set集合的成員。
FD_SET(s,*set);將套接字加入到set集合中。
FD_ZERO(*set);將set集合初始化為空集合。
在開發Windows sockets應用程式時,通過下面的步驟,可以完成對套接字的可讀寫判斷:
1:使用FD_ZERO初始化套接字集合。如FD_ZERO(&readfds);
2:使用FD_SET將某套接字放到readfds內。如:
FD_SET(s,&readfds);
3:以readfds為第二個引數呼叫select函式。select在返回時會返回所有fd_set集合中套接字的總個數,並對每個集合進行相應的更新。將滿足條件的套接字放在相應的集合中。
4:使用FD_ISSET判斷s是否還在某個集合中。如:
FD_ISSET(s,&readfds);
5:呼叫相應的Windows socket api 對某套接字進行操作。
select返回後會修改每個fd_set結構。刪除不存在的或沒有完成IO操作的套接字。這也正是在第四步中可以使用FD_ISSET來判斷一個套接字是否仍在集合中的原因。
看例子,該例演示了一個伺服器程式使用select模型管理套接字。
SOCKET listenSocket;
SOCKET acceptSocket;
FD_SET socketSet;
FD_SET writeSet;
FD_SET readSet;
FD_ZERO(&socketSet);
FD_SET(listenSocket,&socketSet);
while(true)
{
FD_ZERO(&readSet);
FD_ZERO(&writeSet);
readSet=socketSet;
writeSet=socketSet;
//同時檢查套接字的可讀可寫性。
int ret=select(0,&readSet,&writeSet,NULL,NULL);//為等待時間傳入NULL,則永久等待。傳入0立即返回。不要勿用。
if(ret==SOCKET_ERROR)
{
return false;
}
sockaddr_in addr;
int len=sizeof(addr);
//是否存在客戶端的連線請求。
if(FD_ISSET(listenSocket,&readSet))//在readset中會返回已經呼叫過listen的套接字。
{
acceptSocket=accept(listenSocket,(sockaddr*)&addr,&len);
if(acceptSocket==INVALID_SOCKET)
{
return false;
}
else
{
FD_SET(acceptSocket,&socketSet);
}
}
for(int i=0;i<socketSet.fd_count;i++)
{
if(FD_ISSET(socketSet.fd_array[i],&readSet))
{
//呼叫recv,接收資料。
}
if(FD_ISSET(socketSet.fd_array[i]),&writeSet)
{
//呼叫send,傳送資料。
}
}
}
以下展示了一個客戶端程式使用select模型的用法。注意與伺服器用法相區別。主要區別就是不可能有請求進來,也就不需要使用allsocketfds。僅僅對一個套接字進行判斷:
CRemoteFileDownloadClientDlg*pdlg=(CRemoteFileDownloadClientDlg*)ppram;
FD_SET readfds;
FD_SET writefds;
while(pdlg->m_IsConnected)
{
FD_ZERO(&readfds);
FD_ZERO(&writefds);
FD_SET(pdlg->m_ServerSocket,&readfds);
FD_SET(pdlg->m_ServerSocket,&writefds);
int ret=select(0,&readfds,&writefds,NULL,NULL);//NULL為無限等待。0立即返回。
if(ret>0)
{
if(FD_ISSET(pdlg->m_ServerSocket,&readfds));//注意與伺服器此處寫法相區別。
{
pdlg->recvData();
}
if(FD_ISSET(pdlg->m_ServerSocket,&writefds))
{
//可寫。
pdlg->sendData();
}
}
}
本文參考自《精通Windows sockets網路開發--基於Visual C++實現》孫海民著
2012.12.21 14:59山西大同