CAsyncSocket在較低層次上封裝了WinSock API,預設情況下,使用該類創建的socket是非阻塞的socket,所有操作都會立即返回,如果沒有得到結果,返回WSAEWOULDBLOCK,表示是一個阻塞操作。
CSocket建立在CAsyncSocket的基礎上,是CAsyncSocket的派生類。也就是預設情況下使用該類創建的socket是非阻塞的socket,但是CSocket的網路I/O是阻塞的,它在完成任務之後才返回。CSocket的阻塞不是建立在“阻塞”socket的基礎上,而是在“非阻塞”socket上實現的阻塞操作,在阻塞期間,CSocket實現了本執行緒的訊息循環,因此,雖然是阻塞操作,但是並不影響訊息循環,即用戶仍然可以和程式互動。
CAsyncSocket
CAsyncSocket封裝了低層的WinSock API,其成員變數m_hSocket保存其對應的socket句柄。使用CAsyncSocket的方法如下:
首先,在堆或者棧中構造一個CAsyncSocket對象,例如:
CAsyncSocket sock;或者
CAsyncSocket *pSock = new CAsyncSocket;
其次,調用Create創建socket,例如:
使用預設參數創建一個面向連線的socket
sock.Create()
指定參數參數創建一個使用數據報的socket,本地連線埠為30
pSocket.Create(30, SOCK_DGRM);
其三,如果是客戶程式,使用Connect連線到遠地;如果是服務程式,使用Listen監聽遠地的連線請求。
其四,使用成員函式進行網路I/O。
最後,銷毀CAsyncSocket,析構函式調用Close成員函式關閉socket。
下面,分析CAsyncSocket的幾個函式,從中可以看到它是如何封裝低層的WinSock API,簡化有關操作的;還可以看到它是如何實現非阻塞的socket和非阻塞操作。
socket對象的創建和捆綁
(1)Create函式
首先,討論Create函式,分析socket句柄如何被創建並和CAsyncSocket對象關聯。Create的實現如下:
BOOL CAsyncSocket::Create(UINT nSocketPort, int nSocketType,
long lEvent, LPCTSTR lpszSocketAddress)
{
if (Socket(nSocketType, lEvent))
{
if (Bind(nSocketPort,lpszSocketAddress))
return TRUE;
int nResult = GetLastError();
Close();
WSASetLastError(nResult);
}
return FALSE;
}
其中:
參數1表示本socket的連線埠,預設是0,如果要創建數據報的socket,則必須指定一個連線埠號。
參數2表示本socket的類型,預設是SOCK_STREAM,表示面向連線類型。
參數3是禁止位,表示希望對本socket監測的事件,預設是FD_READ | FD_WRITE | FD_OOB | FD_ACCEPT | FD_CONNECT | FD_CLOSE。
參數4表示本socket的IP位址字元串,預設是NULL。
Create調用Socket函式創建一個socket,並把它捆綁在this所指對象上,監測指定的網路事件。參數2和3被傳遞給Socket函式,如果希望創建數據報的socket,不要使用預設參數,指定參數2是SOCK_DGRM。
如果上一步驟成功,則調用bind給新的socket分配連線埠和IP位址。
(2)Socket函式
接著,分析Socket函式,其實現如下:
BOOL CAsyncSocket::Socket(int nSocketType, long lEvent,
int nProtocolType, int nAddressFormat)
{
ASSERT(m_hSocket == INVALID_SOCKET);
m_hSocket = socket(nAddressFormat,nSocketType,nProtocolType);
if (m_hSocket != INVALID_SOCKET)
{
CAsyncSocket::AttachHandle(m_hSocket, this, FALSE);
return AsyncSelect(lEvent);
}
return FALSE;
}
其中:
參數1表示Socket類型,預設值是SOCK_STREAM。
參數2表示希望監測的網路事件,預設值同Create,指定了全部事件。
參數3表示使用的協定,預設是0。實際上,SOCK_STREAM類型的socket使用TCP協定,SOCK_DGRM的socket則使用UDP協定。
參數4表示地址族(地址格式),預設值是PF_INET(等同於AF_INET)。對於TCP/IP來說,協定族和地址族是同值的。
在socket沒有被創建之前,成員變數m_hSocket是一個無效的socket句柄。Socket函式把協定族、socket類型、使用的協定等信息傳遞給WinSock API函式socket,創建一個socket。如果創建成功,則把它捆綁在this所指對象。
(3)捆綁(Attatch)
捆綁過程類似於其他Windows對象,將在模組執行緒狀態的WinSock映射中添加一對新的映射:this所指對象和新創建的socket對象的映射。
另外,如果本模組執行緒狀態的“socket視窗”沒有創建,則創建一個,該視窗在異步操作時用來接收WinSock的通知訊息,視窗句柄保存到模組執行緒狀態的m_hSocketWindow變數中。函式AsyncSelect將指定該視窗為網路事件訊息的接收視窗。
函式AttachHandle的實現在此不列舉了。
(4)指定要監測的網路事件
在捆綁完成之後,調用AsyncSelect指定新創建的socket將監測的網路事件。AsyncSelect實現如下:
BOOL CAsyncSocket::AsyncSelect(long lEvent)
{
ASSERT(m_hSocket != INVALID_SOCKET);
_AFX_SOCK_THREAD_STATE* pState = _afxSockThreadState;
ASSERT(pState->m_hSocketWindow != NULL);
return WSAAsyncSelect(m_hSocket, pState->m_hSocketWindow,
WM_SOCKET_NOTIFY, lEvent) != SOCKET_ERROR;
}
函式參數lEvent表示希望監視的網路事件。
_ afxSockThreadState得到的是當前的模組執行緒狀態,m_ hSocketWindow是本模組在當前執行緒的“socket視窗”,指定監視m_hSocket的網路事件,如指定事件發生,給視窗m_hSocketWindow傳送WM_SOCKET_NOTIFY訊息。
被指定的網路事件對應的網路I/O將是異步操作,是非阻塞操作。例如:指定FR_READ導致Receive是一個異步操作,如果不能立即讀到數據,則返回一個錯誤WSAEWOULDBLOCK。在數據到達之後,WinSock通知視窗m_hSocketWindow,導致OnReceive被調用。
指定FR_WRITE導致Send是一個異步操作,即使數據沒有送出也返回一個錯誤WSAEWOULDBLOCK。在數據可以傳送之後,WinSock通知視窗m_hSocketWindow,導致OnSend被調用。
指定FR_CONNECT導致Connect是一個異步操作,還沒有連線上就返回錯誤信息WSAEWOULDBLOCK,在連線完成之後,WinSock通知視窗m_hSocketWindow,導致OnConnect被調用。
對於其他網路事件,就不一一解釋了。
所以,使用CAsyncSocket時,如果使用Create預設創建socket,則所有網路I/O都是異步操作,進行有關網路I/O時則必須覆蓋以下的相關函式:
OnAccept、OnClose、OnConnect、OnOutOfBandData、OnReceive、OnSend。
(5)Bind函式
經過上述過程,socket創建完畢,下面,調用Bind函式給m_hSocket指定本地連線埠和IP位址。Bind的實現如下:
BOOL CAsyncSocket::Bind(UINT nSocketPort, LPCTSTR lpszSocketAddress)
{
USES_CONVERSION;
//使用WinSock的地址結構構造地址信息
SOCKADDR_IN sockAddr;
memset(&sockAddr,0,sizeof(sockAddr));
//得到地址參數的值
LPSTR lpszAscii = T2A((LPTSTR)lpszSocketAddress);
//指定是Internet地址類型
sockAddr.sin_family = AF_INET;
if (lpszAscii == NULL)
//沒有指定地址,則自動得到一個本地IP位址
//把32比特的數據從主機位元組序轉換成網路位元組序
sockAddr.sin_addr.s_addr = htonl(INADDR_ANY);
else
{
//得到地址
DWORD lResult = inet_addr(lpszAscii);
if (lResult == INADDR_NONE)
{
WSASetLastError(WSAEINVAL);
return FALSE;
}
sockAddr.sin_addr.s_addr = lResult;
}
//如果連線埠為0,則WinSock分配一個連線埠(1024—5000)
//把16比特的數據從主機位元組序轉換成網路位元組序
sockAddr.sin_port = htons((u_short)nSocketPort);
//Bind調用WinSock API函式bind
return Bind((SOCKADDR*)&sockAddr, sizeof(sockAddr));
}
其中:函式參數1指定了連線埠;參數2指定了一個包含本地地址的字元串,預設是NULL。
函式Bind首先使用結構SOCKADDR_IN構造地址信息。該結構的域sin_family表示地址格式(TCP/IP同協定族),賦值為AF_INET(Internet地址格式);域sin_port表示連線埠,如果參數1為0,則WinSock分配一個連線埠給它,範圍在1024和5000之間;域sin_addr是表示地址信息,它是一個聯合體,其中s_addr表示如下形式的字元串,“28.56.22.8”。如果參數沒有指定地址,則WinSock自動地得到本地IP位址(如果有幾個網卡,則使用其中一個的地址)。
(6)總結Create的過程
首先,調用socket函式創建一個socket;然後把創建的socket對象映射到CAsyncSocket對象(捆綁在一起),指定本socket要通知的網路事件,並創建一個“socket視窗”來接收網路事件訊息,最後,指定socket的本地信息。
下一步,是使用成員函式Connect連線遠地主機,配置socket的遠地信息。函式Connect類似於Bind,把指定的遠地地址轉換成SOCKADDR_IN對象表示的地址信息(包括網路位元組序的轉換),然後調用WinSock函式Connect連線遠地主機,配置socket的遠地連線埠和遠地IP位址。