|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
ContentsIntroductionThis brief article shows how to use MFC's Transferring files with On top of that, virtually anything can go wrong during socket communications. The client might disconnect, the server might timeout, the network cable might become disconnected. This adds the element of error-checking on top of an already complicated situation. All the Server-Side CodeLet's dive into the code and then explain what each part does. Here's code for sending a file from the computer that hosts the file (we'll call this the "server", based on convention) to another computer that asks for it (the "client"). #define PRE_AGREED_PORT 8686 #define SEND_BUFFER_SIZE 4096 BOOL CYourServerClass::SendFileToRemoteRecipient(CString fName) { /*************************** // listens for a connection from a remote client and uploads a file to it // the remote client must be running // a counterpart GetFileFromRemoteSender function // Input: CString fName = name of local file // which will be uploaded to remote client // Output: BOOL return value indicates success or failure of the upload ***************************/ // create socket and listen on pre-designated port /// AfxSocketInit(NULL); // make certain this is done somewhere in each thread // (usually in InitInstance for main thread) CSocket sockSrvr; // Creates our server socket sockSrvr.Create(PRE_AGREED_PORT); // Start listening for the client at PORT sockSrvr.Listen(); CSocket sockConnection; // Use another CSocket to accept the connection sockSrvr.Accept(sockConnection); // local variables used in file transfer (declared here // to avoid "goto skips definition"-style compiler errors) // return value BOOL bRet = TRUE; // used to monitor the progress of a sending operation 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; BYTE* bp = (BYTE*)(&fileLength) + sizeof(fileLength) - cbLeftToSend; cbBytesSent = sockConnection.Send( bp, cbLeftToSend ); // 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 = sockConnection.Send( sendData + buffOffset, sendThisTime ); // 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; } // 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) sockConnection.Close(); return bRet; } There are three major functions performed by this code: first listen for and establish a connection with the client, next send the client the length of the file, and then finally send the file to the client in chunks. In the first part, the code creates a After the connection is accepted, the server imposes a very simple protocol on the file transfer. Before actual transfer of file data, the server sends the total length of the file (in bytes). Let's look at that code in detail: // first send length of file fileLength = sourceFile.GetLength(); fileLength = htonl( fileLength ); cbLeftToSend = sizeof( fileLength ); do { int cbBytesSent; BYTE* bp = (BYTE*)(&fileLength) + sizeof(fileLength) - cbLeftToSend; cbBytesSent = sockConnection.Send( bp, cbLeftToSend ); // 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 );
Why is a loop needed? Well, as indicated above, sockets are particular about whether or not they will accept information for transmission. There might be reasons why the socket is not prepared to accept your data, or is not prepared to accept all of it right now. The documentation for Quote from MSDN:
It's therefore possible that a single call to We also check rigorously for errors. After the length of the file is sent, we send the actual bytes of the file itself. The file is sent in chunks of // 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 = sockConnection.Send( sendData + buffOffset, sendThisTime ); // 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; } // data was successfully sent, so account // for it with already-sent data buffOffset += doneSoFar; sendThisTime -= doneSoFar; cbLeftToSend -= doneSoFar; } while ( sendThisTime > 0 ); } while ( cbLeftToSend > 0 ); The integer Client-Side CodeNow, let's look at the client-side code: #define PRE_AGREED_PORT 8686 #define RECV_BUFFER_SIZE 4096 BOOL CYourClientClass::GetFileFromRemoteSender(CString strIP, CString fName) { /*************************** // connects to a remote server and downloads a file from it // the remote server must be running a counterpart // SendFileToRemoteRecipient function // Inputs: CString strIP = IP address of remote server, // in dotted IP format (like "127.0.0.1") or a manchine name (like "localhost") // CString fName = name of local file to which // downlaoded data will be stored // Output: BOOL return value indiactes success or failure of the download ****************************/ // create client socket and connect to server /// AfxSocketInit(NULL); // make certain this is done somewhere in each thread // (usually in InitInstance for main thread) CSocket sockClient; sockClient.Create(); // PRE_AGREED_PORT is #define'd as 8686 sockClient.Connect( strIP, PRE_AGREED_PORT ); // local variables used in file transfer (declared here to avoid // "goto skips definition"-style compiler errors) BOOL bRet = TRUE; // return value // used to monitor the progress of a receive operation int dataLength, cbBytesRet, cbLeftToReceive; // pointer to buffer for receiving data // (memory is allocated after obtaining file size) BYTE* recdData = NULL; 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 ); TRACE( "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 ); /* you should handle the error here */ bRet = FALSE; goto PreReturnCleanup; } // get the file's size first cbLeftToReceive = sizeof( dataLength ); do { BYTE* bp = (BYTE*)(&dataLength) + sizeof(dataLength) - cbLeftToReceive; cbBytesRet = sockClient.Receive( bp, cbLeftToReceive ); // test for errors and get out if they occurred if ( cbBytesRet == SOCKET_ERROR || cbBytesRet == 0 ) { int iErr = ::GetLastError(); TRACE( "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 */ 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 = sockClient.Receive( recdData, iiGet ); // 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; } // 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) sockClient.Close(); return bRet; } There are many parallels between the client-side code and that of the server, and we'll try to avoid redundancy in the explanations. As before, there are three main parts: making a connection, getting the file's length, and then getting the file's data in chunks. In the first part, a // 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 = sockClient.Receive( recdData, iiGet ); // 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; } // good data was retrieved, // so accumulate it with already-received data destFile.Write( recdData, iiRecd); // Write it cbLeftToReceive -= iiRecd; } while ( cbLeftToReceive > 0 ); The only tricky part of this code is the determination of the number of bytes to ask for in Note that the code works even if Demo SoftwareThe "Source" download is a single C++ file containing the above source code. You'll need to adapt the code into your client and server applications, and (for example) must change the The "Demo" download includes a VC++ 6.0 workspace that contains two projects (the client and the server), each implementing the above functions in threads (see footnote 3). This download also contains release-build versions of both executables, so you can test the programs without the need to build them first. Additionally, there are some minor modifications to the code for the above functions, so as to allow them to communicate status results back to the main GUI. To use, start up the server on a first computer, give it a local file name, and then press the "Listen For Connection Attempt" button. This starts up the thread and puts the server's Start up the client and type in the name you want used for local storage of the copied file. The client and server can run on the same computer or on different computers -- your choice -- and if they run on different computers, the computers can connect over a local network (wired or wireless) or over the Internet (all combinations have been tested). Give the client the IP address of the server; there's a drop-down box for commonly-used IP addresses for home networks, and use the localhost address of 127.0.0.1 if the client and server are both running on the same computer. Click the "Get File From This Location" button, and the client will connect to the server and be sent a copy of the file. Please remember to put the server into its listening mode before the client tries to connect to it, or nothing will work. The programs both have counters which display the number of times a send or receive "mismatch event" occurs. A "mismatch event" is a term I coined to refer to a situation where a call to Footnotes
| ||||||||||||||||||||