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