1. 程式人生 > >Windows socket之Select模型開發

Windows socket之Select模型開發

                                       Windows socket select模型開發。

      套接字select模型是一種比較常用的IO模型。利用該模型可以使Windows socket應用程式可以同時管理多個套接字。

     使用select模型,可以使當執行操作的套接字滿足可讀可寫條件時,給應用程式傳送通知。收到這個通知後,應用程式再去呼叫相應的Windows socket API去執行函式呼叫。

     Select模型的核心是select函式。呼叫select函式檢查當前各個套接字的狀態。根據函式的返回值判斷套接字的可讀可寫性。然後呼叫相應的

Windows Sockets API完成資料的傳送、接收等。

阻塞模式和非阻塞模式的優點和不足:

     阻塞模式套接字執行IO操作時,如果執行操作的條件未滿足,執行緒就會阻塞在呼叫的函式上。程式不得不處於等待狀態,但是由於並不知道客戶請求何時到來,因此函式在何時返回不得而知。

    非阻塞模式套接字執行IO操作時,在任何時候函式都會立即返回。但程式設計師必須為此編寫更多的程式碼。這增加了開發Windows socket應用程式的難度。另外由於不斷的迴圈呼叫導致程式效率很低。

     Select模型是Windows sockets中最常見的IO模型。它利用select函式實現

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函式時,readfdswritefdsexceptfds三個引數至少有一個為非空。並且在該非空的引數中,必須至少包含一個套接字。否則select函式將沒有任何套接字可以等待。

timeval結構體用於定義select的等待時間。

structure timeval
{
   long tv_sec;//秒。
    long tv_usec;//毫秒。
};


    當timeval為空指標時,select會一直等待,直到有符合條件的套接字時才返回。

    當tv_sectv_usec之和為0時,無論是否有符合條件的套接字,select都會立即返回。

    當tv_sectv_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山西大同