Socket在阻塞模式下的資訊收發和檔案接收
概述:
收發資料是網路編程的主題,在通訊端上收發資料我們可以使用send和recv,當然還有Winsock2的WSASend和WSARecv。我們這裡只討論send和recv。
通訊端可以工作在阻塞態和非阻塞態,,阻塞態就是函數調用會停住,非阻塞態就是函數調用會立刻返回,待到後面的某個時間點在去取得結果。我們這裡先討論阻塞態。
收發資訊就是在Socket上收發二進位流。而收發檔案實際上也就是收發資訊,只是多了開啟檔案,把檔案資料讀成二進位流傳到SOCKET上去和在接收端把二進位流寫進一個檔案裡去這兩步。
為什麼要寫這篇文章:
為什麼要討論這個主題呢?因為我在一個項目中使用資訊收發的時候,發現會出現一些莫名其妙的現象,就是有時候某台機器可以順利運行,但另外的機器又不能好好執行,而某台機器有時候正常運行,有時候在運行時又會出現運行時的錯誤。
我知道這肯定是一些記憶體的操作引起的,這涉及到具體的項目內容就不談了,這裡重點說一下如何正確的進行SOCKET上資訊的收發和檔案的收發。如果沒耐心的人可以直接到下面去看代碼,把這些函數運用到MFC程式裡去會省去很多事。(代碼用了Cstring,所以只適合MFC程式,如果要其他用途可以根據函數的演算法來重寫)。
關鍵問題(Key Point):
關鍵問題在於send和recv這兩個函數。
send函數原型:
int send(
SOCKET s,
const char FAR *buf,
int len,
int flags
);
這裡S是通訊端控制代碼,buf是存放準備發出去資料的緩衝區,長度是緩衝區中資料的長度。Flag給0吧。暫時不管(可以查看MSDN)。傳回值是實際發出去的位元組長度。(在非阻塞態實際發出的位元組數可能會比指定的len的數量少,這是另一種情況)。
這就很好理解了,我給出一個本地的緩衝區,在len那裡指定資料的長度,然後通過send 就發出去。這有什麼難呢?別急,我們再來看看接收函數recv。
recv函數原型:
int recv(
SOCKET s,
char FAR *buf,
int len,
int flags
);
這裡S是通訊端控制代碼,buf是存放準備接收資料的緩衝區,長度是緩衝區的長度。Flag給0吧。暫時不管(可以查看MSDN)。傳回值是實際接收到的位元組長度。
問題就是這個recv收到的位元組數不一定和我們給出的buf的位元組數一樣。會根據網路狀態隨機變化。那我們如何在發送端和接收端協調好這種位元組的收發呢?(要知道,阻塞態的通訊端講究的就是位元組的協調,就是這個階段我給你多少位元組,你就只能接收多少位元組,多一個或者少一個都會把後面的位元組流打亂了)。
所以我們要想個辦法來定義好位元組數量的收發。定個協議,檢測頭尾?也可以,這可以自己去發揮。為了說明問題,我們這裡用的是最土的方法,就是每次都發送300個位元組,接收方也接收300位元組。
在接收方能不能這樣?
recv(hSocket, szBuf, 300, 0)
這樣就能把300個位元組接收過來了嗎?事實證明這樣是不行的,你可能收到200、220、150等等沒規律的數字,如果這樣,那SOCKET上的位元組流豈不是被搞亂了,對,就是被搞亂了,隨之而來的就是程式的崩潰,莫名其妙的錯誤。
收發資訊--解決方案
我們要如何解決這個問題呢?
首先第一個想法就是,如果收的位元組沒達到我的要求數目時,要繼續接收,並且填在緩衝區中。好這個想法完全正確,那如何在代碼中表現出來呢?首先我需要一個變數表示接收了多少,或者還有多少沒接收,然後迴圈的接收,直到滿足了我要接收的位元組數,這才算一次成功的接收。收發的代碼如下:
/*****************************************************************************/
* hSocket: 通訊端控制代碼
* nRecvSize: 需要接收的位元組數
* strOut: 用字串返回接收到的位元組流
/*****************************************************************************/
BOOL CDGSocket::Recv(SOCKET hSocket, int nRecvSize, CString &strOut)
{
BOOL bRet = TRUE;
char *pszBuf = new char[nRecvSize];
int nLeftToRecv = nRecvSize;
do
{
// 取得要開始寫入的地址
char *pLeftBuf = (char *) ( pszBuf + (nRecvSize - nLeftToRecv) );
// 接收位元組流
int nRecvBytes = recv(hSocket, pLeftBuf, nLeftToRecv, 0);
// 判斷一下是否出錯了
if(nRecvBytes == SOCKET_ERROR)
{
MyOutput("My Recv Error!");
bRet = FALSE;
goto FINAL;
}
nLeftToRecv -= nRecvBytes;
} while(nLeftToRecv > 0);
strOut = pszBuf;
FINAL:
delete []pszBuf;
return bRet;
}
/*****************************************************************************/
* hSocket: 通訊端控制代碼
* nRecvSize: 需要發送的位元組數
* pStr: 要發送出去的位元組流
/*****************************************************************************/
BOOL CDGSocket::Send(SOCKET hSocket, int nSendSize, LPCTSTR pStr)
{
BOOL bRet = TRUE;
if(send(hSocket, pStr, nSendSize, 0) == SOCKET_ERROR)
{
MyOutput("My Send Error!");
bRet = FALSE;
}
return bRet;
}
收發檔案--解決方案
上面已經說過收發檔案實際也是收發資訊,只是多了把檔案資訊變為位元組流這一步。下面給出了配對的範例程式碼:
/*****************************************************************************/
* hSocket: 通訊端控制代碼
* fName: 發送的本地檔案路徑
/*****************************************************************************/
BOOL CDGSocket::SendFile(SOCKET hSocket, CString fName)
{
BOOL bRet = TRUE;
int fileLength, cbLeftToSend;
// pointer to buffer for sending data (memory is allocated after sending file size)
BYTE* sendData = NULL;
CFile sourceFile;
CFileException fe;
BOOL bFileIsOpen = FALSE;
if( !( bFileIsOpen = sourceFile.Open( fName, CFile::modeRead | CFile::typeBinary, &fe ) ) )
{
TCHAR strCause[256];
fe.GetErrorMessage( strCause, 255 );
TRACE( "SendFileToRemoteRecipient encountered an error while opening the local file/n"
"/tFile name = %s/n/tCause = %s/n/tm_cause = %d/n/tm_IOsError = %d/n",
fe.m_strFileName, strCause, fe.m_cause, fe.m_lOsError );
/* you should handle the error here */
bRet = FALSE;
goto PreReturnCleanup;
}
// first send length of file
fileLength = sourceFile.GetLength();
fileLength = htonl( fileLength );
cbLeftToSend = sizeof( fileLength );
do
{
int cbBytesSent;
const char* bp = (const char*)(&fileLength) + sizeof(fileLength) - cbLeftToSend;
cbBytesSent = send(hSocket, bp, cbLeftToSend, 0);
// test for errors and get out if they occurred
if ( cbBytesSent == SOCKET_ERROR )
{
int iErr = ::GetLastError();
TRACE( "SendFileToRemoteRecipient returned a socket error while sending file length/n"
"/tNumber of Bytes sent = %d/n"
"/tGetLastError = %d/n", cbBytesSent, iErr );
/* you should handle the error here */
bRet = FALSE;
goto PreReturnCleanup;
}
// data was successfully sent, so account for it with already-sent data
cbLeftToSend -= cbBytesSent;
}
while ( cbLeftToSend > 0 );
// now send the file's data
sendData = new BYTE[SEND_BUFFER_SIZE];
cbLeftToSend = sourceFile.GetLength();
do
{
// read next chunk of SEND_BUFFER_SIZE bytes from file
int sendThisTime, doneSoFar, buffOffset;
sendThisTime = sourceFile.Read( sendData, SEND_BUFFER_SIZE );
buffOffset = 0;
do
{
doneSoFar = send(hSocket, (const char*)(sendData + buffOffset), sendThisTime, 0);
// test for errors and get out if they occurred
if ( doneSoFar == SOCKET_ERROR )
{
int iErr = ::GetLastError();
TRACE( "SendFileToRemoteRecipient returned a socket error while sending chunked file data/n"
"/tNumber of Bytes sent = %d/n"
"/tGetLastError = %d/n", doneSoFar, iErr );
/* you should handle the error here */
bRet = FALSE;
goto PreReturnCleanup;
}
/***************************
un-comment this code and put a breakpoint here to prove to yourself that sockets can send fewer bytes than requested
if ( doneSoFar != sendThisTime )
{
int ii = 0;
}
****************************/
// data was successfully sent, so account for it with already-sent data
buffOffset += doneSoFar;
sendThisTime -= doneSoFar;
cbLeftToSend -= doneSoFar;
}
while ( sendThisTime > 0 );
}
while ( cbLeftToSend > 0 );
PreReturnCleanup: // labelled goto destination
// free allocated memory
// if we got here from a goto that skipped allocation, delete of NULL pointer
// is permissible under C++ standard and is harmless
delete[] sendData;
if ( bFileIsOpen )
sourceFile.Close(); // only close file if it's open (open might have failed above)
return bRet;
}
/*****************************************************************************/
* hSocket: 通訊端控制代碼
* fName: 要接收到本地的檔案路徑
/*****************************************************************************/
BOOL CDGSocket::RecvFile(SOCKET hSocket, CString fName)
{
BOOL bRet = TRUE; // return value
int dataLength, cbBytesRet, cbLeftToReceive; // used to monitor the progress of a receive operation
BYTE* recdData = NULL; // pointer to buffer for receiving data (memory is allocated after obtaining file size)
CFile destFile;
CFileException fe;
BOOL bFileIsOpen = FALSE;
// open/create target file that receives the transferred data
if( !( bFileIsOpen = destFile.Open( fName, CFile::modeCreate | CFile::modeWrite | CFile::typeBinary, &fe ) ) )
{
TCHAR strCause[256];
fe.GetErrorMessage( strCause, 255 );
MyOutput(fName);
CString strErrMsg;
strErrMsg.Format("GetFileFromRemoteSender encountered an error while opening the local file/n"
"/tFile name = %s/n/tCause = %s/n/tm_cause = %d/n/tm_IOsError = %d/n",
fe.m_strFileName, strCause, fe.m_cause, fe.m_lOsError);
MyOutput( strErrMsg );
/* you should handle the error here */
bRet = FALSE;
goto PreReturnCleanup;
}
// get the file's size first
cbLeftToReceive = sizeof( dataLength );
do
{
char* bp = (char*)(&dataLength) + sizeof(dataLength) - cbLeftToReceive;
cbBytesRet = recv(hSocket, bp, cbLeftToReceive, 0);
// test for errors and get out if they occurred
if ( cbBytesRet == SOCKET_ERROR || cbBytesRet == 0 )
{
int iErr = ::GetLastError();
CString strErr;
strErr.Format("GetFileFromRemoteSite returned a socket error while getting file length/n"
"/tNumber of Bytes received (zero means connection was closed) = %d/n"
"/tGetLastError = %d/n", cbBytesRet, iErr );
/* you should handle the error here */
MyOutput(strErr);
bRet = FALSE;
goto PreReturnCleanup;
}
// good data was retrieved, so accumulate it with already-received data
cbLeftToReceive -= cbBytesRet;
}
while ( cbLeftToReceive > 0 );
dataLength = ntohl( dataLength );
// now get the file in RECV_BUFFER_SIZE chunks at a time
recdData = new byte[RECV_BUFFER_SIZE];
cbLeftToReceive = dataLength;
do
{
int iiGet, iiRecd;
iiGet = (cbLeftToReceive<RECV_BUFFER_SIZE) ? cbLeftToReceive : RECV_BUFFER_SIZE ;
iiRecd = recv(hSocket, (char *)recdData, iiGet, 0);
// test for errors and get out if they occurred
if ( iiRecd == SOCKET_ERROR || iiRecd == 0 )
{
int iErr = ::GetLastError();
TRACE( "GetFileFromRemoteSite returned a socket error while getting chunked file data/n"
"/tNumber of Bytes received (zero means connection was closed) = %d/n"
"/tGetLastError = %d/n", iiRecd, iErr );
/* you should handle the error here */
bRet = FALSE;
goto PreReturnCleanup;
}
/*************************
un-comment this code and put a breakpoint here to prove to yourself that sockets can return fewer bytes than requested
if ( iiGet != iiRecd )
{
int ii=0;
}
***************************/
// good data was retrieved, so accumulate it with already-received data
destFile.Write( recdData, iiRecd); // Write it
cbLeftToReceive -= iiRecd;
}
while ( cbLeftToReceive > 0 );
PreReturnCleanup: // labelled "goto" destination
// free allocated memory
// if we got here from a goto that skipped allocation, delete of NULL pointer
// is permissible under C++ standard and is harmless
delete[] recdData;
if ( bFileIsOpen )
destFile.Close(); // only close file if it's open (open might have failed above)
return bRet;
}