Server-Side Code

Let'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 CSocket object named sockSrvr and configures it to listen on a pre-designated port (which is #define'd as 8686 and must be known to the server). When a connection request is received on the port, a second CSocket object named sockConnection is created to handle the connection. The connection is accepted by the sockConnection object in the call to CSocket::Accept(), which actually hands the connection off to a new port address, leaving sockSrvr free to listen for further connection requests on the originally-designated port. All very standard.

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

CFile::GetLength() retrieves the file's length in bytes, and the next function, htonl(), compensates for differences in machines that store integers in big-endian versus little-endian format (see footnote 2). The interesting part happens next: a loop is entered to send the length of the file to the client. In the loop, CSocket::Send() is called repeatedly until all bytes of the file's length are sent to the client.

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 CSocket::Send() clearly warns of this (actually, it's the documentation for CAsyncSocket::Send(), which is the base class for CSocket; see MSDN):

Quote from MSDN:

"If no error occurs, Send returns the total number of characters sent. (Note that this can be less than the number indicated by nBufLen.) Otherwise, a value of SOCKET_ERROR is returned, and a specific error code can be retrieved by calling GetLastError....

On CAsyncSocket objects of type SOCK_STREAM, the number of bytes written can be between 1 and the requested length, depending on buffer availability on both the local and foreign hosts."

It's therefore possible that a single call to CSocket::Send() will not result in a transmission of all four bytes of the integer that stores the file's length. So, we check the return value of Send(), and until all four bytes are sent, we keep on calling it.

We also check rigorously for errors. CSocket::Send() will report on errors in the return code, and if a value of SOCKET_ERROR is returned, then some error has occurred. We determine the precise nature of the error through the following call to GetLastError(). You should handle the error appropriately. The sample code does nothing except a TRACE output to the debug window, followed by an immediate return with a return code of FALSE.

After the length of the file is sent, we send the actual bytes of the file itself. The file is sent in chunks of SEND_BUFFER_SIZE bytes (which we have #define'd as 4096), and we allocate memory from the heap for this buffer. Then, we call CFile::Read() to read chunks of the file one after the other, and send the chunks out over the socket. Let's look at the code:

    // 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 sendThisTime stores the number of bytes returned by CFile::Read(), and is a convenient way to monitor the last read operation (before EOF) which naturally will contain less than SEND_BUFFER_SIZE bytes. Again, we call CSocket::Send() in a loop, until all of the sendThisTime bytes are actually accepted by the socket and sent out over the network to the client, checking for errors all along the way.

Client-Side Code

Now, 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 CSocket object named sockClient is created and attempts a connection to the server on the pre-designated port. The address of the server is specified by the CString object named strIP which can store either a dotted IP address or a machine name. Once the connection is made, the second part calls CSocket::Receive() in a loop to get the length of the file, which is converted by the ntohl() function into big-endian or little-endian format as appropriate. In the third part, a BYTE buffer of size RECV_BUFFER_SIZE is allocated from the heap, and CSocket::Receive() is called in a loop until all bytes of the file are received. Note that RECV_BUFFER_SIZE can be different from SEND_BUFFER_SIZE, although in this code, they are the same (both are #define'd to 4096). Here's the code for the third part:

    // 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 );
posted on 2006-01-13 01:32  王浩的博客  阅读(503)  评论(0)    收藏  举报