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;
}