1. 程式人生 > >windows下多路複用IO(select,WSAAsyncSelect,WSAEventSelect)

windows下多路複用IO(select,WSAAsyncSelect,WSAEventSelect)

Winsock提供的程式設計介面中socket預設是阻塞的,比如send,recv,connect,可以通過ioctlsocket進行設定非阻塞,server端要管理多個連線可能不是一件容易的事,windows下提供了不少模型可供使用,比如標題的三個,然後完成埠,libevent等庫,此文僅寫標題的三個,另外兩個單獨寫。先看MSDN的介紹
MSDN:
The WSAEventSelect function specifies an event object to be associated with the specified set of FD_XXX network events.
The return value is zero if the application's specification of the network events and the associated event object was successful.

The WSAAsyncSelect function requests Windows message-based notification of network events for a socket.
If the WSAAsyncSelect function succeeds, the return value is zero provided the application's declaration of interest in the network event set was successful. 

The select function determines the status of one or more sockets, waiting if necessary, to perform synchronous I/O.
The select function returns the total number of socket handles that are ready and contained in the fd_set structures, zero if the time limit expired, or SOCKET_ERROR if an error occurred

我個人理解是select需要指定一個集合,把所有需要監聽的socket新增到這個集合中,再輪詢集合,select函式返回後刪除沒有自己所關心事件的socket,然後對有用的socket進行處理。所以缺點就是如果集合內的socket數過多,則效率會直線下降。WSAAsyncSelect是最簡單的一個,但需要建立一個視窗,對於每個socket呼叫WSAAsyncSelect後即為這個socket綁定了一個訊息,當有關心的事件發生就會給這個視窗傳送訊息,然後WPARAM為訊息的控制代碼,LPARAM通過WSAGETSELECTEVENT與 WSAGETSELECTERROR兩個巨集定義可以獲取到事件以及錯誤,然後就像處理一般訊息一樣處理socket的網路訊息。WSAEventSelect與WSAAsyncSelect類似,但是它不需要建立視窗,它對事件的繫結是在一個核心物件上,然後使用一個輔助函式來得到核心物件上發生的網路事件,然後就跟WSAAsyncSelect處理一樣了。WSAAsyncSelect與WSAEventSelect都會自動將socket設定為非阻塞,select不會自動設定。分別看他們的工作流程

分別記錄下每個模型中的注意點:
select:
1.當使用int nFds = select(0, &readSocketFd, &writeSocketFd, NULL, NULL); 當正確呼叫時nFds為當前有多少個socket上有網路事件,加入一個socket上有兩個網路事件,返回值為1,不是2
2.select呼叫後比如有可讀事件,此時readSocketFd中只會保留那個有可讀事件的socket,其他項都會被刪除。
3.select本身不會把socket設為非阻塞模式,所以還是有可能發生阻塞
WSAAsyncSelect:
1.每次未讀完緩衝區的recv()呼叫,都會重新觸發一個FD_READ訊息,所以如果需要迴圈讀取的話則需要先關閉對FD_READ的監聽
2.FD_WRITE的觸發條件(這個應該在其他模型應該也是適用的)

  • 套接字剛建立連線時,表明準備就緒可以立即傳送資料。
  • 一次失敗的send()呼叫後緩衝區再次可用時。如果系統緩衝區已經被填滿,那麼此時呼叫send()傳送資料,將返回SOCKET_ERROR,使用WSAGetLastError()會得到錯誤碼WSAEWOULDBLOCK表明被阻塞。這種情況下當緩衝區重新整理出可用空間後,會嚮應用程式傳送FD_WRITE訊息

     所以說如果需要傳送訊息,直接呼叫send()傳送即可。如果該次呼叫返回值為SOCKET_ERROR且WSAGetLastError()得到錯誤碼WSAEWOULDBLOCK, 這意味著緩衝區已滿暫時無法傳送,此刻需要將待發資料儲存起來,等到系統發出FD_WRITE訊息後嘗試重新發送。  

WSAEventSelect
1.通過 WSACreateEvent();建立的物件預設為要手動重置為非訊號態

下面是記錄下一些socket程式設計時的一些經驗:

  1. 對於非阻塞socket,當發生WSAEWOULDBLOCK之後此時的操作無效,比如傳送資料,其實沒有傳送出去
  2. 對於send函式,有可能想要傳送的長度為10k,但實際只發了7k,此時返回值為7k,那剩下的3k還需要自己重新發送,所以可以自己封裝一個函式
    bool CClient::SendData(char *data, int len, bool isFile)
    {
    	int totalLen = len;
    	int sendLen = 0;
    	int haveSendLen = 0;
    	while (haveSendLen != len)
    	{
    		//send返回值大於0,則為傳送的長度,=0則為關閉了sokcet, 小於0則為異常
    		sendLen = send(m_socket, data + haveSendLen, totalLen, 0);
    		if (sendLen>0)
    		{
    			haveSendLen += sendLen;
    			//此處用於客戶端接收不全的情況,比如傳送10k,但只接收了3k,則sendLen返回3k
    			totalLen -= sendLen;
    		}
    		else if (sendLen == 0)
    		{
    			//關閉了Socket
    			::closesocket(m_socket);
    		}
    		else
    		{
    			if (WSAGetLastError() == WSAEWOULDBLOCK)
    			{
    				sendLen = 0;
    			}
    			else
    			{
    				AddLog("SendData error");
    				return false;
    			}
    		}
    
    	}
    	return true;
    }
    
  3. 緩衝區預設大小為8k,最大可設定64k
  4. 對於檔案的分包傳送(假設服務端傳送給客戶端檔案),可先發送一個數據包頭,裡面包含了檔案大小等其他檔案資訊,然後等待客戶端返回自己已經收到了這個資料包,然後正式開始傳送檔案時肯定需要把檔案分成很多個小份來發送,可每次傳送這個小份前先發送一個類似的資料包給客戶端,表明此次的包的大小,客戶端收到後確認一個應答包,服務端收到後開始正式傳送第一個檔案包,客戶端不斷接收,發現與資料包中給出的長度相同,則傳送確認資訊給服務端,然後服務端再發送下一個資料包頭,表明此次檔案大小,客戶端再確認收到資料包,等待接收。。。。重複以上動作,直到全部發送完成。
  5. 如果在傳送資料的過程中(send()沒有完成,還有資料沒傳送)而呼叫了closesocket(),以前我們一般採取的措施是"從容關閉"shutdown(s,SD_BOTH),但是資料是肯定丟失了,如何設定讓程式滿足具體應用的要求(即讓沒發完的資料傳送出去後在關閉socket)?
    struct linger {
        u_short    l_onoff;
        u_short    l_linger;
    };
    linger m_sLinger;
    m_sLinger.l_onoff=1;//(在closesocket()呼叫,但是還有資料沒傳送完畢的時候容許逗留)
    // 如果m_sLinger.l_onoff=0;則功能和2.)作用相同;
    m_sLinger.l_linger=5;//(容許逗留的時間為5秒)
    setsockopt(s,SOL_SOCKET,SO_LINGER,(const char*)&m_sLinger,sizeof(linger))
     

其中WSAAsyncSelect使用了MFC視窗程式,客戶端用的select,實現的檔案傳輸