這里不打算系統(tǒng)地介紹socket或者WinSock的知識。首先介紹WinSock API函數(shù),講解阻塞/非阻塞的概念;然后介紹socket的使用。
Socket接口是網(wǎng)絡編程(通常是TCP/IP協(xié)議,也可以是其他協(xié)議)的API。最早的Socket接口是Berkeley接口,在Unxi操作系統(tǒng)中實現(xiàn)。WinSock也是一個基于Socket模型的API,在Microsoft Windows操作系統(tǒng)類中使用。它在Berkeley接口函數(shù)的基礎之上,還增加了基于消息驅(qū)動機制的Windows擴展函數(shù)。Winscok1.1只支持TCP/IP網(wǎng)絡,WinSock2.0增加了對更多協(xié)議的支持。這里,討論TCP/IP網(wǎng)絡上的API。
Socket接口包括三類函數(shù):
第一類是WinSock API包含的Berkeley socket函數(shù)。這類函數(shù)分兩部分。第一部分是用于網(wǎng)絡I/O的函數(shù),如
accept、Closesocket、connect、recv、recvfrom、Select、Send、Sendto
另一部分是不涉及網(wǎng)絡I/O、在本地端完成的函數(shù),如
bind、getpeername、getsockname、getsocketopt、htonl、htons、inet_addr、inet_nton
ioctlsocket、listen、ntohl、ntohs、setsocketopt、shutdow、socket等
第二類是檢索有關域名、通信服務和協(xié)議等Internet信息的數(shù)據(jù)庫函數(shù),如
gethostbyaddr、gethostbyname、gethostname、getprotolbyname
getprotolbynumber、getserverbyname、getservbyport。
第三類是Berkekley socket例程的Windows專用的擴展函數(shù),如gethostbyname對應的WSAAsynGetHostByName(其他數(shù)據(jù)庫函數(shù)除了gethostname都有異步版本),select對應的WSAAsynSelect,判斷是否阻塞的函數(shù)WSAIsBlocking,得到上一次Windsock API錯誤信息的WSAGetLastError,等等。
從另外一個角度,這些函數(shù)又可以分為兩類,一是阻塞函數(shù),一是非阻塞函數(shù)。所謂阻塞函數(shù),是指其完成指定的任務之前不允許程序調(diào)用另一個函數(shù),在Windows下還會阻塞本線程消息的發(fā)送。所謂非阻塞函數(shù),是指操作啟動之后,如果可以立即得到結果就返回結果,否則返回表示結果需要等待的錯誤信息,不等待任務完成函數(shù)就返回。
首先,異步函數(shù)是非阻塞函數(shù);
其次,獲取遠地信息的數(shù)據(jù)庫函數(shù)是阻塞函數(shù)(因此,WinSock提供了其異步版本);
在Berkeley socket函數(shù)部分中,不涉及網(wǎng)絡I/O、本地端工作的函數(shù)是非阻塞函數(shù);
在Berkeley socket函數(shù)部分中,網(wǎng)絡I/O的函數(shù)是可阻塞函數(shù),也就是它們可以阻塞執(zhí)行,也可以不阻塞執(zhí)行。這些函數(shù)都使用了一個socket,如果它們使用的socket是阻塞的,則這些函數(shù)是阻塞函數(shù);如果它們使用的socket是非阻塞的,則這些函數(shù)是非阻塞函數(shù)。
創(chuàng)建一個socket時,可以指定它是否阻塞。在缺省情況下,Berkerley的Socket函數(shù)和WinSock都創(chuàng)建“阻塞”的socket。阻塞socket通過使用select函數(shù)或者WSAAsynSelect函數(shù)在指定操作下變成非阻塞的。WSAAsyncSelect函數(shù)原型如下。
int WSAAsyncSelect(
SOCKET s,
HWND hWnd,
u_int wMsg,
long lEvent
);
其中,參數(shù)1指定了要操作的socket句柄;參數(shù)2指定了一個窗口句柄;參數(shù)3指定了一個消息,參數(shù)4指定了網(wǎng)絡事件,可以是多個事件的組合,如:
FD_READ 準備讀
FD_WRITE 準備寫
FD_OOB 帶外數(shù)據(jù)到達
FD_ACCEPT 收到連接
FD_CONNECT 完成連接
FD_CLOSE 關閉socket。
用OR操作組合這些事件值,如FD_READ|FD_WRITE
WSAAsyncSelect函數(shù)表示對socket s監(jiān)測lEvent指定的網(wǎng)絡事件,如果有事件發(fā)生,則給窗口hWnd發(fā)送消息wMsg。
假定應用程序的一個socket s指定了監(jiān)測FD_READ事件,則在FD_READ事件上變成非阻塞的。當read函數(shù)被調(diào)用時,不管是否讀到數(shù)據(jù)都馬上返回,如果返回一個錯誤信息表示還在等待,則在等待的數(shù)據(jù)到達后,消息wMsg發(fā)送給窗口hWnd,應用程序處理該消息讀取網(wǎng)絡數(shù)據(jù)。
對于異步函數(shù)的調(diào)用,以類似的過程最終得到結果數(shù)據(jù)。以gethostbyname的異步版本的使用為例進行說明。該函數(shù)原型如下:
HANDLE WSAAsyncGetHostByName(
HWND hWnd,
u_int wMsg,
const char FAR *name,
char FAR *buf,
int buflen
);
在調(diào)用WSAAsyncGetHostByName啟動操作時,不僅指定主機名字name,還指定了一個窗口句柄hWnd,一個消息ID wMsg,一個緩沖區(qū)及其長度。如果不能立即得到主機地址,則返回一個錯誤信息表示還在等待。當要的數(shù)據(jù)到達時,WinSock DLL給窗口hWnd發(fā)送消息wMsg告知得到了主機地址,窗口過程從指定的緩沖區(qū)buf得到主機地址。
使用異步函數(shù)或者非阻塞的socket,主要是為了不阻塞本線程的執(zhí)行。在多進程或者多線程的情況下,可以使用兩個線程通過同步手段來完成異步函數(shù)或者非阻塞函數(shù)的功能。
WinSock以DLL的形式提供,在調(diào)用任何WinSock API之前,必須調(diào)用函數(shù)WSAStartup進行初始化,最后,調(diào)用函數(shù)WSACleanUp作清理工作。
MFC使用函數(shù)AfxSocketInit包裝了函數(shù)WSAStartup,在WinSock應用程序的初始化函數(shù)IninInstance中調(diào)用AfxSocketInit進行初始化。程序不必調(diào)用WSACleanUp。
Socket是網(wǎng)絡通信過程中端點的抽象表示。Socket在實現(xiàn)中以句柄的形式被創(chuàng)建,包含了進行網(wǎng)絡通信必須的五種信息:連接使用的協(xié)議,本地主機的IP地址,本地進程的協(xié)議端口,遠地主機的IP地址,遠地進程的協(xié)議端口。
要使用socket,首先必須創(chuàng)建一個socket;然后,按要求配置socket;接著,按要求通過socket接收和發(fā)送數(shù)據(jù);最后,程序關閉此socket。
socket_handle = socket(protocol_family. Socket_type, protocol);
其中:protocol_family指定socket使用的協(xié)議,取值PF_INET,表示Internet(TCP/IP)協(xié)議族;Socket_type指socket面向連接或者使用數(shù)據(jù)報;第三個參數(shù)表示使用TCP或者UDP協(xié)議。
當一個socket被創(chuàng)建時,WinSock將為一個內(nèi)部結構分配內(nèi)存,在此結構中保存此socket的信息,到此,socket連接使用的協(xié)議已經(jīng)確定。
對于面向連接的客戶,WinSock自動保存本地IP地址和選擇協(xié)議端口,但是必須使用connect函數(shù)配置遠地IP地址和遠地協(xié)議端口:
result = connect(socket_handle, remote_socket_address, address_length)
remote_socket_address是一個指向特定socket結構的指針,該地址結構為socket保存了地址族、協(xié)議端口、網(wǎng)絡主機地址。
面向連接的服務器則使用bind指定本地信息,使用listen和accept獲取遠地信息。
使用數(shù)據(jù)報的客戶或者服務器使用bind給socket指定本地信息,在發(fā)送或者接收數(shù)據(jù)時指定遠地信息。
bind給socket指定一個本地IP地址和協(xié)議端口,如下:
result = bind( socket_hndle, local_socket_address, address_length)
參數(shù)類型同connect。
函數(shù)listen監(jiān)聽bind指定的端口,如果有遠地客戶請求連接,使用accept接收請求,創(chuàng)建一個新的socket,并保存信息。
socket_new = accept(socket_listen, socket_address, address_length)
面向連接的socket使用send發(fā)送數(shù)據(jù),recv接收數(shù)據(jù);
使用數(shù)據(jù)報的socket使用sendto發(fā)送數(shù)據(jù),recvfrom接收數(shù)據(jù)。
MFC提供了兩個類CAsyncSocket和CSocket來封裝WinSock API,這給程序員提供了一個更簡單的網(wǎng)絡編程接口。
CAsyncSocket在較低層次上封裝了WinSock API,缺省情況下,使用該類創(chuàng)建的socket是非阻塞的socket,所有操作都會立即返回,如果沒有得到結果,返回WSAEWOULDBLOCK,表示是一個阻塞操作。
CSocket建立在CAsyncSocket的基礎上,是CAsyncSocket的派生類。也就是缺省情況下使用該類創(chuàng)建的socket是非阻塞的socket,但是CSocket的網(wǎng)絡I/O是阻塞的,它在完成任務之后才返回。CSocket的阻塞不是建立在“阻塞”socket的基礎上,而是在“非阻塞”socket上實現(xiàn)的阻塞操作,在阻塞期間,CSocket實現(xiàn)了本線程的消息循環(huán),因此,雖然是阻塞操作,但是并不影響消息循環(huán),即用戶仍然可以和程序交互。
CAsyncSocket封裝了低層的WinSock API,其成員變量m_hSocket保存其對應的socket句柄。使用CAsyncSocket的方法如下:
首先,在堆或者棧中構造一個CAsyncSocket對象,例如:
CAsyncSocket sock;或者
CAsyncSocket *pSock = new CAsyncSocket;
其次,調(diào)用Create創(chuàng)建socket,例如:
使用缺省參數(shù)創(chuàng)建一個面向連接的socket
sock.Create()
指定參數(shù)參數(shù)創(chuàng)建一個使用數(shù)據(jù)報的socket,本地端口為30
pSocket.Create(30, SOCK_DGRM);
其三,如果是客戶程序,使用Connect連接到遠地;如果是服務程序,使用Listen監(jiān)聽遠地的連接請求。
其四,使用成員函數(shù)進行網(wǎng)絡I/O。
最后,銷毀CAsyncSocket,析構函數(shù)調(diào)用Close成員函數(shù)關閉socket。
下面,分析CAsyncSocket的幾個函數(shù),從中可以看到它是如何封裝低層的WinSock API,簡化有關操作的;還可以看到它是如何實現(xiàn)非阻塞的socket和非阻塞操作。
(1)Create函數(shù)
首先,討論Create函數(shù),分析socket句柄如何被創(chuàng)建并和CAsyncSocket對象關聯(lián)。Create的實現(xiàn)如下:
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;
}
其中:
參數(shù)1表示本socket的端口,缺省是0,如果要創(chuàng)建數(shù)據(jù)報的socket,則必須指定一個端口號。
參數(shù)2表示本socket的類型,缺省是SOCK_STREAM,表示面向連接類型。
參數(shù)3是屏蔽位,表示希望對本socket監(jiān)測的事件,缺省是FD_READ | FD_WRITE | FD_OOB | FD_ACCEPT | FD_CONNECT | FD_CLOSE。
參數(shù)4表示本socket的IP地址字符串,缺省是NULL。
Create調(diào)用Socket函數(shù)創(chuàng)建一個socket,并把它捆綁在this所指對象上,監(jiān)測指定的網(wǎng)絡事件。參數(shù)2和3被傳遞給Socket函數(shù),如果希望創(chuàng)建數(shù)據(jù)報的socket,不要使用缺省參數(shù),指定參數(shù)2是SOCK_DGRM。
如果上一步驟成功,則調(diào)用bind給新的socket分配端口和IP地址。
(2)Socket函數(shù)
接著,分析Socket函數(shù),其實現(xiàn)如下:
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;
}
其中:
參數(shù)1表示Socket類型,缺省值是SOCK_STREAM。
參數(shù)2表示希望監(jiān)測的網(wǎng)絡事件,缺省值同Create,指定了全部事件。
參數(shù)3表示使用的協(xié)議,缺省是0。實際上,SOCK_STREAM類型的socket使用TCP協(xié)議,SOCK_DGRM的socket則使用UDP協(xié)議。
參數(shù)4表示地址族(地址格式),缺省值是PF_INET(等同于AF_INET)。對于TCP/IP來說,協(xié)議族和地址族是同值的。
在socket沒有被創(chuàng)建之前,成員變量m_hSocket是一個無效的socket句柄。Socket函數(shù)把協(xié)議族、socket類型、使用的協(xié)議等信息傳遞給WinSock API函數(shù)socket,創(chuàng)建一個socket。如果創(chuàng)建成功,則把它捆綁在this所指對象。
(3)捆綁(Attatch)
捆綁過程類似于其他Windows對象,將在模塊線程狀態(tài)的WinSock映射中添加一對新的映射:this所指對象和新創(chuàng)建的socket對象的映射。
另外,如果本模塊線程狀態(tài)的“socket窗口”沒有創(chuàng)建,則創(chuàng)建一個,該窗口在異步操作時用來接收WinSock的通知消息,窗口句柄保存到模塊線程狀態(tài)的m_hSocketWindow變量中。函數(shù)AsyncSelect將指定該窗口為網(wǎng)絡事件消息的接收窗口。
函數(shù)AttachHandle的實現(xiàn)在此不列舉了。
(4)指定要監(jiān)測的網(wǎng)絡事件
在捆綁完成之后,調(diào)用AsyncSelect指定新創(chuàng)建的socket將監(jiān)測的網(wǎng)絡事件。AsyncSelect實現(xiàn)如下:
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;
}
函數(shù)參數(shù)lEvent表示希望監(jiān)視的網(wǎng)絡事件。
_ afxSockThreadState得到的是當前的模塊線程狀態(tài),m_ hSocketWindow是本模塊在當前線程的“socket窗口”,指定監(jiān)視m_hSocket的網(wǎng)絡事件,如指定事件發(fā)生,給窗口m_hSocketWindow發(fā)送WM_SOCKET_NOTIFY消息。
被指定的網(wǎng)絡事件對應的網(wǎng)絡I/O將是異步操作,是非阻塞操作。例如:指定FR_READ導致Receive是一個異步操作,如果不能立即讀到數(shù)據(jù),則返回一個錯誤WSAEWOULDBLOCK。在數(shù)據(jù)到達之后,WinSock通知窗口m_hSocketWindow,導致OnReceive被調(diào)用。
指定FR_WRITE導致Send是一個異步操作,即使數(shù)據(jù)沒有送出也返回一個錯誤WSAEWOULDBLOCK。在數(shù)據(jù)可以發(fā)送之后,WinSock通知窗口m_hSocketWindow,導致OnSend被調(diào)用。
指定FR_CONNECT導致Connect是一個異步操作,還沒有連接上就返回錯誤信息WSAEWOULDBLOCK,在連接完成之后,WinSock通知窗口m_hSocketWindow,導致OnConnect被調(diào)用。
對于其他網(wǎng)絡事件,就不一一解釋了。
所以,使用CAsyncSocket時,如果使用Create缺省創(chuàng)建socket,則所有網(wǎng)絡I/O都是異步操作,進行有關網(wǎng)絡I/O時則必須覆蓋以下的相關函數(shù):
OnAccept、OnClose、OnConnect、OnOutOfBandData、OnReceive、OnSend。
(5)Bind函數(shù)
經(jīng)過上述過程,socket創(chuàng)建完畢,下面,調(diào)用Bind函數(shù)給m_hSocket指定本地端口和IP地址。Bind的實現(xiàn)如下:
BOOL CAsyncSocket::Bind(UINT nSocketPort, LPCTSTR lpszSocketAddress)
{
USES_CONVERSION;
//使用WinSock的地址結構構造地址信息
SOCKADDR_IN sockAddr;
memset(&sockAddr,0,sizeof(sockAddr));
//得到地址參數(shù)的值
LPSTR lpszAscii = T2A((LPTSTR)lpszSocketAddress);
//指定是Internet地址類型
sockAddr.sin_family = AF_INET;
if (lpszAscii == NULL)
//沒有指定地址,則自動得到一個本地IP地址
//把32比特的數(shù)據(jù)從主機字節(jié)序轉(zhuǎn)換成網(wǎng)絡字節(jié)序
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比特的數(shù)據(jù)從主機字節(jié)序轉(zhuǎn)換成網(wǎng)絡字節(jié)序
sockAddr.sin_port = htons((u_short)nSocketPort);
//Bind調(diào)用WinSock API函數(shù)bind
return Bind((SOCKADDR*)&sockAddr, sizeof(sockAddr));
}
其中:函數(shù)參數(shù)1指定了端口;參數(shù)2指定了一個包含本地地址的字符串,缺省是NULL。
函數(shù)Bind首先使用結構SOCKADDR_IN構造地址信息。該結構的域sin_family表示地址格式(TCP/IP同協(xié)議族),賦值為AF_INET(Internet地址格式);域sin_port表示端口,如果參數(shù)1為0,則WinSock分配一個端口給它,范圍在1024和5000之間;域sin_addr是表示地址信息,它是一個聯(lián)合體,其中s_addr表示如下形式的字符串,“28.56.22.8”。如果參數(shù)沒有指定地址,則WinSock自動地得到本地IP地址(如果有幾個網(wǎng)卡,則使用其中一個的地址)。
(6)總結Create的過程
首先,調(diào)用socket函數(shù)創(chuàng)建一個socket;然后把創(chuàng)建的socket對象映射到CAsyncSocket對象(捆綁在一起),指定本socket要通知的網(wǎng)絡事件,并創(chuàng)建一個“socket窗口”來接收網(wǎng)絡事件消息,最后,指定socket的本地信息。
下一步,是使用成員函數(shù)Connect連接遠地主機,配置socket的遠地信息。函數(shù)Connect類似于Bind,把指定的遠地地址轉(zhuǎn)換成SOCKADDR_IN對象表示的地址信息(包括網(wǎng)絡字節(jié)序的轉(zhuǎn)換),然后調(diào)用WinSock函數(shù)Connect連接遠地主機,配置socket的遠地端口和遠地IP地址。
當網(wǎng)絡事件發(fā)生時,“socket窗口”接收WM_SOCKET_NOTIFY消息,消息處理函數(shù)OnSocketNotify被調(diào)用。“socket窗口”的定義和消息處理是MFC實現(xiàn)的,這里不作詳細的討論。
OnSocketNotify回調(diào)CAsyncSocket的成員函數(shù)DoCallBack,DoCallBack調(diào)用事件處理函數(shù),如OnRead、OnWrite等。摘錄DoCallBack的一段代碼如下:
switch (WSAGETSELECTEVENT(lParam))
{
case FD_READ:
{
DWORD nBytes;
//得到可以一次讀取的字節(jié)數(shù)
pSocket->IOCtl(FIONREAD, &nBytes);
if (nBytes != 0)
pSocket->OnReceive(nErrorCode);
}
break;
case FD_WRITE:
pSocket->OnSend(nErrorCode);
break;
case FD_OOB:
pSocket->OnOutOfBandData(nErrorCode);
break;
case FD_ACCEPT:
pSocket->OnAccept(nErrorCode);
break;
case FD_CONNECT:
pSocket->OnConnect(nErrorCode);
break;
case FD_CLOSE:
pSocket->OnClose(nErrorCode);
break;
lParam是WM_SOCKET_NOFITY的消息參數(shù),OnSocketNotify傳遞給函數(shù)DoCallBack,表示通知事件。
函數(shù)IOCtl是CAsyncSocket的成員函數(shù),用來對socket的I/O進行控制。這里的使用表示本次調(diào)用Receive函數(shù)至多可以讀nBytes個字節(jié)。
從上面的討論可以看出,從創(chuàng)建socket到網(wǎng)絡I/O,CAsyncSocket直接封裝了低層的WinSock API,簡化了WinSock編程,實現(xiàn)了一個異步操作的界面。如果希望某個操作是阻塞操作,則在調(diào)用Create時不要指定該操作對應的網(wǎng)絡事件。例如,希望Connect和Send是阻塞操作,在任務完成之后才返回,則可以使用如下的語句:
pSocket->Create(0, SOCK_STREAM,
FR_WRITE|FR_OOB|FR_ACCEPT|FR_CLOSE);
這樣,在Connect和Send時,如果是用戶界面線程的話,可能阻塞線程消息循環(huán)。所以,最好在工作者線程中使用阻塞操作。
如果希望在用戶界面線程中使用阻塞socket,則可以使用CSocket。它在非阻塞socket基礎之上實現(xiàn)了阻塞操作,在阻塞期間實現(xiàn)了消息循環(huán)。
對于CSocket,處理網(wǎng)絡事件通知的函數(shù)OnAccept、OnClose、OnReceive仍然可以使用,OnConnect、OnSend在CSocket中永遠不會被調(diào)用,另外OnOutOfBandData在CSocket中不鼓勵使用。
CSocket對象在調(diào)用Connect、Send、Accept、Close、Receive等成員函數(shù)后,這些函數(shù)在完成任務之后(連接被建立、數(shù)據(jù)被發(fā)送、連接請求被接收、socket被關閉、數(shù)據(jù)被讀取)之后才會返回。因此,Connect和Send不會導致OnConnect和OnSend被調(diào)用。如果覆蓋虛擬函數(shù)OnReceive、OnAccept、OnClose,不主動調(diào)用Receive、Accept、Close,則在網(wǎng)絡事件到達之后導致對應的虛擬函數(shù)被調(diào)用,虛擬函數(shù)的實現(xiàn)應該調(diào)用Receive、Accept、Close來完成操作。下面,就一個函數(shù)Receive來考察CSocket如何實現(xiàn)阻塞操作和消息循環(huán)的。
int CSocket::Receive(void* lpBuf, int nBufLen, int nFlags)
{
//m_pbBlocking是CSocket的成員變量,用來標識當前是否正在進行
//阻塞操作。但不能同時進行兩個阻塞操作。
if (m_pbBlocking != NULL)
{
WSASetLastError(WSAEINPROGRESS);
return FALSE;
}
//完成數(shù)據(jù)讀取
int nResult;
while ((nResult = CAsyncSocket::Receive(lpBuf, nBufLen, nFlags))
== SOCKET_ERROR)
{
if (GetLastError() == WSAEWOULDBLOCK)
{
//進入消息循環(huán),等待網(wǎng)絡事件FD_READ
if (!PumpMessages(FD_READ))
return SOCKET_ERROR;
}
else
return SOCKET_ERROR;
}
return nResult;
}
其中:
參數(shù)1指定一個緩沖區(qū)保存讀取的數(shù)據(jù);參數(shù)2指定緩沖區(qū)的大??;參數(shù)3取值MSG_PEEK(數(shù)據(jù)拷貝到緩沖區(qū),但不從輸入隊列移走),或者MSG_OOB(處理帶外數(shù)據(jù)),或者MSG_PEEK|MSG_OOB。
Receive函數(shù)首先判斷當前CSocket對象是否正在處理一個阻塞操作,如果是,則返回錯誤WSAEINPROGRESS;否則,開始數(shù)據(jù)讀取的處理。
讀取數(shù)據(jù)時,如果基類CAsyncSocket的Receive讀取到了數(shù)據(jù),則返回;否則,如果返回一個錯誤,而且錯誤號是WSAEWOULDBLOCK,則表示操作阻塞,于是調(diào)用PumpMessage進入消息循環(huán)等待數(shù)據(jù)到達(網(wǎng)絡事件FD_READ發(fā)生)。數(shù)據(jù)到達之后退出消息循環(huán),再次調(diào)用CAsyncSocket的Receive讀取數(shù)據(jù),直到?jīng)]有數(shù)據(jù)可讀為止。
PumpMessages是CSocket的成員函數(shù),它完成以下工作:
(1)設置m_pbBlocking,表示進入阻塞操作。
(2)進行消息循環(huán),如果有以下事件發(fā)生則退出消息循環(huán):收到指定定時器的定時事件消息WM_TIMER,退出循環(huán),返回TRUE;收到發(fā)送給本socket的消息WM_SOCKET_NOTIFY,網(wǎng)絡事件FD_CLOSE或者等待的網(wǎng)絡事件發(fā)生,退出循環(huán),返回TRUE;發(fā)送錯誤或者收到WM_QUIT消息,退出循環(huán),返回FALSE;
(3)在消息循環(huán)中,把WM_SOCKET_DEAD消息和發(fā)送給其他socket的通知消息WM_SOCKET_NOFITY放進模塊線程狀態(tài)的通知消息列表m_listSocketNotifications,在阻塞操作完成之后處理;對其他消息,則把它們送給目的窗口的窗口過程處理。
MFC還提供了一個網(wǎng)絡編程模式,可以充分利用CSocket的特性。該模式的基礎是CSocketFile類。使用方法如下:
首先,構造一個CSocket對象;調(diào)用Create函數(shù)創(chuàng)建一個socket對象(SOCK_STREAM類型)。
接著,如果是客戶程序,調(diào)用Connect連接到遠地主機;如果是服務器程序,先調(diào)用Listen監(jiān)聽socket端口,收到連接請求后調(diào)用Accept接收請求。
然后,創(chuàng)建一個和CSocket對象關聯(lián)的CSocketFile對象,創(chuàng)建一個和CSocketFile對象關聯(lián)的CArchive對象,指定CArchive對象是用于讀或者寫。如果既要讀又要寫,則創(chuàng)建兩個CArchive對象。
創(chuàng)建工作完成之后,使用CArchive對象在客戶和服務器之間傳送數(shù)據(jù)
使用完畢,銷毀CArchive對象、CSocketFile對象、CSocket對象。
從前面的章節(jié)可以知道,CArchive可以以一個CFile對象為基礎,通過<<和>>操作符完成對文件的二進制流的操作。所以可以從CFile派生一個類,實現(xiàn)CFile的操作界面(Read和Write)。由于CSocket提供了阻塞操作,所以完全可以像讀寫文件一樣讀寫socket數(shù)據(jù)。
下面,分析CSocketFile的設計和實現(xiàn)。
CSocketFile::CSocketFile(CSocket* pSocket, BOOL bArchiveCompatible)
{
m_pSocket = pSocket;
m_bArchiveCompatible = bArchiveCompatible;
#ifdef _DEBUG
ASSERT(m_pSocket != NULL);
ASSERT(m_pSocket->m_hSocket != INVALID_SOCKET);
int nType = 0;
int nTypeLen = sizeof(int);
ASSERT(m_pSocket->GetSockOpt(SO_TYPE,&nType,&nTypeLen));
ASSERT(nType == SOCK_STREAM);
#endif // _DEBUG
}
其中:
構造函數(shù)的參數(shù)1指向關聯(lián)的CSocket對象,被保存在成員變量m_pSocket中;
參數(shù)2指定該對象是否和一個CArchive對象關聯(lián)(不關聯(lián)則獨立使用),被保存在成員變量bArchiveCompatible中。
Degug部分用于檢測m_pSocket是否是SOCK_STREAM類型。
CSocketFile::~CSocketFile()
{
}
(2)CSocketFile的讀寫的實現(xiàn)
分析CSocketFile如何用文件的讀寫實現(xiàn)網(wǎng)絡I/O。
UINT CSocketFile::Read(void* lpBuf, UINT nCount)
{
ASSERT(m_pSocket != NULL);
int nRead;
//CSocketFile對象獨立使用
if (!m_bArchiveCompatible)
{
int nLeft = nCount;
PBYTE pBuf = (PBYTE)lpBuf;
//讀完nCount個字節(jié)的數(shù)據(jù)
while(nLeft > 0)
{
//CSocket的Receive,阻塞操作,讀取到數(shù)據(jù)才繼續(xù)
nRead = m_pSocket->Receive(pBuf, nLeft);
if (nRead == SOCKET_ERROR)
{
int nError = m_pSocket->GetLastError();
AfxThrowFileException(CFileException::generic, nError);
ASSERT(FALSE);
}
else if (nRead == 0)
{
return nCount - nLeft;
}
nLeft -= nRead;
pBuf += nRead;
}
return nCount - nLeft;
}
//和一個CArchive對象關聯(lián)使用
//讀取數(shù)據(jù),能讀多少是多少
nRead = m_pSocket->Receive(lpBuf, nCount, 0);
if (nRead == SOCKET_ERROR)
{
int nError = m_pSocket->GetLastError();
AfxThrowFileException(CFileException::generic, nError);
ASSERT(FALSE);
}
return nRead;
}
void CSocketFile::Write(const void* lpBuf, UINT nCount)
{
ASSERT (m_pSocket!=NULL);
//CSocket的函數(shù)Send,阻塞操作,發(fā)送完畢才繼續(xù)
int nWritten = m_pSocket->Send(lpBuf, nCount);
if (nWritten == SOCKET_ERROR)
{
int nError = m_pSocket->GetLastError();
AfxThrowFileException(CFileException::generic, nError);
}
}
從CSockefFile的讀寫實現(xiàn)可以看出,CSocketFile如果獨立使用,在Read操作時可能出現(xiàn)無限等待,因為數(shù)據(jù)是分多個消息多次送達的,沒有讀取到指定長度的數(shù)據(jù)并不表示數(shù)據(jù)讀取完畢。但是和CArchive配合使用,則僅僅讀取到數(shù)據(jù)就返回。至于數(shù)據(jù)是否讀取完畢,可以使用CArchive的IsBufferEmpty函數(shù)來判斷。
其他CFile界面,CSocketFile沒有實現(xiàn)。
從CScocketFile的設計和實現(xiàn)來看,CSocketFile是使用CSocket的一個很好的例子,也是使用CFile的一個例子。